Cybersecurity-Projects/PROJECTS/intermediate/docker-security-audit/internal/analyzer/container_test.go

529 lines
13 KiB
Go

/*
© AngelaMos | 2026
container_test.go
Tests: ContainerAnalyzer detection logic against JSON container inspect
fixtures
Verifies that analyzeContainer correctly flags privileged mode, critical
and high-severity capabilities, Docker socket mounts, sensitive path
mounts, host namespace modes, missing resource limits, and writable
root filesystem. Also confirms that a secure fixture produces minimal
findings with no CRITICAL severity.
Tests:
TestContainerAnalyzer_PrivilegedContainer - all dangerous flags detected
TestContainerAnalyzer_SecureContainer - clean container produces few
findings
TestContainerAnalyzer_TargetInfo - finding target type, name, and ID
correct
TestContainerAnalyzer_CategoryAndRemediation - CIS links and remediation
present
TestContainerAnalyzer_Comparison - privileged vs secure severity
distribution
Connects to:
container.go - tests analyzeContainer() directly
finding.go - asserts on Severity constants and RuleID values
*/
package analyzer
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/CarterPerez-dev/docksec/internal/finding"
"github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func loadContainerJSON(
t *testing.T,
filename string,
) container.InspectResponse {
t.Helper()
path := filepath.Join(
"..",
"..",
"tests",
"testdata",
"containers",
filename,
)
data, err := os.ReadFile(path)
require.NoError(t, err, "Failed to read container JSON file")
var ctr container.InspectResponse
err = json.Unmarshal(data, &ctr)
require.NoError(t, err, "Failed to unmarshal container JSON")
return ctr
}
func TestContainerAnalyzer_PrivilegedContainer(t *testing.T) {
container := loadContainerJSON(t, "privileged-container.json")
analyzer := &ContainerAnalyzer{}
findings := analyzer.analyzeContainer(container)
t.Run("detects privileged mode", func(t *testing.T) {
found := false
for _, f := range findings {
if f.RuleID == "CIS-5.4" {
found = true
assert.Equal(t, finding.SeverityCritical, f.Severity)
assert.Contains(t, f.Target.Name, "dangerous-container")
break
}
}
assert.True(t, found, "Should detect privileged: true")
})
t.Run("detects critical capabilities", func(t *testing.T) {
criticalCaps := []string{"SYS_ADMIN", "SYS_PTRACE", "SYS_MODULE"}
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 capability %s", capName)
}
})
t.Run("detects high severity capabilities", func(t *testing.T) {
found := false
for _, f := range findings {
if containsIgnoreCase(f.Title, "NET_ADMIN") {
found = true
assert.GreaterOrEqual(t, f.Severity, finding.SeverityHigh)
break
}
}
assert.True(t, found, "Should detect NET_ADMIN capability")
})
t.Run("detects docker socket mount", func(t *testing.T) {
found := false
for _, f := range findings {
if f.RuleID == "CIS-5.31" {
found = true
assert.Equal(t, finding.SeverityCritical, f.Severity)
break
}
}
assert.True(t, found, "Should detect Docker socket mount")
})
t.Run("detects sensitive path mounts", func(t *testing.T) {
sensitivePaths := []string{
"/etc/passwd",
"/root/.ssh",
"/proc",
"/sys",
"/",
}
foundCount := 0
for _, path := range sensitivePaths {
for _, f := range findings {
if containsIgnoreCase(f.Title, path) &&
f.RuleID == "CIS-5.5" {
foundCount++
assert.GreaterOrEqual(t, f.Severity, finding.SeverityHigh,
"Mount %s should be HIGH or CRITICAL", path)
break
}
}
}
assert.GreaterOrEqual(t, foundCount, 3,
"Should detect multiple sensitive path mounts")
})
t.Run("detects host PID mode", func(t *testing.T) {
found := false
for _, f := range findings {
if f.RuleID == "CIS-5.15" {
found = true
assert.Equal(t, finding.SeverityHigh, f.Severity)
break
}
}
assert.True(t, found, "Should detect pid: host")
})
t.Run("detects host IPC mode", func(t *testing.T) {
found := false
for _, f := range findings {
if f.RuleID == "CIS-5.16" {
found = true
assert.Equal(t, finding.SeverityHigh, f.Severity)
break
}
}
assert.True(t, found, "Should detect ipc: host")
})
t.Run("detects host network mode", func(t *testing.T) {
found := false
for _, f := range findings {
if f.RuleID == "CIS-5.9" {
found = true
assert.Equal(t, finding.SeverityHigh, f.Severity)
break
}
}
assert.True(t, found, "Should detect network_mode: host")
})
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 no read-only root filesystem", func(t *testing.T) {
found := false
for _, f := range findings {
if f.RuleID == "CIS-5.12" {
found = true
assert.Equal(t, finding.SeverityMedium, f.Severity)
break
}
}
assert.True(t, found, "Should detect writable root filesystem")
})
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, 5,
"Should have at least 5 CRITICAL findings")
})
t.Run("has 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 at least 10 HIGH+ severity findings")
})
}
func TestContainerAnalyzer_SecureContainer(t *testing.T) {
container := loadContainerJSON(t, "secure-container.json")
analyzer := &ContainerAnalyzer{}
findings := analyzer.analyzeContainer(container)
t.Run("no privileged mode", 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 critical capabilities", func(t *testing.T) {
criticalCapCount := 0
for _, f := range findings {
if f.RuleID == "CIS-5.3" &&
f.Severity == finding.SeverityCritical {
criticalCapCount++
}
}
assert.Equal(t, 0, criticalCapCount,
"Should have no CRITICAL capability findings")
})
t.Run("no docker socket mount", 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 sensitive path mounts", func(t *testing.T) {
sensitiveMountCount := 0
for _, f := range findings {
if f.RuleID == "CIS-5.5" && f.Severity >= finding.SeverityHigh {
sensitiveMountCount++
}
}
assert.Equal(t, 0, sensitiveMountCount,
"Should have no sensitive path mounts")
})
t.Run("no host namespace modes", func(t *testing.T) {
hostNamespaces := []string{"CIS-5.9", "CIS-5.15", "CIS-5.16"}
for _, ruleID := range hostNamespaces {
found := false
for _, f := range findings {
if f.RuleID == ruleID {
found = true
}
}
assert.False(t, found, "Should NOT have %s finding", ruleID)
}
})
t.Run("has memory limit", func(t *testing.T) {
hasNoMemLimit := false
for _, f := range findings {
if f.RuleID == "CIS-5.10" {
hasNoMemLimit = true
}
}
assert.False(t, hasNoMemLimit, "Should have memory limit configured")
})
t.Run("has CPU limit", func(t *testing.T) {
hasNoCPULimit := false
for _, f := range findings {
if f.RuleID == "CIS-5.11" {
hasNoCPULimit = true
}
}
assert.False(t, hasNoCPULimit, "Should have CPU limit configured")
})
t.Run("has PIDs limit", func(t *testing.T) {
hasNoPIDsLimit := false
for _, f := range findings {
if f.RuleID == "CIS-5.28" {
hasNoPIDsLimit = true
}
}
assert.False(t, hasNoPIDsLimit, "Should have PIDs limit configured")
})
t.Run("has read-only root filesystem", func(t *testing.T) {
hasNoReadOnly := false
for _, f := range findings {
if f.RuleID == "CIS-5.12" {
hasNoReadOnly = true
}
}
assert.False(
t,
hasNoReadOnly,
"Should have read-only root filesystem",
)
})
t.Run("no critical findings", func(t *testing.T) {
assert.False(
t,
findings.HasSeverityAtOrAbove(finding.SeverityCritical),
"Secure container should have no CRITICAL findings",
)
})
t.Run("minimal high findings", func(t *testing.T) {
highCount := 0
for _, f := range findings {
if f.Severity >= finding.SeverityHigh {
highCount++
}
}
assert.LessOrEqual(t, highCount, 2,
"Secure container should have minimal HIGH findings")
})
t.Run("total findings count", func(t *testing.T) {
assert.LessOrEqual(t, len(findings), 5,
"Secure container should have very few findings total")
})
}
func TestContainerAnalyzer_TargetInfo(t *testing.T) {
container := loadContainerJSON(t, "privileged-container.json")
analyzer := &ContainerAnalyzer{}
findings := analyzer.analyzeContainer(container)
require.NotEmpty(t, findings, "Should have findings")
t.Run("target has correct type", func(t *testing.T) {
for _, f := range findings {
assert.Equal(t, finding.TargetContainer, f.Target.Type)
}
})
t.Run("target has container name", func(t *testing.T) {
for _, f := range findings {
assert.Equal(t, "dangerous-container", f.Target.Name)
break
}
})
t.Run("target has container ID", func(t *testing.T) {
for _, f := range findings {
assert.NotEmpty(t, f.Target.ID)
assert.Equal(
t,
"abc123def456789012345678901234567890123456789012345678901234567890",
f.Target.ID,
)
break
}
})
}
func TestContainerAnalyzer_CategoryAndRemediation(t *testing.T) {
container := loadContainerJSON(t, "privileged-container.json")
analyzer := &ContainerAnalyzer{}
findings := analyzer.analyzeContainer(container)
require.NotEmpty(t, findings, "Should have findings")
t.Run("findings have category", func(t *testing.T) {
for _, f := range findings {
assert.Equal(t, string(CategoryContainerRuntime), f.Category)
}
})
t.Run("findings have remediation", func(t *testing.T) {
for _, f := range findings {
assert.NotEmpty(t, f.Remediation,
"Finding %s should have remediation", f.RuleID)
}
})
t.Run("CIS findings have control info", func(t *testing.T) {
for _, f := range findings {
if len(f.RuleID) >= 4 && f.RuleID[:4] == "CIS-" {
assert.NotNil(t, f.CISControl,
"CIS finding %s should have CISControl", f.RuleID)
}
}
})
}
func TestContainerAnalyzer_Comparison(t *testing.T) {
privileged := loadContainerJSON(t, "privileged-container.json")
secure := loadContainerJSON(t, "secure-container.json")
analyzer := &ContainerAnalyzer{}
privilegedFindings := analyzer.analyzeContainer(privileged)
secureFindings := analyzer.analyzeContainer(secure)
t.Run("privileged has more findings than secure", func(t *testing.T) {
assert.Greater(t, len(privilegedFindings), len(secureFindings),
"Privileged container should have more findings")
})
t.Run(
"privileged has critical findings, secure does not",
func(t *testing.T) {
assert.True(
t,
privilegedFindings.HasSeverityAtOrAbove(
finding.SeverityCritical,
),
"Privileged should have CRITICAL findings",
)
assert.False(
t,
secureFindings.HasSeverityAtOrAbove(finding.SeverityCritical),
"Secure should NOT have CRITICAL findings",
)
},
)
t.Run("severity distribution differs", func(t *testing.T) {
privCritical := privilegedFindings.BySeverity(
finding.SeverityCritical,
)
secureCritical := secureFindings.BySeverity(finding.SeverityCritical)
assert.Greater(t, len(privCritical), len(secureCritical),
"Privileged should have more CRITICAL findings")
})
}
func containsIgnoreCase(s, substr string) bool {
s = toLower(s)
substr = toLower(substr)
return contains(s, substr)
}
func toLower(s string) string {
result := make([]rune, len(s))
for i, r := range s {
if r >= 'A' && r <= 'Z' {
result[i] = r + 32
} else {
result[i] = r
}
}
return string(result)
}
func contains(s, substr string) bool {
if len(substr) == 0 {
return true
}
if len(s) < len(substr) {
return false
}
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}