Skip to content

Commit eaa5c38

Browse files
authored
Resolve Repo Local Actions (#213)
* resolving local githubactions s that we have complete purls for them * fix unpinnable action rule * error out for invalid local resolution
1 parent 7ec9820 commit eaa5c38

File tree

7 files changed

+78
-42
lines changed

7 files changed

+78
-42
lines changed

models/purl.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ func (p *Purl) Normalize() {
2727
ns += "/"
2828
}
2929
parts := strings.SplitN(ns+p.Name, "/", 3)
30-
p.Namespace = strings.ToLower(parts[0])
31-
p.Name = strings.ToLower(parts[1])
30+
if len(parts) >= 2 {
31+
p.Namespace = strings.ToLower(parts[0])
32+
p.Name = strings.ToLower(parts[1])
33+
}
3234

3335
if len(parts) == 3 {
3436
p.Subpath = parts[2]
@@ -71,16 +73,27 @@ func PurlFromDockerImage(image string) (Purl, error) {
7173
return Purl{PackageURL: purl}, err
7274
}
7375

74-
func PurlFromGithubActions(uses string) (Purl, error) {
76+
func PurlFromGithubActions(uses string, sourceGitRepo string, sourceGitRef string) (Purl, error) {
7577
purl := Purl{}
7678

7779
if len(uses) == 0 {
7880
return purl, fmt.Errorf("invalid uses string")
7981
}
8082

81-
is_local := uses[0] == '.'
82-
if is_local {
83-
return purl, fmt.Errorf("local actions are not supported")
83+
isLocal := uses[0] == '.'
84+
if isLocal {
85+
if strings.Contains(uses, "..") {
86+
return purl, fmt.Errorf("invalid uses string")
87+
}
88+
subPath := uses[2:]
89+
purl.Subpath = subPath
90+
purl.Type = "githubactions"
91+
92+
purl.Name = sourceGitRepo
93+
purl.Version = sourceGitRef
94+
95+
purl.Normalize()
96+
return purl, nil
8497
}
8598

8699
if strings.HasPrefix(uses, "docker://") {
@@ -94,12 +107,12 @@ func PurlFromGithubActions(uses string) (Purl, error) {
94107
return purl, fmt.Errorf("invalid uses string")
95108
}
96109

97-
action_name := parts[0]
98-
action_version := parts[1]
110+
actionName := parts[0]
111+
actionVersion := parts[1]
99112

100113
purl.Type = "githubactions"
101-
purl.Name = action_name
102-
purl.Version = action_version
114+
purl.Name = actionName
115+
purl.Version = actionVersion
103116

104117
purl.Normalize()
105118
return purl, nil

models/purl_test.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ func TestNewPurl(t *testing.T) {
3636

3737
func TestPurlFromGithubActions(t *testing.T) {
3838
cases := []struct {
39-
uses string
40-
expected string
41-
error bool
39+
uses string
40+
sourceGitRepo string
41+
sourceGitRef string
42+
expected string
43+
error bool
4244
}{
4345

4446
{
@@ -49,11 +51,6 @@ func TestPurlFromGithubActions(t *testing.T) {
4951
uses: "github/codeql-action/Analyze@v4",
5052
expected: "pkg:githubactions/github/codeql-action@v4#Analyze",
5153
},
52-
{
53-
uses: "./.github/actions/custom",
54-
expected: "",
55-
error: true,
56-
},
5754
{
5855
uses: "docker://alpine:latest",
5956
expected: "pkg:docker/alpine%3Alatest",
@@ -74,10 +71,20 @@ func TestPurlFromGithubActions(t *testing.T) {
7471
uses: "invalid",
7572
error: true,
7673
},
74+
{
75+
uses: "./.github/workflows/trigger_dep_builds.yml",
76+
sourceGitRepo: "FasterXML/jackson-databind",
77+
sourceGitRef: "2.18",
78+
expected: "pkg:githubactions/fasterxml/[email protected]#.github/workflows/trigger_dep_builds.yml",
79+
},
80+
{
81+
uses: "./../action/init",
82+
error: true,
83+
},
7784
}
7885

7986
for _, c := range cases {
80-
p, err := PurlFromGithubActions(c.uses)
87+
p, err := PurlFromGithubActions(c.uses, c.sourceGitRepo, c.sourceGitRef)
8188

8289
if !c.error {
8390
assert.Nil(t, err)

opa/builtins.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,28 @@ func registerBuiltinFunctions() {
2929
},
3030
)
3131

32-
rego.RegisterBuiltin1(
32+
rego.RegisterBuiltin3(
3333
&rego.Function{
3434
Name: "purl.parse_github_actions",
35-
Decl: types.NewFunction(types.Args(types.S), types.S),
35+
Decl: types.NewFunction(types.Args(types.S, types.S, types.S), types.S),
3636
},
37-
func(_ rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
37+
func(_ rego.BuiltinContext, a *ast.Term, b *ast.Term, c *ast.Term) (*ast.Term, error) {
3838
var uses string
3939
if err := ast.As(a.Value, &uses); err != nil {
4040
return nil, err
4141
}
4242

43-
purl, err := models.PurlFromGithubActions(uses)
43+
var sourceGitRepo string
44+
if err := ast.As(b.Value, &sourceGitRepo); err != nil {
45+
return nil, err
46+
}
47+
48+
var sourceGitRef string
49+
if err := ast.As(c.Value, &sourceGitRef); err != nil {
50+
return nil, err
51+
}
52+
53+
purl, err := models.PurlFromGithubActions(uses, sourceGitRepo, sourceGitRef)
4454
if err != nil {
4555
return nil, err
4656
}

opa/opa_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ func TestOpaBuiltins(t *testing.T) {
3333
}{
3434
{
3535
builtin: "purl.parse_github_actions",
36-
input: "actions/checkout@v4",
36+
input: `"actions/checkout@v4","",""`,
3737
expected: "pkg:githubactions/actions/checkout@v4",
3838
},
3939
{
4040
builtin: "purl.parse_docker_image",
41-
input: "alpine:latest",
41+
input: `"alpine:latest"`,
4242
expected: "pkg:docker/alpine%3Alatest",
4343
},
4444
}
@@ -50,7 +50,8 @@ func TestOpaBuiltins(t *testing.T) {
5050

5151
for _, c := range cases {
5252
var result interface{}
53-
err := opa.Eval(context.TODO(), c.builtin+"(\""+c.input+"\")", nil, &result)
53+
query := fmt.Sprintf(`%s(%s)`, c.builtin, c.input)
54+
err := opa.Eval(context.TODO(), query, nil, &result)
5455
noOpaErrors(t, err)
5556

5657
assert.Equal(t, c.expected, result)

opa/rego/poutine/inventory/github_actions.rego

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import data.poutine.utils
77
build_dependencies contains dep if {
88
pkg := input.packages[_]
99
step := pkg.github_actions_workflows[_].jobs[_].steps[_]
10-
11-
dep := purl.parse_github_actions(step.uses)
10+
dep := purl.parse_github_actions(step.uses, pkg.source_git_repo, pkg.source_git_ref)
1211
}
1312

1413
build_dependencies contains dep if {
@@ -24,15 +23,13 @@ build_dependencies contains dep if {
2423
job := pkg.github_actions_workflows[_].jobs[_]
2524
uses := job.uses
2625
not utils.empty(uses)
27-
28-
dep := purl.parse_github_actions(uses)
26+
dep := purl.parse_github_actions(uses, pkg.source_git_repo, pkg.source_git_ref)
2927
}
3028

3129
package_dependencies contains dep if {
3230
pkg := input.packages[_]
3331
step := pkg.github_actions_metadata[_].runs.steps[_]
34-
35-
dep := purl.parse_github_actions(step.uses)
32+
dep := purl.parse_github_actions(step.uses, pkg.source_git_repo, pkg.source_git_ref)
3633
}
3734

3835
package_dependencies contains dep if {
@@ -41,6 +38,5 @@ package_dependencies contains dep if {
4138

4239
runs.using == "docker"
4340
startswith(runs.image, "docker://")
44-
45-
dep := purl.parse_github_actions(runs.image)
41+
dep := purl.parse_github_actions(runs.image, pkg.source_git_repo, pkg.source_git_ref)
4642
}

opa/rego/rules/unpinnable_action.rego

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ results contains poutine.finding(rule, pkg.purl, {
2020
}) if {
2121
pkg := input.packages[_]
2222
action := pkg.github_actions_metadata[_]
23-
purls := data.poutine.inventory.package_dependencies with input.packages as [{"github_actions_metadata": [action]}]
23+
source_git_repo := pkg.source_git_repo
24+
source_git_ref := pkg.source_git_ref
25+
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}]
2426

2527
unpinned_purls := [p |
2628
p := purls[_]

scanner/inventory_test.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ func TestPurls(t *testing.T) {
1616
})
1717
i := NewInventory(o, nil, "", "")
1818
pkg := &models.PackageInsights{
19-
Purl: "pkg:github/org/owner",
19+
Purl: "pkg:github/org/owner",
20+
SourceGitRepo: "org/owner",
21+
SourceGitRef: "main",
2022
}
2123
_ = pkg.NormalizePurl()
2224
scannedPackage, err := i.ScanPackage(context.Background(), *pkg, "testdata")
@@ -39,17 +41,16 @@ func TestPurls(t *testing.T) {
3941
"pkg:gitlabci/include/project?file_name=%2Ftemplates%2F.gitlab-ci-template.yml&project=my-group%2Fmy-project&ref=main",
4042
"pkg:gitlabci/include/remote?download_url=https%3A%2F%2Fexample.com%2F.gitlab-ci.yml",
4143
"pkg:gitlabci/include/component?project=my-org%2Fsecurity-components%2Fsecret-detection&ref=1.0&repository_url=gitlab.example.com",
42-
// "pkg:gitlabci/include/local?file_name=%2F.local-ci-template.yml",
43-
// "pkg:gitlabci/include/local?file_name=.gitlab-ci.yml",
4444
"pkg:githubactions/org/repo@main",
4545
"pkg:docker/debian%3Avuln",
4646
"pkg:githubactions/bridgecrewio/checkov-action@main",
4747
"pkg:githubactions/org/repo@main#.github/workflows/Reusable.yml",
4848
"pkg:azurepipelinestask/DownloadPipelineArtifact@2",
4949
"pkg:azurepipelinestask/Cache@2",
50+
"pkg:githubactions/org/owner@main#.github/workflows/ci.yml",
5051
}
5152
assert.ElementsMatch(t, i.Purls(*scannedPackage), purls)
52-
assert.Equal(t, 18, len(scannedPackage.BuildDependencies))
53+
assert.Equal(t, 19, len(scannedPackage.BuildDependencies))
5354
assert.Equal(t, 4, len(scannedPackage.PackageDependencies))
5455
}
5556

@@ -60,7 +61,9 @@ func TestFindings(t *testing.T) {
6061
i := NewInventory(o, nil, "gitlab", "")
6162
purl := "pkg:github/org/owner"
6263
pkg := &models.PackageInsights{
63-
Purl: purl,
64+
Purl: purl,
65+
SourceGitRepo: "org/owner",
66+
SourceGitRef: "main",
6467
}
6568
_ = pkg.NormalizePurl()
6669

@@ -436,7 +439,9 @@ func TestSkipRule(t *testing.T) {
436439
purl := "pkg:github/org/owner"
437440
rule_id := "known_vulnerability_in_build_component"
438441
pkg := &models.PackageInsights{
439-
Purl: purl,
442+
Purl: purl,
443+
SourceGitRepo: "org/owner",
444+
SourceGitRef: "main",
440445
}
441446
_ = pkg.NormalizePurl()
442447

@@ -484,7 +489,9 @@ func TestRulesConfig(t *testing.T) {
484489
rule_id := "pr_runs_on_self_hosted"
485490
path := ".github/workflows/allowed_pr_runner.yml"
486491
pkg := &models.PackageInsights{
487-
Purl: purl,
492+
Purl: purl,
493+
SourceGitRepo: "org/owner",
494+
SourceGitRef: "main",
488495
}
489496
_ = pkg.NormalizePurl()
490497

0 commit comments

Comments
 (0)