diff --git a/opa/rego/poutine/queries/findings.rego b/opa/rego/poutine/queries/findings.rego index af350f51..56f9471e 100644 --- a/opa/rego/poutine/queries/findings.rego +++ b/opa/rego/poutine/queries/findings.rego @@ -5,6 +5,29 @@ import rego.v1 rules_by_id[id] = rules[id].rule +_purl_match(finding_purl, skip_purl) if { + finding_purl == skip_purl +} + +# Prefix match with structural purl boundary: ensures that e.g. +# "pkg:githubactions/foo/bar" matches "pkg:githubactions/foo/bar@v1" +# (boundary is @) but not "pkg:githubactions/foo/bar-baz@v1". +_purl_match(finding_purl, skip_purl) if { + startswith(finding_purl, skip_purl) + rest := substring(finding_purl, count(skip_purl), -1) + regex.match("^[@#]", rest) +} + +# No purl constraint in skip rule: purl matches by default. +_skip_purl(_, s) if { + not s.purl +} + +_skip_purl(o, s) if { + skip_purl := s.purl[_] + _purl_match(o.purl, skip_purl) +} + skip(f) if { s := data.config.skip[_] o := object.union( @@ -13,11 +36,12 @@ skip(f) if { "rule": f.rule_id, "level": rules_by_id[rule_id].level, }, - object.filter(f.meta, {"osv_id", "job", "path"}), + object.filter(f.meta, {"osv_id", "job", "path", "purl"}), ) count(s) > 0 - [attr | s[attr]; not o[attr] in s[attr]] == [] + [attr | s[attr]; attr != "purl"; not o[attr] in s[attr]] == [] + _skip_purl(o, s) } skip(f) if { diff --git a/opa/rego/rules/github_action_from_unverified_creator_used.rego b/opa/rego/rules/github_action_from_unverified_creator_used.rego index 0e9641ef..f4e10118 100644 --- a/opa/rego/rules/github_action_from_unverified_creator_used.rego +++ b/opa/rego/rules/github_action_from_unverified_creator_used.rego @@ -23,6 +23,7 @@ results contains poutine.finding(rule, pkg.purl, { "job": job.id, "step": i, "details": step.uses, + "purl": dep, "event_triggers": [event | event := workflow.events[j].name], }) if { pkg := input.packages[_] @@ -39,6 +40,7 @@ results contains poutine.finding(rule, pkg.purl, { "line": step.lines.uses, "step": i, "details": step.uses, + "purl": dep, }) if { pkg := input.packages[_] action := pkg.github_actions_metadata[_] diff --git a/scanner/inventory_test.go b/scanner/inventory_test.go index ab2f8d93..dafce0e6 100644 --- a/scanner/inventory_test.go +++ b/scanner/inventory_test.go @@ -24,7 +24,7 @@ func TestPurls(t *testing.T) { } _ = pkg.NormalizePurl() scannedPackage, err := i.ScanPackage(context.Background(), *pkg, "testdata") - assert.NoError(t, err) + require.NoError(t, err) purls := []string{ "pkg:docker/node%3Alatest", @@ -74,7 +74,7 @@ func TestFindings(t *testing.T) { _ = pkg.NormalizePurl() scannedPackage, err := i.ScanPackage(context.Background(), *pkg, "testdata") - assert.NoError(t, err) + require.NoError(t, err) analysisResults := scannedPackage.FindingsResults @@ -700,7 +700,7 @@ func TestSkipRule(t *testing.T) { _ = pkg.NormalizePurl() updatedPkg, err := i.ScanPackage(ctx, *pkg, "testdata") - assert.NoError(t, err) + require.NoError(t, err) analysisResults := updatedPkg.FindingsResults @@ -718,10 +718,10 @@ func TestSkipRule(t *testing.T) { }, }, }) - assert.NoError(t, err) + require.NoError(t, err) secondUpdatedPkg, err := i.ScanPackage(context.Background(), *pkg, "testdata") - assert.NoError(t, err) + require.NoError(t, err) analysisResults = secondUpdatedPkg.FindingsResults @@ -733,6 +733,86 @@ func TestSkipRule(t *testing.T) { assert.NotContains(t, rule_ids, rule_id) } +func TestSkipRuleByActionPurl(t *testing.T) { + o, _ := opa.NewOpa(context.TODO(), &models.Config{ + Include: []models.ConfigInclude{}, + }) + i := NewInventory(o, nil, "", "") + ctx := context.TODO() + purl := "pkg:github/org/owner" + rule_id := "github_action_from_unverified_creator_used" + pkg := &models.PackageInsights{ + Purl: purl, + SourceGitRepo: "org/owner", + SourceGitRef: "main", + } + _ = pkg.NormalizePurl() + + updatedPkg, err := i.ScanPackage(ctx, *pkg, "testdata") + require.NoError(t, err) + + // Collect findings for the unverified creator rule + var unverifiedFindings []results.Finding + for _, f := range updatedPkg.FindingsResults.Findings { + if f.RuleId == rule_id { + unverifiedFindings = append(unverifiedFindings, f) + } + } + require.NotEmpty(t, unverifiedFindings, "expected unverified creator findings before skip") + + // Test 1: Versionless skip should match ALL versions of kartverket/github-workflows + err = o.WithConfig(ctx, &models.Config{ + Skip: []models.ConfigSkip{ + { + Rule: []string{rule_id}, + Purl: []string{"pkg:githubactions/kartverket/github-workflows"}, + }, + }, + }) + require.NoError(t, err) + + secondUpdatedPkg, err := i.ScanPackage(context.Background(), *pkg, "testdata") + require.NoError(t, err) + + for _, f := range secondUpdatedPkg.FindingsResults.Findings { + if f.RuleId == rule_id { + assert.NotContains(t, f.Meta.Details, "kartverket/github-workflows", + "versionless skip should remove all versions of kartverket/github-workflows") + } + } + + // Test 2: Version-specific skip should only match that exact version + err = o.WithConfig(ctx, &models.Config{ + Skip: []models.ConfigSkip{ + { + Rule: []string{rule_id}, + Purl: []string{"pkg:githubactions/kartverket/github-workflows@v2.7.1"}, + }, + }, + }) + require.NoError(t, err) + + thirdUpdatedPkg, err := i.ScanPackage(context.Background(), *pkg, "testdata") + require.NoError(t, err) + + var remainingDetails []string + for _, f := range thirdUpdatedPkg.FindingsResults.Findings { + if f.RuleId == rule_id { + remainingDetails = append(remainingDetails, f.Meta.Details) + } + } + // v2.7.1 should be skipped, but @main and @v2.2 should remain + for _, d := range remainingDetails { + assert.NotContains(t, d, "@v2.7.1", + "version-specific skip should remove only that version") + } + // Verify other versions are still present + assert.Contains(t, remainingDetails, "kartverket/github-workflows/.github/workflows/run-terraform.yml@main", + "@main should not be skipped by version-specific skip") + assert.Contains(t, remainingDetails, "kartverket/github-workflows/.github/workflows/run-terraform.yml@v2.2", + "@v2.2 should not be skipped by version-specific skip") +} + func TestRulesConfig(t *testing.T) { o, _ := opa.NewOpa(context.TODO(), &models.Config{ Include: []models.ConfigInclude{}, @@ -750,7 +830,7 @@ func TestRulesConfig(t *testing.T) { _ = pkg.NormalizePurl() scannedPackage, err := i.ScanPackage(ctx, *pkg, "testdata") - assert.NoError(t, err) + require.NoError(t, err) labels := []string{} for _, f := range scannedPackage.FindingsResults.Findings { @@ -767,10 +847,10 @@ func TestRulesConfig(t *testing.T) { }, }, }) - assert.NoError(t, err) + require.NoError(t, err) reScannedPackage, err := i.ScanPackage(ctx, *pkg, "testdata") - assert.NoError(t, err) + require.NoError(t, err) labels = []string{} for _, f := range reScannedPackage.FindingsResults.Findings {