Files

356 lines
8.7 KiB
Go
Raw Permalink Normal View History

2025-11-09 11:06:04 -05:00
package main
import (
"fmt"
"io"
"log"
2025-11-09 11:06:04 -05:00
"os"
"regexp"
"strings"
2025-11-09 11:06:04 -05:00
2025-11-11 13:33:14 -05:00
"code.jakeyoungdev.com/go/compose-parser/compose"
"code.jakeyoungdev.com/go/compose-parser/issue"
2025-11-09 11:06:04 -05:00
"gopkg.in/yaml.v3"
)
2025-11-11 13:33:14 -05:00
//shoutouts and/or blame should go to: https://regex101.com/ it helped me construct some unholy regex
const (
PRIVILEGE_OPT = "no-new-privileges=true"
ROOT_USER_GROUP = "1000:1000"
ROOT_USER = "1000"
ROOT_GROUP = "1000"
2025-11-11 13:33:14 -05:00
//this is an insane regex to detect IP:PORT:PORT in port configuration but also supports the ability to detect secrets.* and vars.* from workflows
IP_PORT_PORT_REGEX = `^(\${\s*\w+\s*}|[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}){1}:(\${\s*\w+\s*}|[0-9]+){1}:(\${\s*\w+\s*}|[0-9]+){1}$`
ENV_PORT_PORT = `^\${\s*\w+\s*}{1}:\${\s*\w+\s*}{1}$`
)
2025-11-09 11:06:04 -05:00
func main() {
filePath := os.Getenv("COMPOSE_FILE_PATH")
if filePath == "" {
filePath = "compose.yaml"
2025-11-09 11:06:04 -05:00
}
f, err := os.Open(filePath)
if err != nil {
log.Fatal(err)
2025-11-09 11:06:04 -05:00
}
defer f.Close()
fd, err := io.ReadAll(f)
if err != nil {
log.Fatal(err)
2025-11-09 11:06:04 -05:00
}
2025-11-11 13:33:14 -05:00
var data compose.Compose
2025-11-09 11:06:04 -05:00
err = yaml.Unmarshal(fd, &data)
if err != nil {
log.Fatal(err)
2025-11-09 11:06:04 -05:00
}
2025-11-11 13:33:14 -05:00
type report struct {
Name string
2025-11-11 13:33:14 -05:00
Issues []*issue.Issue
}
2025-11-11 13:33:14 -05:00
var issues []report
2025-11-09 11:06:04 -05:00
for name, serviceConf := range data.Services {
2025-11-11 13:33:14 -05:00
rprt := report{
2025-11-09 11:06:04 -05:00
Name: name,
}
2025-11-11 12:58:06 -05:00
i := ResourceCheck(&serviceConf)
if i != nil {
rprt.Issues = append(rprt.Issues, i)
}
2025-11-09 11:06:04 -05:00
j := SecurityOptCheck(&serviceConf)
if j != nil {
rprt.Issues = append(rprt.Issues, j)
2025-11-09 11:06:04 -05:00
}
k, err := UserCheck(&serviceConf)
if err != nil {
log.Fatal(err)
2025-11-09 11:06:04 -05:00
}
if k != nil {
rprt.Issues = append(rprt.Issues, k)
}
l, err := PortCheck(&serviceConf)
2025-11-11 12:58:06 -05:00
if err != nil {
log.Fatal(err)
}
if len(l) > 0 {
rprt.Issues = append(rprt.Issues, l...)
}
m := PolicyCheck(&serviceConf)
2025-11-11 12:58:06 -05:00
if m != nil {
rprt.Issues = append(rprt.Issues, m)
}
n := PrivilegedCheck(&serviceConf)
2025-11-11 13:33:14 -05:00
if n != nil {
rprt.Issues = append(rprt.Issues, n)
}
issues = append(issues, rprt)
}
2025-11-11 16:27:26 -05:00
lvl := os.Getenv("LOG_LEVEL")
2025-11-11 18:04:21 -05:00
fatalCount := 0
for _, p := range issues {
2025-11-11 16:27:26 -05:00
fmt.Println()
fmt.Println("----------------------------------------------------------------------------")
fmt.Println(p.Name)
for _, x := range p.Issues {
2025-11-11 18:04:21 -05:00
if x.Level == issue.FATAL {
fatalCount++
}
2025-11-11 16:27:26 -05:00
if lvl == "all" {
fmt.Printf("\tsafe: %t\n\tlevel: %s\n\tMessages:\n\t\t%s\n\n", x.Safe, x.Level, strings.Join(x.Messages, "\n\t\t"))
} else if lvl == "fatal" {
if x.Level == issue.FATAL {
fmt.Printf("\tsafe: %t\n\tlevel: %s\n\tMessages:\n\t\t%s\n\n", x.Safe, x.Level, strings.Join(x.Messages, "\n\t\t"))
}
}
2025-11-09 11:06:04 -05:00
}
}
2025-11-11 18:04:21 -05:00
2025-11-11 18:07:33 -05:00
if strings.EqualFold(os.Getenv("FAIL_ON_FATAL"), "yes") && fatalCount > 0 {
os.Exit(1)
2025-11-11 18:04:21 -05:00
}
}
2025-11-09 11:06:04 -05:00
2025-11-11 13:33:14 -05:00
// ensure cpus and mem_limit are set on the service
func ResourceCheck(srv *compose.ServiceConfig) *issue.Issue {
2025-11-11 13:33:14 -05:00
i := &issue.Issue{}
if srv.Cpus == nil {
i.Warning()
i.Messages = append(i.Messages, "there is no cpu limit set for the service, set cpus to define the number of (potentially virtual) CPUs to allocate to service containers")
}
if srv.MemLimit == nil {
i.Warning()
i.Messages = append(i.Messages, "there is no memory limit set for the service, set mem_limit to configure a limit on the amount of memory a container can allocate")
}
if srv.Cpus == nil && srv.MemLimit == nil {
i.Fatal()
i.Messages = append(i.Messages, "there are no resource limits set for the service, its the wild west")
}
2025-11-11 17:37:51 -05:00
if len(i.Messages) > 0 {
return i
}
i.Passed()
i.Messages = append(i.Messages, "resource limits are configured properly")
2025-11-11 12:58:06 -05:00
return i
}
2025-11-11 13:33:14 -05:00
// make sure service is not ran as privileged
func PrivilegedCheck(srv *compose.ServiceConfig) *issue.Issue {
2025-11-11 13:33:14 -05:00
i := &issue.Issue{}
2025-11-11 12:58:06 -05:00
if srv.Privileged == nil {
i.Passed()
i.Messages = append(i.Messages, "no privileged flag set")
return i
}
if *srv.Privileged == true {
i.Fatal()
i.Messages = append(i.Messages, "privileged flag is set to true, remove it to prevent the service container from running with elevated privileges")
2025-11-11 12:58:06 -05:00
return i
}
i.Passed()
i.Messages = append(i.Messages, "privileged flag is set to false")
return i
}
2025-11-11 13:33:14 -05:00
// checks for specific security options
func SecurityOptCheck(srv *compose.ServiceConfig) *issue.Issue {
2025-11-11 13:33:14 -05:00
i := &issue.Issue{}
if srv.SecOpts == nil {
i.Fatal()
i.Messages = append(i.Messages, "set no-new-privileges=true on the service security_opts to disable container processes from gaining new privileges")
return i
}
for _, opt := range *srv.SecOpts {
if strings.EqualFold(opt, PRIVILEGE_OPT) {
i.Passed()
2025-11-11 17:45:34 -05:00
i.Messages = append(i.Messages, "security options are safe")
return i
}
}
i.Fatal()
i.Messages = append(i.Messages, "set no-new-privileges=true on the container security_opts to disable container processes from gaining new privileges")
return i
}
2025-11-11 13:33:14 -05:00
// checking port to ensure it is behind a reverse proxy
func PortCheck(srv *compose.ServiceConfig) ([]*issue.Issue, error) {
2025-11-11 13:33:14 -05:00
il := []*issue.Issue{}
2025-11-11 12:58:06 -05:00
if srv.Ports == nil {
2025-11-11 13:33:14 -05:00
i := &issue.Issue{}
2025-11-11 12:58:06 -05:00
i.Passed()
i.Messages = append(i.Messages, "no ports exposed")
il = append(il, i)
return il, nil
}
for _, prt := range *srv.Ports {
2025-11-11 13:33:14 -05:00
i := &issue.Issue{}
2025-11-11 12:58:06 -05:00
m, err := regexp.Match(`^[0-9]*:[0-9]*`, []byte(prt))
if err != nil {
return nil, err
}
if m {
i.Fatal()
i.Messages = append(i.Messages, fmt.Sprintf("%s is fully exposed", prt))
il = append(il, i)
continue
}
ms, err := regexp.Match(ENV_PORT_PORT, []byte(prt))
2025-11-11 13:33:14 -05:00
if err != nil {
log.Fatal(err)
}
if ms {
i.Warning()
i.Messages = append(i.Messages, fmt.Sprintf("%s could be exposed", prt))
il = append(il, i)
continue
}
m2, err := regexp.Match(IP_PORT_PORT_REGEX, []byte(prt))
2025-11-11 12:58:06 -05:00
if err != nil {
return nil, err
}
if m2 {
i.Passed()
i.Messages = append(i.Messages, fmt.Sprintf("%s is safe", prt))
il = append(il, i)
continue
} else {
i.Warning()
i.Messages = append(i.Messages, fmt.Sprintf("unable to parse port configuration for %s", prt))
il = append(il, i)
continue
}
}
return il, nil
}
2025-11-11 13:33:14 -05:00
// ensures the user value is set and nonroot in the service
func UserCheck(srv *compose.ServiceConfig) (*issue.Issue, error) {
2025-11-11 13:33:14 -05:00
i := &issue.Issue{}
if srv.User == nil {
i.Warning()
i.Messages = append(i.Messages, "no user is specified in the compose file")
return i, nil
}
2025-11-11 12:58:06 -05:00
mtch, err := regexp.Match("[0-9]*:[0-9]*", []byte(*srv.User))
if err != nil {
return nil, err
}
if mtch {
if strings.EqualFold(ROOT_USER_GROUP, *srv.User) {
i.Fatal()
i.Messages = append(i.Messages, "a root user set in the compose file")
} else {
i.Passed()
2025-11-11 12:58:06 -05:00
i.Messages = append(i.Messages, "user configuration is safe")
}
return i, nil
} else {
users := strings.Split(*srv.User, ":")
if len(users) != 2 {
i.Fatal()
i.Messages = append(i.Messages, "user value seems misconfigured, check compose file")
return i, nil
}
leftUser, err := regexp.Match(`(?m)\${{\s*(vars|secrets)\..*}}`, []byte(users[0]))
if err != nil {
return nil, err
}
rightUser, err := regexp.Match(`(?m)\${{\s*(vars|secrets)\..*}}`, []byte(users[1]))
if err != nil {
return nil, err
}
if leftUser && rightUser {
i.Passed()
2025-11-11 12:58:06 -05:00
i.Messages = append(i.Messages, "user configuration is safe")
return i, nil
}
if leftUser {
if strings.EqualFold(users[1], ROOT_GROUP) {
i.Warning()
i.Messages = append(i.Messages, "a root user is present in configuration")
} else {
i.Passed()
2025-11-11 12:58:06 -05:00
i.Messages = append(i.Messages, "user configuration is safe")
}
return i, nil
}
if rightUser {
if strings.EqualFold(users[0], ROOT_USER) {
i.Warning()
i.Messages = append(i.Messages, "a root user is present in configuration")
} else {
i.Passed()
2025-11-11 12:58:06 -05:00
i.Messages = append(i.Messages, "user configuration is safe")
}
return i, nil
2025-11-09 11:06:04 -05:00
}
}
i.Fatal()
i.Messages = append(i.Messages, "unable to parse user configuration")
return i, nil
2025-11-09 11:06:04 -05:00
}
2025-11-11 13:33:14 -05:00
// ensures a restart policy is set in the service configuration with at least max_attempts and restart condition set
func PolicyCheck(srv *compose.ServiceConfig) *issue.Issue {
2025-11-11 13:33:14 -05:00
i := &issue.Issue{}
2025-11-11 12:58:06 -05:00
if srv.Deploy != nil {
if srv.Deploy.Policy != nil {
if srv.Deploy.Policy.Condition != nil {
if srv.Deploy.Policy.MaxAttempts != nil {
i.Passed()
i.Messages = append(i.Messages, "restart policy configuration is safe")
return i
} else {
i.Warning()
i.Messages = append(i.Messages, "no max_attempts value set on restart policy container could death loop")
return i
}
} else {
i.Fatal()
i.Messages = append(i.Messages, "no restart condition is set on the container")
return i
}
} else {
i.Fatal()
i.Messages = append(i.Messages, "no restart policy is set on the container")
return i
}
}
i.Fatal()
i.Messages = append(i.Messages, "no restart policy is set on the container")
return i
}