diff --git a/.github/tests/fix-ci-security.test.mjs b/.github/tests/fix-ci-security.test.mjs index 09564cac..f5fe05d9 100644 --- a/.github/tests/fix-ci-security.test.mjs +++ b/.github/tests/fix-ci-security.test.mjs @@ -43,7 +43,7 @@ test('only failed runs of same-repository PRs are handled', () => { test('actions are pinned and checkouts do not persist credentials', () => { for (const entry of Object.keys(jobs).flatMap(steps).filter((entry) => entry.includes('uses: '))) { - assert.match(entry, /uses: [\w/-]+@[a-f0-9]{40}(?:\s|$)/); + assert.match(entry, /uses: [\w./-]+@[a-f0-9]{40}(?:\s|$)/); if (entry.includes('uses: actions/checkout@')) { assert.match(entry, /persist-credentials: false/); assert.doesNotMatch(entry, /token:/); @@ -60,7 +60,9 @@ test('expressions are not interpolated into shell scripts', () => { test('the generate job has no write credentials', () => { assert.match(workflow, /^permissions:\n contents: read\n\n/m); const generate = jobs.generate.join(''); - assert.doesNotMatch(generate, /: write\b/); + // id-token: write only lets the job mint an OIDC token for short-lived model credentials. + assert.equal(generate.match(/: write\b/g).length, 1); + assert.match(header('generate'), /^ permissions:\n(?: [\w-]+: \w+\n)* id-token: write\n/m); assert.doesNotMatch(generate, /create-github-app-token|ADMIN_APP/); assert.doesNotMatch(step('generate', 'Fix CI failure'), /GH_TOKEN:|GITHUB_TOKEN:|github\.token|outputs\.token/); // Agent and PR code run in this job, so caches it saves could reach other workflows. diff --git a/.github/workflows/fix-ci.yaml b/.github/workflows/fix-ci.yaml index 83f0ddef..8790c74f 100644 --- a/.github/workflows/fix-ci.yaml +++ b/.github/workflows/fix-ci.yaml @@ -22,6 +22,7 @@ jobs: contents: read actions: read pull-requests: read + id-token: write outputs: changed: ${{ steps.snapshot.outputs.changed }} pr_number: ${{ steps.pr.outputs.number }} @@ -100,12 +101,22 @@ jobs: npm install -g --ignore-scripts @earendil-works/pi-coding-agent pi --version + - name: Model credentials + if: steps.pr.outputs.number != '' + uses: kernel/security-workflows/.github/actions/llm-credentials@4144994f1d6977e07ef5a5832599ee4faaae9550 # main + with: + model: ${{ vars.PI_PREFERRED_MODEL }} + anthropic-federation-rule-id: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }} + anthropic-organization-id: ${{ vars.ANTHROPIC_ORGANIZATION_ID }} + anthropic-service-account-id: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }} + openai-identity-provider-id: ${{ vars.OPENAI_IDENTITY_PROVIDER_ID }} + openai-service-account-id: ${{ vars.OPENAI_SERVICE_ACCOUNT_ID }} + openai-audience: ${{ vars.OPENAI_WIF_AUDIENCE }} + - name: Fix CI failure if: steps.pr.outputs.number != '' working-directory: pr env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} PI_PREFERRED_MODEL: ${{ vars.PI_PREFERRED_MODEL }} PI_SKIP_VERSION_CHECK: '1' PI_TELEMETRY: '0' @@ -123,6 +134,12 @@ jobs: --model "$PI_PREFERRED_MODEL" --thinking max \ @"$RUNNER_TEMP/fix-ci-prompt.md" + - name: Stop model credentials + if: always() + uses: kernel/security-workflows/.github/actions/llm-credentials@4144994f1d6977e07ef5a5832599ee4faaae9550 # main + with: + stop: true + - name: Snapshot publishable changes id: snapshot if: steps.pr.outputs.number != '' diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 32fec528..75e5f834 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -7,6 +7,8 @@ on: permissions: contents: read pull-requests: write + # semgrep exchanges the job's OIDC token for short-lived model credentials. + id-token: write jobs: scan: diff --git a/.github/workflows/vuln-remediation.yml b/.github/workflows/vuln-remediation.yml index 22ee0653..35f30c82 100644 --- a/.github/workflows/vuln-remediation.yml +++ b/.github/workflows/vuln-remediation.yml @@ -8,6 +8,8 @@ on: permissions: contents: write pull-requests: write + # vuln-remediation exchanges the job's OIDC token for short-lived model credentials. + id-token: write jobs: remediate: