Skip to content

Commit 985017b

Browse files
mbarberoclaudeCopilot
authored
Fix SARIF formatter silently dropping findings and missing locations (#393)
The SARIF formatter only iterated PackageDependencies when collecting findings, causing all findings from BuildDependencies to be silently omitted. Additionally, two rego rules produced findings with purls that could never match a package in the formatter's purl-based lookup: - github_action_from_unverified_creator_used used the action purl as the finding identifier and emitted coarse "Used in N repo(s)" aggregates with no file path or line number, resulting in empty SARIF locations. Rewritten to emit per-step findings with path, line, job, step, and event triggers, keyed by pkg.purl. - known_vulnerability_in_build_platform used input.provider (e.g. "gitlab") as the finding purl. Changed to use pkg.purl so findings are discoverable by the formatter. The formatter now iterates both PackageDependencies and BuildDependencies for purl lookup, with deduplication via a seen set, and avoids append into a foreign slice's backing array. Fixes #390 Signed-off-by: Mikaël Barbero <mikael.barbero@eclipse-foundation.org> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 4d07fc4 commit 985017b

5 files changed

Lines changed: 199 additions & 18 deletions

File tree

formatters/sarif/sarif.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,17 @@ func (f *Format) Format(ctx context.Context, packages []*models.PackageInsights)
9999
}
100100

101101
pkgFindings := findingsByPurl[pkg.Purl]
102-
for _, depPurl := range pkg.PackageDependencies {
103-
normalizedDepPurl := normalizePurl(depPurl)
104-
if depFindings, exists := findingsByPurl[normalizedDepPurl]; exists {
105-
pkgFindings = append(pkgFindings, depFindings...)
102+
seenPurls := map[string]bool{normalizePurl(pkg.Purl): true}
103+
for _, depSlice := range [][]string{pkg.PackageDependencies, pkg.BuildDependencies} {
104+
for _, depPurl := range depSlice {
105+
normalizedDepPurl := normalizePurl(depPurl)
106+
if seenPurls[normalizedDepPurl] {
107+
continue
108+
}
109+
seenPurls[normalizedDepPurl] = true
110+
if depFindings, exists := findingsByPurl[normalizedDepPurl]; exists {
111+
pkgFindings = append(pkgFindings, depFindings...)
112+
}
106113
}
107114
}
108115

formatters/sarif/sarif_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,92 @@ import (
1313
"github.com/stretchr/testify/require"
1414
)
1515

16+
// TestSarifFormatBuildDependencyFindings validates that findings keyed by a
17+
// build dependency purl are included in the SARIF output. This exercises
18+
// the formatter's BuildDependencies lookup path.
19+
func TestSarifFormatBuildDependencyFindings(t *testing.T) {
20+
actionPurl := "pkg:githubactions/unverified-owner/some-action"
21+
pkgPurl := "pkg:github/test/repo@main"
22+
pkg := &models.PackageInsights{
23+
Purl: pkgPurl,
24+
SourceGitRepo: "test/repo",
25+
SourceGitRef: "main",
26+
SourceScmType: "github",
27+
BuildDependencies: []string{actionPurl + "@v1.0"},
28+
FindingsResults: results.FindingsResult{
29+
Findings: []results.Finding{
30+
{
31+
RuleId: "github_action_from_unverified_creator_used",
32+
// Finding keyed by the build dependency purl (version stripped
33+
// by the rego rule). This tests the BuildDependencies lookup.
34+
Purl: actionPurl,
35+
Meta: results.FindingMeta{
36+
Path: ".github/workflows/ci.yml",
37+
Line: 5,
38+
Job: "build",
39+
Step: "2",
40+
Details: "unverified-owner/some-action@v1.0",
41+
},
42+
},
43+
},
44+
Rules: map[string]results.Rule{
45+
"github_action_from_unverified_creator_used": {
46+
Id: "github_action_from_unverified_creator_used",
47+
Title: "Github Action from Unverified Creator used",
48+
Description: "Usage of GitHub Actions from unverified creators was detected.",
49+
Level: "note",
50+
},
51+
},
52+
},
53+
}
54+
55+
var buf bytes.Buffer
56+
formatter := NewFormat(&buf, "1.0.0")
57+
err := formatter.Format(context.Background(), []*models.PackageInsights{pkg})
58+
require.NoError(t, err)
59+
60+
var sarifOutput map[string]interface{}
61+
err = json.Unmarshal(buf.Bytes(), &sarifOutput)
62+
require.NoError(t, err)
63+
64+
runs, ok := sarifOutput["runs"].([]interface{})
65+
require.True(t, ok)
66+
require.Len(t, runs, 1)
67+
68+
run, ok := runs[0].(map[string]interface{})
69+
require.True(t, ok)
70+
71+
sarifResults, ok := run["results"].([]interface{})
72+
require.True(t, ok, "results should be present in the SARIF run")
73+
require.Len(t, sarifResults, 1, "build dependency finding should appear in SARIF output")
74+
75+
result, ok := sarifResults[0].(map[string]interface{})
76+
require.True(t, ok)
77+
require.Equal(t, "github_action_from_unverified_creator_used", result["ruleId"])
78+
79+
locations, ok := result["locations"].([]interface{})
80+
require.True(t, ok, "locations should be present")
81+
require.Len(t, locations, 1)
82+
83+
location, ok := locations[0].(map[string]interface{})
84+
require.True(t, ok)
85+
86+
physicalLocation, ok := location["physicalLocation"].(map[string]interface{})
87+
require.True(t, ok)
88+
89+
artifactLocation, ok := physicalLocation["artifactLocation"].(map[string]interface{})
90+
require.True(t, ok)
91+
uri, ok := artifactLocation["uri"].(string)
92+
require.True(t, ok, "uri should be a string")
93+
require.Equal(t, ".github/workflows/ci.yml", uri, "uri should be the workflow file path")
94+
95+
region, ok := physicalLocation["region"].(map[string]interface{})
96+
require.True(t, ok)
97+
startLine, ok := region["startLine"].(float64)
98+
require.True(t, ok, "startLine should be a number")
99+
require.InDelta(t, float64(5), startLine, 0.0001, "startLine should match the finding line")
100+
}
101+
16102
func TestSarifFormat(t *testing.T) {
17103
// Create a test package with findings
18104
pkg := &models.PackageInsights{

opa/rego/rules/github_action_from_unverified_creator_used.rego

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,34 @@ github_verified_partners contains p if some p in ["1password", "42crunch", "acti
1717
# Consider input package namespaces as verified
1818
github_verified_partners contains input.packages[_].package_namespace
1919

20-
results contains poutine.finding(
21-
rule,
22-
repo_purl,
23-
{"details": sprintf("Used in %d repo(s)", [count(unverified_github_actions[repo_purl])])},
24-
)
25-
26-
unverified_github_actions[action_repo] contains pkg.purl if {
20+
results contains poutine.finding(rule, pkg.purl, {
21+
"path": workflow.path,
22+
"line": step.lines.uses,
23+
"job": job.id,
24+
"step": i,
25+
"details": step.uses,
26+
"event_triggers": [event | event := workflow.events[j].name],
27+
}) if {
2728
pkg := input.packages[_]
28-
dep := array.concat(pkg.build_dependencies, pkg.package_dependencies)[_]
29+
workflow := pkg.github_actions_workflows[_]
30+
job := workflow.jobs[_]
31+
step := job.steps[i]
32+
dep := purl.parse_github_actions(step.uses, pkg.source_git_repo, pkg.source_git_ref)
2933
startswith(dep, "pkg:githubactions/")
34+
not regex.match(sprintf("pkg:githubactions/(%s)/", [concat("|", github_verified_partners)]), dep)
35+
}
3036

31-
action_repo := split(dep, "@")[0]
37+
results contains poutine.finding(rule, pkg.purl, {
38+
"path": action.path,
39+
"line": step.lines.uses,
40+
"step": i,
41+
"details": step.uses,
42+
}) if {
43+
pkg := input.packages[_]
44+
action := pkg.github_actions_metadata[_]
45+
action.runs.using == "composite"
46+
step := action.runs.steps[i]
47+
dep := purl.parse_github_actions(step.uses, pkg.source_git_repo, pkg.source_git_ref)
48+
startswith(dep, "pkg:githubactions/")
3249
not regex.match(sprintf("pkg:githubactions/(%s)/", [concat("|", github_verified_partners)]), dep)
3350
}

opa/rego/rules/known_vulnerability_in_build_platform.rego

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ import rego.v1
1515

1616
rule := poutine.rule(rego.metadata.chain())
1717

18-
results contains poutine.finding(rule, input.provider, {
18+
results contains poutine.finding(rule, pkg.purl, {
1919
"osv_id": advisory.osv_id,
2020
"details": sprintf("Provider: %s", [input.provider]),
2121
}) if {
22+
pkg := input.packages[_]
2223
advisory := advisories[input.provider][osv_id]
2324
regex.match("^[0-9]+(\\.[0-9]+)*?$", input.version)
2425
semver.constraint_check(advisory.vulnerable_version_ranges[_], input.version)

scanner/inventory_test.go

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,38 @@ func TestFindings(t *testing.T) {
335335
},
336336
{
337337
RuleId: "github_action_from_unverified_creator_used",
338-
Purl: "pkg:githubactions/kartverket/github-workflows",
338+
Purl: purl,
339339
Meta: results.FindingMeta{
340-
Details: "Used in 1 repo(s)",
340+
Path: ".github/workflows/valid.yml",
341+
Line: 35,
342+
Job: "build",
343+
Step: "4",
344+
Details: "kartverket/github-workflows/.github/workflows/run-terraform.yml@main",
345+
EventTriggers: []string{"push", "pull_request_target"},
346+
},
347+
},
348+
{
349+
RuleId: "github_action_from_unverified_creator_used",
350+
Purl: purl,
351+
Meta: results.FindingMeta{
352+
Path: ".github/workflows/valid.yml",
353+
Line: 39,
354+
Job: "build",
355+
Step: "5",
356+
Details: "kartverket/github-workflows/.github/workflows/run-terraform.yml@v2.7.1",
357+
EventTriggers: []string{"push", "pull_request_target"},
358+
},
359+
},
360+
{
361+
RuleId: "github_action_from_unverified_creator_used",
362+
Purl: purl,
363+
Meta: results.FindingMeta{
364+
Path: ".github/workflows/valid.yml",
365+
Line: 43,
366+
Job: "build",
367+
Step: "6",
368+
Details: "kartverket/github-workflows/.github/workflows/run-terraform.yml@v2.2",
369+
EventTriggers: []string{"push", "pull_request_target"},
341370
},
342371
},
343372
{
@@ -617,9 +646,14 @@ func TestFindings(t *testing.T) {
617646
},
618647
{
619648
RuleId: "github_action_from_unverified_creator_used",
620-
Purl: "pkg:githubactions/some/action",
649+
Purl: purl,
621650
Meta: results.FindingMeta{
622-
Details: "Used in 1 repo(s)",
651+
Path: ".github/workflows/test_new_fields.yml",
652+
Line: 44,
653+
Job: "vulnerable_checkout",
654+
Step: "3",
655+
Details: "some/action@v1",
656+
EventTriggers: []string{"pull_request_target"},
623657
},
624658
},
625659
{
@@ -747,6 +781,42 @@ func TestRulesConfig(t *testing.T) {
747781
assert.Empty(t, labels)
748782
}
749783

784+
// TestKnownVulnerabilityInBuildPlatformFinding validates that findings from the
785+
// known_vulnerability_in_build_platform rule use pkg.purl as the finding purl (not
786+
// input.provider), so they are not silently dropped from SARIF output.
787+
func TestKnownVulnerabilityInBuildPlatformFinding(t *testing.T) {
788+
o, _ := opa.NewOpa(context.TODO(), &models.Config{
789+
Include: []models.ConfigInclude{},
790+
})
791+
// provider="gitlab", version="17.3.0" matches advisory PVE-2024-00001 (>=17.3, <17.3.3)
792+
i := NewInventory(o, nil, "gitlab", "17.3.0")
793+
purl := "pkg:github/org/owner"
794+
pkg := &models.PackageInsights{
795+
Purl: purl,
796+
SourceGitRepo: "org/owner",
797+
SourceGitRef: "main",
798+
}
799+
_ = pkg.NormalizePurl()
800+
801+
scannedPackage, err := i.ScanPackage(context.Background(), *pkg, "testdata")
802+
require.NoError(t, err)
803+
804+
var platformFinding *results.Finding
805+
for idx, f := range scannedPackage.FindingsResults.Findings {
806+
if f.RuleId == "known_vulnerability_in_build_platform" {
807+
platformFinding = &scannedPackage.FindingsResults.Findings[idx]
808+
break
809+
}
810+
}
811+
812+
require.NotNil(t, platformFinding, "expected a known_vulnerability_in_build_platform finding")
813+
// The finding purl must be pkg.purl (not input.provider like "gitlab"), so it
814+
// is not dropped from SARIF output by the formatter's purl-based lookup.
815+
assert.Equal(t, purl, platformFinding.Purl, "finding purl should be pkg.purl, not the provider string")
816+
assert.NotEmpty(t, platformFinding.Meta.OsvId, "osv_id should be populated")
817+
assert.NotEmpty(t, platformFinding.Meta.Details, "details should be populated")
818+
}
819+
750820
func TestStructuredFindingFields(t *testing.T) {
751821
o, _ := opa.NewOpa(context.TODO(), &models.Config{
752822
Include: []models.ConfigInclude{},

0 commit comments

Comments
 (0)