Cybersecurity-Projects/PROJECTS/intermediate/docker-security-audit/tests/e2e/e2e_test.go

432 lines
10 KiB
Go

/*
©AngelaMos | 2026
e2e_test.go
End-to-end tests that run real analyzers against testdata fixtures
Exercises DockerfileAnalyzer and ComposeAnalyzer through the public
Analyze() method using fixtures from testdata/. Tests cover severity
expectations, finding counts, required field completeness, severity
filtering, and file-not-found error handling. All tests skip in
short mode.
Tests:
TestE2E_DockerfileAnalysis - bad-secrets and good-security Dockerfiles
TestE2E_ComposeAnalysis - bad-docker-socket and good-production compose
TestE2E_MultipleFiles - sequential analysis of mixed file types
TestE2E_FindingProperties - all required finding fields present
TestE2E_SeverityFiltering - BySeverity and AtOrAbove filter correctness
TestE2E_FileNotFound - proper errors returned for missing files
Connects to:
analyzer/dockerfile.go - DockerfileAnalyzer under test
analyzer/compose.go - ComposeAnalyzer under test
finding.go - asserts on Collection methods and Severity values
*/
package e2e_test
import (
"context"
"os"
"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 TestE2E_DockerfileAnalysis(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
ctx := context.Background()
t.Run("analyze bad-secrets.Dockerfile end-to-end", func(t *testing.T) {
path := filepath.Join(
"..",
"testdata",
"dockerfiles",
"bad-secrets.Dockerfile",
)
require.FileExists(t, path, "Test file should exist")
a := analyzer.NewDockerfileAnalyzer(path)
findings, err := a.Analyze(ctx)
require.NoError(t, err, "Analyze should not return error")
require.NotEmpty(t, findings, "Should have findings")
assert.True(t, findings.HasSeverityAtOrAbove(finding.SeverityHigh),
"Should detect HIGH+ severity issues")
counts := findings.CountBySeverity()
t.Logf("Findings by severity: %+v", counts)
t.Logf("Total findings: %d", findings.Total())
assert.Greater(t, counts[finding.SeverityHigh], 0,
"Should have HIGH severity findings")
})
t.Run("analyze good-security.Dockerfile end-to-end", func(t *testing.T) {
path := filepath.Join(
"..",
"testdata",
"dockerfiles",
"good-security.Dockerfile",
)
require.FileExists(t, path, "Test file should exist")
a := analyzer.NewDockerfileAnalyzer(path)
findings, err := a.Analyze(ctx)
require.NoError(t, err, "Analyze should not return error")
assert.False(t, findings.HasSeverityAtOrAbove(finding.SeverityHigh),
"Good Dockerfile should have no HIGH+ findings")
counts := findings.CountBySeverity()
t.Logf("Findings by severity: %+v", counts)
t.Logf("Total findings: %d", findings.Total())
})
}
func TestE2E_ComposeAnalysis(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
ctx := context.Background()
t.Run("analyze bad-docker-socket.yml end-to-end", func(t *testing.T) {
path := filepath.Join(
"..",
"testdata",
"compose",
"bad-docker-socket.yml",
)
require.FileExists(t, path, "Test file should exist")
a := analyzer.NewComposeAnalyzer(path)
findings, err := a.Analyze(ctx)
require.NoError(t, err, "Analyze should not return error")
require.NotEmpty(t, findings, "Should have findings")
assert.True(
t,
findings.HasSeverityAtOrAbove(finding.SeverityCritical),
"Should detect CRITICAL issues",
)
counts := findings.CountBySeverity()
t.Logf("Findings by severity: %+v", counts)
t.Logf("Total findings: %d", findings.Total())
assert.Greater(t, counts[finding.SeverityCritical], 0,
"Should have CRITICAL findings for Docker socket")
})
t.Run("analyze good-production.yml end-to-end", func(t *testing.T) {
path := filepath.Join(
"..",
"testdata",
"compose",
"good-production.yml",
)
require.FileExists(t, path, "Test file should exist")
a := analyzer.NewComposeAnalyzer(path)
findings, err := a.Analyze(ctx)
require.NoError(t, err, "Analyze should not return error")
assert.False(
t,
findings.HasSeverityAtOrAbove(finding.SeverityCritical),
"Production compose should have no CRITICAL findings",
)
counts := findings.CountBySeverity()
t.Logf("Findings by severity: %+v", counts)
t.Logf("Total findings: %d", findings.Total())
})
}
func TestE2E_MultipleFiles(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
ctx := context.Background()
files := []struct {
path string
analyzer func(string) analyzer.Analyzer
wantIssues bool
}{
{
path: filepath.Join(
"..",
"testdata",
"dockerfiles",
"bad-secrets.Dockerfile",
),
analyzer: func(p string) analyzer.Analyzer { return analyzer.NewDockerfileAnalyzer(p) },
wantIssues: true,
},
{
path: filepath.Join(
"..",
"testdata",
"dockerfiles",
"good-minimal.Dockerfile",
),
analyzer: func(p string) analyzer.Analyzer { return analyzer.NewDockerfileAnalyzer(p) },
wantIssues: false,
},
{
path: filepath.Join(
"..",
"testdata",
"compose",
"bad-privileged.yml",
),
analyzer: func(p string) analyzer.Analyzer { return analyzer.NewComposeAnalyzer(p) },
wantIssues: true,
},
{
path: filepath.Join(
"..",
"testdata",
"compose",
"good-production.yml",
),
analyzer: func(p string) analyzer.Analyzer { return analyzer.NewComposeAnalyzer(p) },
wantIssues: false,
},
}
t.Run("analyze multiple files in sequence", func(t *testing.T) {
var allFindings finding.Collection
for _, f := range files {
require.FileExists(t, f.path, "File should exist: %s", f.path)
a := f.analyzer(f.path)
findings, err := a.Analyze(ctx)
require.NoError(t, err, "Analyze should not error for %s", f.path)
if f.wantIssues {
assert.NotEmpty(
t,
findings,
"File %s should have findings",
f.path,
)
}
allFindings = append(allFindings, findings...)
t.Logf("%s: %d findings", f.path, len(findings))
}
t.Logf("Total findings across all files: %d", allFindings.Total())
assert.NotEmpty(
t,
allFindings,
"Should have findings across all files",
)
counts := allFindings.CountBySeverity()
t.Logf("Overall severity distribution: %+v", counts)
})
}
func TestE2E_FindingProperties(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
ctx := context.Background()
path := filepath.Join("..", "testdata", "compose", "bad-caps.yml")
a := analyzer.NewComposeAnalyzer(path)
findings, err := a.Analyze(ctx)
require.NoError(t, err)
require.NotEmpty(t, findings)
t.Run("findings have required fields", func(t *testing.T) {
for _, f := range findings {
assert.NotEmpty(t, f.ID, "Finding should have ID")
assert.NotEmpty(t, f.RuleID, "Finding should have RuleID")
assert.NotEmpty(t, f.Title, "Finding should have Title")
assert.NotEmpty(
t,
f.Description,
"Finding should have Description",
)
assert.NotEmpty(t, f.Category, "Finding should have Category")
assert.NotEmpty(
t,
f.Remediation,
"Finding should have Remediation",
)
assert.NotZero(t, f.Severity, "Finding should have Severity")
assert.NotEmpty(
t,
f.Target.Type,
"Finding should have Target.Type",
)
assert.NotEmpty(
t,
f.Target.Name,
"Finding should have Target.Name",
)
}
})
t.Run("findings have location info", func(t *testing.T) {
hasLocation := false
for _, f := range findings {
if f.Location != nil {
hasLocation = true
assert.NotEmpty(
t,
f.Location.Path,
"Location should have Path",
)
assert.Greater(
t,
f.Location.Line,
0,
"Location should have Line > 0",
)
break
}
}
assert.True(
t,
hasLocation,
"At least one finding should have location info",
)
})
t.Run("CIS findings have CIS control info", func(t *testing.T) {
hasCIS := false
for _, f := range findings {
if len(f.RuleID) >= 4 && f.RuleID[:4] == "CIS-" {
hasCIS = true
if f.CISControl != nil {
assert.NotEmpty(
t,
f.CISControl.ID,
"CISControl should have ID",
)
assert.NotEmpty(
t,
f.CISControl.Title,
"CISControl should have Title",
)
}
break
}
}
assert.True(t, hasCIS, "Should have at least one CIS finding")
})
}
func TestE2E_SeverityFiltering(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
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)
require.NotEmpty(t, findings)
t.Run("filter by severity", func(t *testing.T) {
critical := findings.BySeverity(finding.SeverityCritical)
high := findings.BySeverity(finding.SeverityHigh)
medium := findings.BySeverity(finding.SeverityMedium)
t.Logf(
"CRITICAL: %d, HIGH: %d, MEDIUM: %d",
len(critical),
len(high),
len(medium),
)
assert.NotEmpty(t, critical, "Should have CRITICAL findings")
for _, f := range critical {
assert.Equal(t, finding.SeverityCritical, f.Severity)
}
})
t.Run("filter at or above severity", func(t *testing.T) {
highAndAbove := findings.AtOrAbove(finding.SeverityHigh)
mediumAndAbove := findings.AtOrAbove(finding.SeverityMedium)
assert.NotEmpty(t, highAndAbove, "Should have HIGH+ findings")
assert.GreaterOrEqual(t, len(mediumAndAbove), len(highAndAbove),
"MEDIUM+ should include HIGH+ findings")
for _, f := range highAndAbove {
assert.GreaterOrEqual(t, f.Severity, finding.SeverityHigh)
}
})
}
func TestE2E_FileNotFound(t *testing.T) {
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
ctx := context.Background()
t.Run("nonexistent Dockerfile", func(t *testing.T) {
path := filepath.Join(
"..",
"testdata",
"dockerfiles",
"does-not-exist.Dockerfile",
)
a := analyzer.NewDockerfileAnalyzer(path)
_, err := a.Analyze(ctx)
assert.Error(t, err, "Should return error for missing file")
assert.True(t, os.IsNotExist(err), "Error should be file not found")
})
t.Run("nonexistent compose file", func(t *testing.T) {
path := filepath.Join(
"..",
"testdata",
"compose",
"does-not-exist.yml",
)
a := analyzer.NewComposeAnalyzer(path)
_, err := a.Analyze(ctx)
assert.Error(t, err, "Should return error for missing file")
})
}