diff --git a/action.yaml b/action.yaml index a42e6f8..d331acc 100644 --- a/action.yaml +++ b/action.yaml @@ -6,7 +6,7 @@ inputs: required: true default: "compose.yaml" ignore: - description: "checks to ignore" + description: "checks to ignore (doesn't work yet)" required: false outputs: report: diff --git a/compose/compose.go b/compose/compose.go new file mode 100644 index 0000000..dc3106b --- /dev/null +++ b/compose/compose.go @@ -0,0 +1,26 @@ +package compose + +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"` + Privileged *bool `yaml:"privileged"` +} + +type Compose struct { + Services map[string]ServiceConfig `yaml:"services"` +} diff --git a/issue/issue.go b/issue/issue.go new file mode 100644 index 0000000..b44af5d --- /dev/null +++ b/issue/issue.go @@ -0,0 +1,30 @@ +package issue + +const ( + 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 +} diff --git a/main.go b/main.go index 90fbc5a..79717e1 100644 --- a/main.go +++ b/main.go @@ -8,69 +8,22 @@ import ( "regexp" "strings" + "code.jakeyoungdev.com/go/compose-parser/compose" + "code.jakeyoungdev.com/go/compose-parser/issue" "gopkg.in/yaml.v3" ) +//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" - IM_SO_SORRY = `^(\${{\s*(vars|secrets)\..*}}|[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}):(\${{\s*(vars|secrets)\..*}}|[0-9]+):(\${{\s*(vars|secrets)\..*}}|[0-9]+)` - - FATAL IssueLevel = "FATAL" - WARNING IssueLevel = "WARNING" - PASSED IssueLevel = "PASSED" + //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 + IM_SO_SORRY = `^(\${{\s*(vars|secrets)\.[[:alnum:]]+\s*}}|[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}){1}:(\${{\s*(vars|secrets)\.[[:alnum:]]+\s*}}|[0-9]+){1}:(\${{\s*(vars|secrets)\.[[:alnum:]]+\s*}}|[0-9]+){1}$` ) -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 -} - -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"` - Privileged *bool `yaml:"privileged"` -} - -type Compose struct { - Services map[string]ServiceConfig `yaml:"services"` -} - func main() { filePath := os.Getenv("COMPOSE_FILE_PATH") if filePath == "" { @@ -87,45 +40,20 @@ func main() { log.Fatal(err) } - var data Compose + var data compose.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 { + type report struct { Name string - Issues []*Issue + Issues []*issue.Issue } - var issues []NewReport + var issues []report for name, serviceConf := range data.Services { - rprt := NewReport{ + rprt := report{ Name: name, } @@ -160,6 +88,11 @@ func main() { rprt.Issues = append(rprt.Issues, m) } + n := PrivilegedCheck(serviceConf) + if n != nil { + rprt.Issues = append(rprt.Issues, n) + } + issues = append(issues, rprt) } @@ -171,8 +104,9 @@ func main() { } } -func ResourceCheck(srv ServiceConfig) *Issue { - i := &Issue{} +// ensure cpus and mem_limit are set on the service +func ResourceCheck(srv compose.ServiceConfig) *issue.Issue { + i := &issue.Issue{} if srv.Cpus == nil { i.Warning() i.Messages = append(i.Messages, "there is no cpu limit set for the service") @@ -193,8 +127,9 @@ func ResourceCheck(srv ServiceConfig) *Issue { return i } -func PrivilegedCheck(srv ServiceConfig) *Issue { - i := &Issue{} +// make sure service is not ran as privileged +func PrivilegedCheck(srv compose.ServiceConfig) *issue.Issue { + i := &issue.Issue{} if srv.Privileged == nil { i.Passed() i.Messages = append(i.Messages, "no privileged flag set") @@ -212,8 +147,9 @@ func PrivilegedCheck(srv ServiceConfig) *Issue { return i } -func SecurityOptCheck(srv ServiceConfig) *Issue { - i := &Issue{} +// checks for specific security options +func SecurityOptCheck(srv compose.ServiceConfig) *issue.Issue { + i := &issue.Issue{} if srv.SecOpts == nil { i.Fatal() i.Messages = append(i.Messages, "set no-new-privileges=true on the service security_opts") @@ -233,10 +169,11 @@ func SecurityOptCheck(srv ServiceConfig) *Issue { return i } -func PortCheck(srv ServiceConfig) ([]*Issue, error) { - il := []*Issue{} +// checking port to ensure it is behind a reverse proxy +func PortCheck(srv compose.ServiceConfig) ([]*issue.Issue, error) { + il := []*issue.Issue{} if srv.Ports == nil { - i := &Issue{} + i := &issue.Issue{} i.Passed() i.Messages = append(i.Messages, "no ports exposed") il = append(il, i) @@ -244,7 +181,7 @@ func PortCheck(srv ServiceConfig) ([]*Issue, error) { } for _, prt := range *srv.Ports { - i := &Issue{} + i := &issue.Issue{} m, err := regexp.Match(`^[0-9]*:[0-9]*`, []byte(prt)) if err != nil { return nil, err @@ -257,6 +194,18 @@ func PortCheck(srv ServiceConfig) ([]*Issue, error) { continue } + ms, err := regexp.Match(`^\${{\s*(vars|secrets)\.[[:alnum:]]+\s*}}{1}:\${{\s*(vars|secrets)\.[[:alnum:]]+\s*}}{1}$`, []byte(prt)) + 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(IM_SO_SORRY, []byte(prt)) if err != nil { return nil, err @@ -278,8 +227,9 @@ func PortCheck(srv ServiceConfig) ([]*Issue, error) { return il, nil } -func UserCheck(srv ServiceConfig) (*Issue, error) { - i := &Issue{} +// ensures the user value is set and nonroot in the service +func UserCheck(srv compose.ServiceConfig) (*issue.Issue, error) { + i := &issue.Issue{} if srv.User == nil { i.Warning() i.Messages = append(i.Messages, "no user is specified in the compose file") @@ -307,7 +257,6 @@ func UserCheck(srv ServiceConfig) (*Issue, error) { 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 @@ -351,8 +300,9 @@ func UserCheck(srv ServiceConfig) (*Issue, error) { return i, nil } -func PolicyCheck(srv ServiceConfig) *Issue { - i := &Issue{} +// 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 { + i := &issue.Issue{} if srv.Deploy != nil { if srv.Deploy.Policy != nil { if srv.Deploy.Policy.Condition != nil {