From 5fab5baf7a758b287886827ab27d4df043bdf80e Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Sat, 14 Nov 2026 01:12:38 +0000 Subject: [PATCH] Reference exact action releases and lock their commits in actions.lock --- .github/workflows/actions.lock | 133 ++++++------------ .github/workflows/breakglass-merge.yml | 8 +- .github/workflows/breakglass-tests.yml | 2 +- .../workflows/container-remediation-tests.yml | 6 +- .github/workflows/container-remediation.yml | 50 +++---- .github/workflows/container-scan.yml | 4 +- .github/workflows/pr-risk-tests.yml | 4 +- .github/workflows/pr-risk.yml | 6 +- .github/workflows/semgrep-tests.yml | 2 +- .github/workflows/semgrep.yml | 47 ++++++- .../workflows/vuln-remediation-self-test.yml | 4 + .github/workflows/vuln-remediation.yml | 66 ++++----- scripts/action_pins.py | 34 +++++ scripts/test_container_remediation.py | 5 +- scripts/test_semgrep_triage.py | 34 +++++ scripts/test_vuln_remediation_workflow.py | 36 ++++- 16 files changed, 274 insertions(+), 167 deletions(-) create mode 100644 scripts/action_pins.py diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 89540ee..cba9759 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -4,56 +4,56 @@ version: 'v0.0.3' workflows: '.github/workflows/breakglass-merge.yml': - - 'actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547' + - 'actions/create-github-app-token@v1.12.0' '.github/workflows/breakglass-tests.yml': - - 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' + - 'actions/checkout@v4.2.2' '.github/workflows/container-remediation-self-test.yml': [] '.github/workflows/container-remediation-tests.yml': - - 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' + - 'actions/checkout@v4.2.2' '.github/workflows/container-remediation.yml': - - 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' - - 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' - - 'actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547' - - 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093' - - 'actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff' - - 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' - - 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' + - 'actions/checkout@v4.2.2' + - 'actions/checkout@v4.3.1' + - 'actions/create-github-app-token@v1.12.0' + - 'actions/download-artifact@v4.3.0' + - 'actions/setup-go@v5.6.0' + - 'actions/setup-node@v4.4.0' + - 'actions/upload-artifact@v4.6.2' - 'aquasecurity/trivy-action@38b337fff0a2f984d5be173c99e3f633438167ec' - - 'aws-actions/amazon-ecr-login@03f1aad4c6c7ffd436567f42f9384779290529bd' - - 'aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a' + - 'aws-actions/amazon-ecr-login@v2.1.7' + - 'aws-actions/configure-aws-credentials@v4.3.1' '.github/workflows/container-scan.yml': - 'actions/checkout@v4.4.0' - 'actions/upload-artifact@v4.6.2' - - 'aquasecurity/trivy-action@master' + - 'aquasecurity/trivy-action@38b337fff0a2f984d5be173c99e3f633438167ec' - 'aws-actions/amazon-ecr-login@v2.1.7' - 'aws-actions/configure-aws-credentials@v4.3.1' '.github/workflows/pr-risk-tests.yml': - - 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' + - 'actions/checkout@v4.2.2' '.github/workflows/pr-risk.yml': - - 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' - - 'actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547' - - 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' + - 'actions/checkout@v4.2.2' + - 'actions/create-github-app-token@v1.12.0' + - 'actions/setup-node@v4.4.0' '.github/workflows/semgrep-self-test.yml': [] '.github/workflows/semgrep-tests.yml': - - 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' + - 'actions/checkout@v4.2.2' '.github/workflows/semgrep.yml': - - 'actions/checkout@v4' - - 'actions/download-artifact@v4' - - 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' - - 'actions/upload-artifact@v4' + - 'actions/checkout@v4.4.0' + - 'actions/download-artifact@v4.3.0' + - 'actions/setup-node@v4.4.0' + - 'actions/upload-artifact@v4.6.2' - 'kernel/security-workflows@main' '.github/workflows/vuln-remediation-self-test.yml': - 'actions/checkout@v4.4.0' '.github/workflows/vuln-remediation.yml': - - 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' - - 'actions/checkout@11d5960a326750d5838078e36cf38b85af677262' - - 'actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547' - - 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093' - - 'actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff' - - 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' - - 'actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065' - - 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' - - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' + - 'actions/checkout@v4.2.2' + - 'actions/checkout@v4.4.0' + - 'actions/create-github-app-token@v1.12.0' + - 'actions/download-artifact@v4.3.0' + - 'actions/setup-go@v5.6.0' + - 'actions/setup-node@v4.4.0' + - 'actions/setup-python@v5.6.0' + - 'actions/upload-artifact@v4.6.2' + - 'oven-sh/setup-bun@v2.2.0' dependencies: 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9': ref: 'v6.1.0' @@ -65,76 +65,51 @@ dependencies: commit: 'sha1-9255dc7a253b0ccc959486e2bca901246202afeb' owner_id: 44036562 repo_id: 215566462 - 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683': - ref: 'v4.2.2' - commit: 'sha1-11bd71901bbe5b1630ceea73d27597364c9af683' + 'actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8': + ref: 'v6.0.1' + commit: 'sha1-8e8c483db84b4bee98b60c0593521ed34d9990e8' owner_id: 44036562 repo_id: 197814629 - 'actions/checkout@11d5960a326750d5838078e36cf38b85af677262': - ref: 'v4.4.0' - commit: 'sha1-11d5960a326750d5838078e36cf38b85af677262' + 'actions/checkout@v4.2.2': + ref: 'v4.2.2' + commit: 'sha1-11bd71901bbe5b1630ceea73d27597364c9af683' owner_id: 44036562 repo_id: 197814629 - 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5': + 'actions/checkout@v4.3.1': ref: 'v4.3.1' commit: 'sha1-34e114876b0b11c390a56381ad16ebd13914f8d5' owner_id: 44036562 repo_id: 197814629 - 'actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8': - ref: 'v6.0.1' - commit: 'sha1-8e8c483db84b4bee98b60c0593521ed34d9990e8' - owner_id: 44036562 - repo_id: 197814629 - 'actions/checkout@v4': - ref: 'v4' - commit: 'sha1-11d5960a326750d5838078e36cf38b85af677262' - owner_id: 44036562 - repo_id: 197814629 'actions/checkout@v4.4.0': ref: 'v4.4.0' commit: 'sha1-11d5960a326750d5838078e36cf38b85af677262' owner_id: 44036562 repo_id: 197814629 - 'actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547': + 'actions/create-github-app-token@v1.12.0': ref: 'v1.12.0' commit: 'sha1-d72941d797fd3113feb6b93fd0dec494b13a2547' owner_id: 44036562 repo_id: 642580244 - 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093': + 'actions/download-artifact@v4.3.0': ref: 'v4.3.0' commit: 'sha1-d3f86a106a0bac45b974a628896c90dbdf5c8093' owner_id: 44036562 repo_id: 192626254 - 'actions/download-artifact@v4': - ref: 'v4' - commit: 'sha1-d3f86a106a0bac45b974a628896c90dbdf5c8093' - owner_id: 44036562 - repo_id: 192626254 - 'actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff': + 'actions/setup-go@v5.6.0': ref: 'v5.6.0' commit: 'sha1-40f1582b2485089dde7abd97c1529aa768e1baff' owner_id: 44036562 repo_id: 192624594 - 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020': + 'actions/setup-node@v4.4.0': ref: 'v4.4.0' commit: 'sha1-49933ea5288caeca8642d1e84afbd3f7d6820020' owner_id: 44036562 repo_id: 189476904 - 'actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065': + 'actions/setup-python@v5.6.0': ref: 'v5.6.0' commit: 'sha1-a26af69be951a213d495a4c3e4e4022e16d87065' owner_id: 44036562 repo_id: 192625525 - 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02': - ref: 'v4.6.2' - commit: 'sha1-ea165f8d65b6e75b540449e92b4886f43607fa02' - owner_id: 44036562 - repo_id: 192625955 - 'actions/upload-artifact@v4': - ref: 'v4' - commit: 'sha1-ea165f8d65b6e75b540449e92b4886f43607fa02' - owner_id: 44036562 - repo_id: 192625955 'actions/upload-artifact@v4.6.2': ref: 'v4.6.2' commit: 'sha1-ea165f8d65b6e75b540449e92b4886f43607fa02' @@ -156,29 +131,11 @@ dependencies: uses: - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' - 'aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567' - 'aquasecurity/trivy-action@master': - ref: 'master' - commit: 'sha1-38b337fff0a2f984d5be173c99e3f633438167ec' - owner_id: 12783832 - repo_id: 268833180 - uses: - - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' - - 'aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567' - 'aws-actions/amazon-ecr-login@03f1aad4c6c7ffd436567f42f9384779290529bd': - ref: 'v2.1.7' - commit: 'sha1-03f1aad4c6c7ffd436567f42f9384779290529bd' - owner_id: 57468012 - repo_id: 220389860 'aws-actions/amazon-ecr-login@v2.1.7': ref: 'v2.1.7' commit: 'sha1-03f1aad4c6c7ffd436567f42f9384779290529bd' owner_id: 57468012 repo_id: 220389860 - 'aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a': - ref: 'v4.3.1' - commit: 'sha1-7474bc4690e29a8392af63c5b98e7449536d5c3a' - owner_id: 57468012 - repo_id: 220391287 'aws-actions/configure-aws-credentials@v4.3.1': ref: 'v4.3.1' commit: 'sha1-7474bc4690e29a8392af63c5b98e7449536d5c3a' @@ -186,10 +143,10 @@ dependencies: repo_id: 220391287 'kernel/security-workflows@main': ref: 'main' - commit: 'sha1-56bdfc09970ce29b2ad4df3daa17adb9a7ed4ce0' + commit: 'sha1-af490855f90e83f89dcaf4ea18da962ec294350e' owner_id: 194616459 repo_id: 1207196574 - 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6': + 'oven-sh/setup-bun@v2.2.0': ref: 'v2.2.0' commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' owner_id: 108928776 diff --git a/.github/workflows/breakglass-merge.yml b/.github/workflows/breakglass-merge.yml index 06974ac..aff415b 100644 --- a/.github/workflows/breakglass-merge.yml +++ b/.github/workflows/breakglass-merge.yml @@ -43,10 +43,10 @@ jobs: steps: - name: Generate app token id: app-token - # Pinned to a commit: this step is handed the app's private key, so a mutable tag - # would let whoever controls it run their own code with that key. d72941d is v1.12.0, - # the commit `v1` already resolved to, so pinning changed no behaviour. - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 + # Locked to a commit in actions.lock: this step is handed the app's private key, so a + # moved tag must not change what runs. v1.12.0 is d72941d, the commit this step was + # pinned to before the lockfile. + uses: actions/create-github-app-token@v1.12.0 with: app-id: ${{ secrets.BREAKGLASS_APP_ID }} private-key: ${{ secrets.BREAKGLASS_APP_PRIVATE_KEY }} diff --git a/.github/workflows/breakglass-tests.yml b/.github/workflows/breakglass-tests.yml index 976c224..83f49d5 100644 --- a/.github/workflows/breakglass-tests.yml +++ b/.github/workflows/breakglass-tests.yml @@ -20,7 +20,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@v4.2.2 - run: | python3 -m venv /tmp/breakglass-tests /tmp/breakglass-tests/bin/pip install PyYAML==6.0.3 diff --git a/.github/workflows/container-remediation-tests.yml b/.github/workflows/container-remediation-tests.yml index e08ae87..79f34ff 100644 --- a/.github/workflows/container-remediation-tests.yml +++ b/.github/workflows/container-remediation-tests.yml @@ -7,6 +7,8 @@ on: - .github/workflows/container-remediation-tests.yml - .github/workflows/container-scan/** - scripts/test_container_remediation.py + - scripts/action_pins.py + - .github/workflows/actions.lock push: branches: [main] paths: @@ -14,6 +16,8 @@ on: - .github/workflows/container-remediation-tests.yml - .github/workflows/container-scan/** - scripts/test_container_remediation.py + - scripts/action_pins.py + - .github/workflows/actions.lock permissions: contents: read @@ -22,7 +26,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@v4.2.2 - run: | python3 -m venv /tmp/container-remediation-tests /tmp/container-remediation-tests/bin/pip install PyYAML==6.0.3 diff --git a/.github/workflows/container-remediation.yml b/.github/workflows/container-remediation.yml index 452f0f0..503de4e 100644 --- a/.github/workflows/container-remediation.yml +++ b/.github/workflows/container-remediation.yml @@ -52,23 +52,23 @@ jobs: has-findings: ${{ steps.check.outputs.has_findings }} timeout-minutes: 15 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@v4.3.1 with: persist-credentials: false - name: Configure AWS credentials if: inputs.ecr-role-to-assume != '' - uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 + uses: aws-actions/configure-aws-credentials@v4.3.1 with: role-to-assume: ${{ inputs.ecr-role-to-assume }} aws-region: ${{ inputs.aws-region }} - name: Log in to ECR if: inputs.ecr-role-to-assume != '' - uses: aws-actions/amazon-ecr-login@03f1aad4c6c7ffd436567f42f9384779290529bd # v2.1.7 + uses: aws-actions/amazon-ecr-login@v2.1.7 - name: Scan container image - uses: aquasecurity/trivy-action@38b337fff0a2f984d5be173c99e3f633438167ec # master + uses: aquasecurity/trivy-action@38b337fff0a2f984d5be173c99e3f633438167ec with: image-ref: ${{ inputs.image-ref }} severity: 'CRITICAL,HIGH' @@ -90,7 +90,7 @@ jobs: fi - name: Upload scan results - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: container-scan-report path: trivy-results.json @@ -105,17 +105,17 @@ jobs: id-token: write timeout-minutes: 15 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@v4.3.1 with: persist-credentials: false - name: Download scan results - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: container-scan-report - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@v4.4.0 with: node-version: '22' @@ -131,7 +131,7 @@ jobs: # git so the agent's changes never include it and `git clean` keeps it for the stop step. - name: Fetch credentials action id: credentials-action - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@v4.2.2 with: repository: kernel/security-workflows ref: ${{ inputs.security-workflows-ref }} @@ -174,7 +174,7 @@ jobs: stop: true - name: Upload triage results - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: container-triage-result path: triage-result.json @@ -199,13 +199,13 @@ jobs: steps: # The fix job builds on this same main commit. - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + uses: actions/checkout@v4.3.1 with: ref: main persist-credentials: false - name: Setup Go - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + uses: actions/setup-go@v5.6.0 with: go-version-file: ${{ inputs.go-version-file }} # Private module sources must not land in the repository's Actions cache. @@ -224,7 +224,7 @@ jobs: - name: Generate Go module read token id: module-token if: steps.go-private.outputs.repos != '' - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@v1.12.0 with: app-id: ${{ secrets.ADMIN_APP_ID }} private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }} @@ -247,7 +247,7 @@ jobs: - name: Upload private Go modules if: steps.go-private.outputs.repos != '' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: container-remediation-go-modules path: ${{ steps.download.outputs.dir }} @@ -274,19 +274,19 @@ jobs: GOPRIVATE: github.com/kernel/* steps: - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + uses: actions/checkout@v4.3.1 with: fetch-depth: 0 persist-credentials: false - name: Download scan results - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: container-scan-report path: ${{ runner.temp }}/container-scan - name: Download triage results - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: container-triage-result path: ${{ runner.temp }}/container-scan @@ -313,7 +313,7 @@ jobs: - name: Setup Go if: inputs.go-version-file != '' - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + uses: actions/setup-go@v5.6.0 with: go-version-file: ${{ inputs.go-version-file }} # The agent can write to the module cache, so this job must not save it. @@ -321,7 +321,7 @@ jobs: - name: Download private Go modules if: needs.go-modules.outputs.private-modules == 'true' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: container-remediation-go-modules path: ${{ runner.temp }}/go-modules @@ -334,7 +334,7 @@ jobs: cp -R "$RUNNER_TEMP/go-modules/." "$dir/" - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@v4.4.0 with: node-version: '22' @@ -350,7 +350,7 @@ jobs: # git so the agent's changes never include it and `git clean` keeps it for the stop step. - name: Fetch credentials action id: credentials-action - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@v4.2.2 with: repository: kernel/security-workflows ref: ${{ inputs.security-workflows-ref }} @@ -430,7 +430,7 @@ jobs: - name: Upload validated changes if: steps.snapshot.outputs.changed == 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: container-remediation-patch path: ${{ runner.temp }}/changes.patch @@ -451,13 +451,13 @@ jobs: UPDATE_SHA: ${{ needs.fix.outputs.update_sha }} steps: - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + uses: actions/checkout@v4.3.1 with: fetch-depth: 0 persist-credentials: false - name: Download validated changes - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: container-remediation-patch path: ${{ runner.temp }}/container-remediation @@ -498,7 +498,7 @@ jobs: - name: Generate app token id: app-token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@v1.12.0 with: app-id: ${{ secrets.ADMIN_APP_ID }} private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }} diff --git a/.github/workflows/container-scan.yml b/.github/workflows/container-scan.yml index 89871d9..e9c6dbd 100644 --- a/.github/workflows/container-scan.yml +++ b/.github/workflows/container-scan.yml @@ -61,7 +61,7 @@ jobs: uses: aws-actions/amazon-ecr-login@v2.1.7 - name: Scan container image (table) - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@38b337fff0a2f984d5be173c99e3f633438167ec with: image-ref: ${{ inputs.image-ref }} severity: ${{ inputs.severity }} @@ -70,7 +70,7 @@ jobs: format: 'table' - name: Scan container image (json) - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@38b337fff0a2f984d5be173c99e3f633438167ec with: image-ref: ${{ inputs.image-ref }} severity: ${{ inputs.severity }} diff --git a/.github/workflows/pr-risk-tests.yml b/.github/workflows/pr-risk-tests.yml index a9cddc2..2e66d3c 100644 --- a/.github/workflows/pr-risk-tests.yml +++ b/.github/workflows/pr-risk-tests.yml @@ -28,7 +28,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@v4.2.2 - run: | python3 -m venv /tmp/pr-risk-tests /tmp/pr-risk-tests/bin/pip install PyYAML==6.0.3 @@ -38,7 +38,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@v4.2.2 - name: shellcheck and actionlint run: | set -euo pipefail diff --git a/.github/workflows/pr-risk.yml b/.github/workflows/pr-risk.yml index b88fced..4ee428c 100644 --- a/.github/workflows/pr-risk.yml +++ b/.github/workflows/pr-risk.yml @@ -85,7 +85,7 @@ jobs: - name: Setup Node if: steps.gather.outputs.eligible == 'true' - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@v4.4.0 with: node-version: '22' @@ -103,7 +103,7 @@ jobs: - name: Fetch credentials action id: credentials-action if: steps.gather.outputs.eligible == 'true' - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@v4.2.2 with: repository: kernel/security-workflows ref: ${{ inputs.security-workflows-ref }} @@ -152,7 +152,7 @@ jobs: id: app-token if: ${{ !cancelled() && (steps.gather.outputs.eligible == 'true' || steps.gather.outputs.commented == 'true') }} # Pinned to a commit because this step is handed the app's private key. d72941d is v1.12.0. - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 + uses: actions/create-github-app-token@v1.12.0 with: app-id: ${{ secrets.ADMIN_APP_ID }} private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }} diff --git a/.github/workflows/semgrep-tests.yml b/.github/workflows/semgrep-tests.yml index 4f14d6e..02e885c 100644 --- a/.github/workflows/semgrep-tests.yml +++ b/.github/workflows/semgrep-tests.yml @@ -22,7 +22,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@v4.2.2 - run: | python3 -m venv /tmp/semgrep-tests /tmp/semgrep-tests/bin/pip install PyYAML==6.0.3 diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index a70e98c..55c0b38 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -31,17 +31,54 @@ jobs: outputs: findings_count: ${{ steps.count.outputs.findings_count }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4.4.0 with: fetch-depth: 0 - name: Run Semgrep run: | + # Replaces the registry's github-actions-mutable-action-tag rule, which only accepts + # commit SHAs. Workflows lock action commits in actions.lock, so an exact vX.Y.Z tag + # and a same-repo $/ action are fixed refs too. Floating tags and branches still match. + cat > "$RUNNER_TEMP/github-actions-mutable-action-tag.yml" <<'RULE' + rules: + - id: github-actions-mutable-action-tag + message: >- + GitHub Actions step uses a floating tag or branch reference. Tags and branch + names can be silently repointed by the action owner, enabling supply-chain + attacks. Reference an exact release tag (e.g. `actions/checkout@v4.4.0`) and + lock it with `gh actions-lock`, or a full 40-character commit SHA if the + commit has no release tag. + severity: WARNING + languages: [yaml] + metadata: + category: security + cwe: + - "CWE-1357: Reliance on Insufficiently Trustworthy Component" + - "CWE-353: Missing Support for Integrity Check" + references: + - https://docs.github.com/en/actions/concepts/security/workflow-dependency-lockfiles + patterns: + - pattern-inside: "{steps: ...}" + - pattern: | + uses: "$ACTION" + - metavariable-pattern: + metavariable: $ACTION + language: generic + patterns: + - pattern-not-regex: '^\./' + - pattern-not-regex: '^\$/' + - pattern-not-regex: '^docker://' + - pattern-not-regex: '@[0-9a-f]{40}(\s|$)' + - pattern-not-regex: '@v[0-9]+\.[0-9]+\.[0-9]+(\s|$)' + RULE semgrep scan \ --config p/security-audit \ --config p/owasp-top-ten \ --config p/secrets \ --config p/github-actions \ + --config "$RUNNER_TEMP/github-actions-mutable-action-tag.yml" \ + --exclude-rule yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag \ ${{ inputs.extra-configs }} \ --severity ERROR --severity WARNING \ --baseline-commit ${{ github.event.pull_request.base.sha }} \ @@ -63,7 +100,7 @@ jobs: - name: Upload results if: steps.count.outputs.findings_count != '0' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.6.2 with: name: semgrep-results path: semgrep-results.json @@ -111,13 +148,13 @@ jobs: # Semgrep scanned the merge commit, but review comments go on the PR head. Depth 2 fetches # both, so the findings can be moved onto the head before the agent reads it. - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v4.4.0 with: fetch-depth: 2 persist-credentials: false - name: Download results - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v4.3.0 with: name: semgrep-results @@ -137,7 +174,7 @@ jobs: git checkout -q --detach "$HEAD_SHA" - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@v4.4.0 with: node-version: '22' diff --git a/.github/workflows/vuln-remediation-self-test.yml b/.github/workflows/vuln-remediation-self-test.yml index 2b22f43..fd180b5 100644 --- a/.github/workflows/vuln-remediation-self-test.yml +++ b/.github/workflows/vuln-remediation-self-test.yml @@ -9,6 +9,8 @@ on: - 'scripts/vuln_remediation.py' - 'scripts/test_vuln_remediation.py' - 'scripts/test_vuln_remediation_workflow.py' + - 'scripts/action_pins.py' + - '.github/workflows/actions.lock' push: branches: - main @@ -19,6 +21,8 @@ on: - 'scripts/vuln_remediation.py' - 'scripts/test_vuln_remediation.py' - 'scripts/test_vuln_remediation_workflow.py' + - 'scripts/action_pins.py' + - '.github/workflows/actions.lock' workflow_dispatch: inputs: run-live-remediation: diff --git a/.github/workflows/vuln-remediation.yml b/.github/workflows/vuln-remediation.yml index cb07412..f0bdddd 100644 --- a/.github/workflows/vuln-remediation.yml +++ b/.github/workflows/vuln-remediation.yml @@ -43,13 +43,13 @@ jobs: steps: - name: Generate app token id: app-token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@v1.12.0 with: app-id: ${{ secrets.ADMIN_APP_ID }} private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }} - name: Checkout repository - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@v4.4.0 with: token: ${{ steps.app-token.outputs.token }} @@ -120,7 +120,7 @@ jobs: fi - name: Upload scan report - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: socket-report path: socket-report.json @@ -133,7 +133,7 @@ jobs: --output remediation-input.json - name: Upload normalized remediation input - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: remediation-input path: remediation-input.json @@ -143,10 +143,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@v4.4.0 - name: Download normalized remediation input - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: remediation-input @@ -183,7 +183,7 @@ jobs: cp remediation-context.json triage-result.json - name: Upload triage results - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: triage-result path: | @@ -203,13 +203,13 @@ jobs: run_agent: ${{ steps.snapshot.outputs.run_agent }} steps: - name: Checkout repository - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@v4.4.0 with: fetch-depth: 0 persist-credentials: false - name: Download triage results - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: triage-result path: ${{ runner.temp }}/remediation @@ -242,7 +242,7 @@ jobs: - name: Generate private module read token id: read-token if: steps.check.outputs.skip != 'true' - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@v1.12.0 with: app-id: ${{ secrets.ADMIN_APP_ID }} private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }} @@ -250,7 +250,7 @@ jobs: - name: Setup Go if: steps.check.outputs.skip != 'true' && inputs.go-version-file != '' - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + uses: actions/setup-go@v5.6.0 with: go-version-file: ${{ inputs.go-version-file }} env: @@ -258,7 +258,7 @@ jobs: - name: Setup Bun if: steps.check.outputs.skip != 'true' && inputs.setup-bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + uses: oven-sh/setup-bun@v2.2.0 - name: Install JS dependencies if: steps.check.outputs.skip != 'true' && inputs.setup-bun @@ -270,7 +270,7 @@ jobs: - name: Setup Python if: steps.check.outputs.skip != 'true' && inputs.setup-python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@v5.6.0 with: python-version: ${{ inputs.python-version }} @@ -341,7 +341,7 @@ jobs: - name: Upload private Go modules if: steps.snapshot.outputs.run_agent == 'true' && inputs.go-version-file != '' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: go-private-modules path: ${{ runner.temp }}/go-private-modules.tgz @@ -349,7 +349,7 @@ jobs: - name: Upload Socket changes if: steps.snapshot.outputs.has_changes == 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: remediation-patch path: | @@ -358,7 +358,7 @@ jobs: - name: Upload Socket fix output if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: socket-fix-apply path: ${{ runner.temp }}/remediation/socket-fix-apply.json @@ -380,13 +380,13 @@ jobs: has_changes: ${{ steps.snapshot.outputs.has_changes }} steps: - name: Checkout repository - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@v4.4.0 with: ref: ${{ needs.fix.outputs.base_sha }} persist-credentials: false - name: Download triage results - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: triage-result path: ${{ runner.temp }}/remediation @@ -396,7 +396,7 @@ jobs: - name: Setup Go if: inputs.go-version-file != '' - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + uses: actions/setup-go@v5.6.0 with: go-version-file: ${{ inputs.go-version-file }} cache: false @@ -405,7 +405,7 @@ jobs: - name: Download private Go modules if: inputs.go-version-file != '' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: go-private-modules path: ${{ runner.temp }} @@ -419,7 +419,7 @@ jobs: - name: Setup Bun if: inputs.setup-bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + uses: oven-sh/setup-bun@v2.2.0 with: no-cache: true @@ -429,7 +429,7 @@ jobs: - name: Setup Python if: inputs.setup-python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@v5.6.0 with: python-version: ${{ inputs.python-version }} @@ -438,7 +438,7 @@ jobs: run: pip install uv - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@v4.4.0 with: node-version: '22' @@ -454,7 +454,7 @@ jobs: # git so the agent's changes never include it and `git clean` keeps it for the stop step. - name: Fetch credentials action id: credentials-action - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@v4.2.2 with: repository: kernel/security-workflows ref: ${{ inputs.security-workflows-ref }} @@ -520,7 +520,7 @@ jobs: - name: Upload agent changes if: steps.snapshot.outputs.has_changes == 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: remediation-patch path: | @@ -540,19 +540,19 @@ jobs: publish: ${{ steps.result.outputs.publish }} steps: - name: Checkout repository - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@v4.4.0 with: ref: ${{ needs.fix.outputs.base_sha }} persist-credentials: false - name: Download triage results - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: triage-result path: ${{ runner.temp }}/remediation - name: Download remediation patch - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: remediation-patch path: ${{ runner.temp }}/remediation @@ -624,7 +624,7 @@ jobs: - name: Upload fix results if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@v4.6.2 with: name: fix-result path: | @@ -648,19 +648,19 @@ jobs: UPDATE_SHA: ${{ needs.fix.outputs.update_sha }} steps: - name: Checkout repository - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@v4.4.0 with: ref: ${{ needs.fix.outputs.base_sha }} persist-credentials: false - name: Download triage results - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: triage-result path: ${{ runner.temp }}/remediation - name: Download fix results - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@v4.3.0 with: name: fix-result path: ${{ runner.temp }}/remediation @@ -699,7 +699,7 @@ jobs: - name: Generate app token id: app-token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@v1.12.0 with: app-id: ${{ secrets.ADMIN_APP_ID }} private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }} diff --git a/scripts/action_pins.py b/scripts/action_pins.py new file mode 100644 index 0000000..689aa89 --- /dev/null +++ b/scripts/action_pins.py @@ -0,0 +1,34 @@ +"""Checks that workflow `uses:` refs are exact releases recorded in actions.lock.""" +from __future__ import annotations + +import re +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +LOCKFILE = ROOT / ".github/workflows/actions.lock" +EXACT_TAG = re.compile(r"v\d+\.\d+\.\d+") +SHA = re.compile(r"[0-9a-f]{40}") + + +def lock_problem(workflow: Path, uses: str) -> str | None: + """Return why `uses` in `workflow` isn't an exact, locked ref, or None if it is. + + A ref must be an exact vX.Y.Z tag, or a full commit SHA for a commit with no release + tag. Either way the workflow must be onboarded in actions.lock with an entry for that + exact ref. `$/` actions always run at the workflow's own commit. + """ + if uses.startswith("$/"): + return None + name, _, ref = uses.partition("@") + if not (EXACT_TAG.fullmatch(ref) or SHA.fullmatch(ref)): + return f"{uses}: ref must be an exact vX.Y.Z tag or a full commit SHA" + lock = yaml.safe_load(LOCKFILE.read_text()) + key = "/".join(name.split("/")[:2]) + "@" + ref + if key not in lock["workflows"].get(str(workflow.relative_to(ROOT)), []): + return f"{uses}: {workflow.name} has no actions.lock entry for {key}" + locked_ref = lock["dependencies"][key]["ref"] + if SHA.fullmatch(ref) and EXACT_TAG.fullmatch(locked_ref): + return f"{uses}: {ref[:7]} is release {locked_ref}; reference the tag instead" + return None diff --git a/scripts/test_container_remediation.py b/scripts/test_container_remediation.py index d31c043..1d81ded 100644 --- a/scripts/test_container_remediation.py +++ b/scripts/test_container_remediation.py @@ -8,6 +8,8 @@ import yaml +from action_pins import lock_problem + ROOT = Path(__file__).resolve().parents[1] WORKFLOW = ROOT / ".github/workflows/container-remediation.yml" @@ -48,7 +50,8 @@ def test_actions_are_pinned_and_checkouts_do_not_persist_credentials(self): if "uses" not in step: continue # The credentials action is this repo's own, checked out at security-workflows-ref. - self.assertRegex(step["uses"], r"^([\w.-]+/[\w.-]+@[0-9a-f]{40}|\./\.security-workflows/\.github/actions/llm-credentials)$") + if step["uses"] != "./.security-workflows/.github/actions/llm-credentials": + self.assertIsNone(lock_problem(WORKFLOW, step["uses"])) if step["uses"].startswith("actions/checkout@"): self.assertIs(step["with"]["persist-credentials"], False) self.assertNotIn("token", step["with"]) diff --git a/scripts/test_semgrep_triage.py b/scripts/test_semgrep_triage.py index c6ecc7d..3e2420a 100644 --- a/scripts/test_semgrep_triage.py +++ b/scripts/test_semgrep_triage.py @@ -1,6 +1,7 @@ import io import json import os +import re from contextlib import redirect_stdout from pathlib import Path import subprocess @@ -285,5 +286,38 @@ def test_model_credentials_exist_only_while_the_agent_runs(self): self.assertNotIn("id-token", jobs[name]["permissions"]) +class MutableActionTagRuleTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + steps = yaml.safe_load(WORKFLOW.read_text())["jobs"]["scan"]["steps"] + cls.script = next(s for s in steps if s.get("name") == "Run Semgrep")["run"] + rule_text = cls.script.split("<<'RULE'\n", 1)[1].split("\nRULE\n", 1)[0] + (rule,) = yaml.safe_load(rule_text)["rules"] + cls.allowed = [p["pattern-not-regex"] for p in rule["patterns"][2]["metavariable-pattern"]["patterns"]] + + def flagged(self, uses): + return not any(re.search(regex, uses) for regex in self.allowed) + + def test_replaces_the_registry_rule(self): + self.assertIn("--config p/github-actions", self.script) + self.assertIn("--exclude-rule yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag", self.script) + self.assertIn('--config "$RUNNER_TEMP/github-actions-mutable-action-tag.yml"', self.script) + + def test_allows_exact_tags_shas_and_same_repo_actions(self): + for uses in ( + "actions/checkout@v4.4.0", + "kernel/security-workflows/.github/actions/llm-credentials@v1.2.3", + "actions/checkout@11d5960a326750d5838078e36cf38b85af677262", + "$/.github/actions/build", + "./.github/actions/build", + "docker://alpine:3", + ): + self.assertFalse(self.flagged(uses), uses) + + def test_flags_floating_tags_and_branches(self): + for uses in ("actions/checkout@v4", "actions/checkout@v4.4", "actions/checkout@main", "actions/setup-node@v4.4.0-beta.1"): + self.assertTrue(self.flagged(uses), uses) + + if __name__ == "__main__": unittest.main() diff --git a/scripts/test_vuln_remediation_workflow.py b/scripts/test_vuln_remediation_workflow.py index 3b719e8..097040c 100644 --- a/scripts/test_vuln_remediation_workflow.py +++ b/scripts/test_vuln_remediation_workflow.py @@ -8,9 +8,13 @@ import tempfile import unittest from pathlib import Path +from unittest import mock import yaml +import action_pins +from action_pins import lock_problem + ROOT = Path(__file__).resolve().parents[1] WORKFLOW_PATH = ROOT / ".github/workflows/vuln-remediation.yml" TOKEN_REF = re.compile(r"GH_TOKEN|GITHUB_TOKEN|steps\.[\w-]+\.outputs\.token|github\.token") @@ -73,11 +77,41 @@ def test_actions_are_pinned_and_checkouts_do_not_persist_credentials(self): if not uses: continue if uses != CREDENTIALS_ACTION: - self.assertRegex(uses, r"^[\w/-]+@[0-9a-f]{40}$", f"{job_name}: {uses}") + self.assertIsNone(lock_problem(WORKFLOW_PATH, uses), job_name) if job_name in ("fix", "agent", "verify", "pr") and uses.startswith("actions/checkout@"): self.assertIs(entry["with"]["persist-credentials"], False) self.assertNotIn("token", entry["with"]) + def test_lock_check_rejects_floating_unlocked_and_tagged_sha_refs(self): + with tempfile.TemporaryDirectory() as tmp: + lockfile = Path(tmp) / "actions.lock" + lockfile.write_text(yaml.safe_dump({ + "workflows": {".github/workflows/vuln-remediation.yml": [ + "actions/checkout@v4.4.0", + "actions/checkout@11d5960a326750d5838078e36cf38b85af677262", + "aquasecurity/trivy-action@38b337fff0a2f984d5be173c99e3f633438167ec", + ]}, + "dependencies": { + "actions/checkout@v4.4.0": {"ref": "v4.4.0"}, + "actions/checkout@11d5960a326750d5838078e36cf38b85af677262": {"ref": "v4.4.0"}, + "aquasecurity/trivy-action@38b337fff0a2f984d5be173c99e3f633438167ec": {"ref": "master"}, + }, + })) + with mock.patch.object(action_pins, "LOCKFILE", lockfile): + for uses in ( + "actions/checkout@v4.4.0", + "aquasecurity/trivy-action@38b337fff0a2f984d5be173c99e3f633438167ec", + "$/.github/actions/llm-credentials", + ): + self.assertIsNone(lock_problem(WORKFLOW_PATH, uses), uses) + for uses in ( + "actions/checkout@v4", + "actions/checkout@main", + "actions/checkout@v4.5.0", + "actions/checkout@11d5960a326750d5838078e36cf38b85af677262", + ): + self.assertIsNotNone(lock_problem(WORKFLOW_PATH, uses), uses) + def test_default_token_is_read_only(self): self.assertEqual(yaml.safe_load(WORKFLOW_PATH.read_text())["permissions"], {"contents": "read"}) # Only the agent job adds anything: an OIDC token for the llm-credentials action.