package main import ( "fmt" "io" "log" "os" "regexp" "strings" "gopkg.in/yaml.v3" ) const ( PRIVILEGE_OPT = "no-new-privileges=true" ROOT_USER = "1000:1000" FATAL IssueLevel = "FATAL" WARNING IssueLevel = "WARNING" PASSED IssueLevel = "PASSED" ) type IssueLevel string type RestartConfig struct { Condition *string `yaml:"condition"` Delay *string `yaml:"delay"` MaxAttempts *int `yaml:"max_attempts"` Window *string `yaml:"window"` } type DeployConfig struct { Policy *RestartConfig `yaml:"restart_policy"` } type ServiceConfig struct { MemLimit *string `yaml:"mem_limit"` Cpus *float64 `yaml:"cpus"` Ports *[]string `yaml:"ports"` User *string `yaml:"user"` Deploy *DeployConfig `yaml:"deploy"` SecOpts *[]string `yaml:"security_opt"` } type Compose struct { Services map[string]ServiceConfig `yaml:"services"` } func main() { filePath := os.Getenv("COMPOSE_FILE_PATH") if filePath == "" { filePath = "./compose.yaml" } f, err := os.Open(filePath) if err != nil { log.Fatal(err) } defer f.Close() fd, err := io.ReadAll(f) if err != nil { log.Fatal(err) } var data Compose err = yaml.Unmarshal(fd, &data) if err != nil { log.Fatal(err) } type report struct { Name string //service name //restart policy PolicyPresent bool //is it present PolicyLevel IssueLevel //how serious PolicyIssues []string //are ports behind a proxy? PortSafe bool PortIssues []string PortLevel IssueLevel //memory limits and cpu ResourceSafe bool ResourceIssues []string ResourceLevel IssueLevel //proper security options SecurityOptSafe bool SecurityOptLevel IssueLevel SecurityOptIssues []string //are users nonroot UserSafe bool UserIssues []string UserIssueLevel IssueLevel } var issues []report for name, serviceConf := range data.Services { r := report{ Name: name, } if serviceConf.Ports != nil { matched, err := regexp.Match(`.*:.*:.*`, []byte((*serviceConf.Ports)[0])) if err != nil { log.Fatal(err) } portMatched, err := regexp.Match(`.*:.*`, []byte((*serviceConf.Ports)[0])) if err != nil { log.Fatal(err) } if !matched { r.PortSafe = false r.PortIssues = append(r.PortIssues, "host port is fully exposed, consider adding an IP address to keep it behind the proxy") r.PortLevel = WARNING if !portMatched { r.PortIssues = append(r.PortIssues, "ports seem misconfigured validate ports in compose file") r.PortLevel = FATAL } } else { r.PortSafe = true r.PortLevel = PASSED } } else { r.PortSafe = true r.PortLevel = PASSED } r.ResourceSafe = true r.ResourceLevel = PASSED if serviceConf.MemLimit == nil { r.ResourceSafe = false r.ResourceIssues = append(r.ResourceIssues, "no memory limit has been set for the container") r.ResourceLevel = WARNING } if serviceConf.Cpus == nil { r.ResourceSafe = false r.ResourceIssues = append(r.ResourceIssues, "no cpu limit set for the container") r.ResourceLevel = WARNING } if serviceConf.Cpus == nil && serviceConf.MemLimit == nil { r.ResourceLevel = FATAL r.ResourceIssues = append(r.ResourceIssues, "no resource limits set, its the wild west") } if serviceConf.Deploy != nil && serviceConf.Deploy.Policy != nil { r.PolicyPresent = true r.PolicyLevel = PASSED } else { r.PolicyPresent = false r.PolicyLevel = WARNING //this should check fields bettera and suggest better conditions r.PolicyIssues = append(r.PolicyIssues, "set a restart policy for the container") } r.SecurityOptSafe = false if serviceConf.SecOpts != nil { for _, opt := range *serviceConf.SecOpts { if opt == PRIVILEGE_OPT { r.SecurityOptSafe = true r.SecurityOptLevel = PASSED } } } if !r.SecurityOptSafe { r.SecurityOptLevel = FATAL r.SecurityOptIssues = append(r.SecurityOptIssues, "set no-new-privileges=true on the container security_opts") } r.UserSafe = true r.UserIssueLevel = PASSED if serviceConf.User != nil { matchnum, err := regexp.Match("[0-9].*:[0-9].*", []byte(*serviceConf.User)) if err != nil { log.Fatal(err) } if matchnum { if strings.EqualFold(*serviceConf.User, ROOT_USER) { r.UserSafe = false r.UserIssueLevel = FATAL r.UserIssues = append(r.UserIssues, "a root user is set in the compose file") } } else { match, err := regexp.Match(".*:.*", []byte(*serviceConf.User)) if err != nil { log.Fatal(err) } if !match { r.UserIssues = append(r.UserIssues, "user configuration seems off, check compose file") r.UserIssueLevel = WARNING r.UserSafe = false } } } else { r.UserSafe = false r.UserIssues = append(r.UserIssues, "a nonroot user should be set") r.UserIssueLevel = FATAL } issues = append(issues, r) } for _, p := range issues { fmt.Println("##########################################") fmt.Println(p.Name) fmt.Println("------------------------------------------") fmt.Println("[SECURITY_OPTS]") fmt.Println(p.SecurityOptLevel) fmt.Printf("safe: %t\n\n", p.SecurityOptSafe) fmt.Println(strings.Join(p.SecurityOptIssues, "\n")) fmt.Println("\n[USER]") fmt.Println(p.UserIssueLevel) fmt.Printf("safe: %t\n\n", p.UserSafe) fmt.Println(strings.Join(p.UserIssues, "\n")) fmt.Println("\n[RESOURCE]") fmt.Println(p.ResourceLevel) fmt.Printf("safe: %t\n\n", p.ResourceSafe) fmt.Println(strings.Join(p.ResourceIssues, "\n")) fmt.Println("\n[PORT]") fmt.Println(p.PortLevel) fmt.Printf("safe: %t\n\n", p.PortSafe) fmt.Println(strings.Join(p.PortIssues, "\n")) fmt.Println("\n[POLICY]") fmt.Println(p.PolicyLevel) fmt.Printf("safe: %t\n\n", p.PolicyPresent) fmt.Println(strings.Join(p.PolicyIssues, "\n")) } }