Skip to content

Commit 204c263

Browse files
authored
Merge pull request #49 from ParanoiHack/hotfix/hash_finding
Fix vuln unique ID logic
2 parents 8a20416 + 3359b4b commit 204c263

5 files changed

Lines changed: 67 additions & 11 deletions

File tree

config.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
title = "Scope-guardian configuration file"
1+
title = "ScopeGuardian configuration file"
22

3-
protected_branches = ["main", "master"]
4-
path = "./WebGoat"
3+
protected_branches = ["main"]
4+
path = "."
55

66
[kics]
77
platform = "Dockerfile"
88
# exclude = ["**/vendor/**", "**/node_modules/**"]
99

1010
[grype]
1111
ignore_states = "not-fixed,unknown,wont-fix"
12+
syft_exclude = ["**/*_test.go"]
1213
transitive_libraries = false
1314
# syft_exclude = ["**/src/test/**"] # glob patterns passed to Syft --exclude to skip paths during SBOM generation
1415

@@ -22,4 +23,4 @@ exclude_rule = []
2223
# http_proxy = "http://proxy.company.com:3128"
2324
# https_proxy = "http://proxy.company.com:3128"
2425
# no_proxy = "localhost,127.0.0.1"
25-
# ssl_cert_file = "/path/to/ca.pem" # PEM-encoded CA certificate (e.g. Burp Suite CA)
26+
# ssl_cert_file = "/path/to/ca.pem" # PEM-encoded CA certificate (e.g. Burp Suite CA)

domains/models/finding.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ func FilterFindingsByStatus(findings []Finding, statuses []string) []Finding {
7979
// hash(lower(severity) | lower(sinkFile) | sinkLine | lower(recommendation))
8080
//
8181
// Scanner-specific notes:
82-
// - Grype: recommendation is the "Upgrade to X" string derived from fix.versions.
82+
// - Grype: the fourth field passed is the CVE/GHSA id (VulnId), not
83+
// Recommendation. DefectDojo's Anchore Grype parser synthesizes its own
84+
// Mitigation wording (e.g. "Upgrade to version: X"), so matching on that
85+
// free text is unreliable; the vulnerability id is copied through
86+
// verbatim and returned via vulnerability_ids.
8387
// - OpenGrep: recommendation is always "" because DefectDojo's Semgrep parser stores
8488
// extra.message in description, not mitigation. The hash is additionally
8589
// injected into extra.fingerprint before upload so that DefectDojo stores

features/scans/grype/service.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,12 @@ func (s *GrypeServiceImpl) LoadFindings() ([]models.Finding, error) {
117117
SinkFile: sinkFile,
118118
Recommendation: recommendation,
119119
}
120-
f.Hash = models.ComputeFindingHash(f.Severity, f.SinkFile, f.SinkLine, f.Recommendation)
120+
// Hashed on the CVE/GHSA id rather than Recommendation: DefectDojo's Anchore
121+
// Grype parser synthesizes its own mitigation wording (e.g. "Upgrade to
122+
// version: X" instead of "Upgrade to X"), so matching on that text is
123+
// unreliable. The vulnerability id is copied through verbatim by DD's
124+
// parser and returned in vulnerability_ids, giving a stable matching key.
125+
f.Hash = models.ComputeFindingHash(f.Severity, f.SinkFile, f.SinkLine, cveId)
121126
findings = append(findings, f)
122127
}
123128

features/sync/sync.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,18 @@ func GetEngagementFindings(ddService defectdojo.DefectDojoService, projectName s
205205
// findings just created by the sync import). All local findings are returned —
206206
// nothing is filtered out.
207207
//
208-
// Two complementary hash strategies are used to match a local finding to its DD
208+
// Three complementary hash strategies are used to match a local finding to its DD
209209
// counterpart:
210210
//
211-
// 1. hash(severity|filePath|line|mitigation) — primary path for Grype and KICS.
212-
// 2. UniqueIdFromTool — covers OpenGrep (hash injected into extra.fingerprint
211+
// 1. hash(severity|filePath|line|mitigation) — primary path for KICS, whose
212+
// Recommendation is a raw field (expected_value) that DD's parser passes
213+
// through verbatim into Mitigation.
214+
// 2. hash(severity|filePath|line|vulnerability_id) — primary path for Grype.
215+
// DefectDojo's Anchore Grype parser synthesizes its own Mitigation wording
216+
// (e.g. "Upgrade to version: X" instead of ScopeGuardian's "Upgrade to X"),
217+
// so matching on that text is unreliable. The CVE/GHSA id, however, is
218+
// copied through verbatim and returned in vulnerability_ids.
219+
// 3. UniqueIdFromTool — covers OpenGrep (hash injected into extra.fingerprint
213220
// before upload; DD's Semgrep parser stores it as unique_id_from_tool).
214221
func MarkFindingsByDDFindings(local []models.Finding, ddFindings []defectdojo.Finding) []models.Finding {
215222
type ddStatus struct {
@@ -228,9 +235,13 @@ func MarkFindingsByDDFindings(local []models.Finding, ddFindings []defectdojo.Fi
228235
riskAccepted: f.RiskAccepted,
229236
falseP: f.FalseP,
230237
}
231-
// Strategy 1: hash from API fields — covers Grype and KICS.
238+
// Strategy 1: hash from API fields — covers KICS.
232239
ddMap[models.ComputeFindingHash(f.Severity, f.FilePath, f.Line, f.Mitigation)] = s
233-
// Strategy 2: UniqueIdFromTool — covers OpenGrep.
240+
// Strategy 2: hash from vulnerability id — covers Grype.
241+
for _, v := range f.VulnerabilityIds {
242+
ddMap[models.ComputeFindingHash(f.Severity, f.FilePath, f.Line, v.VulnerabilityId)] = s
243+
}
244+
// Strategy 3: UniqueIdFromTool — covers OpenGrep.
234245
if f.UniqueIdFromTool != "" {
235246
ddMap[f.UniqueIdFromTool] = s
236247
}

features/sync/sync_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,41 @@ func TestMarkFindingsByDDFindings(t *testing.T) {
540540
assert.Equal(t, "Missing User Instruction", result[0].Name)
541541
})
542542

543+
t.Run("Should match Grype finding via vulnerability id despite mismatched DD mitigation wording", func(t *testing.T) {
544+
// Grype's LoadFindings hashes on VulnId (CVE/GHSA), not Recommendation —
545+
// see features/scans/grype/service.go.
546+
local := []models.Finding{
547+
{
548+
Severity: "HIGH",
549+
Name: "github.com/docker/docker v28.5.2",
550+
VulnId: "CVE-2026-34040",
551+
SinkFile: "/LAMA/go.mod",
552+
SinkLine: 0,
553+
Recommendation: "Upgrade to 29.3.1",
554+
Hash: models.ComputeFindingHash("HIGH", "/LAMA/go.mod", 0, "CVE-2026-34040"),
555+
},
556+
}
557+
ddFindings := []defectdojo.Finding{
558+
{
559+
Title: "github.com/docker/docker v28.5.2",
560+
Severity: "High",
561+
FilePath: "/LAMA/go.mod",
562+
Line: 0,
563+
Mitigation: "Upgrade to version: 29.3.1",
564+
VulnerabilityIds: []defectdojo.VulnerabilityId{
565+
{VulnerabilityId: "CVE-2026-34040"},
566+
},
567+
Active: true,
568+
Duplicate: false,
569+
},
570+
}
571+
572+
result := MarkFindingsByDDFindings(local, ddFindings)
573+
574+
assert.Len(t, result, 1)
575+
assert.Equal(t, models.FindingStatusActive, result[0].Status)
576+
})
577+
543578
t.Run("Should return empty slice when local findings list is empty", func(t *testing.T) {
544579
ddFindings := []defectdojo.Finding{
545580
{Title: "SQL Injection", Severity: "High", FilePath: "src/db.go", Line: 42,

0 commit comments

Comments
 (0)