package main import ( "fmt" "io" "log" "os" "regexp" "strings" "gopkg.in/yaml.v3" ) const ( PRIVILEGE_OPT = "no-new-privileges=true" ROOT_USER_GROUP = "1000:1000" ROOT_USER = "1000" ROOT_GROUP = "1000" FATAL IssueLevel = "FATAL" WARNING IssueLevel = "WARNING" PASSED IssueLevel = "PASSED" ) type IssueLevel string type Issue struct { Level IssueLevel Safe bool Messages []string } func (i *Issue) Passed() { i.Level = PASSED i.Safe = true } func (i *Issue) Warning() { i.Level = WARNING i.Safe = true } func (i *Issue) Fatal() { i.Level = FATAL i.Safe = false } //have functions that take the service configuration and they check certain fields then //will return an issue if there is one, which we will append. //then loop through to print //this should be a struct w methods like Passed() that handles the true/FATAL stuff //then the printing and stuff can just be loops it doesn't have to be so hardcoded 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 } type NewReport struct { Name string Issues []*Issue } // var issues []report var issues []NewReport for name, serviceConf := range data.Services { // var i NewReport rprt := NewReport{ Name: name, } // i.Name = name // _ = serviceConf i := ResourceCheck(serviceConf) if i != nil { // issues = append(issues, NewReport{Name: name, Issues: []*Issue{i}}) rprt.Issues = append(rprt.Issues, i) } j := SecurityOptCheck(serviceConf) if j != nil { rprt.Issues = append(rprt.Issues, j) } k, err := UserCheck(serviceConf) if err != nil { log.Fatal(err) } if k != nil { rprt.Issues = append(rprt.Issues, k) } issues = append(issues, rprt) // if serviceConf.MemLimit == nil { // i.Issues = append(i.Issues, Issue{ // Level: WARNING, // Safe: true, // }) // // 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 // } // 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, "no user is set in the compose file") // r.UserIssueLevel = WARNING // } // issues = append(issues, r) } for _, p := range issues { fmt.Println(p.Name) for _, x := range p.Issues { fmt.Println(*x) } } // 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")) // } } func ResourceCheck(srv ServiceConfig) *Issue { i := &Issue{} if srv.Cpus == nil { i.Warning() i.Messages = append(i.Messages, "there is no cpu limit set for the service") } if srv.MemLimit == nil { i.Warning() i.Messages = append(i.Messages, "there is no memory limit set for the service") } if srv.Cpus == nil && srv.MemLimit == nil { i.Fatal() i.Messages = append(i.Messages, "there are no resource limits set for the service") } i.Passed() return i } func SecurityOptCheck(srv ServiceConfig) *Issue { i := &Issue{} if srv.SecOpts == nil { i.Fatal() i.Messages = append(i.Messages, "set no-new-privileges=true on the service security_opts") return i } for _, opt := range *srv.SecOpts { if strings.EqualFold(opt, PRIVILEGE_OPT) { i.Passed() return i } } i.Fatal() i.Messages = append(i.Messages, "set no-new-privileges=true on the container security_opts") return i } func UserCheck(srv ServiceConfig) (*Issue, error) { i := &Issue{} if srv.User == nil { i.Warning() i.Messages = append(i.Messages, "no user is specified in the compose file") return i, nil } 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() } 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 } //shoutouts and/or blame should go to: https://regex101.com/ -- seems like a cool tool, but time will tell 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() 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() } 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() } return i, nil } } i.Fatal() i.Messages = append(i.Messages, "unable to parse user configuration") return i, nil } // func PortCheck(srv ServiceConfig) (*Issue, error) { // i := &Issue{} // if srv.Ports == nil { // i.Passed() // return i, nil // } // for _, p := range *srv.Ports { // mtc, err := regexp.Match(`.*:.*:.*`, []byte(p)) // if err != nil { // return nil, err // } // if mtc { // continue // } // pmtc, err := regexp.Match(`.*:.*`, []byte(p)) // if err != nil { // return nil, err // } // if pmtc { // i.Warning() // i.Messages = append(i.Messages, "ports are fully exposed, use an IP for the host port to move behind the proxy") // } // } // // 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 // // } // }