Files
compose-parser/main.go
2025-11-09 16:12:11 -05:00

234 lines
5.7 KiB
Go

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, "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"))
}
}