Skip to content

Commit 7a26697

Browse files
authored
Merge pull request #47 from ParanoiHack/feature/findings_status
Add DD support for false positive, risk_accepted and out-of-scope
2 parents 0c8d787 + fe2ee90 commit 7a26697

3 files changed

Lines changed: 101 additions & 8 deletions

File tree

connectors/defectdojo/dto.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ type Finding struct {
6565
// finding as a duplicate of another finding in the product. Corresponds to the
6666
// "duplicate" field returned by the findings API.
6767
Duplicate bool `json:"duplicate"`
68+
// OutOfScope is true when a triager has marked this finding as out of scope.
69+
OutOfScope bool `json:"out_of_scope"`
70+
// RiskAccepted is true when a triager has accepted the risk for this finding.
71+
RiskAccepted bool `json:"risk_accepted"`
72+
// FalseP is true when a triager has marked this finding as a false positive.
73+
// Corresponds to the "false_p" field returned by the findings API.
74+
FalseP bool `json:"false_p"`
6875
}
6976

7077
type GetFindingsResponse struct {

features/sync/sync.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,13 @@ func GetEngagementFindings(ddService defectdojo.DefectDojoService, projectName s
193193
}
194194

195195
// MarkFindingsByDDFindings sets the Status field on every local finding based on
196-
// the corresponding DefectDojo finding's "active" and "duplicate" fields:
196+
// the corresponding DefectDojo finding's "active", "duplicate", "out_of_scope",
197+
// "risk_accepted", and "false_p" fields:
197198
//
198-
// - duplicate=true → DUPLICATE (DD deduplication identified a prior occurrence)
199-
// - active=false → INACTIVE (suppressed, false-positive, or accepted risk)
200-
// - active=true → ACTIVE (open, confirmed finding)
199+
// - duplicate=true → DUPLICATE (DD deduplication identified a prior occurrence)
200+
// - active=false, out_of_scope=true, risk_accepted=true,
201+
// or false_p=true → INACTIVE (suppressed, out of scope, accepted risk, or false-positive)
202+
// - none of the above → ACTIVE (open, confirmed finding)
201203
//
202204
// Local findings with no matching DD finding default to ACTIVE (they are brand-new
203205
// findings just created by the sync import). All local findings are returned —
@@ -211,12 +213,21 @@ func GetEngagementFindings(ddService defectdojo.DefectDojoService, projectName s
211213
// before upload; DD's Semgrep parser stores it as unique_id_from_tool).
212214
func MarkFindingsByDDFindings(local []models.Finding, ddFindings []defectdojo.Finding) []models.Finding {
213215
type ddStatus struct {
214-
active bool
215-
duplicate bool
216+
active bool
217+
duplicate bool
218+
outOfScope bool
219+
riskAccepted bool
220+
falseP bool
216221
}
217222
ddMap := make(map[string]ddStatus, len(ddFindings)*2)
218223
for _, f := range ddFindings {
219-
s := ddStatus{active: f.Active, duplicate: f.Duplicate}
224+
s := ddStatus{
225+
active: f.Active,
226+
duplicate: f.Duplicate,
227+
outOfScope: f.OutOfScope,
228+
riskAccepted: f.RiskAccepted,
229+
falseP: f.FalseP,
230+
}
220231
// Strategy 1: hash from API fields — covers Grype and KICS.
221232
ddMap[models.ComputeFindingHash(f.Severity, f.FilePath, f.Line, f.Mitigation)] = s
222233
// Strategy 2: UniqueIdFromTool — covers OpenGrep.
@@ -232,7 +243,7 @@ func MarkFindingsByDDFindings(local []models.Finding, ddFindings []defectdojo.Fi
232243
switch {
233244
case s.duplicate:
234245
result[i].Status = models.FindingStatusDuplicate
235-
case !s.active:
246+
case !s.active || s.outOfScope || s.riskAccepted || s.falseP:
236247
result[i].Status = models.FindingStatusInactive
237248
default:
238249
result[i].Status = models.FindingStatusActive

features/sync/sync_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,81 @@ func TestMarkFindingsByDDFindings(t *testing.T) {
370370
assert.Equal(t, models.FindingStatusDuplicate, result[0].Status)
371371
})
372372

373+
t.Run("Should mark finding as INACTIVE when DD finding has out_of_scope=true", func(t *testing.T) {
374+
local := []models.Finding{
375+
localFinding("LOW", "Info leak", "src/leak.go", 3, "Redact output"),
376+
}
377+
ddFindings := []defectdojo.Finding{
378+
{Title: "Info leak", Severity: "Low", FilePath: "src/leak.go", Line: 3,
379+
Mitigation: "Redact output", Active: false, Duplicate: false, OutOfScope: true},
380+
}
381+
382+
result := MarkFindingsByDDFindings(local, ddFindings)
383+
384+
assert.Len(t, result, 1)
385+
assert.Equal(t, models.FindingStatusInactive, result[0].Status)
386+
})
387+
388+
t.Run("Should mark finding as INACTIVE when DD finding has risk_accepted=true", func(t *testing.T) {
389+
local := []models.Finding{
390+
localFinding("HIGH", "Weak Cipher", "src/crypto.go", 8, "Use AES-256"),
391+
}
392+
ddFindings := []defectdojo.Finding{
393+
{Title: "Weak Cipher", Severity: "High", FilePath: "src/crypto.go", Line: 8,
394+
Mitigation: "Use AES-256", Active: false, Duplicate: false, RiskAccepted: true},
395+
}
396+
397+
result := MarkFindingsByDDFindings(local, ddFindings)
398+
399+
assert.Len(t, result, 1)
400+
assert.Equal(t, models.FindingStatusInactive, result[0].Status)
401+
})
402+
403+
t.Run("Should mark finding as INACTIVE when DD finding has false_p=true", func(t *testing.T) {
404+
local := []models.Finding{
405+
localFinding("MEDIUM", "Path Traversal", "src/files.go", 21, "Validate path"),
406+
}
407+
ddFindings := []defectdojo.Finding{
408+
{Title: "Path Traversal", Severity: "Medium", FilePath: "src/files.go", Line: 21,
409+
Mitigation: "Validate path", Active: false, Duplicate: false, FalseP: true},
410+
}
411+
412+
result := MarkFindingsByDDFindings(local, ddFindings)
413+
414+
assert.Len(t, result, 1)
415+
assert.Equal(t, models.FindingStatusInactive, result[0].Status)
416+
})
417+
418+
t.Run("Should mark finding as INACTIVE when risk_accepted=true even if active=true", func(t *testing.T) {
419+
local := []models.Finding{
420+
localFinding("CRITICAL", "Insecure Deserialization", "src/serde.go", 55, "Use safe parser"),
421+
}
422+
ddFindings := []defectdojo.Finding{
423+
{Title: "Insecure Deserialization", Severity: "Critical", FilePath: "src/serde.go", Line: 55,
424+
Mitigation: "Use safe parser", Active: true, Duplicate: false, RiskAccepted: true},
425+
}
426+
427+
result := MarkFindingsByDDFindings(local, ddFindings)
428+
429+
assert.Len(t, result, 1)
430+
assert.Equal(t, models.FindingStatusInactive, result[0].Status)
431+
})
432+
433+
t.Run("Should prioritise duplicate=true over risk_accepted=true", func(t *testing.T) {
434+
local := []models.Finding{
435+
localFinding("HIGH", "Vuln2", "src/main2.go", 6, "Fix it"),
436+
}
437+
ddFindings := []defectdojo.Finding{
438+
{Title: "Vuln2", Severity: "High", FilePath: "src/main2.go", Line: 6,
439+
Mitigation: "Fix it", Active: false, Duplicate: true, RiskAccepted: true},
440+
}
441+
442+
result := MarkFindingsByDDFindings(local, ddFindings)
443+
444+
assert.Len(t, result, 1)
445+
assert.Equal(t, models.FindingStatusDuplicate, result[0].Status)
446+
})
447+
373448
t.Run("Should default to ACTIVE when local finding has no DD counterpart", func(t *testing.T) {
374449
local := []models.Finding{
375450
localFinding("HIGH", "New Finding", "src/new.go", 1, "Fix it"),

0 commit comments

Comments
 (0)