diff --git a/config.toml b/config.toml index 3dc7f1b..432f943 100644 --- a/config.toml +++ b/config.toml @@ -1,7 +1,7 @@ -title = "Scope-guardian configuration file" +title = "ScopeGuardian configuration file" -protected_branches = ["main", "master"] -path = "./WebGoat" +protected_branches = ["main"] +path = "." [kics] platform = "Dockerfile" @@ -9,6 +9,7 @@ platform = "Dockerfile" [grype] ignore_states = "not-fixed,unknown,wont-fix" +syft_exclude = ["**/*_test.go"] transitive_libraries = false # syft_exclude = ["**/src/test/**"] # glob patterns passed to Syft --exclude to skip paths during SBOM generation @@ -22,4 +23,4 @@ exclude_rule = [] # http_proxy = "http://proxy.company.com:3128" # https_proxy = "http://proxy.company.com:3128" # no_proxy = "localhost,127.0.0.1" -# ssl_cert_file = "/path/to/ca.pem" # PEM-encoded CA certificate (e.g. Burp Suite CA) +# ssl_cert_file = "/path/to/ca.pem" # PEM-encoded CA certificate (e.g. Burp Suite CA) \ No newline at end of file diff --git a/domains/models/finding.go b/domains/models/finding.go index c13541b..f4a2462 100644 --- a/domains/models/finding.go +++ b/domains/models/finding.go @@ -79,7 +79,11 @@ func FilterFindingsByStatus(findings []Finding, statuses []string) []Finding { // hash(lower(severity) | lower(sinkFile) | sinkLine | lower(recommendation)) // // Scanner-specific notes: -// - Grype: recommendation is the "Upgrade to X" string derived from fix.versions. +// - Grype: the fourth field passed is the CVE/GHSA id (VulnId), not +// Recommendation. DefectDojo's Anchore Grype parser synthesizes its own +// Mitigation wording (e.g. "Upgrade to version: X"), so matching on that +// free text is unreliable; the vulnerability id is copied through +// verbatim and returned via vulnerability_ids. // - OpenGrep: recommendation is always "" because DefectDojo's Semgrep parser stores // extra.message in description, not mitigation. The hash is additionally // injected into extra.fingerprint before upload so that DefectDojo stores diff --git a/features/scans/grype/service.go b/features/scans/grype/service.go index 03bd513..dc6b807 100644 --- a/features/scans/grype/service.go +++ b/features/scans/grype/service.go @@ -117,7 +117,12 @@ func (s *GrypeServiceImpl) LoadFindings() ([]models.Finding, error) { SinkFile: sinkFile, Recommendation: recommendation, } - f.Hash = models.ComputeFindingHash(f.Severity, f.SinkFile, f.SinkLine, f.Recommendation) + // Hashed on the CVE/GHSA id rather than Recommendation: DefectDojo's Anchore + // Grype parser synthesizes its own mitigation wording (e.g. "Upgrade to + // version: X" instead of "Upgrade to X"), so matching on that text is + // unreliable. The vulnerability id is copied through verbatim by DD's + // parser and returned in vulnerability_ids, giving a stable matching key. + f.Hash = models.ComputeFindingHash(f.Severity, f.SinkFile, f.SinkLine, cveId) findings = append(findings, f) } diff --git a/features/sync/sync.go b/features/sync/sync.go index c3c664c..54f59f7 100644 --- a/features/sync/sync.go +++ b/features/sync/sync.go @@ -205,11 +205,18 @@ func GetEngagementFindings(ddService defectdojo.DefectDojoService, projectName s // findings just created by the sync import). All local findings are returned — // nothing is filtered out. // -// Two complementary hash strategies are used to match a local finding to its DD +// Three complementary hash strategies are used to match a local finding to its DD // counterpart: // -// 1. hash(severity|filePath|line|mitigation) — primary path for Grype and KICS. -// 2. UniqueIdFromTool — covers OpenGrep (hash injected into extra.fingerprint +// 1. hash(severity|filePath|line|mitigation) — primary path for KICS, whose +// Recommendation is a raw field (expected_value) that DD's parser passes +// through verbatim into Mitigation. +// 2. hash(severity|filePath|line|vulnerability_id) — primary path for Grype. +// DefectDojo's Anchore Grype parser synthesizes its own Mitigation wording +// (e.g. "Upgrade to version: X" instead of ScopeGuardian's "Upgrade to X"), +// so matching on that text is unreliable. The CVE/GHSA id, however, is +// copied through verbatim and returned in vulnerability_ids. +// 3. UniqueIdFromTool — covers OpenGrep (hash injected into extra.fingerprint // before upload; DD's Semgrep parser stores it as unique_id_from_tool). func MarkFindingsByDDFindings(local []models.Finding, ddFindings []defectdojo.Finding) []models.Finding { type ddStatus struct { @@ -228,9 +235,13 @@ func MarkFindingsByDDFindings(local []models.Finding, ddFindings []defectdojo.Fi riskAccepted: f.RiskAccepted, falseP: f.FalseP, } - // Strategy 1: hash from API fields — covers Grype and KICS. + // Strategy 1: hash from API fields — covers KICS. ddMap[models.ComputeFindingHash(f.Severity, f.FilePath, f.Line, f.Mitigation)] = s - // Strategy 2: UniqueIdFromTool — covers OpenGrep. + // Strategy 2: hash from vulnerability id — covers Grype. + for _, v := range f.VulnerabilityIds { + ddMap[models.ComputeFindingHash(f.Severity, f.FilePath, f.Line, v.VulnerabilityId)] = s + } + // Strategy 3: UniqueIdFromTool — covers OpenGrep. if f.UniqueIdFromTool != "" { ddMap[f.UniqueIdFromTool] = s } diff --git a/features/sync/sync_test.go b/features/sync/sync_test.go index d53420c..8455da8 100644 --- a/features/sync/sync_test.go +++ b/features/sync/sync_test.go @@ -540,6 +540,41 @@ func TestMarkFindingsByDDFindings(t *testing.T) { assert.Equal(t, "Missing User Instruction", result[0].Name) }) + t.Run("Should match Grype finding via vulnerability id despite mismatched DD mitigation wording", func(t *testing.T) { + // Grype's LoadFindings hashes on VulnId (CVE/GHSA), not Recommendation — + // see features/scans/grype/service.go. + local := []models.Finding{ + { + Severity: "HIGH", + Name: "github.com/docker/docker v28.5.2", + VulnId: "CVE-2026-34040", + SinkFile: "/LAMA/go.mod", + SinkLine: 0, + Recommendation: "Upgrade to 29.3.1", + Hash: models.ComputeFindingHash("HIGH", "/LAMA/go.mod", 0, "CVE-2026-34040"), + }, + } + ddFindings := []defectdojo.Finding{ + { + Title: "github.com/docker/docker v28.5.2", + Severity: "High", + FilePath: "/LAMA/go.mod", + Line: 0, + Mitigation: "Upgrade to version: 29.3.1", + VulnerabilityIds: []defectdojo.VulnerabilityId{ + {VulnerabilityId: "CVE-2026-34040"}, + }, + Active: true, + Duplicate: false, + }, + } + + result := MarkFindingsByDDFindings(local, ddFindings) + + assert.Len(t, result, 1) + assert.Equal(t, models.FindingStatusActive, result[0].Status) + }) + t.Run("Should return empty slice when local findings list is empty", func(t *testing.T) { ddFindings := []defectdojo.Finding{ {Title: "SQL Injection", Severity: "High", FilePath: "src/db.go", Line: 42,