699 lines
17 KiB
Go
699 lines
17 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
compose_test.go
|
|
|
|
Integration tests for ComposeAnalyzer against all testdata compose
|
|
fixtures
|
|
|
|
Each function targets a specific bad-*.yml or good-*.yml fixture and
|
|
asserts expected findings at correct severities. Covers Docker socket
|
|
mounts, privileged mode, dangerous capabilities, sensitive filesystem
|
|
mounts, hardcoded secrets, missing resource limits, and a clean
|
|
production-grade configuration.
|
|
|
|
Tests:
|
|
TestComposeAnalyzer_BadDockerSocket - socket, caps, secrets, host
|
|
network
|
|
TestComposeAnalyzer_BadPrivileged - privileged mode, pid/ipc host,
|
|
sensitive mounts
|
|
TestComposeAnalyzer_BadCaps - critical and high capabilities all
|
|
detected
|
|
TestComposeAnalyzer_BadMounts - runtime sockets, /etc, /proc, /sys, /dev
|
|
TestComposeAnalyzer_BadSecrets - AWS, database, and API keys in
|
|
environment
|
|
TestComposeAnalyzer_BadNoLimits - missing memory, CPU, and PIDs limits
|
|
TestComposeAnalyzer_GoodProduction - clean file produces no CRITICAL
|
|
findings
|
|
TestComposeAnalyzer_AllFiles - table-driven coverage across all fixtures
|
|
|
|
Connects to:
|
|
analyzer/compose.go - ComposeAnalyzer under test
|
|
finding.go - asserts on Severity, RuleID, and Collection filtering
|
|
*/
|
|
|
|
package integration_test
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/CarterPerez-dev/docksec/internal/analyzer"
|
|
"github.com/CarterPerez-dev/docksec/internal/finding"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestComposeAnalyzer_BadDockerSocket(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join(
|
|
"..",
|
|
"testdata",
|
|
"compose",
|
|
"bad-docker-socket.yml",
|
|
)
|
|
|
|
a := analyzer.NewComposeAnalyzer(path)
|
|
findings, err := a.Analyze(ctx)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("detects privileged mode", func(t *testing.T) {
|
|
hasPrivileged := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.4" {
|
|
hasPrivileged = true
|
|
assert.Equal(t, finding.SeverityCritical, f.Severity)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasPrivileged, "Should detect privileged: true")
|
|
})
|
|
|
|
t.Run("detects docker socket mount", func(t *testing.T) {
|
|
hasDockerSocket := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.31" {
|
|
hasDockerSocket = true
|
|
assert.Equal(t, finding.SeverityCritical, f.Severity)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasDockerSocket, "Should detect Docker socket mount")
|
|
})
|
|
|
|
t.Run("detects dangerous capabilities", func(t *testing.T) {
|
|
dangerousCaps := []string{"SYS_ADMIN", "NET_ADMIN", "SYS_PTRACE"}
|
|
for _, capName := range dangerousCaps {
|
|
found := false
|
|
for _, f := range findings {
|
|
if containsIgnoreCase(f.Title, capName) {
|
|
found = true
|
|
assert.GreaterOrEqual(t, f.Severity, finding.SeverityHigh,
|
|
"Capability %s should be HIGH or CRITICAL", capName)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect capability %s", capName)
|
|
}
|
|
})
|
|
|
|
t.Run("detects host network mode", func(t *testing.T) {
|
|
hasHostNet := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.9" {
|
|
hasHostNet = true
|
|
assert.Equal(t, finding.SeverityHigh, f.Severity)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasHostNet, "Should detect network_mode: host")
|
|
})
|
|
|
|
t.Run("detects hardcoded secrets", func(t *testing.T) {
|
|
hasSecrets := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-4.10" &&
|
|
containsIgnoreCase(f.Description, "secret") {
|
|
hasSecrets = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(
|
|
t,
|
|
hasSecrets,
|
|
"Should detect hardcoded secrets in environment",
|
|
)
|
|
})
|
|
|
|
t.Run("detects sensitive path mounts", func(t *testing.T) {
|
|
sensitivePaths := []string{"/etc/passwd", "/root/.ssh"}
|
|
for _, path := range sensitivePaths {
|
|
found := false
|
|
for _, f := range findings {
|
|
if containsIgnoreCase(f.Title, path) ||
|
|
containsIgnoreCase(f.Description, path) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect mount of %s", path)
|
|
}
|
|
})
|
|
|
|
t.Run("has critical findings", func(t *testing.T) {
|
|
assert.True(
|
|
t,
|
|
findings.HasSeverityAtOrAbove(finding.SeverityCritical),
|
|
"Should have CRITICAL severity findings",
|
|
)
|
|
})
|
|
}
|
|
|
|
func TestComposeAnalyzer_BadPrivileged(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join(
|
|
"..",
|
|
"testdata",
|
|
"compose",
|
|
"bad-privileged.yml",
|
|
)
|
|
|
|
a := analyzer.NewComposeAnalyzer(path)
|
|
findings, err := a.Analyze(ctx)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("detects privileged container", func(t *testing.T) {
|
|
hasPrivileged := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.4" {
|
|
hasPrivileged = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasPrivileged, "Should detect privileged: true")
|
|
})
|
|
|
|
t.Run("detects pid host mode", func(t *testing.T) {
|
|
hasPidHost := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.15" {
|
|
hasPidHost = true
|
|
assert.Equal(t, finding.SeverityHigh, f.Severity)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasPidHost, "Should detect pid: host")
|
|
})
|
|
|
|
t.Run("detects ipc host mode", func(t *testing.T) {
|
|
hasIpcHost := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.16" {
|
|
hasIpcHost = true
|
|
assert.Equal(t, finding.SeverityHigh, f.Severity)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasIpcHost, "Should detect ipc: host")
|
|
})
|
|
|
|
t.Run("detects sensitive filesystem mounts", func(t *testing.T) {
|
|
hasSensitiveMounts := 0
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.5" {
|
|
hasSensitiveMounts++
|
|
}
|
|
}
|
|
assert.GreaterOrEqual(t, hasSensitiveMounts, 2,
|
|
"Should detect multiple sensitive filesystem mounts")
|
|
})
|
|
}
|
|
|
|
func TestComposeAnalyzer_BadCaps(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join("..", "testdata", "compose", "bad-caps.yml")
|
|
|
|
a := analyzer.NewComposeAnalyzer(path)
|
|
findings, err := a.Analyze(ctx)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("detects critical capabilities", func(t *testing.T) {
|
|
criticalCaps := []string{
|
|
"SYS_MODULE",
|
|
"SYS_RAWIO",
|
|
"SYS_PTRACE",
|
|
"SYS_ADMIN",
|
|
"MAC_ADMIN",
|
|
"BPF",
|
|
}
|
|
|
|
for _, capName := range criticalCaps {
|
|
found := false
|
|
for _, f := range findings {
|
|
if containsIgnoreCase(f.Title, capName) {
|
|
found = true
|
|
assert.Equal(t, finding.SeverityCritical, f.Severity,
|
|
"Capability %s should be CRITICAL", capName)
|
|
break
|
|
}
|
|
}
|
|
assert.True(
|
|
t,
|
|
found,
|
|
"Should detect critical capability %s",
|
|
capName,
|
|
)
|
|
}
|
|
})
|
|
|
|
t.Run("detects high severity capabilities", func(t *testing.T) {
|
|
highCaps := []string{"DAC_OVERRIDE", "NET_ADMIN"}
|
|
|
|
for _, capName := range highCaps {
|
|
found := false
|
|
for _, f := range findings {
|
|
if containsIgnoreCase(f.Title, capName) {
|
|
found = true
|
|
assert.GreaterOrEqual(t, f.Severity, finding.SeverityHigh,
|
|
"Capability %s should be HIGH or CRITICAL", capName)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect capability %s", capName)
|
|
}
|
|
})
|
|
|
|
t.Run("has multiple critical findings", func(t *testing.T) {
|
|
criticalCount := 0
|
|
for _, f := range findings {
|
|
if f.Severity == finding.SeverityCritical {
|
|
criticalCount++
|
|
}
|
|
}
|
|
assert.GreaterOrEqual(
|
|
t,
|
|
criticalCount,
|
|
5,
|
|
"Should have at least 5 CRITICAL findings for dangerous capabilities",
|
|
)
|
|
})
|
|
}
|
|
|
|
func TestComposeAnalyzer_BadMounts(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join("..", "testdata", "compose", "bad-mounts.yml")
|
|
|
|
a := analyzer.NewComposeAnalyzer(path)
|
|
findings, err := a.Analyze(ctx)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("detects container runtime sockets", func(t *testing.T) {
|
|
hasDockerSocket := false
|
|
hasContainerdSocket := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.31" {
|
|
if containsIgnoreCase(f.Title, "docker.sock") ||
|
|
containsIgnoreCase(f.Description, "docker.sock") {
|
|
hasDockerSocket = true
|
|
assert.Equal(t, finding.SeverityCritical, f.Severity)
|
|
}
|
|
}
|
|
if containsIgnoreCase(f.Title, "containerd") ||
|
|
containsIgnoreCase(f.Description, "containerd") {
|
|
hasContainerdSocket = true
|
|
}
|
|
}
|
|
assert.True(t, hasDockerSocket || hasContainerdSocket,
|
|
"Should detect at least one container runtime socket mount")
|
|
})
|
|
|
|
t.Run("detects system config directories", func(t *testing.T) {
|
|
paths := []string{"/etc", "/etc/passwd", "/etc/shadow"}
|
|
for _, path := range paths {
|
|
found := false
|
|
for _, f := range findings {
|
|
if containsIgnoreCase(f.Title, path) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect %s mount", path)
|
|
}
|
|
})
|
|
|
|
t.Run("detects kubernetes directories", func(t *testing.T) {
|
|
paths := []string{"/etc/kubernetes", "/var/lib/kubelet"}
|
|
for _, path := range paths {
|
|
found := false
|
|
for _, f := range findings {
|
|
if containsIgnoreCase(f.Title, path) {
|
|
found = true
|
|
assert.Equal(t, finding.SeverityCritical, f.Severity)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect %s mount", path)
|
|
}
|
|
})
|
|
|
|
t.Run("detects device mounts", func(t *testing.T) {
|
|
found := false
|
|
for _, f := range findings {
|
|
if containsIgnoreCase(f.Title, "/dev") {
|
|
found = true
|
|
assert.Equal(t, finding.SeverityCritical, f.Severity)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect /dev mount")
|
|
})
|
|
|
|
t.Run("detects proc and sys mounts", func(t *testing.T) {
|
|
paths := []string{"/proc", "/sys"}
|
|
for _, path := range paths {
|
|
found := false
|
|
for _, f := range findings {
|
|
if containsIgnoreCase(f.Title, path) {
|
|
found = true
|
|
assert.Equal(t, finding.SeverityCritical, f.Severity)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect %s mount", path)
|
|
}
|
|
})
|
|
|
|
t.Run("has many critical findings", func(t *testing.T) {
|
|
criticalCount := 0
|
|
for _, f := range findings {
|
|
if f.Severity == finding.SeverityCritical {
|
|
criticalCount++
|
|
}
|
|
}
|
|
assert.GreaterOrEqual(t, criticalCount, 10,
|
|
"Should have many CRITICAL findings for sensitive mounts")
|
|
})
|
|
}
|
|
|
|
func TestComposeAnalyzer_BadSecrets(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join(
|
|
"..",
|
|
"testdata",
|
|
"compose",
|
|
"bad-secrets.yml",
|
|
)
|
|
|
|
a := analyzer.NewComposeAnalyzer(path)
|
|
findings, err := a.Analyze(ctx)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("detects hardcoded AWS credentials", func(t *testing.T) {
|
|
found := false
|
|
for _, f := range findings {
|
|
if containsIgnoreCase(f.Title, "AWS") ||
|
|
containsIgnoreCase(f.Description, "AWS") {
|
|
found = true
|
|
assert.GreaterOrEqual(t, f.Severity, finding.SeverityHigh)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect AWS credentials")
|
|
})
|
|
|
|
t.Run("detects database passwords", func(t *testing.T) {
|
|
dbTypes := []string{
|
|
"DATABASE_URL",
|
|
"MONGODB_URI",
|
|
"POSTGRES_PASSWORD",
|
|
}
|
|
for _, dbType := range dbTypes {
|
|
found := false
|
|
for _, f := range findings {
|
|
if containsIgnoreCase(f.Title, dbType) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect %s", dbType)
|
|
}
|
|
})
|
|
|
|
t.Run("detects API keys", func(t *testing.T) {
|
|
apis := []string{"STRIPE", "GITHUB", "OPENAI"}
|
|
for _, api := range apis {
|
|
found := false
|
|
for _, f := range findings {
|
|
if containsIgnoreCase(f.Title, api) ||
|
|
containsIgnoreCase(f.Description, api) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect %s API key", api)
|
|
}
|
|
})
|
|
|
|
t.Run("has many high severity findings", func(t *testing.T) {
|
|
highCount := 0
|
|
for _, f := range findings {
|
|
if f.Severity >= finding.SeverityHigh {
|
|
highCount++
|
|
}
|
|
}
|
|
assert.GreaterOrEqual(t, highCount, 10,
|
|
"Should have many HIGH severity findings for secrets")
|
|
})
|
|
}
|
|
|
|
func TestComposeAnalyzer_BadNoLimits(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join(
|
|
"..",
|
|
"testdata",
|
|
"compose",
|
|
"bad-no-limits.yml",
|
|
)
|
|
|
|
a := analyzer.NewComposeAnalyzer(path)
|
|
findings, err := a.Analyze(ctx)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("detects missing memory limit", func(t *testing.T) {
|
|
found := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.10" {
|
|
found = true
|
|
assert.Equal(t, finding.SeverityMedium, f.Severity)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect missing memory limit")
|
|
})
|
|
|
|
t.Run("detects missing CPU limit", func(t *testing.T) {
|
|
found := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.11" {
|
|
found = true
|
|
assert.Equal(t, finding.SeverityMedium, f.Severity)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect missing CPU limit")
|
|
})
|
|
|
|
t.Run("detects missing PIDs limit", func(t *testing.T) {
|
|
found := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.28" {
|
|
found = true
|
|
assert.Equal(t, finding.SeverityMedium, f.Severity)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect missing PIDs limit")
|
|
})
|
|
|
|
t.Run("detects missing user", func(t *testing.T) {
|
|
found := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-4.1" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect missing user specification")
|
|
})
|
|
|
|
t.Run("detects no read-only filesystem", func(t *testing.T) {
|
|
found := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.12" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Should detect missing read_only: true")
|
|
})
|
|
}
|
|
|
|
func TestComposeAnalyzer_GoodProduction(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join(
|
|
"..",
|
|
"testdata",
|
|
"compose",
|
|
"good-production.yml",
|
|
)
|
|
|
|
a := analyzer.NewComposeAnalyzer(path)
|
|
findings, err := a.Analyze(ctx)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("has no critical findings", func(t *testing.T) {
|
|
assert.False(
|
|
t,
|
|
findings.HasSeverityAtOrAbove(finding.SeverityCritical),
|
|
"Production compose should have no CRITICAL findings",
|
|
)
|
|
})
|
|
|
|
t.Run("has minimal high findings", func(t *testing.T) {
|
|
highCount := 0
|
|
for _, f := range findings {
|
|
if f.Severity >= finding.SeverityHigh {
|
|
highCount++
|
|
}
|
|
}
|
|
assert.LessOrEqual(t, highCount, 2,
|
|
"Production compose should have minimal HIGH findings")
|
|
})
|
|
|
|
t.Run("no privileged containers", func(t *testing.T) {
|
|
hasPrivileged := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.4" {
|
|
hasPrivileged = true
|
|
}
|
|
}
|
|
assert.False(t, hasPrivileged, "Should NOT have privileged finding")
|
|
})
|
|
|
|
t.Run("no docker socket mounts", func(t *testing.T) {
|
|
hasSocket := false
|
|
for _, f := range findings {
|
|
if f.RuleID == "CIS-5.31" {
|
|
hasSocket = true
|
|
}
|
|
}
|
|
assert.False(t, hasSocket, "Should NOT have docker socket mount")
|
|
})
|
|
|
|
t.Run("no hardcoded secrets", func(t *testing.T) {
|
|
secretsCount := 0
|
|
for _, f := range findings {
|
|
if containsIgnoreCase(f.Title, "secret") &&
|
|
f.Severity >= finding.SeverityHigh {
|
|
secretsCount++
|
|
}
|
|
}
|
|
assert.Equal(t, 0, secretsCount, "Should have no hardcoded secrets")
|
|
})
|
|
}
|
|
|
|
func TestComposeAnalyzer_AllFiles(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
file string
|
|
wantCritical bool
|
|
wantHigh bool
|
|
minFindings int
|
|
specificFindings []string
|
|
}{
|
|
{
|
|
name: "bad-docker-socket.yml",
|
|
file: "bad-docker-socket.yml",
|
|
wantCritical: true,
|
|
wantHigh: true,
|
|
minFindings: 8,
|
|
specificFindings: []string{
|
|
"CIS-5.4",
|
|
"CIS-5.31",
|
|
"CIS-5.9",
|
|
},
|
|
},
|
|
{
|
|
name: "bad-privileged.yml",
|
|
file: "bad-privileged.yml",
|
|
wantCritical: true,
|
|
wantHigh: true,
|
|
minFindings: 5,
|
|
specificFindings: []string{
|
|
"CIS-5.4",
|
|
"CIS-5.15",
|
|
"CIS-5.16",
|
|
},
|
|
},
|
|
{
|
|
name: "bad-caps.yml",
|
|
file: "bad-caps.yml",
|
|
wantCritical: true,
|
|
wantHigh: true,
|
|
minFindings: 6,
|
|
specificFindings: []string{
|
|
"CIS-5.3",
|
|
},
|
|
},
|
|
{
|
|
name: "bad-secrets.yml",
|
|
file: "bad-secrets.yml",
|
|
wantCritical: false,
|
|
wantHigh: true,
|
|
minFindings: 10,
|
|
specificFindings: []string{
|
|
"CIS-4.10",
|
|
},
|
|
},
|
|
{
|
|
name: "good-production.yml",
|
|
file: "good-production.yml",
|
|
wantCritical: false,
|
|
wantHigh: false,
|
|
minFindings: 0,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join("..", "testdata", "compose", tc.file)
|
|
|
|
a := analyzer.NewComposeAnalyzer(path)
|
|
findings, err := a.Analyze(ctx)
|
|
require.NoError(t, err, "Analyze should not return error")
|
|
|
|
if tc.wantCritical {
|
|
assert.True(
|
|
t,
|
|
findings.HasSeverityAtOrAbove(finding.SeverityCritical),
|
|
"Should have CRITICAL findings",
|
|
)
|
|
} else {
|
|
assert.False(
|
|
t,
|
|
findings.HasSeverityAtOrAbove(finding.SeverityCritical),
|
|
"Should NOT have CRITICAL findings",
|
|
)
|
|
}
|
|
|
|
if tc.wantHigh {
|
|
assert.True(
|
|
t,
|
|
findings.HasSeverityAtOrAbove(finding.SeverityHigh),
|
|
"Should have HIGH findings",
|
|
)
|
|
}
|
|
|
|
assert.GreaterOrEqual(t, len(findings), tc.minFindings,
|
|
"Should have at least %d findings", tc.minFindings)
|
|
|
|
for _, ruleID := range tc.specificFindings {
|
|
found := false
|
|
for _, f := range findings {
|
|
if f.RuleID == ruleID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(
|
|
t,
|
|
found,
|
|
"Should have finding with RuleID %s",
|
|
ruleID,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|