From 866dab6618683c5e8f85743aea342611cbb798c2 Mon Sep 17 00:00:00 2001 From: Alexis-Maurer Fortin Date: Wed, 18 Sep 2024 17:27:22 -0400 Subject: [PATCH 1/3] resolving local githubactions s that we have complete purls for them --- models/purl.go | 30 ++++++++++++------- models/purl_test.go | 21 +++++++------ opa/builtins.go | 18 ++++++++--- opa/opa_test.go | 7 +++-- .../poutine/inventory/github_actions.rego | 12 +++----- scanner/inventory_test.go | 21 ++++++++----- 6 files changed, 68 insertions(+), 41 deletions(-) diff --git a/models/purl.go b/models/purl.go index 1095e27..85d1f23 100644 --- a/models/purl.go +++ b/models/purl.go @@ -27,8 +27,10 @@ func (p *Purl) Normalize() { ns += "/" } parts := strings.SplitN(ns+p.Name, "/", 3) - p.Namespace = strings.ToLower(parts[0]) - p.Name = strings.ToLower(parts[1]) + if len(parts) >= 2 { + p.Namespace = strings.ToLower(parts[0]) + p.Name = strings.ToLower(parts[1]) + } if len(parts) == 3 { p.Subpath = parts[2] @@ -71,16 +73,24 @@ func PurlFromDockerImage(image string) (Purl, error) { return Purl{PackageURL: purl}, err } -func PurlFromGithubActions(uses string) (Purl, error) { +func PurlFromGithubActions(uses string, sourceGitRepo string, sourceGitRef string) (Purl, error) { purl := Purl{} if len(uses) == 0 { return purl, fmt.Errorf("invalid uses string") } - is_local := uses[0] == '.' - if is_local { - return purl, fmt.Errorf("local actions are not supported") + isLocal := uses[0] == '.' + if isLocal { + subPath := uses[2:] + purl.Subpath = subPath + purl.Type = "githubactions" + + purl.Name = sourceGitRepo + purl.Version = sourceGitRef + + purl.Normalize() + return purl, nil } if strings.HasPrefix(uses, "docker://") { @@ -94,12 +104,12 @@ func PurlFromGithubActions(uses string) (Purl, error) { return purl, fmt.Errorf("invalid uses string") } - action_name := parts[0] - action_version := parts[1] + actionName := parts[0] + actionVersion := parts[1] purl.Type = "githubactions" - purl.Name = action_name - purl.Version = action_version + purl.Name = actionName + purl.Version = actionVersion purl.Normalize() return purl, nil diff --git a/models/purl_test.go b/models/purl_test.go index 148b64e..1ea9267 100644 --- a/models/purl_test.go +++ b/models/purl_test.go @@ -36,9 +36,11 @@ func TestNewPurl(t *testing.T) { func TestPurlFromGithubActions(t *testing.T) { cases := []struct { - uses string - expected string - error bool + uses string + sourceGitRepo string + sourceGitRef string + expected string + error bool }{ { @@ -49,11 +51,6 @@ func TestPurlFromGithubActions(t *testing.T) { uses: "github/codeql-action/Analyze@v4", expected: "pkg:githubactions/github/codeql-action@v4#Analyze", }, - { - uses: "./.github/actions/custom", - expected: "", - error: true, - }, { uses: "docker://alpine:latest", expected: "pkg:docker/alpine%3Alatest", @@ -74,10 +71,16 @@ func TestPurlFromGithubActions(t *testing.T) { uses: "invalid", error: true, }, + { + uses: "./.github/workflows/trigger_dep_builds.yml", + sourceGitRepo: "FasterXML/jackson-databind", + sourceGitRef: "2.18", + expected: "pkg:githubactions/fasterxml/jackson-databind@2.18#.github/workflows/trigger_dep_builds.yml", + }, } for _, c := range cases { - p, err := PurlFromGithubActions(c.uses) + p, err := PurlFromGithubActions(c.uses, c.sourceGitRepo, c.sourceGitRef) if !c.error { assert.Nil(t, err) diff --git a/opa/builtins.go b/opa/builtins.go index 913a8ab..388690c 100644 --- a/opa/builtins.go +++ b/opa/builtins.go @@ -29,18 +29,28 @@ func registerBuiltinFunctions() { }, ) - rego.RegisterBuiltin1( + rego.RegisterBuiltin3( ®o.Function{ Name: "purl.parse_github_actions", - Decl: types.NewFunction(types.Args(types.S), types.S), + Decl: types.NewFunction(types.Args(types.S, types.S, types.S), types.S), }, - func(_ rego.BuiltinContext, a *ast.Term) (*ast.Term, error) { + func(_ rego.BuiltinContext, a *ast.Term, b *ast.Term, c *ast.Term) (*ast.Term, error) { var uses string if err := ast.As(a.Value, &uses); err != nil { return nil, err } - purl, err := models.PurlFromGithubActions(uses) + var sourceGitRepo string + if err := ast.As(b.Value, &sourceGitRepo); err != nil { + return nil, err + } + + var sourceGitRef string + if err := ast.As(c.Value, &sourceGitRef); err != nil { + return nil, err + } + + purl, err := models.PurlFromGithubActions(uses, sourceGitRepo, sourceGitRef) if err != nil { return nil, err } diff --git a/opa/opa_test.go b/opa/opa_test.go index 841cd1f..48fb1a2 100644 --- a/opa/opa_test.go +++ b/opa/opa_test.go @@ -33,12 +33,12 @@ func TestOpaBuiltins(t *testing.T) { }{ { builtin: "purl.parse_github_actions", - input: "actions/checkout@v4", + input: `"actions/checkout@v4","",""`, expected: "pkg:githubactions/actions/checkout@v4", }, { builtin: "purl.parse_docker_image", - input: "alpine:latest", + input: `"alpine:latest"`, expected: "pkg:docker/alpine%3Alatest", }, } @@ -50,7 +50,8 @@ func TestOpaBuiltins(t *testing.T) { for _, c := range cases { var result interface{} - err := opa.Eval(context.TODO(), c.builtin+"(\""+c.input+"\")", nil, &result) + query := fmt.Sprintf(`%s(%s)`, c.builtin, c.input) + err := opa.Eval(context.TODO(), query, nil, &result) noOpaErrors(t, err) assert.Equal(t, c.expected, result) diff --git a/opa/rego/poutine/inventory/github_actions.rego b/opa/rego/poutine/inventory/github_actions.rego index 9c383c0..a1d758a 100644 --- a/opa/rego/poutine/inventory/github_actions.rego +++ b/opa/rego/poutine/inventory/github_actions.rego @@ -7,8 +7,7 @@ import data.poutine.utils build_dependencies contains dep if { pkg := input.packages[_] step := pkg.github_actions_workflows[_].jobs[_].steps[_] - - dep := purl.parse_github_actions(step.uses) + dep := purl.parse_github_actions(step.uses, pkg.source_git_repo, pkg.source_git_ref) } build_dependencies contains dep if { @@ -24,15 +23,13 @@ build_dependencies contains dep if { job := pkg.github_actions_workflows[_].jobs[_] uses := job.uses not utils.empty(uses) - - dep := purl.parse_github_actions(uses) + dep := purl.parse_github_actions(uses, pkg.source_git_repo, pkg.source_git_ref) } package_dependencies contains dep if { pkg := input.packages[_] step := pkg.github_actions_metadata[_].runs.steps[_] - - dep := purl.parse_github_actions(step.uses) + dep := purl.parse_github_actions(step.uses, pkg.source_git_repo, pkg.source_git_ref) } package_dependencies contains dep if { @@ -41,6 +38,5 @@ package_dependencies contains dep if { runs.using == "docker" startswith(runs.image, "docker://") - - dep := purl.parse_github_actions(runs.image) + dep := purl.parse_github_actions(runs.image, pkg.source_git_repo, pkg.source_git_ref) } diff --git a/scanner/inventory_test.go b/scanner/inventory_test.go index db53073..6ebe946 100644 --- a/scanner/inventory_test.go +++ b/scanner/inventory_test.go @@ -16,7 +16,9 @@ func TestPurls(t *testing.T) { }) i := NewInventory(o, nil, "", "") pkg := &models.PackageInsights{ - Purl: "pkg:github/org/owner", + Purl: "pkg:github/org/owner", + SourceGitRepo: "org/owner", + SourceGitRef: "main", } _ = pkg.NormalizePurl() scannedPackage, err := i.ScanPackage(context.Background(), *pkg, "testdata") @@ -39,17 +41,16 @@ func TestPurls(t *testing.T) { "pkg:gitlabci/include/project?file_name=%2Ftemplates%2F.gitlab-ci-template.yml&project=my-group%2Fmy-project&ref=main", "pkg:gitlabci/include/remote?download_url=https%3A%2F%2Fexample.com%2F.gitlab-ci.yml", "pkg:gitlabci/include/component?project=my-org%2Fsecurity-components%2Fsecret-detection&ref=1.0&repository_url=gitlab.example.com", - // "pkg:gitlabci/include/local?file_name=%2F.local-ci-template.yml", - // "pkg:gitlabci/include/local?file_name=.gitlab-ci.yml", "pkg:githubactions/org/repo@main", "pkg:docker/debian%3Avuln", "pkg:githubactions/bridgecrewio/checkov-action@main", "pkg:githubactions/org/repo@main#.github/workflows/Reusable.yml", "pkg:azurepipelinestask/DownloadPipelineArtifact@2", "pkg:azurepipelinestask/Cache@2", + "pkg:githubactions/org/owner@main#.github/workflows/ci.yml", } assert.ElementsMatch(t, i.Purls(*scannedPackage), purls) - assert.Equal(t, 18, len(scannedPackage.BuildDependencies)) + assert.Equal(t, 19, len(scannedPackage.BuildDependencies)) assert.Equal(t, 4, len(scannedPackage.PackageDependencies)) } @@ -60,7 +61,9 @@ func TestFindings(t *testing.T) { i := NewInventory(o, nil, "gitlab", "") purl := "pkg:github/org/owner" pkg := &models.PackageInsights{ - Purl: purl, + Purl: purl, + SourceGitRepo: "org/owner", + SourceGitRef: "main", } _ = pkg.NormalizePurl() @@ -436,7 +439,9 @@ func TestSkipRule(t *testing.T) { purl := "pkg:github/org/owner" rule_id := "known_vulnerability_in_build_component" pkg := &models.PackageInsights{ - Purl: purl, + Purl: purl, + SourceGitRepo: "org/owner", + SourceGitRef: "main", } _ = pkg.NormalizePurl() @@ -484,7 +489,9 @@ func TestRulesConfig(t *testing.T) { rule_id := "pr_runs_on_self_hosted" path := ".github/workflows/allowed_pr_runner.yml" pkg := &models.PackageInsights{ - Purl: purl, + Purl: purl, + SourceGitRepo: "org/owner", + SourceGitRef: "main", } _ = pkg.NormalizePurl() From c74b4895815eeb81e1384b927011ce965bc09abd Mon Sep 17 00:00:00 2001 From: Alexis-Maurer Fortin Date: Wed, 18 Sep 2024 17:38:00 -0400 Subject: [PATCH 2/3] fix unpinnable action rule --- opa/rego/rules/unpinnable_action.rego | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opa/rego/rules/unpinnable_action.rego b/opa/rego/rules/unpinnable_action.rego index 427704e..535b627 100644 --- a/opa/rego/rules/unpinnable_action.rego +++ b/opa/rego/rules/unpinnable_action.rego @@ -20,7 +20,9 @@ results contains poutine.finding(rule, pkg.purl, { }) if { pkg := input.packages[_] action := pkg.github_actions_metadata[_] - purls := data.poutine.inventory.package_dependencies with input.packages as [{"github_actions_metadata": [action]}] + source_git_repo := pkg.source_git_repo + source_git_ref := pkg.source_git_ref + purls := data.poutine.inventory.package_dependencies with input.packages as [{"github_actions_metadata": [action], "source_git_repo": source_git_repo, "source_git_ref": source_git_ref}] unpinned_purls := [p | p := purls[_] From 6868844a4ffd14d8f0f080d6340edc82cfff1966 Mon Sep 17 00:00:00 2001 From: Alexis-Maurer Fortin Date: Fri, 20 Sep 2024 09:55:59 -0400 Subject: [PATCH 3/3] error out for invalid local resolution --- models/purl.go | 3 +++ models/purl_test.go | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/models/purl.go b/models/purl.go index 85d1f23..5242dfc 100644 --- a/models/purl.go +++ b/models/purl.go @@ -82,6 +82,9 @@ func PurlFromGithubActions(uses string, sourceGitRepo string, sourceGitRef strin isLocal := uses[0] == '.' if isLocal { + if strings.Contains(uses, "..") { + return purl, fmt.Errorf("invalid uses string") + } subPath := uses[2:] purl.Subpath = subPath purl.Type = "githubactions" diff --git a/models/purl_test.go b/models/purl_test.go index 1ea9267..e1057f7 100644 --- a/models/purl_test.go +++ b/models/purl_test.go @@ -77,6 +77,10 @@ func TestPurlFromGithubActions(t *testing.T) { sourceGitRef: "2.18", expected: "pkg:githubactions/fasterxml/jackson-databind@2.18#.github/workflows/trigger_dep_builds.yml", }, + { + uses: "./../action/init", + error: true, + }, } for _, c := range cases {