From b7389117acae9207ea86d9793b1f4a7a77b6aeca Mon Sep 17 00:00:00 2001 From: jake Date: Mon, 10 Nov 2025 16:37:49 -0500 Subject: [PATCH] breaking push - this push breaks what little functionality we had - WIP push - cleaning house, stand by --- action.yaml | 4 + main.go | 490 +++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 374 insertions(+), 120 deletions(-) diff --git a/action.yaml b/action.yaml index e68d5a2..a42e6f8 100644 --- a/action.yaml +++ b/action.yaml @@ -5,6 +5,9 @@ inputs: description: "path to docker compose file" required: true default: "compose.yaml" + ignore: + description: "checks to ignore" + required: false outputs: report: description: "results of the scan" @@ -13,3 +16,4 @@ runs: image: Dockerfile env: COMPOSE_FILE_PATH: ${{ inputs.path }} + IGNORED_CHECKS: ${{ inputs.ignore }} diff --git a/main.go b/main.go index 746e995..ae685a7 100644 --- a/main.go +++ b/main.go @@ -12,8 +12,10 @@ import ( ) const ( - PRIVILEGE_OPT = "no-new-privileges=true" - ROOT_USER = "1000:1000" + PRIVILEGE_OPT = "no-new-privileges=true" + ROOT_USER_GROUP = "1000:1000" + ROOT_USER = "1000" + ROOT_GROUP = "1000" FATAL IssueLevel = "FATAL" WARNING IssueLevel = "WARNING" @@ -22,6 +24,35 @@ const ( 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"` @@ -49,7 +80,7 @@ type Compose struct { func main() { filePath := os.Getenv("COMPOSE_FILE_PATH") if filePath == "" { - filePath = "./compose.yaml" + filePath = "compose.yaml" } f, err := os.Open(filePath) if err != nil { @@ -93,141 +124,360 @@ func main() { UserIssueLevel IssueLevel } - var issues []report + type NewReport struct { + Name string + Issues []*Issue + } + + // var issues []report + var issues []NewReport for name, serviceConf := range data.Services { - r := report{ + // var i NewReport + rprt := NewReport{ 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 + // 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) } - 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 + j := SecurityOptCheck(serviceConf) + if j != nil { + rprt.Issues = append(rprt.Issues, j) } - if serviceConf.Cpus == nil && serviceConf.MemLimit == nil { - r.ResourceLevel = FATAL - r.ResourceIssues = append(r.ResourceIssues, "no resource limits set, its the wild west") + k, err := UserCheck(serviceConf) + if err != nil { + log.Fatal(err) + } + if k != nil { + rprt.Issues = append(rprt.Issues, k) } - 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") - } + 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.SecurityOptSafe = false - if serviceConf.SecOpts != nil { - for _, opt := range *serviceConf.SecOpts { - if opt == PRIVILEGE_OPT { - r.SecurityOptSafe = true - r.SecurityOptLevel = PASSED - } - } - } + // r := report{ + // Name: name, + // } - if !r.SecurityOptSafe { - r.SecurityOptLevel = FATAL - r.SecurityOptIssues = append(r.SecurityOptIssues, "set no-new-privileges=true on the container security_opts") - } + // if serviceConf.Ports != nil { + // matched, err := regexp.Match(`.*:.*:.*`, []byte((*serviceConf.Ports)[0])) + // if err != nil { + // log.Fatal(err) + // } - 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) - } + // portMatched, err := regexp.Match(`.*:.*`, []byte((*serviceConf.Ports)[0])) + // 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 !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 + // } - 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 - } + // 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 + // } - issues = append(issues, r) + // 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("##########################################") 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")) + 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 +// // } +// }