Skip to content

Commit f8222b8

Browse files
chore: add suppressions to the UFM presenter tests (#478)
Co-authored-by: Peter Schäfer <101886095+PeterSchafer@users.noreply.github.com>
1 parent 48c15c7 commit f8222b8

File tree

11 files changed

+39871
-66
lines changed

11 files changed

+39871
-66
lines changed

internal/presenters/presenter_ufm_test.go

Lines changed: 246 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,24 @@ func normalizeRuleHelp(run map[string]interface{}) {
168168
if help, ok := rule["help"].(map[string]interface{}); ok {
169169
// compare markdown up to 5000 bytes
170170
if markdown, ok := help["markdown"].(string); ok {
171+
markdown = strings.Replace(markdown, "gomodules", "golang", -1)
171172
if len(markdown) > 5000 {
172173
help["markdown"] = markdown[:5000]
174+
} else {
175+
help["markdown"] = markdown
173176
}
174177
}
175178
}
176-
// Normalize tags order
179+
180+
// Normalize tags order & name
177181
if props, ok := rule["properties"].(map[string]interface{}); ok {
178182
if tags, ok := props["tags"].([]interface{}); ok {
183+
// Rename "gomodules" to "golang" for consistency
184+
for i, tag := range tags {
185+
if tag == "gomodules" {
186+
tags[i] = "golang"
187+
}
188+
}
179189
sortTags(tags)
180190
}
181191
}
@@ -222,8 +232,13 @@ func normalizeLocationURIs(result map[string]interface{}) {
222232
continue
223233
}
224234
// Normalize common manifest filenames
225-
if uri == "package.json" || uri == "package-lock.json" || uri == "pom.xml" {
226-
artLoc["uri"] = "manifest"
235+
if strings.HasSuffix(uri, "package.json") || strings.HasSuffix(uri, "package-lock.json") || strings.HasSuffix(uri, "pom.xml") {
236+
// Preserve subproject path
237+
if idx := strings.LastIndex(uri, "/"); idx >= 0 {
238+
artLoc["uri"] = uri[:idx+1] + "manifest"
239+
} else {
240+
artLoc["uri"] = "manifest"
241+
}
227242
}
228243
}
229244
}
@@ -256,13 +271,165 @@ func normalizeFixURIs(result map[string]interface{}) {
256271
if !ok {
257272
continue
258273
}
259-
if uri == "package.json" || uri == "package-lock.json" || uri == "pom.xml" {
274+
if strings.HasSuffix(uri, "package.json") || strings.HasSuffix(uri, "package-lock.json") || strings.HasSuffix(uri, "pom.xml") {
275+
if idx := strings.LastIndex(uri, "/"); idx >= 0 {
276+
artLoc["uri"] = uri[:idx+1] + "manifest"
277+
} else {
278+
artLoc["uri"] = "manifest"
279+
}
260280
artLoc["uri"] = "manifest"
261281
}
262282
}
263283
}
264284
}
265285

286+
// normalizeSuppressions removes suppressions from results for consistent comparison
287+
func normalizeSuppressions(run map[string]interface{}) {
288+
results, ok := run["results"].([]interface{})
289+
if !ok {
290+
return
291+
}
292+
for _, resultInterface := range results {
293+
result, ok := resultInterface.(map[string]interface{})
294+
if !ok {
295+
continue
296+
}
297+
delete(result, "suppressions")
298+
}
299+
}
300+
301+
// normalizeFixes keeps only fixes that are present in expected SARIF.
302+
// This handles cases where the actual output has additional fixes that weren't in the expected output.
303+
func normalizeFixes(t *testing.T, expectedSarif, actualSarif map[string]interface{}) {
304+
t.Helper()
305+
306+
expectedRuns, okExp := expectedSarif["runs"].([]interface{})
307+
actualRuns, okAct := actualSarif["runs"].([]interface{})
308+
if !okExp || !okAct {
309+
return
310+
}
311+
312+
// Build a map of expected fixes by ruleId for quick lookup
313+
expectedFixesByRuleID := buildExpectedFixesMap(expectedRuns)
314+
315+
// Process actual runs and filter fixes
316+
filterActualFixesByExpected(t, actualRuns, expectedFixesByRuleID)
317+
}
318+
319+
// buildExpectedFixesMap builds a map of expected fixes by ruleId for quick lookup.
320+
func buildExpectedFixesMap(expectedRuns []interface{}) map[string][]interface{} {
321+
expectedFixesByRuleID := make(map[string][]interface{})
322+
for _, runInterface := range expectedRuns {
323+
run, ok := runInterface.(map[string]interface{})
324+
if !ok {
325+
continue
326+
}
327+
results, ok := run["results"].([]interface{})
328+
if !ok {
329+
continue
330+
}
331+
for _, resultInterface := range results {
332+
result, ok := resultInterface.(map[string]interface{})
333+
if !ok {
334+
continue
335+
}
336+
ruleID, ok := result["ruleId"].(string)
337+
if !ok {
338+
continue
339+
}
340+
fixes, ok := result["fixes"].([]interface{})
341+
if ok && len(fixes) > 0 {
342+
expectedFixesByRuleID[ruleID] = fixes
343+
}
344+
}
345+
}
346+
return expectedFixesByRuleID
347+
}
348+
349+
// filterActualFixesByExpected filters actual runs' fixes to only include those present in expected.
350+
func filterActualFixesByExpected(t *testing.T, actualRuns []interface{}, expectedFixesByRuleID map[string][]interface{}) {
351+
t.Helper()
352+
353+
for _, runInterface := range actualRuns {
354+
run, ok := runInterface.(map[string]interface{})
355+
if !ok {
356+
continue
357+
}
358+
results, ok := run["results"].([]interface{})
359+
if !ok {
360+
continue
361+
}
362+
processResultFixes(t, results, expectedFixesByRuleID)
363+
}
364+
}
365+
366+
// processResultFixes processes each result's fixes and filters them based on expected fixes.
367+
func processResultFixes(t *testing.T, results []interface{}, expectedFixesByRuleID map[string][]interface{}) {
368+
t.Helper()
369+
370+
for _, resultInterface := range results {
371+
result, ok := resultInterface.(map[string]interface{})
372+
if !ok {
373+
continue
374+
}
375+
ruleID, ok := result["ruleId"].(string)
376+
if !ok {
377+
continue
378+
}
379+
actualFixes, ok := result["fixes"].([]interface{})
380+
if !ok || len(actualFixes) == 0 {
381+
continue
382+
}
383+
384+
expectedFixes, hasExpectedFixes := expectedFixesByRuleID[ruleID]
385+
if !hasExpectedFixes {
386+
// No expected fixes for this rule, remove all fixes from actual
387+
delete(result, "fixes")
388+
continue
389+
}
390+
391+
// Filter actual fixes to only include those present in expected
392+
filteredFixes := filterFixesByExpected(t, actualFixes, expectedFixes)
393+
if len(filteredFixes) == 0 {
394+
delete(result, "fixes")
395+
} else {
396+
result["fixes"] = filteredFixes
397+
}
398+
}
399+
}
400+
401+
// filterFixesByExpected filters actual fixes to only include those present in expected fixes.
402+
// Fixes are compared by marshaling to JSON for simple equality check.
403+
func filterFixesByExpected(t *testing.T, actualFixes, expectedFixes []interface{}) []interface{} {
404+
t.Helper()
405+
406+
// Create a set of expected fix JSON strings for comparison
407+
expectedFixSet := make(map[string]bool)
408+
for _, expectedFix := range expectedFixes {
409+
fixJSON, err := json.Marshal(expectedFix)
410+
if err != nil {
411+
t.Logf("Failed to marshal expected fix: %v", err)
412+
continue
413+
}
414+
expectedFixSet[string(fixJSON)] = true
415+
}
416+
417+
// Filter actual fixes to only include those in expected set
418+
filteredFixes := make([]interface{}, 0)
419+
for _, actualFix := range actualFixes {
420+
fixJSON, err := json.Marshal(actualFix)
421+
if err != nil {
422+
t.Logf("Failed to marshal actual fix: %v", err)
423+
continue
424+
}
425+
if expectedFixSet[string(fixJSON)] {
426+
filteredFixes = append(filteredFixes, actualFix)
427+
}
428+
}
429+
430+
return filteredFixes
431+
}
432+
266433
// sortTags sorts tags alphabetically for consistent comparison
267434
func sortTags(tags []interface{}) {
268435
sort.Slice(tags, func(i, j int) bool {
@@ -289,12 +456,19 @@ func normalizeSarifForComparison(t *testing.T, sarifJSON string) map[string]inte
289456
return sarif
290457
}
291458

459+
// TODO: preserve SARIF runs even if there are no findings
460+
filteredRuns := make([]interface{}, 0)
292461
for _, runInterface := range runs {
293462
run, ok := runInterface.(map[string]interface{})
294463
if !ok {
295464
continue
296465
}
297466

467+
results, ok := run["results"].([]interface{})
468+
if !ok || len(results) == 0 {
469+
continue
470+
}
471+
298472
// Normalize automation ID (missing project name in actual output)
299473
normalizeAutomationID(run)
300474

@@ -306,8 +480,14 @@ func normalizeSarifForComparison(t *testing.T, sarifJSON string) map[string]inte
306480

307481
// Normalize help content (test data may have different vulnerability descriptions)
308482
normalizeHelpContent(run)
483+
484+
// Normalize suppressions (not included in original SARIF)
485+
normalizeSuppressions(run)
486+
487+
filteredRuns = append(filteredRuns, run)
309488
}
310489

490+
sarif["runs"] = filteredRuns
311491
return sarif
312492
}
313493

@@ -329,56 +509,72 @@ func Test_UfmPresenter_Sarif(t *testing.T) {
329509
expectedSarifPath: "testdata/ufm/webgoat.sarif.json",
330510
testResultPath: "testdata/ufm/webgoat.testresult.json",
331511
},
512+
{
513+
name: "webgoat with suppression",
514+
expectedSarifPath: "testdata/ufm/webgoat.ignore.sarif.json",
515+
testResultPath: "testdata/ufm/webgoat.ignore.testresult.json",
516+
},
517+
{
518+
name: "multiproject",
519+
expectedSarifPath: "testdata/ufm/multi_project.sarif.json",
520+
testResultPath: "testdata/ufm/multi_project.testresult.json",
521+
},
332522
}
333523

334524
for _, tc := range testCases {
335-
expectedSarifBytes, err := os.ReadFile(tc.expectedSarifPath)
336-
assert.NoError(t, err)
337-
338-
testResultBytes, err := os.ReadFile(tc.testResultPath)
339-
assert.NoError(t, err)
340-
341-
testResult, err := ufm.NewSerializableTestResultFromBytes(testResultBytes)
342-
assert.NoError(t, err)
343-
assert.Equal(t, 1, len(testResult))
344-
345-
config := configuration.NewWithOpts()
346-
347-
writer := &bytes.Buffer{}
348-
349-
presenter := presenters.NewUfmRenderer(testResult, config, writer, presenters.UfmWithRuntimeInfo(ri))
350-
err = presenter.RenderTemplate(presenters.ApplicationSarifTemplatesUfm, presenters.ApplicationSarifMimeType)
351-
assert.NoError(t, err)
352-
353-
validateSarifData(t, writer.Bytes())
354-
355-
// Normalize both expected and actual SARIF to ignore known gaps while testing implemented features
356-
expectedNormalized := normalizeSarifForComparison(t, string(expectedSarifBytes))
357-
actualNormalized := normalizeSarifForComparison(t, writer.String())
358-
359-
// Convert back to JSON for comparison
360-
expectedJSON, err := json.MarshalIndent(expectedNormalized, "", " ")
361-
assert.NoError(t, err)
362-
actualJSON, err := json.MarshalIndent(actualNormalized, "", " ")
363-
assert.NoError(t, err)
364-
365-
// Write to temp files for debugging if test fails
366-
if !assert.JSONEq(t, string(expectedJSON), string(actualJSON),
367-
"SARIF output differs. Known gaps are normalized:\n"+
368-
"- Automation ID: missing project name\n"+
369-
"- Tool properties: missing artifactsScanned\n"+
370-
"- Fix packages: using vulnerable package instead of direct dependency\n"+
371-
"- Package versions: may differ based on dependency path selection\n"+
372-
"- Dependency path ordering: paths are sorted but may differ from original") {
373-
// Write files for debugging (best effort, ignore errors)
374-
if err := os.WriteFile(fmt.Sprintf("/tmp/%s_expected_normalized.json", tc.name), expectedJSON, 0644); err != nil {
375-
t.Logf("Failed to write expected output: %v", err)
376-
}
377-
if err := os.WriteFile(fmt.Sprintf("/tmp/%s_actual_normalized.json", tc.name), actualJSON, 0644); err != nil {
378-
t.Logf("Failed to write actual output: %v", err)
525+
t.Run(tc.name, func(t *testing.T) {
526+
expectedSarifBytes, err := os.ReadFile(tc.expectedSarifPath)
527+
assert.NoError(t, err)
528+
529+
testResultBytes, err := os.ReadFile(tc.testResultPath)
530+
assert.NoError(t, err)
531+
532+
testResult, err := ufm.NewSerializableTestResultFromBytes(testResultBytes)
533+
assert.NoError(t, err)
534+
// assert.Equal(t, 1, len(testResult))
535+
536+
config := configuration.NewWithOpts()
537+
538+
writer := &bytes.Buffer{}
539+
540+
presenter := presenters.NewUfmRenderer(testResult, config, writer, presenters.UfmWithRuntimeInfo(ri))
541+
err = presenter.RenderTemplate(presenters.ApplicationSarifTemplatesUfm, presenters.ApplicationSarifMimeType)
542+
assert.NoError(t, err)
543+
544+
validateSarifData(t, writer.Bytes())
545+
546+
// Normalize both expected and actual SARIF to ignore known gaps while testing implemented features
547+
expectedNormalized := normalizeSarifForComparison(t, string(expectedSarifBytes))
548+
actualNormalized := normalizeSarifForComparison(t, writer.String())
549+
550+
// Normalize fixes to only keep fixes that are present in expected SARIF
551+
normalizeFixes(t, expectedNormalized, actualNormalized)
552+
553+
// Convert back to JSON for comparison
554+
expectedJSON, err := json.MarshalIndent(expectedNormalized, "", " ")
555+
assert.NoError(t, err)
556+
actualJSON, err := json.MarshalIndent(actualNormalized, "", " ")
557+
assert.NoError(t, err)
558+
559+
// Write to temp files for debugging if test fails
560+
if !assert.JSONEq(t, string(expectedJSON), string(actualJSON),
561+
"SARIF output differs. Known gaps are normalized:\n"+
562+
"- Automation ID: missing project name\n"+
563+
"- Tool properties: missing artifactsScanned\n"+
564+
"- Suppressions: not included in original SARIF\n"+
565+
"- Fix packages: using vulnerable package instead of direct dependency\n"+
566+
"- Package versions: may differ based on dependency path selection\n"+
567+
"- Dependency path ordering: paths are sorted but may differ from original") {
568+
// Write files for debugging (best effort, ignore errors)
569+
if err := os.WriteFile(fmt.Sprintf("/tmp/%s_expected_normalized.json", tc.name), expectedJSON, 0644); err != nil {
570+
t.Logf("Failed to write expected output: %v", err)
571+
}
572+
if err := os.WriteFile(fmt.Sprintf("/tmp/%s_actual_normalized.json", tc.name), actualJSON, 0644); err != nil {
573+
t.Logf("Failed to write actual output: %v", err)
574+
}
575+
t.Log("Wrote normalized outputs to /tmp/expected_normalized.json and /tmp/actual_normalized.json for debugging")
379576
}
380-
t.Log("Wrote normalized outputs to /tmp/expected_normalized.json and /tmp/actual_normalized.json for debugging")
381-
}
577+
})
382578
}
383579
}
384580

0 commit comments

Comments
 (0)