2025-11-09 11:06:04 -05:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2025-11-09 16:10:01 -05:00
|
|
|
"log"
|
2025-11-09 11:06:04 -05:00
|
|
|
"os"
|
|
|
|
|
"regexp"
|
2025-11-09 16:10:01 -05:00
|
|
|
"strings"
|
2025-11-09 11:06:04 -05:00
|
|
|
|
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
|
)
|
|
|
|
|
|
2025-11-09 16:10:01 -05:00
|
|
|
const (
|
2025-11-10 16:37:49 -05:00
|
|
|
PRIVILEGE_OPT = "no-new-privileges=true"
|
|
|
|
|
ROOT_USER_GROUP = "1000:1000"
|
|
|
|
|
ROOT_USER = "1000"
|
|
|
|
|
ROOT_GROUP = "1000"
|
2025-11-11 12:58:06 -05:00
|
|
|
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]+)`
|
2025-11-09 16:10:01 -05:00
|
|
|
|
|
|
|
|
FATAL IssueLevel = "FATAL"
|
|
|
|
|
WARNING IssueLevel = "WARNING"
|
|
|
|
|
PASSED IssueLevel = "PASSED"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type IssueLevel string
|
|
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-09 11:06:04 -05:00
|
|
|
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 {
|
2025-11-11 12:58:06 -05:00
|
|
|
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"`
|
2025-11-09 11:06:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Compose struct {
|
|
|
|
|
Services map[string]ServiceConfig `yaml:"services"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
filePath := os.Getenv("COMPOSE_FILE_PATH")
|
|
|
|
|
if filePath == "" {
|
2025-11-10 16:37:49 -05:00
|
|
|
filePath = "compose.yaml"
|
2025-11-09 11:06:04 -05:00
|
|
|
}
|
|
|
|
|
f, err := os.Open(filePath)
|
|
|
|
|
if err != nil {
|
2025-11-09 16:10:01 -05:00
|
|
|
log.Fatal(err)
|
2025-11-09 11:06:04 -05:00
|
|
|
}
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
|
|
fd, err := io.ReadAll(f)
|
|
|
|
|
if err != nil {
|
2025-11-09 16:10:01 -05:00
|
|
|
log.Fatal(err)
|
2025-11-09 11:06:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var data Compose
|
|
|
|
|
err = yaml.Unmarshal(fd, &data)
|
|
|
|
|
if err != nil {
|
2025-11-09 16:10:01 -05:00
|
|
|
log.Fatal(err)
|
2025-11-09 11:06:04 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-11 12:58:06 -05:00
|
|
|
// 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
|
|
|
|
|
// }
|
2025-11-09 11:06:04 -05:00
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
type NewReport struct {
|
|
|
|
|
Name string
|
|
|
|
|
Issues []*Issue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var issues []NewReport
|
2025-11-09 11:06:04 -05:00
|
|
|
for name, serviceConf := range data.Services {
|
2025-11-10 16:37:49 -05:00
|
|
|
rprt := NewReport{
|
2025-11-09 11:06:04 -05:00
|
|
|
Name: name,
|
|
|
|
|
}
|
2025-11-11 12:58:06 -05:00
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
i := ResourceCheck(serviceConf)
|
|
|
|
|
if i != nil {
|
|
|
|
|
rprt.Issues = append(rprt.Issues, i)
|
|
|
|
|
}
|
2025-11-09 11:06:04 -05:00
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
j := SecurityOptCheck(serviceConf)
|
|
|
|
|
if j != nil {
|
|
|
|
|
rprt.Issues = append(rprt.Issues, j)
|
2025-11-09 11:06:04 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
k, err := UserCheck(serviceConf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
2025-11-09 11:06:04 -05:00
|
|
|
}
|
2025-11-10 16:37:49 -05:00
|
|
|
if k != nil {
|
|
|
|
|
rprt.Issues = append(rprt.Issues, k)
|
2025-11-09 16:10:01 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-11 12:58:06 -05:00
|
|
|
l, err := PortCheck(serviceConf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(l) > 0 {
|
|
|
|
|
rprt.Issues = append(rprt.Issues, l...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m := PolicyCheck(serviceConf)
|
|
|
|
|
if m != nil {
|
|
|
|
|
rprt.Issues = append(rprt.Issues, m)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
issues = append(issues, rprt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, p := range issues {
|
|
|
|
|
fmt.Println(p.Name)
|
|
|
|
|
for _, x := range p.Issues {
|
|
|
|
|
fmt.Println(*x)
|
2025-11-09 11:06:04 -05:00
|
|
|
}
|
2025-11-10 16:37:49 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-09 11:06:04 -05:00
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
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()
|
2025-11-11 12:58:06 -05:00
|
|
|
i.Messages = append(i.Messages, "resource limits configuration is safe")
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func PrivilegedCheck(srv ServiceConfig) *Issue {
|
|
|
|
|
i := &Issue{}
|
|
|
|
|
if srv.Privileged == nil {
|
|
|
|
|
i.Passed()
|
|
|
|
|
i.Messages = append(i.Messages, "no privileged flag set")
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if *srv.Privileged == true {
|
|
|
|
|
i.Fatal()
|
|
|
|
|
i.Messages = append(i.Messages, "privileged flag is set to true")
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i.Passed()
|
|
|
|
|
i.Messages = append(i.Messages, "privileged flag is set to false")
|
2025-11-10 16:37:49 -05:00
|
|
|
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()
|
2025-11-11 12:58:06 -05:00
|
|
|
i.Messages = append(i.Messages, "security option are safe")
|
2025-11-10 16:37:49 -05:00
|
|
|
return i
|
2025-11-09 16:10:01 -05:00
|
|
|
}
|
2025-11-10 16:37:49 -05:00
|
|
|
}
|
2025-11-09 16:10:01 -05:00
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
i.Fatal()
|
|
|
|
|
i.Messages = append(i.Messages, "set no-new-privileges=true on the container security_opts")
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-11 12:58:06 -05:00
|
|
|
func PortCheck(srv ServiceConfig) ([]*Issue, error) {
|
|
|
|
|
il := []*Issue{}
|
|
|
|
|
if srv.Ports == nil {
|
|
|
|
|
i := &Issue{}
|
|
|
|
|
i.Passed()
|
|
|
|
|
i.Messages = append(i.Messages, "no ports exposed")
|
|
|
|
|
il = append(il, i)
|
|
|
|
|
return il, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, prt := range *srv.Ports {
|
|
|
|
|
i := &Issue{}
|
|
|
|
|
m, err := regexp.Match(`^[0-9]*:[0-9]*`, []byte(prt))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if m {
|
|
|
|
|
i.Fatal()
|
|
|
|
|
i.Messages = append(i.Messages, fmt.Sprintf("%s is fully exposed", prt))
|
|
|
|
|
il = append(il, i)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m2, err := regexp.Match(IM_SO_SORRY, []byte(prt))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if m2 {
|
|
|
|
|
i.Passed()
|
|
|
|
|
i.Messages = append(i.Messages, fmt.Sprintf("%s is safe", prt))
|
|
|
|
|
il = append(il, i)
|
|
|
|
|
continue
|
|
|
|
|
} else {
|
|
|
|
|
i.Warning()
|
|
|
|
|
i.Messages = append(i.Messages, fmt.Sprintf("unable to parse port configuration for %s", prt))
|
|
|
|
|
il = append(il, i)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return il, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-11 12:58:06 -05:00
|
|
|
mtch, err := regexp.Match("[0-9]*:[0-9]*", []byte(*srv.User))
|
2025-11-10 16:37:49 -05:00
|
|
|
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()
|
2025-11-11 12:58:06 -05:00
|
|
|
i.Messages = append(i.Messages, "user configuration is safe")
|
2025-11-10 16:37:49 -05:00
|
|
|
}
|
|
|
|
|
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
|
2025-11-09 16:10:01 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
if leftUser && rightUser {
|
|
|
|
|
i.Passed()
|
2025-11-11 12:58:06 -05:00
|
|
|
i.Messages = append(i.Messages, "user configuration is safe")
|
2025-11-10 16:37:49 -05:00
|
|
|
return i, nil
|
2025-11-09 16:10:01 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
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()
|
2025-11-11 12:58:06 -05:00
|
|
|
i.Messages = append(i.Messages, "user configuration is safe")
|
2025-11-09 16:10:01 -05:00
|
|
|
}
|
2025-11-10 16:37:49 -05:00
|
|
|
return i, nil
|
|
|
|
|
}
|
2025-11-09 16:10:01 -05:00
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
if rightUser {
|
|
|
|
|
if strings.EqualFold(users[0], ROOT_USER) {
|
|
|
|
|
i.Warning()
|
|
|
|
|
i.Messages = append(i.Messages, "a root user is present in configuration")
|
2025-11-09 16:10:01 -05:00
|
|
|
} else {
|
2025-11-10 16:37:49 -05:00
|
|
|
i.Passed()
|
2025-11-11 12:58:06 -05:00
|
|
|
i.Messages = append(i.Messages, "user configuration is safe")
|
2025-11-09 16:10:01 -05:00
|
|
|
}
|
2025-11-10 16:37:49 -05:00
|
|
|
return i, nil
|
2025-11-09 11:06:04 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-09 16:10:01 -05:00
|
|
|
|
2025-11-10 16:37:49 -05:00
|
|
|
i.Fatal()
|
|
|
|
|
i.Messages = append(i.Messages, "unable to parse user configuration")
|
|
|
|
|
return i, nil
|
2025-11-09 11:06:04 -05:00
|
|
|
}
|
2025-11-10 16:37:49 -05:00
|
|
|
|
2025-11-11 12:58:06 -05:00
|
|
|
func PolicyCheck(srv ServiceConfig) *Issue {
|
|
|
|
|
i := &Issue{}
|
|
|
|
|
if srv.Deploy != nil {
|
|
|
|
|
if srv.Deploy.Policy != nil {
|
|
|
|
|
if srv.Deploy.Policy.Condition != nil {
|
|
|
|
|
if srv.Deploy.Policy.MaxAttempts != nil {
|
|
|
|
|
i.Passed()
|
|
|
|
|
i.Messages = append(i.Messages, "restart policy configuration is safe")
|
|
|
|
|
return i
|
|
|
|
|
} else {
|
|
|
|
|
i.Warning()
|
|
|
|
|
i.Messages = append(i.Messages, "no max_attempts value set on restart policy container could death loop")
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
i.Fatal()
|
|
|
|
|
i.Messages = append(i.Messages, "no restart condition is set on the container")
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
i.Fatal()
|
|
|
|
|
i.Messages = append(i.Messages, "no restart policy is set on the container")
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i.Fatal()
|
|
|
|
|
i.Messages = append(i.Messages, "no restart policy is set on the container")
|
|
|
|
|
return i
|
|
|
|
|
}
|