Cybersecurity-Projects/PROJECTS/intermediate/docker-security-audit/tests/integration/dockerfile_test.go

443 lines
10 KiB
Go

/*
©AngelaMos | 2026
dockerfile_test.go
Integration tests for DockerfileAnalyzer against all testdata Dockerfile
fixtures
Each function targets a specific fixture and asserts expected findings
at correct rule IDs and severities. Covers secret detection, missing
USER and HEALTHCHECK, :latest tags, ADD vs COPY, and clean best-
practice Dockerfiles that should produce minimal findings.
Tests:
TestDockerfileAnalyzer_BadSecrets - AWS, GitHub, DB, Stripe, OpenAI
secrets
TestDockerfileAnalyzer_BadRootUser - missing USER and HEALTHCHECK
TestDockerfileAnalyzer_BadPrivileged - :latest tag and missing USER
TestDockerfileAnalyzer_BadAddCommand - ADD vs COPY and ADD with URL
TestDockerfileAnalyzer_GoodMinimal - no critical or high findings
TestDockerfileAnalyzer_GoodSecurity - best-practice Dockerfile
near-clean
TestDockerfileAnalyzer_AllFiles - table-driven coverage across all
fixtures
Connects to:
analyzer/dockerfile.go - DockerfileAnalyzer under test
finding.go - asserts on Severity, RuleID, and Collection methods
*/
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 TestDockerfileAnalyzer_BadSecrets(t *testing.T) {
ctx := context.Background()
path := filepath.Join(
"..",
"testdata",
"dockerfiles",
"bad-secrets.Dockerfile",
)
a := analyzer.NewDockerfileAnalyzer(path)
findings, err := a.Analyze(ctx)
require.NoError(t, err)
t.Run("detects hardcoded secrets", func(t *testing.T) {
assert.True(t, findings.HasSeverityAtOrAbove(finding.SeverityHigh),
"Should detect HIGH or CRITICAL severity secrets")
secretTypes := []string{
"AWS",
"github",
"database",
"stripe",
"openai",
}
for _, secretType := range secretTypes {
hasSecret := false
for _, f := range findings {
if containsIgnoreCase(f.Title, secretType) ||
containsIgnoreCase(f.Description, secretType) {
hasSecret = true
break
}
}
assert.True(t, hasSecret, "Should detect %s secrets", secretType)
}
})
t.Run("detects sensitive environment variable names", func(t *testing.T) {
sensitiveVars := false
for _, f := range findings {
if containsIgnoreCase(f.Title, "PASSWORD") ||
containsIgnoreCase(f.Title, "API_KEY") ||
containsIgnoreCase(f.Title, "SECRET") {
sensitiveVars = true
break
}
}
assert.True(
t,
sensitiveVars,
"Should detect sensitive env variable names",
)
})
t.Run("has CIS 4.10 finding", func(t *testing.T) {
hasCIS410 := false
for _, f := range findings {
if f.RuleID == "CIS-4.10" {
hasCIS410 = true
break
}
}
assert.True(t, hasCIS410, "Should have CIS-4.10 finding for secrets")
})
}
func TestDockerfileAnalyzer_BadRootUser(t *testing.T) {
ctx := context.Background()
path := filepath.Join(
"..",
"testdata",
"dockerfiles",
"bad-root-user.Dockerfile",
)
a := analyzer.NewDockerfileAnalyzer(path)
findings, err := a.Analyze(ctx)
require.NoError(t, err)
t.Run("detects missing USER instruction", func(t *testing.T) {
hasNoUser := false
for _, f := range findings {
if f.RuleID == "CIS-4.1" {
hasNoUser = true
assert.Equal(t, finding.SeverityMedium, f.Severity)
break
}
}
assert.True(t, hasNoUser, "Should detect missing USER instruction")
})
t.Run("detects missing HEALTHCHECK", func(t *testing.T) {
hasNoHealthcheck := false
for _, f := range findings {
if f.RuleID == "CIS-4.6" {
hasNoHealthcheck = true
break
}
}
assert.True(t, hasNoHealthcheck, "Should detect missing HEALTHCHECK")
})
}
func TestDockerfileAnalyzer_BadPrivileged(t *testing.T) {
ctx := context.Background()
path := filepath.Join(
"..",
"testdata",
"dockerfiles",
"bad-privileged.Dockerfile",
)
a := analyzer.NewDockerfileAnalyzer(path)
findings, err := a.Analyze(ctx)
require.NoError(t, err)
t.Run("detects latest tag", func(t *testing.T) {
hasLatestTag := false
for _, f := range findings {
if f.RuleID == "DS-LATEST-TAG" {
hasLatestTag = true
assert.Equal(t, finding.SeverityMedium, f.Severity)
break
}
}
assert.True(t, hasLatestTag, "Should detect :latest tag usage")
})
t.Run("detects missing USER", func(t *testing.T) {
hasNoUser := false
for _, f := range findings {
if f.RuleID == "CIS-4.1" {
hasNoUser = true
break
}
}
assert.True(t, hasNoUser, "Should detect missing USER instruction")
})
}
func TestDockerfileAnalyzer_BadAddCommand(t *testing.T) {
ctx := context.Background()
path := filepath.Join(
"..",
"testdata",
"dockerfiles",
"bad-add-command.Dockerfile",
)
a := analyzer.NewDockerfileAnalyzer(path)
findings, err := a.Analyze(ctx)
require.NoError(t, err)
t.Run("detects ADD instead of COPY", func(t *testing.T) {
hasAddIssue := false
for _, f := range findings {
if f.RuleID == "CIS-4.9" || f.RuleID == "DS-ADD-URL" {
hasAddIssue = true
break
}
}
assert.True(t, hasAddIssue, "Should detect ADD usage issues")
})
t.Run("detects ADD with URL", func(t *testing.T) {
hasAddURL := false
for _, f := range findings {
if f.RuleID == "DS-ADD-URL" {
hasAddURL = true
assert.Equal(t, finding.SeverityMedium, f.Severity)
break
}
}
assert.True(t, hasAddURL, "Should detect ADD with URL")
})
}
func TestDockerfileAnalyzer_GoodMinimal(t *testing.T) {
ctx := context.Background()
path := filepath.Join(
"..",
"testdata",
"dockerfiles",
"good-minimal.Dockerfile",
)
a := analyzer.NewDockerfileAnalyzer(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),
"Good Dockerfile should have no CRITICAL findings",
)
})
t.Run("has no high severity findings", func(t *testing.T) {
assert.False(t, findings.HasSeverityAtOrAbove(finding.SeverityHigh),
"Good Dockerfile should have no HIGH findings")
})
t.Run("has USER instruction", func(t *testing.T) {
hasNoUserFinding := false
for _, f := range findings {
if f.RuleID == "CIS-4.1" {
hasNoUserFinding = true
}
}
assert.False(
t,
hasNoUserFinding,
"Should NOT have missing USER finding",
)
})
t.Run("has HEALTHCHECK", func(t *testing.T) {
hasNoHealthcheck := false
for _, f := range findings {
if f.RuleID == "CIS-4.6" {
hasNoHealthcheck = true
}
}
assert.False(
t,
hasNoHealthcheck,
"Should NOT have missing HEALTHCHECK finding",
)
})
}
func TestDockerfileAnalyzer_GoodSecurity(t *testing.T) {
ctx := context.Background()
path := filepath.Join(
"..",
"testdata",
"dockerfiles",
"good-security.Dockerfile",
)
a := analyzer.NewDockerfileAnalyzer(path)
findings, err := a.Analyze(ctx)
require.NoError(t, err)
t.Run("has minimal findings", func(t *testing.T) {
assert.LessOrEqual(t, len(findings), 2,
"Best-practice Dockerfile should have very few findings")
})
t.Run("no critical or high findings", func(t *testing.T) {
assert.False(
t,
findings.HasSeverityAtOrAbove(finding.SeverityHigh),
"Best-practice Dockerfile should have no HIGH or CRITICAL findings",
)
})
}
func TestDockerfileAnalyzer_AllFiles(t *testing.T) {
testCases := []struct {
name string
file string
wantCritical bool
wantHigh bool
minFindings int
specificFindings []string
}{
{
name: "bad-secrets.Dockerfile",
file: "bad-secrets.Dockerfile",
wantCritical: false,
wantHigh: true,
minFindings: 5,
specificFindings: []string{
"CIS-4.10",
"CIS-4.1",
},
},
{
name: "bad-root-user.Dockerfile",
file: "bad-root-user.Dockerfile",
wantCritical: false,
wantHigh: false,
minFindings: 2,
specificFindings: []string{
"CIS-4.1",
"CIS-4.6",
},
},
{
name: "bad-privileged.Dockerfile",
file: "bad-privileged.Dockerfile",
wantCritical: false,
wantHigh: false,
minFindings: 1,
specificFindings: []string{
"DS-LATEST-TAG",
},
},
{
name: "good-minimal.Dockerfile",
file: "good-minimal.Dockerfile",
wantCritical: false,
wantHigh: false,
minFindings: 0,
},
{
name: "good-security.Dockerfile",
file: "good-security.Dockerfile",
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",
"dockerfiles",
tc.file,
)
a := analyzer.NewDockerfileAnalyzer(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",
)
}
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,
)
}
})
}
}
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
}