5 Commits

Author SHA1 Message Date
8b03056977 updates
- moving cpus to string to allow for env vars and other config options besides hardcoded floats
2025-11-11 23:44:38 -05:00
9cddc4c83e better erroring 2025-11-11 18:07:33 -05:00
f88893748a adding fails and fail skips 2025-11-11 18:04:21 -05:00
b6dfca1cf2 regex fixes for environment variables 2025-11-11 17:45:34 -05:00
a3c723cc32 bugfix for resource logging 2025-11-11 17:37:51 -05:00
4 changed files with 52 additions and 28 deletions

View File

@@ -9,12 +9,14 @@ inputs:
description: "what level of issues to show (all|fatal)" description: "what level of issues to show (all|fatal)"
required: false required: false
default: "all" default: "all"
outputs: fail:
report: description: "determines whether or not the workflow fails upon finding fatal issues (yes(default)|no)"
description: "results of the scan" required: false
default: "yes"
runs: runs:
using: docker using: docker
image: Dockerfile image: Dockerfile
env: env:
COMPOSE_FILE_PATH: ${{ inputs.path }} COMPOSE_FILE_PATH: ${{ inputs.path }}
LOG_LEVEL: ${{ inputs.show }} LOG_LEVEL: ${{ inputs.show }}
FAIL_ON_FATAL: ${{ inputs.fail }}

View File

@@ -13,7 +13,7 @@ type DeployConfig struct {
type ServiceConfig struct { type ServiceConfig struct {
MemLimit *string `yaml:"mem_limit"` MemLimit *string `yaml:"mem_limit"`
Cpus *float64 `yaml:"cpus"` Cpus *string `yaml:"cpus"`
Ports *[]string `yaml:"ports"` Ports *[]string `yaml:"ports"`
User *string `yaml:"user"` User *string `yaml:"user"`
Deploy *DeployConfig `yaml:"deploy"` Deploy *DeployConfig `yaml:"deploy"`

View File

@@ -28,3 +28,13 @@ func (i *Issue) Fatal() {
i.Level = FATAL i.Level = FATAL
i.Safe = false i.Safe = false
} }
func (i *Issue) Serious() {
if i.Level == FATAL {
return
} else if i.Level == WARNING {
i.Level = FATAL
} else if i.Level == PASSED {
i.Level = WARNING
}
}

60
main.go
View File

@@ -21,7 +21,8 @@ const (
ROOT_USER = "1000" ROOT_USER = "1000"
ROOT_GROUP = "1000" ROOT_GROUP = "1000"
//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 //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}$` 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}$`
) )
func main() { func main() {
@@ -57,17 +58,17 @@ func main() {
Name: name, Name: name,
} }
i := ResourceCheck(serviceConf) i := ResourceCheck(&serviceConf)
if i != nil { if i != nil {
rprt.Issues = append(rprt.Issues, i) rprt.Issues = append(rprt.Issues, i)
} }
j := SecurityOptCheck(serviceConf) j := SecurityOptCheck(&serviceConf)
if j != nil { if j != nil {
rprt.Issues = append(rprt.Issues, j) rprt.Issues = append(rprt.Issues, j)
} }
k, err := UserCheck(serviceConf) k, err := UserCheck(&serviceConf)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -75,7 +76,7 @@ func main() {
rprt.Issues = append(rprt.Issues, k) rprt.Issues = append(rprt.Issues, k)
} }
l, err := PortCheck(serviceConf) l, err := PortCheck(&serviceConf)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -83,12 +84,12 @@ func main() {
rprt.Issues = append(rprt.Issues, l...) rprt.Issues = append(rprt.Issues, l...)
} }
m := PolicyCheck(serviceConf) m := PolicyCheck(&serviceConf)
if m != nil { if m != nil {
rprt.Issues = append(rprt.Issues, m) rprt.Issues = append(rprt.Issues, m)
} }
n := PrivilegedCheck(serviceConf) n := PrivilegedCheck(&serviceConf)
if n != nil { if n != nil {
rprt.Issues = append(rprt.Issues, n) rprt.Issues = append(rprt.Issues, n)
} }
@@ -96,13 +97,16 @@ func main() {
issues = append(issues, rprt) issues = append(issues, rprt)
} }
//this is better printing, it should probably group up the port issues in a better printing. Not sure how
lvl := os.Getenv("LOG_LEVEL") lvl := os.Getenv("LOG_LEVEL")
fatalCount := 0
for _, p := range issues { for _, p := range issues {
fmt.Println() fmt.Println()
fmt.Println("----------------------------------------------------------------------------") fmt.Println("----------------------------------------------------------------------------")
fmt.Println(p.Name) fmt.Println(p.Name)
for _, x := range p.Issues { for _, x := range p.Issues {
if x.Level == issue.FATAL {
fatalCount++
}
if lvl == "all" { 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")) 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" { } else if lvl == "fatal" {
@@ -112,33 +116,41 @@ func main() {
} }
} }
} }
if strings.EqualFold(os.Getenv("FAIL_ON_FATAL"), "yes") && fatalCount > 0 {
os.Exit(1)
}
} }
// ensure cpus and mem_limit are set on the service // ensure cpus and mem_limit are set on the service
func ResourceCheck(srv compose.ServiceConfig) *issue.Issue { func ResourceCheck(srv *compose.ServiceConfig) *issue.Issue {
i := &issue.Issue{} i := &issue.Issue{}
if srv.Cpus == nil { if srv.Cpus == nil {
i.Warning() i.Warning()
i.Messages = append(i.Messages, "there is no cpu limit set for the service") 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 { if srv.MemLimit == nil {
i.Warning() i.Warning()
i.Messages = append(i.Messages, "there is no memory limit set for the service") 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 { if srv.Cpus == nil && srv.MemLimit == nil {
i.Fatal() i.Fatal()
i.Messages = append(i.Messages, "there are no resource limits set for the service") i.Messages = append(i.Messages, "there are no resource limits set for the service, its the wild west")
}
if len(i.Messages) > 0 {
return i
} }
i.Passed() i.Passed()
i.Messages = append(i.Messages, "resource limits configuration is safe") i.Messages = append(i.Messages, "resource limits are configured properly")
return i return i
} }
// make sure service is not ran as privileged // make sure service is not ran as privileged
func PrivilegedCheck(srv compose.ServiceConfig) *issue.Issue { func PrivilegedCheck(srv *compose.ServiceConfig) *issue.Issue {
i := &issue.Issue{} i := &issue.Issue{}
if srv.Privileged == nil { if srv.Privileged == nil {
i.Passed() i.Passed()
@@ -148,7 +160,7 @@ func PrivilegedCheck(srv compose.ServiceConfig) *issue.Issue {
if *srv.Privileged == true { if *srv.Privileged == true {
i.Fatal() i.Fatal()
i.Messages = append(i.Messages, "privileged flag is set to true") i.Messages = append(i.Messages, "privileged flag is set to true, remove it to prevent the service container from running with elevated privileges")
return i return i
} }
@@ -158,29 +170,29 @@ func PrivilegedCheck(srv compose.ServiceConfig) *issue.Issue {
} }
// checks for specific security options // checks for specific security options
func SecurityOptCheck(srv compose.ServiceConfig) *issue.Issue { func SecurityOptCheck(srv *compose.ServiceConfig) *issue.Issue {
i := &issue.Issue{} i := &issue.Issue{}
if srv.SecOpts == nil { if srv.SecOpts == nil {
i.Fatal() i.Fatal()
i.Messages = append(i.Messages, "set no-new-privileges=true on the service security_opts") 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 return i
} }
for _, opt := range *srv.SecOpts { for _, opt := range *srv.SecOpts {
if strings.EqualFold(opt, PRIVILEGE_OPT) { if strings.EqualFold(opt, PRIVILEGE_OPT) {
i.Passed() i.Passed()
i.Messages = append(i.Messages, "security option are safe") i.Messages = append(i.Messages, "security options are safe")
return i return i
} }
} }
i.Fatal() i.Fatal()
i.Messages = append(i.Messages, "set no-new-privileges=true on the container security_opts") 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 return i
} }
// checking port to ensure it is behind a reverse proxy // checking port to ensure it is behind a reverse proxy
func PortCheck(srv compose.ServiceConfig) ([]*issue.Issue, error) { func PortCheck(srv *compose.ServiceConfig) ([]*issue.Issue, error) {
il := []*issue.Issue{} il := []*issue.Issue{}
if srv.Ports == nil { if srv.Ports == nil {
i := &issue.Issue{} i := &issue.Issue{}
@@ -204,7 +216,7 @@ func PortCheck(srv compose.ServiceConfig) ([]*issue.Issue, error) {
continue continue
} }
ms, err := regexp.Match(`^\${{\s*(vars|secrets)\.[[:alnum:]]+\s*}}{1}:\${{\s*(vars|secrets)\.[[:alnum:]]+\s*}}{1}$`, []byte(prt)) ms, err := regexp.Match(ENV_PORT_PORT, []byte(prt))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -216,7 +228,7 @@ func PortCheck(srv compose.ServiceConfig) ([]*issue.Issue, error) {
continue continue
} }
m2, err := regexp.Match(IM_SO_SORRY, []byte(prt)) m2, err := regexp.Match(IP_PORT_PORT_REGEX, []byte(prt))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -238,7 +250,7 @@ func PortCheck(srv compose.ServiceConfig) ([]*issue.Issue, error) {
} }
// ensures the user value is set and nonroot in the service // ensures the user value is set and nonroot in the service
func UserCheck(srv compose.ServiceConfig) (*issue.Issue, error) { func UserCheck(srv *compose.ServiceConfig) (*issue.Issue, error) {
i := &issue.Issue{} i := &issue.Issue{}
if srv.User == nil { if srv.User == nil {
i.Warning() i.Warning()
@@ -311,7 +323,7 @@ func UserCheck(srv compose.ServiceConfig) (*issue.Issue, error) {
} }
// ensures a restart policy is set in the service configuration with at least max_attempts and restart condition set // 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 { func PolicyCheck(srv *compose.ServiceConfig) *issue.Issue {
i := &issue.Issue{} i := &issue.Issue{}
if srv.Deploy != nil { if srv.Deploy != nil {
if srv.Deploy.Policy != nil { if srv.Deploy.Policy != nil {