diff --git a/docs/content/en/rules/untrusted_checkout_exec.md b/docs/content/en/rules/untrusted_checkout_exec.md index bf987e9..2ebd859 100644 --- a/docs/content/en/rules/untrusted_checkout_exec.md +++ b/docs/content/en/rules/untrusted_checkout_exec.md @@ -10,7 +10,7 @@ severity: error The workflow appears to checkout untrusted code from a fork and uses a command that is known to allow code execution. -Supported `actions/checkout` releases refuse fork pull request code by default for `pull_request_target` and pull-request-originated `workflow_run` events. Poutine suppresses this finding when that guard applies. The finding remains when the checkout uses `allow-unsafe-pr-checkout: true`, a dynamic value for that input, a known vulnerable commit or a semantic version below its release line's fixed version, or the workflow also uses an event that the guard does not cover. Moving major tags from v2 onward and branch-like refs are assumed to contain the guard. +Supported `actions/checkout` releases refuse fork pull request code by default for `pull_request_target` and pull-request-originated `workflow_run` events. Poutine suppresses this finding when that guard applies. Steps that execute shell scripts from `RUNNER_TEMP` (paths using `$RUNNER_TEMP` or `${RUNNER_TEMP}`) are also suppressed because those files are not part of the checked-out workspace. The finding remains when the checkout uses `allow-unsafe-pr-checkout: true`, a dynamic value for that input, a known vulnerable commit or a semantic version below its release line's fixed version, or the workflow also uses an event that the guard does not cover. Moving major tags from v2 onward and branch-like refs are assumed to contain the guard. Using workflows with `pull_request_target` has the added benefit (as opposed to `pull_request`) of allowing access to secrets even in forked repositories. There can be good reasons to do so if you need to use API Keys to talk to some external services or want to interact with the GitHub API with `write` permissions. However, this comes at the cost of paying extra attention to the tools you use in your workflow. diff --git a/opa/rego/rules/untrusted_checkout_exec.rego b/opa/rego/rules/untrusted_checkout_exec.rego index fbbb30f..fb31a11 100644 --- a/opa/rego/rules/untrusted_checkout_exec.rego +++ b/opa/rego/rules/untrusted_checkout_exec.rego @@ -99,6 +99,11 @@ build_commands[cmd] = { "yarn": {"yarn "}, }[cmd] +# Scripts under RUNNER_TEMP are provisioned by the runner, not the checked-out workspace. +_run_uses_runner_temp_path(run) if { + regex.match(`(?i)(?:\$\{\s*RUNNER_TEMP\s*\}|\$RUNNER_TEMP)/`, run) +} + results contains poutine.finding(rule, pkg_purl, object.union( { "path": workflow_path, @@ -116,6 +121,7 @@ results contains poutine.finding(rule, pkg_purl, object.union( sprintf("([^a-z]|^)(%v)", [concat("|", build_commands[cmd])]), step.run, ) + not _run_uses_runner_temp_path(step.run) } results contains poutine.finding(rule, pkg_purl, { diff --git a/scanner/inventory_scanner_test.go b/scanner/inventory_scanner_test.go index f63c3e1..c6d28bb 100644 --- a/scanner/inventory_scanner_test.go +++ b/scanner/inventory_scanner_test.go @@ -36,6 +36,7 @@ func TestGithubWorkflows(t *testing.T) { ".github/workflows/anchors_multiple.yml", ".github/workflows/anchors_with_vulnerability.yml", ".github/workflows/test_new_fields.yml", + ".github/workflows/runner_temp_script.yml", }) } diff --git a/scanner/runner_temp_test.go b/scanner/runner_temp_test.go new file mode 100644 index 0000000..e8fc1eb --- /dev/null +++ b/scanner/runner_temp_test.go @@ -0,0 +1,41 @@ +package scanner + +import ( + "context" + "testing" + + "github.com/boostsecurityio/poutine/models" + "github.com/boostsecurityio/poutine/opa" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRunnerTempScriptNotFlaggedAsUntrustedCheckoutExec(t *testing.T) { + o, err := opa.NewOpa(context.TODO(), &models.Config{ + Include: []models.ConfigInclude{}, + }) + require.NoError(t, err) + + i := NewInventory(o, nil, "", "") + pkg := &models.PackageInsights{ + Purl: "pkg:github/org/owner", + SourceGitRepo: "org/owner", + SourceGitRef: "main", + } + require.NoError(t, pkg.NormalizePurl()) + + scannedPackage, err := i.ScanPackage(context.Background(), *pkg, "testdata") + require.NoError(t, err) + + for _, finding := range scannedPackage.FindingsResults.Findings { + if finding.RuleId != "untrusted_checkout_exec" { + continue + } + assert.NotEqual( + t, + ".github/workflows/runner_temp_script.yml", + finding.Meta.Path, + "bash scripts under RUNNER_TEMP should not be treated as checked-out workspace code", + ) + } +} diff --git a/scanner/testdata/.github/workflows/runner_temp_script.yml b/scanner/testdata/.github/workflows/runner_temp_script.yml new file mode 100644 index 0000000..16fa6c3 --- /dev/null +++ b/scanner/testdata/.github/workflows/runner_temp_script.yml @@ -0,0 +1,19 @@ +name: runner-temp-script +on: + workflow_call: + inputs: + ref: + type: string + required: true +permissions: + contents: read +jobs: + run: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + sparse-checkout: .github + persist-credentials: false + - run: bash "${RUNNER_TEMP}/tooling/setup.sh"