Skip to content

Commit 4f6ca1b

Browse files
authored
feat: add unverified_script_exec rule (#129)
* add unverified_script_exec rule * add safe patterns --------- Co-authored-by: Becojo <[email protected]>
1 parent 422dab5 commit 4f6ca1b

File tree

4 files changed

+136
-0
lines changed

4 files changed

+136
-0
lines changed

opa/opa_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"github.com/boostsecurityio/poutine/models"
66
"github.com/open-policy-agent/opa/ast"
77

8+
"fmt"
89
"github.com/stretchr/testify/assert"
910
"testing"
1011
)
@@ -178,3 +179,24 @@ func TestCapabilities(t *testing.T) {
178179
}
179180
}
180181
}
182+
183+
func TestRulesMetadataLevel(t *testing.T) {
184+
opa, err := NewOpa()
185+
noOpaErrors(t, err)
186+
187+
query := `{rule_id: rule.level |
188+
rule := data.rules[rule_id].rule;
189+
not input[rule.level]
190+
}`
191+
192+
var result map[string]string
193+
err = opa.Eval(context.TODO(), query, map[string]interface{}{
194+
"note": true,
195+
"warning": true,
196+
"error": true,
197+
"none": true,
198+
}, &result)
199+
noOpaErrors(t, err)
200+
201+
assert.Empty(t, result, fmt.Sprintf("rules with invalid levels: %v", result))
202+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# METADATA
2+
# title: Unverified Script Execution
3+
# description: |-
4+
# The pipeline executes a script or binary fetched from a remote
5+
# server without verifying its integrity.
6+
# custom:
7+
# level: note
8+
package rules.unverified_script_exec
9+
10+
import data.poutine
11+
import data.poutine.utils
12+
import rego.v1
13+
14+
rule := poutine.rule(rego.metadata.chain())
15+
16+
patterns.shell contains sprintf("(%s)", [concat("|", [
17+
`(bash|source) <\(curl [^\)\n]+?\)`,
18+
`(curl|wget|iwr)[^\n]{0,256}(\|(|.*?[^a-z])((ba)?sh|python|php|node|iex|perl)|chmod ([aug]?\+x|[75]))`,
19+
`iex[^\n]{0,512}\.DownloadString\([^\)]+?\)`,
20+
`deno (run|install) (-A|--allow-all)[^\n]{0,128}https://[^\s]{0,128}`,
21+
])])
22+
23+
patterns.safe contains sprintf("(%s)", [concat("|", [
24+
`https://raw\.githubusercontent\.com/[^/]+/[^/]+/[a-f0-9]{40}/`,
25+
`https://github\.com/[^/]+/[^/]+/raw/[a-f0-9]{40}/`,
26+
])])
27+
28+
results contains poutine.finding(rule, pkg_purl, _scripts[pkg_purl][_])
29+
30+
_unverified_scripts(script) = [sprintf("Command: %s", [match]) |
31+
match := regex.find_n(patterns.shell[_], script, -1)[_]
32+
not _is_safe(match)
33+
]
34+
35+
_is_safe(match) = regex.match(patterns.safe[_], match)
36+
37+
_scripts[pkg.purl] contains {
38+
"path": workflow.path,
39+
"step": step_id,
40+
"job": job.id,
41+
"line": step.lines.run,
42+
"details": details,
43+
} if {
44+
pkg := input.packages[_]
45+
workflow := pkg.github_actions_workflows[_]
46+
job := workflow.jobs[_]
47+
step := job.steps[step_id]
48+
details := _unverified_scripts(step.run)[_]
49+
}
50+
51+
_scripts[pkg.purl] contains {
52+
"path": action.path,
53+
"step": step_id,
54+
"line": step.lines.run,
55+
"details": details,
56+
} if {
57+
pkg := input.packages[_]
58+
action := pkg.github_actions_metadata[_]
59+
step := action.runs.steps[step_id]
60+
details := _unverified_scripts(step.run)[_]
61+
}
62+
63+
_scripts[pkg.purl] contains {
64+
"path": config.path,
65+
"line": script.line,
66+
"job": job.name,
67+
"details": details,
68+
} if {
69+
some attr in {"before_script", "after_script", "script"}
70+
pkg := input.packages[_]
71+
config := pkg.gitlabci_configs[_]
72+
job := array.concat(config.jobs, [config["default"]])[_]
73+
script := job[attr][_]
74+
details := _unverified_scripts(script.run)[_]
75+
}

scanner/inventory_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ func TestFindings(t *testing.T) {
8282
"github_action_from_unverified_creator_used",
8383
"debug_enabled",
8484
"job_all_secrets",
85+
"unverified_script_exec",
8586
})
8687

8788
findings := []opa.Finding{
@@ -308,6 +309,28 @@ func TestFindings(t *testing.T) {
308309
Step: "3",
309310
},
310311
},
312+
{
313+
RuleId: "unverified_script_exec",
314+
Purl: purl,
315+
Meta: opa.FindingMeta{
316+
Path: ".github/workflows/valid.yml",
317+
Line: 70,
318+
Job: "build",
319+
Step: "12",
320+
Details: "Command: curl https://example.com | bash",
321+
},
322+
},
323+
{
324+
RuleId: "unverified_script_exec",
325+
Purl: purl,
326+
Meta: opa.FindingMeta{
327+
Path: ".github/workflows/valid.yml",
328+
Line: 75,
329+
Job: "build",
330+
Step: "13",
331+
Details: "Command: curl https://raw.githubusercontent.com/org/repo/main/install.sh | bash",
332+
},
333+
},
311334
}
312335

313336
assert.Equal(t, len(findings), len(results.Findings))

scanner/testdata/.github/workflows/valid.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,19 @@ jobs:
6464
run: |
6565
# substring of go\ generate should not trigger
6666
cargo generate
67+
68+
# unverified_script_exec
69+
- id: 12
70+
run: |
71+
curl https://example.com | bash
72+
73+
# unverified_script_exec
74+
- id: 13
75+
run: |
76+
curl https://raw.githubusercontent.com/org/repo/main/install.sh | bash
77+
78+
# safe unverified_script_exec
79+
- id: 13
80+
run: |
81+
curl https://raw.githubusercontent.com/org/repo/0a727065ae5a2313e8e6acf172844e8ca30c1822/install.sh | bash
82+
curl https://github.com/org/repo/raw/0a727065ae5a2313e8e6acf172844e8ca30c1822/install.sh | bash

0 commit comments

Comments
 (0)