diff --git a/pkg/clouds/pulumi/docker/security_report.go b/pkg/clouds/pulumi/docker/security_report.go index 3f7ae753..38b3ac73 100644 --- a/pkg/clouds/pulumi/docker/security_report.go +++ b/pkg/clouds/pulumi/docker/security_report.go @@ -19,10 +19,14 @@ func buildSecurityReportScript(imageRef, imageName string, security *api.Securit // Build the report script. Uses jq for JSON parsing if available, falls back to grep. // The script is self-contained — no SC CLI dependency. + // + // The heading includes imageName so stacks with multiple images (e.g. web+worker) + // produce visibly-distinct report sections in $GITHUB_STEP_SUMMARY — otherwise + // identical-looking headers make them appear as duplicates. var sb strings.Builder sb.WriteString("set +e\n") // Don't exit on error — report is best-effort sb.WriteString("REPORT=''\n") - sb.WriteString("REPORT=\"${REPORT}## Security Pipeline Summary\\n\\n\"\n") + sb.WriteString(fmt.Sprintf("REPORT=\"${REPORT}## Security Pipeline Summary — %s\\n\\n\"\n", shellEscape(imageName))) sb.WriteString(fmt.Sprintf("REPORT=\"${REPORT}**Image:** \\`%s\\`\\n\\n\"\n", shellEscape(imageRef))) sb.WriteString("REPORT=\"${REPORT}| Step | Status | Details |\\n\"\n") sb.WriteString("REPORT=\"${REPORT}| --- | --- | --- |\\n\"\n") diff --git a/pkg/security/scan/trivy.go b/pkg/security/scan/trivy.go index 005ca5f5..5f91e8ae 100644 --- a/pkg/security/scan/trivy.go +++ b/pkg/security/scan/trivy.go @@ -14,7 +14,7 @@ import ( // DefaultTrivyVersion is the pinned install version. Bump here to upgrade cluster-wide, // or override per-scan via SC config (ScanToolConfig.Version) or SC_TRIVY_VERSION env var. -const DefaultTrivyVersion = "0.69.3" +const DefaultTrivyVersion = "0.70.0" // TrivyScanner implements Scanner interface using Trivy type TrivyScanner struct { @@ -46,6 +46,7 @@ func (t *TrivyScanner) Scan(ctx context.Context, image string) (*ScanResult, err if err != nil { return nil, err } + defer cleanupTrivyCacheDir(cacheDir) // Scan from the local Docker daemon (docker-daemon: prefix). // The image must already be present locally — callers are expected to scan @@ -303,18 +304,41 @@ func parseTrivyVersion(output string) (string, error) { return "", fmt.Errorf("failed to parse trivy version from: %s", output) } +// ensureTrivyCacheDir returns a per-invocation Trivy cache directory under the +// user cache root. Using a unique subdirectory per scan eliminates the cache +// lock contention that Trivy exhibits when the same directory is shared across +// concurrent processes or reused with stale locks from crashed runs. The cost +// is re-downloading the vulnerability DB per scan (~100MB, a few seconds), +// which is acceptable in CI and avoids the "cache may be in use by another +// process: timeout" failure mode documented at +// https://trivy.dev/docs/guide/references/troubleshooting#database-and-cache-lock-errors +// +// Caller is responsible for cleanup (see cleanupTrivyCacheDir). func ensureTrivyCacheDir() (string, error) { cacheRoot, err := os.UserCacheDir() if err != nil { cacheRoot = os.TempDir() } - cacheDir := filepath.Join(cacheRoot, "trivy") - if err := os.MkdirAll(cacheDir, 0o755); err != nil { - return "", fmt.Errorf("create trivy cache directory: %w", err) + parent := filepath.Join(cacheRoot, "trivy") + if err := os.MkdirAll(parent, 0o755); err != nil { + return "", fmt.Errorf("create trivy cache parent directory: %w", err) + } + cacheDir, err := os.MkdirTemp(parent, "scan-*") + if err != nil { + return "", fmt.Errorf("create trivy cache scratch directory: %w", err) } return cacheDir, nil } +// cleanupTrivyCacheDir removes a per-invocation cache directory created by +// ensureTrivyCacheDir. Best-effort: errors are ignored. +func cleanupTrivyCacheDir(cacheDir string) { + if cacheDir == "" { + return + } + _ = os.RemoveAll(cacheDir) +} + func trivyDBPresent(cacheDir string) bool { return fileExists(filepath.Join(cacheDir, "db", "metadata.json")) } diff --git a/pkg/security/scan/trivy_test.go b/pkg/security/scan/trivy_test.go index a66578fb..62cab023 100644 --- a/pkg/security/scan/trivy_test.go +++ b/pkg/security/scan/trivy_test.go @@ -218,7 +218,24 @@ func TestEnsureTrivyCacheDir(t *testing.T) { cacheDir, err := ensureTrivyCacheDir() Expect(err).ToNot(HaveOccurred()) - Expect(cacheDir).To(Equal(filepath.Join(cacheRoot, "trivy"))) + defer cleanupTrivyCacheDir(cacheDir) + + // Per-invocation cache dir lives under /trivy/scan-* so + // concurrent scans can't clobber each other's lock files. + parent := filepath.Join(cacheRoot, "trivy") + Expect(cacheDir).To(HavePrefix(parent+string(filepath.Separator)+"scan-")) + Expect(cacheDir).To(BeADirectory()) + + // Second call returns a different directory (thread-safety property). + cacheDir2, err := ensureTrivyCacheDir() + Expect(err).ToNot(HaveOccurred()) + defer cleanupTrivyCacheDir(cacheDir2) + Expect(cacheDir2).ToNot(Equal(cacheDir)) + + // Cleanup removes the directory. + cleanupTrivyCacheDir(cacheDir) + _, statErr := os.Stat(cacheDir) + Expect(os.IsNotExist(statErr)).To(BeTrue()) } func TestTrivyDBPresenceHelpers(t *testing.T) {