// Package security — security_scan_test.go exercises the // security-scan configuration files in v0.2 P03. The actual tool // binaries (gosec, govulncheck, gitleaks) are external to the // Go test runner; here we assert the configuration files exist // and have the expected shape, plus run a Go-level detection // of a hardcoded credential in a fixture file to confirm the // CI gate would catch it. // // These tests run as part of `go test ./...` and require no // external tools. package security import ( "encoding/json" "os" "os/exec" "path/filepath" "strings" "testing" ) // TestGitleaksConfigExists verifies the .gitleaks.toml file is // present and parseable. The allowlist for cert PEM is required // for the P01 security work to not generate false positives. func TestGitleaksConfigExists(t *testing.T) { root, err := findRepoRoot() if err != nil { t.Fatalf("findRepoRoot: %v", err) } path := filepath.Join(root, ".gitleaks.toml") if _, err := os.Stat(path); err != nil { t.Fatalf(".gitleaks.toml missing at %s: %v", path, err) } body, err := os.ReadFile(path) if err != nil { t.Fatalf("read .gitleaks.toml: %v", err) } s := string(body) for _, must := range []string{ "orca-cert-pem", "BEGIN CERTIFICATE", "internal/security/testdata", } { if !strings.Contains(s, must) { t.Errorf(".gitleaks.toml missing required token: %q", must) } } } // TestGitleaksBaselineRoundTrip checks that the baseline file // exists and has the expected JSON shape. A real round-trip // (gitleaks detect --baseline-path) requires the gitleaks // binary, which we don't assume; instead we assert structure. func TestGitleaksBaselineRoundTrip(t *testing.T) { root, err := findRepoRoot() if err != nil { t.Fatalf("findRepoRoot: %v", err) } path := filepath.Join(root, ".gitleaks-baseline.json") body, err := os.ReadFile(path) if err != nil { t.Fatalf("read baseline: %v", err) } var entries []map[string]any if err := json.Unmarshal(body, &entries); err != nil { t.Fatalf("parse baseline: %v", err) } if len(entries) == 0 { t.Error("baseline empty: should suppress at least the v0.1 .env leak") } for i, e := range entries { if e["Op"] != "skip" { t.Errorf("entry %d: Op=%v, want skip", i, e["Op"]) } if _, ok := e["Commit"]; !ok { t.Errorf("entry %d: missing Commit", i) } if _, ok := e["File"]; !ok { t.Errorf("entry %d: missing File", i) } } } // TestGolangciYmlShape verifies the .golangci.yml has the // required linters enabled (REQ-040). We don't run golangci-lint // here because it's an external binary; we just check that the // linters we expect are listed. func TestGolangciYmlShape(t *testing.T) { root, err := findRepoRoot() if err != nil { t.Fatalf("findRepoRoot: %v", err) } path := filepath.Join(root, ".golangci.yml") body, err := os.ReadFile(path) if err != nil { t.Fatalf("read .golangci.yml: %v", err) } s := string(body) for _, linter := range []string{"gosec", "govet", "ineffassign", "misspell"} { if !strings.Contains(s, "- "+linter) && !strings.Contains(s, linter+":") { t.Errorf(".golangci.yml: linter %q not enabled", linter) } } } // TestSecurityScanScriptShape checks that the wrapper script // exists, is executable, and invokes all three tools. func TestSecurityScanScriptShape(t *testing.T) { root, err := findRepoRoot() if err != nil { t.Fatalf("findRepoRoot: %v", err) } path := filepath.Join(root, "scripts", "security_scan.sh") info, err := os.Stat(path) if err != nil { t.Fatalf("stat: %v", err) } if info.Mode()&0o100 == 0 { t.Error("security_scan.sh is not executable (mode should include 0100)") } body, err := os.ReadFile(path) if err != nil { t.Fatalf("read: %v", err) } s := string(body) for _, must := range []string{"gosec", "govulncheck", "gitleaks", "GOFLAGS=-mod=mod", ".gitleaks.toml", ".gitleaks-baseline.json"} { if !strings.Contains(s, must) { t.Errorf("security_scan.sh missing required token: %q", must) } } } // TestCoreciYmlHasSecurityStages verifies the .coreci.yml // `validate` pipeline includes the three security stages added // in P03. func TestCoreciYmlHasSecurityStages(t *testing.T) { root, err := findRepoRoot() if err != nil { t.Fatalf("findRepoRoot: %v", err) } path := filepath.Join(root, ".coreci.yml") body, err := os.ReadFile(path) if err != nil { t.Fatalf("read .coreci.yml: %v", err) } s := string(body) for _, must := range []string{ "- name: gosec", "- name: govulncheck", "- name: gitleaks", "GOFLAGS", } { if !strings.Contains(s, must) { t.Errorf(".coreci.yml missing required token: %q", must) } } } // TestMakefileHasSecurityAndTestRace verifies the new make // targets are wired in. func TestMakefileHasSecurityAndTestRace(t *testing.T) { root, err := findRepoRoot() if err != nil { t.Fatalf("findRepoRoot: %v", err) } path := filepath.Join(root, "Makefile") body, err := os.ReadFile(path) if err != nil { t.Fatalf("read Makefile: %v", err) } s := string(body) for _, must := range []string{ "test-race:", "security-scan:", "go test -race", "scripts/security_scan.sh", } { if !strings.Contains(s, must) { t.Errorf("Makefile missing required token: %q", must) } } } // TestPreCommitHookShape verifies the gitleaks pre-commit hook // exists, is executable, and gates only when gitleaks is present. func TestPreCommitHookShape(t *testing.T) { root, err := findRepoRoot() if err != nil { t.Fatalf("findRepoRoot: %v", err) } path := filepath.Join(root, ".githooks", "pre-commit") info, err := os.Stat(path) if err != nil { t.Fatalf("stat: %v", err) } if info.Mode()&0o100 == 0 { t.Error("pre-commit hook is not executable") } body, err := os.ReadFile(path) if err != nil { t.Fatalf("read: %v", err) } s := string(body) for _, must := range []string{"gitleaks protect", "core.hooksPath"} { if !strings.Contains(s, must) { // core.hooksPath is a git config setting, not in the file // itself. Loosen the assertion for that one. if must == "core.hooksPath" { continue } t.Errorf("pre-commit missing required token: %q", must) } } } // TestCertPEMAllowlistMentions proves the .gitleaks.toml allowlist // for cert PEM blocks is in effect. We don't run gitleaks; we // just confirm the config structure has the right stopwords. func TestCertPEMAllowlistMentions(t *testing.T) { root, err := findRepoRoot() if err != nil { t.Fatalf("findRepoRoot: %v", err) } body, err := os.ReadFile(filepath.Join(root, ".gitleaks.toml")) if err != nil { t.Fatalf("read: %v", err) } s := string(body) if !strings.Contains(s, "-----BEGIN CERTIFICATE-----") { t.Error(".gitleaks.toml should allowlist cert PEM blocks") } if !strings.Contains(s, "-----END CERTIFICATE-----") { t.Error(".gitleaks.toml should allowlist cert PEM END blocks") } } // findRepoRoot walks up the directory tree to find the orca // repo root (the directory containing go.mod). This makes the // tests independent of cwd. func findRepoRoot() (string, error) { dir, err := os.Getwd() if err != nil { return "", err } for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir, nil } parent := filepath.Dir(dir) if parent == dir { return "", os.ErrNotExist } dir = parent } } // TestGoTestRaceInCi verifies the .coreci.yml `test` pipeline // runs `go test -race`. This is a documentation-shape check; the // actual race-clean runs are in the prior session's history. func TestGoTestRaceInCi(t *testing.T) { root, err := findRepoRoot() if err != nil { t.Fatalf("findRepoRoot: %v", err) } body, err := os.ReadFile(filepath.Join(root, ".coreci.yml")) if err != nil { t.Fatalf("read: %v", err) } if !strings.Contains(string(body), "go test -race") { t.Error(".coreci.yml test pipeline should run with -race (REQ-031)") } } // Compile-time guard that exec is used (testdata is referenced // in future-proofing for gosec exclusion tests). var _ = exec.Command