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
2025-11-11 13:33:14 -05:00
"code.jakeyoungdev.com/go/compose-parser/compose"
"code.jakeyoungdev.com/go/compose-parser/issue"
2025-11-09 11:06:04 -05:00
"gopkg.in/yaml.v3"
)
2025-11-11 13:33:14 -05:00
//shoutouts and/or blame should go to: https://regex101.com/ it helped me construct some unholy regex
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 13:33:14 -05:00
//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
2025-11-11 23:44:38 -05:00
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}$ `
2025-11-09 16:10:01 -05:00
)
2025-11-09 11:06:04 -05:00
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
}
2025-11-11 13:33:14 -05:00
var data compose . Compose
2025-11-09 11:06:04 -05:00
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 13:33:14 -05:00
type report struct {
2025-11-10 16:37:49 -05:00
Name string
2025-11-11 13:33:14 -05:00
Issues [ ] * issue . Issue
2025-11-10 16:37:49 -05:00
}
2025-11-11 13:33:14 -05:00
var issues [ ] report
2025-11-09 11:06:04 -05:00
for name , serviceConf := range data . Services {
2025-11-11 13:33:14 -05:00
rprt := report {
2025-11-09 11:06:04 -05:00
Name : name ,
}
2025-11-11 12:58:06 -05:00
2025-11-11 23:44:38 -05:00
i := ResourceCheck ( & serviceConf )
2025-11-10 16:37:49 -05:00
if i != nil {
rprt . Issues = append ( rprt . Issues , i )
}
2025-11-09 11:06:04 -05:00
2025-11-11 23:44:38 -05:00
j := SecurityOptCheck ( & serviceConf )
2025-11-10 16:37:49 -05:00
if j != nil {
rprt . Issues = append ( rprt . Issues , j )
2025-11-09 11:06:04 -05:00
}
2025-11-11 23:44:38 -05:00
k , err := UserCheck ( & serviceConf )
2025-11-10 16:37:49 -05:00
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 23:44:38 -05:00
l , err := PortCheck ( & serviceConf )
2025-11-11 12:58:06 -05:00
if err != nil {
log . Fatal ( err )
}
if len ( l ) > 0 {
rprt . Issues = append ( rprt . Issues , l ... )
}
2025-11-11 23:44:38 -05:00
m := PolicyCheck ( & serviceConf )
2025-11-11 12:58:06 -05:00
if m != nil {
rprt . Issues = append ( rprt . Issues , m )
}
2025-11-11 23:44:38 -05:00
n := PrivilegedCheck ( & serviceConf )
2025-11-11 13:33:14 -05:00
if n != nil {
rprt . Issues = append ( rprt . Issues , n )
}
2025-11-10 16:37:49 -05:00
issues = append ( issues , rprt )
}
2025-11-11 16:27:26 -05:00
lvl := os . Getenv ( "LOG_LEVEL" )
2025-11-11 18:04:21 -05:00
fatalCount := 0
2025-11-10 16:37:49 -05:00
for _ , p := range issues {
2025-11-11 16:27:26 -05:00
fmt . Println ( )
fmt . Println ( "----------------------------------------------------------------------------" )
2025-11-10 16:37:49 -05:00
fmt . Println ( p . Name )
for _ , x := range p . Issues {
2025-11-11 18:04:21 -05:00
if x . Level == issue . FATAL {
fatalCount ++
}
2025-11-11 16:27:26 -05:00
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" ) )
} else if lvl == "fatal" {
if x . Level == issue . FATAL {
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" ) )
}
}
2025-11-09 11:06:04 -05:00
}
2025-11-10 16:37:49 -05:00
}
2025-11-11 18:04:21 -05:00
2025-11-11 18:07:33 -05:00
if strings . EqualFold ( os . Getenv ( "FAIL_ON_FATAL" ) , "yes" ) && fatalCount > 0 {
os . Exit ( 1 )
2025-11-11 18:04:21 -05:00
}
2025-11-10 16:37:49 -05:00
}
2025-11-09 11:06:04 -05:00
2025-11-11 13:33:14 -05:00
// ensure cpus and mem_limit are set on the service
2025-11-11 23:44:38 -05:00
func ResourceCheck ( srv * compose . ServiceConfig ) * issue . Issue {
2025-11-11 13:33:14 -05:00
i := & issue . Issue { }
2025-11-10 16:37:49 -05:00
if srv . Cpus == nil {
i . Warning ( )
2025-11-11 23:44:38 -05:00
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" )
2025-11-10 16:37:49 -05:00
}
if srv . MemLimit == nil {
i . Warning ( )
2025-11-11 23:44:38 -05:00
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" )
2025-11-10 16:37:49 -05:00
}
if srv . Cpus == nil && srv . MemLimit == nil {
i . Fatal ( )
2025-11-11 23:44:38 -05:00
i . Messages = append ( i . Messages , "there are no resource limits set for the service, its the wild west" )
2025-11-10 16:37:49 -05:00
}
2025-11-11 17:37:51 -05:00
if len ( i . Messages ) > 0 {
return i
}
2025-11-10 16:37:49 -05:00
i . Passed ( )
2025-11-11 23:44:38 -05:00
i . Messages = append ( i . Messages , "resource limits are configured properly" )
2025-11-11 12:58:06 -05:00
return i
}
2025-11-11 13:33:14 -05:00
// make sure service is not ran as privileged
2025-11-11 23:44:38 -05:00
func PrivilegedCheck ( srv * compose . ServiceConfig ) * issue . Issue {
2025-11-11 13:33:14 -05:00
i := & issue . Issue { }
2025-11-11 12:58:06 -05:00
if srv . Privileged == nil {
i . Passed ( )
i . Messages = append ( i . Messages , "no privileged flag set" )
return i
}
if * srv . Privileged == true {
i . Fatal ( )
2025-11-11 23:44:38 -05:00
i . Messages = append ( i . Messages , "privileged flag is set to true, remove it to prevent the service container from running with elevated privileges" )
2025-11-11 12:58:06 -05:00
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
}
2025-11-11 13:33:14 -05:00
// checks for specific security options
2025-11-11 23:44:38 -05:00
func SecurityOptCheck ( srv * compose . ServiceConfig ) * issue . Issue {
2025-11-11 13:33:14 -05:00
i := & issue . Issue { }
2025-11-10 16:37:49 -05:00
if srv . SecOpts == nil {
i . Fatal ( )
2025-11-11 23:44:38 -05:00
i . Messages = append ( i . Messages , "set no-new-privileges=true on the service security_opts to disable container processes from gaining new privileges" )
2025-11-10 16:37:49 -05:00
return i
}
for _ , opt := range * srv . SecOpts {
if strings . EqualFold ( opt , PRIVILEGE_OPT ) {
i . Passed ( )
2025-11-11 17:45:34 -05:00
i . Messages = append ( i . Messages , "security options 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 ( )
2025-11-11 23:44:38 -05:00
i . Messages = append ( i . Messages , "set no-new-privileges=true on the container security_opts to disable container processes from gaining new privileges" )
2025-11-10 16:37:49 -05:00
return i
}
2025-11-11 13:33:14 -05:00
// checking port to ensure it is behind a reverse proxy
2025-11-11 23:44:38 -05:00
func PortCheck ( srv * compose . ServiceConfig ) ( [ ] * issue . Issue , error ) {
2025-11-11 13:33:14 -05:00
il := [ ] * issue . Issue { }
2025-11-11 12:58:06 -05:00
if srv . Ports == nil {
2025-11-11 13:33:14 -05:00
i := & issue . Issue { }
2025-11-11 12:58:06 -05:00
i . Passed ( )
i . Messages = append ( i . Messages , "no ports exposed" )
il = append ( il , i )
return il , nil
}
for _ , prt := range * srv . Ports {
2025-11-11 13:33:14 -05:00
i := & issue . Issue { }
2025-11-11 12:58:06 -05:00
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
}
2025-11-11 23:44:38 -05:00
ms , err := regexp . Match ( ENV_PORT_PORT , [ ] byte ( prt ) )
2025-11-11 13:33:14 -05:00
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
}
2025-11-11 23:44:38 -05:00
m2 , err := regexp . Match ( IP_PORT_PORT_REGEX , [ ] byte ( prt ) )
2025-11-11 12:58:06 -05:00
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-11 13:33:14 -05:00
// ensures the user value is set and nonroot in the service
2025-11-11 23:44:38 -05:00
func UserCheck ( srv * compose . ServiceConfig ) ( * issue . Issue , error ) {
2025-11-11 13:33:14 -05:00
i := & issue . Issue { }
2025-11-10 16:37:49 -05:00
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
}
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 13:33:14 -05:00
// ensures a restart policy is set in the service configuration with at least max_attempts and restart condition set
2025-11-11 23:44:38 -05:00
func PolicyCheck ( srv * compose . ServiceConfig ) * issue . Issue {
2025-11-11 13:33:14 -05:00
i := & issue . Issue { }
2025-11-11 12:58:06 -05:00
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
}