GitHub Actions hardening: before vs after
Before hardening
0/100 · F
5 workflow(s) · critical: 1, high: 13, medium: 13, low: 9
After hardening
100/100 · A
5 workflow(s) · no findings
Before hardening
| Severity | Rule | Location | Finding | Fix |
|---|---|---|---|---|
| critical | GHA004 pwn-request | examples/before/workflows/pr-labeler.yml:16 | `pull_request_target` job `test-and-comment` checks out untrusted PR code (`ref: ${{ github.event.pull_request.head.sha }}`). | Use `pull_request` for building PR code. If you must use `pull_request_target`, never check out the PR head, or split into an unprivileged build and a privileged comment step. |
| high | GHA001 unpinned-action | examples/before/workflows/ci.yml:20 | `tj-actions/changed-files@v45` uses a mutable reference `v45`. | Pin to a 40-character commit SHA and keep the tag as a comment (tools/pin_actions.py does this). Let Dependabot or Renovate update the pins. |
| high | GHA006 long-lived-cloud-credentials | examples/before/workflows/deploy.yml:8 | Workflow env `AWS_ACCESS_KEY_ID` comes from a long-lived secret. | Use OpenID Connect: `aws-actions/configure-aws-credentials` with `role-to-assume` and `permissions: id-token: write`. |
| high | GHA006 long-lived-cloud-credentials | examples/before/workflows/deploy.yml:9 | Workflow env `AWS_SECRET_ACCESS_KEY` comes from a long-lived secret. | Use OpenID Connect: `aws-actions/configure-aws-credentials` with `role-to-assume` and `permissions: id-token: write`. |
| high | GHA013 unsecure-commands | examples/before/workflows/deploy.yml:10 | ACTIONS_ALLOW_UNSECURE_COMMANDS is enabled. | Remove it and write to $GITHUB_ENV / $GITHUB_PATH instead. |
| high | GHA001 unpinned-action | examples/before/workflows/deploy.yml:17 | `aws-actions/configure-aws-credentials@v4` uses a mutable reference `v4`. | Pin to a 40-character commit SHA and keep the tag as a comment (tools/pin_actions.py does this). Let Dependabot or Renovate update the pins. |
| high | GHA006 long-lived-cloud-credentials | examples/before/workflows/deploy.yml:19 | Input `aws-access-key-id` is a long-lived secret. | Use OpenID Connect: `aws-actions/configure-aws-credentials` with `role-to-assume` and `permissions: id-token: write`. |
| high | GHA006 long-lived-cloud-credentials | examples/before/workflows/deploy.yml:20 | Input `aws-secret-access-key` is a long-lived secret. | Use OpenID Connect: `aws-actions/configure-aws-credentials` with `role-to-assume` and `permissions: id-token: write`. |
| high | GHA005 script-injection | examples/before/workflows/issue-triage.yml:16 | `${{ github.event.issue.title }}` is expanded inside a script. | Pass the value through an environment variable: `env: TITLE: ${{ github.event.issue.title }}` and use "$TITLE". |
| high | GHA005 script-injection | examples/before/workflows/issue-triage.yml:20 | `${{ github.event.issue.body }}` is expanded inside a script. | Pass the value through an environment variable: `env: TITLE: ${{ github.event.issue.title }}` and use "$TITLE". |
| high | GHA003 write-all-permissions | examples/before/workflows/pr-labeler.yml:8 | Workflow-level `permissions: write-all`. | List only the scopes the job needs, e.g. `contents: read`, `id-token: write`. |
| high | GHA005 script-injection | examples/before/workflows/pr-labeler.yml:22 | `${{ github.event.pull_request.title }}` is expanded inside a script. | Pass the value through an environment variable: `env: TITLE: ${{ github.event.issue.title }}` and use "$TITLE". |
| high | GHA011 self-hosted-runner-on-pr | examples/before/workflows/release.yml:10 | Job `build` runs pull-request code on a self-hosted runner. | Use GitHub-hosted runners for untrusted code, or ephemeral, isolated self-hosted runners. |
| high | GHA001 unpinned-action | examples/before/workflows/release.yml:17 | `some-vendor/shared-workflows/.github/workflows/publish.yml@main` uses a mutable reference `main`. | Pin to a 40-character commit SHA and keep the tag as a comment (tools/pin_actions.py does this). Let Dependabot or Renovate update the pins. |
| medium | GHA012 no-action-updates | .github/dependabot.yml:1 | Nothing updates pinned actions; pins will go stale. | Add `.github/dependabot.yml` with `package-ecosystem: github-actions`. |
| medium | GHA002 missing-permissions | examples/before/workflows/ci.yml:1 | No top-level `permissions:` and 1 job(s) without their own: build. | Add `permissions: {}` (or `contents: read`) at the top of the workflow and grant extra scopes per job. |
| medium | GHA001 unpinned-action | examples/before/workflows/ci.yml:12 | `actions/checkout@v4` uses a mutable reference `v4`. | Pin to a 40-character commit SHA and keep the tag as a comment (tools/pin_actions.py does this). Let Dependabot or Renovate update the pins. |
| medium | GHA001 unpinned-action | examples/before/workflows/ci.yml:13 | `actions/setup-node@v4` uses a mutable reference `v4`. | Pin to a 40-character commit SHA and keep the tag as a comment (tools/pin_actions.py does this). Let Dependabot or Renovate update the pins. |
| medium | GHA010 curl-pipe-shell | examples/before/workflows/ci.yml:17 | Remote script piped straight into a shell. | Download a pinned version, verify its checksum or signature, then run it - or use a pinned action. |
| medium | GHA002 missing-permissions | examples/before/workflows/deploy.yml:1 | No top-level `permissions:` and 1 job(s) without their own: deploy. | Add `permissions: {}` (or `contents: read`) at the top of the workflow and grant extra scopes per job. |
| medium | GHA001 unpinned-action | examples/before/workflows/deploy.yml:16 | `actions/checkout@v4` uses a mutable reference `v4`. | Pin to a 40-character commit SHA and keep the tag as a comment (tools/pin_actions.py does this). Let Dependabot or Renovate update the pins. |
| medium | GHA014 inline-secret-in-script | examples/before/workflows/deploy.yml:24 | `${{ secrets.SLACK_WEBHOOK_URL }}` is expanded inside a script. | Pass secrets through `env:` and reference "$NAME" so they are not written into the generated script. |
| medium | GHA001 unpinned-action | examples/before/workflows/issue-triage.yml:17 | `actions/github-script@v7` uses a mutable reference `v7`. | Pin to a 40-character commit SHA and keep the tag as a comment (tools/pin_actions.py does this). Let Dependabot or Renovate update the pins. |
| medium | GHA001 unpinned-action | examples/before/workflows/pr-labeler.yml:14 | `actions/checkout@v4` uses a mutable reference `v4`. | Pin to a 40-character commit SHA and keep the tag as a comment (tools/pin_actions.py does this). Let Dependabot or Renovate update the pins. |
| medium | GHA002 missing-permissions | examples/before/workflows/release.yml:1 | No top-level `permissions:` and 2 job(s) without their own: build, publish. | Add `permissions: {}` (or `contents: read`) at the top of the workflow and grant extra scopes per job. |
| medium | GHA001 unpinned-action | examples/before/workflows/release.yml:13 | `actions/checkout@v4` uses a mutable reference `v4`. | Pin to a 40-character commit SHA and keep the tag as a comment (tools/pin_actions.py does this). Let Dependabot or Renovate update the pins. |
| medium | GHA007 secrets-inherit | examples/before/workflows/release.yml:18 | Job `publish` passes all secrets to `some-vendor/shared-workflows/.github/workflows/publish.yml@main`. | Pass only the secrets the called workflow needs. |
| low | GHA009 missing-timeout | examples/before/workflows/ci.yml:9 | Job `build` has no timeout-minutes. | Set `timeout-minutes` on every job to cap runaway or abused runs. |
| low | GHA008 checkout-persists-credentials | examples/before/workflows/ci.yml:12 | Checkout keeps the token in .git/config. | Add `with: persist-credentials: false` unless a later step really needs to push. |
| low | GHA009 missing-timeout | examples/before/workflows/deploy.yml:13 | Job `deploy` has no timeout-minutes. | Set `timeout-minutes` on every job to cap runaway or abused runs. |
| low | GHA008 checkout-persists-credentials | examples/before/workflows/deploy.yml:16 | Checkout keeps the token in .git/config. | Add `with: persist-credentials: false` unless a later step really needs to push. |
| low | GHA009 missing-timeout | examples/before/workflows/issue-triage.yml:10 | Job `triage` has no timeout-minutes. | Set `timeout-minutes` on every job to cap runaway or abused runs. |
| low | GHA009 missing-timeout | examples/before/workflows/pr-labeler.yml:11 | Job `test-and-comment` has no timeout-minutes. | Set `timeout-minutes` on every job to cap runaway or abused runs. |
| low | GHA008 checkout-persists-credentials | examples/before/workflows/pr-labeler.yml:14 | Checkout keeps the token in .git/config. | Add `with: persist-credentials: false` unless a later step really needs to push. |
| low | GHA009 missing-timeout | examples/before/workflows/release.yml:10 | Job `build` has no timeout-minutes. | Set `timeout-minutes` on every job to cap runaway or abused runs. |
| low | GHA008 checkout-persists-credentials | examples/before/workflows/release.yml:13 | Checkout keeps the token in .git/config. | Add `with: persist-credentials: false` unless a later step really needs to push. |
After hardening
| Severity | Rule | Location | Finding | Fix |
|---|---|---|---|---|
| No findings. | ||||
Generated by ghaudit 0.5.0 on 2026-11-08 14:01 UTC.