From 4a3e858595b1a3ae7d60b0ea32ca2725e7758d2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:11:59 +0900 Subject: [PATCH 001/180] test(ci): require bounded SARIF merge provenance --- .../src/source-verification-workflow.test.mjs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index 388e61815..9409dabf1 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -75,6 +75,47 @@ test('required source-verification jobs explicitly checkout the contributor head 'AppGuardrail is not bound to the contributor head', ); + const sarifProvenance = stepBlock( + appguardrail, + 'Materialize AppGuardrail SARIF PR merge provenance', + ); + assert.ok( + sarifProvenance.includes( + 'PR_NUMBER: ${{ github.event.pull_request.number }}', + ), + 'AppGuardrail SARIF provenance is not bound to the pull request number', + ); + assert.ok( + sarifProvenance.includes('EXPECTED_MERGE_SHA: ${{ github.sha }}'), + 'AppGuardrail SARIF provenance is not bound to the advertised merge SHA', + ); + assert.ok( + sarifProvenance.includes( + 'git fetch --no-tags --depth=1 origin "$merge_ref"', + ), + 'AppGuardrail SARIF provenance must fetch only the bounded PR merge ref', + ); + assert.ok( + sarifProvenance.includes('git rev-parse FETCH_HEAD'), + 'AppGuardrail SARIF provenance must verify the fetched merge identity', + ); + assert.ok( + sarifProvenance.includes( + 'git cat-file -e "${EXPECTED_MERGE_SHA}^{commit}"', + ), + 'AppGuardrail SARIF provenance must verify the merge commit object locally', + ); + assert.equal( + sarifProvenance.includes('fetch-depth: 0'), + false, + 'AppGuardrail SARIF provenance must not broaden checkout history', + ); + assert.equal( + sarifProvenance.includes('git checkout'), + false, + 'AppGuardrail SARIF provenance must not replace the analyzed contributor head', + ); + const sarifUpload = stepBlock( appguardrail, 'Upload AppGuardrail SARIF to code scanning', From ebedd2fc53a29070ce2067d42b53d5a297d9e605 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:12:40 +0900 Subject: [PATCH 002/180] ci(test): run SARIF provenance RED verifier --- .../tmp-sarif-provenance-verifier.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/tmp-sarif-provenance-verifier.yml diff --git a/.github/workflows/tmp-sarif-provenance-verifier.yml b/.github/workflows/tmp-sarif-provenance-verifier.yml new file mode 100644 index 000000000..dfa5f8a5b --- /dev/null +++ b/.github/workflows/tmp-sarif-provenance-verifier.yml @@ -0,0 +1,32 @@ +name: Temporary SARIF provenance verifier + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js 24 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Run focused SARIF provenance contract + run: node --test packages/commercial-readiness/src/source-verification-workflow.test.mjs From e3fabc8373f7d0672141fd6d47314a311b3027f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:15:51 +0900 Subject: [PATCH 003/180] fix(ci): materialize bounded SARIF PR provenance --- .github/workflows/appguardrail.yml | 31 ++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/appguardrail.yml b/.github/workflows/appguardrail.yml index 3ffeb130d..9e7b322ae 100644 --- a/.github/workflows/appguardrail.yml +++ b/.github/workflows/appguardrail.yml @@ -124,6 +124,33 @@ jobs: ) PY + - name: Materialize AppGuardrail SARIF PR merge provenance + if: >- + github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + EXPECTED_MERGE_SHA: ${{ github.sha }} + run: | + set -euo pipefail + if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::Pull request number is not a positive integer." + exit 1 + fi + if ! [[ "$EXPECTED_MERGE_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::Advertised pull request merge SHA is not a full commit SHA." + exit 1 + fi + + merge_ref="refs/pull/${PR_NUMBER}/merge" + git fetch --no-tags --depth=1 origin "$merge_ref" + fetched_merge_sha="$(git rev-parse FETCH_HEAD)" + if [ "$fetched_merge_sha" != "$EXPECTED_MERGE_SHA" ]; then + echo "::error::Fetched pull request merge provenance does not match github.sha." + exit 1 + fi + git cat-file -e "${EXPECTED_MERGE_SHA}^{commit}" + - name: Upload AppGuardrail SARIF to code scanning if: >- always() @@ -140,8 +167,6 @@ jobs: id: stage_evidence if: always() continue-on-error: true - env: - REDACTION_OUTCOME: ${{ steps.redact_evidence.outcome }} run: | set -euo pipefail evidence_dir="${RUNNER_TEMP}/appguardrail-evidence" @@ -160,6 +185,8 @@ jobs: else echo "available=false" >> "$GITHUB_OUTPUT" fi + env: + REDACTION_OUTCOME: ${{ steps.redact_evidence.outcome }} - name: Upload AppGuardrail evidence if: >- From fc0dea421ceddad6bb5d7f06b8c7766944b9b913 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:18:20 +0900 Subject: [PATCH 004/180] ci: retire SARIF provenance verifier --- .../tmp-sarif-provenance-verifier.yml | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 .github/workflows/tmp-sarif-provenance-verifier.yml diff --git a/.github/workflows/tmp-sarif-provenance-verifier.yml b/.github/workflows/tmp-sarif-provenance-verifier.yml deleted file mode 100644 index dfa5f8a5b..000000000 --- a/.github/workflows/tmp-sarif-provenance-verifier.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Temporary SARIF provenance verifier - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 5 - steps: - - name: Checkout exact candidate - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js 24 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - - - name: Run focused SARIF provenance contract - run: node --test packages/commercial-readiness/src/source-verification-workflow.test.mjs From 8d5feb5e145398f0d530bbe85b3153ff599317d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:18:44 +0900 Subject: [PATCH 005/180] ci(test): verify SARIF provenance candidate --- .../tmp-sarif-provenance-full-verifier.yml | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/tmp-sarif-provenance-full-verifier.yml diff --git a/.github/workflows/tmp-sarif-provenance-full-verifier.yml b/.github/workflows/tmp-sarif-provenance-full-verifier.yml new file mode 100644 index 000000000..aa094a9a0 --- /dev/null +++ b/.github/workflows/tmp-sarif-provenance-full-verifier.yml @@ -0,0 +1,47 @@ +name: Temporary SARIF provenance full verifier + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js 24 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + package-manager-cache: false + + - name: Enable Corepack + run: corepack enable + + - name: Install exact dependency graph + run: pnpm install --frozen-lockfile + + - name: Verify SARIF provenance source and package + run: | + set -euo pipefail + pnpm exec prettier --check \ + .github/workflows/appguardrail.yml \ + packages/commercial-readiness/src/source-verification-workflow.test.mjs + pnpm --filter @life-os/commercial-readiness test + pnpm --filter @life-os/commercial-readiness typecheck + pnpm --filter @life-os/commercial-readiness build + git diff --check From cb6f00f49a02333594150502cb144b4e6f4baa24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:23:19 +0900 Subject: [PATCH 006/180] ci(test): expose canonical SARIF provenance formatting --- .../workflows/tmp-sarif-provenance-full-verifier.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tmp-sarif-provenance-full-verifier.yml b/.github/workflows/tmp-sarif-provenance-full-verifier.yml index aa094a9a0..03af6ba04 100644 --- a/.github/workflows/tmp-sarif-provenance-full-verifier.yml +++ b/.github/workflows/tmp-sarif-provenance-full-verifier.yml @@ -35,13 +35,12 @@ jobs: - name: Install exact dependency graph run: pnpm install --frozen-lockfile - - name: Verify SARIF provenance source and package + - name: Render canonical candidate diff run: | set -euo pipefail - pnpm exec prettier --check \ + pnpm exec prettier --write \ + .github/workflows/appguardrail.yml \ + packages/commercial-readiness/src/source-verification-workflow.test.mjs + git diff -- \ .github/workflows/appguardrail.yml \ packages/commercial-readiness/src/source-verification-workflow.test.mjs - pnpm --filter @life-os/commercial-readiness test - pnpm --filter @life-os/commercial-readiness typecheck - pnpm --filter @life-os/commercial-readiness build - git diff --check From 65bb8c18ca2eadbd01c027f75fba9adcde2f6050 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:27:06 +0900 Subject: [PATCH 007/180] fix(ci): keep AppGuardrail evidence env ordering unchanged --- .github/workflows/appguardrail.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/appguardrail.yml b/.github/workflows/appguardrail.yml index 9e7b322ae..a57c185fe 100644 --- a/.github/workflows/appguardrail.yml +++ b/.github/workflows/appguardrail.yml @@ -167,6 +167,8 @@ jobs: id: stage_evidence if: always() continue-on-error: true + env: + REDACTION_OUTCOME: ${{ steps.redact_evidence.outcome }} run: | set -euo pipefail evidence_dir="${RUNNER_TEMP}/appguardrail-evidence" @@ -185,8 +187,6 @@ jobs: else echo "available=false" >> "$GITHUB_OUTPUT" fi - env: - REDACTION_OUTCOME: ${{ steps.redact_evidence.outcome }} - name: Upload AppGuardrail evidence if: >- @@ -224,4 +224,4 @@ jobs: if [ ! -s appguardrail-findings.json ] || [ ! -s appguardrail.sarif ]; then echo "::error::AppGuardrail evidence was not produced." exit 1 - fi + fi \ No newline at end of file From 65aa16bac720285069a9ef1cdaa20acbd3e83843 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:29:51 +0900 Subject: [PATCH 008/180] style(test): canonicalize SARIF provenance contract --- .../src/source-verification-workflow.test.mjs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index 9409dabf1..e5c923e19 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -5,14 +5,16 @@ import { join } from 'node:path'; import test from 'node:test'; const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); -const SOURCE_REF = 'ref: ${{ github.event.pull_request.head.sha || github.sha }}'; +const SOURCE_REF = + 'ref: ${{ github.event.pull_request.head.sha || github.sha }}'; const ADVERTISED_MERGE_REF = 'ref: refs/pull/${{ github.event.pull_request.number }}/merge'; const LIVE_SOURCE_REF = 'ref: ${{ steps.live-identities.outputs.current_source }}'; const SARIF_SOURCE_REF = "ref: ${{ github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}"; -const SARIF_SOURCE_SHA = 'sha: ${{ github.event.pull_request.head.sha || github.sha }}'; +const SARIF_SOURCE_SHA = + 'sha: ${{ github.event.pull_request.head.sha || github.sha }}'; /** Reads one repository workflow as UTF-8 text. */ function readWorkflow(name) { @@ -37,7 +39,9 @@ function jobBlock(workflow, jobName) { /** Extracts one named workflow step from an already bounded job block. */ function stepBlock(job, stepName) { const lines = job.split('\n'); - const start = lines.findIndex((line) => line.trim() === `- name: ${stepName}`); + const start = lines.findIndex( + (line) => line.trim() === `- name: ${stepName}`, + ); assert.notEqual(start, -1, `missing step ${stepName}`); let end = lines.length; for (let index = start + 1; index < lines.length; index += 1) { @@ -142,7 +146,9 @@ test('merge compatibility reconstructs a fresh integration tree from current API assert.ok(block.includes('id: live-identities')); assert.ok(block.includes('GITHUB_TOKEN: ${{ github.token }}')); assert.ok(block.includes('/pulls/${{ github.event.pull_request.number }}')); - assert.ok(block.includes('/commits/${{ github.event.pull_request.base.ref }}')); + assert.ok( + block.includes('/commits/${{ github.event.pull_request.base.ref }}'), + ); assert.ok(block.includes(LIVE_SOURCE_REF)); assert.ok( block.includes('fetch-depth: 0'), From 7c08c7fe63f26e7d8f3716989b88db39491c2487 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:30:19 +0900 Subject: [PATCH 009/180] ci(test): verify exact SARIF provenance candidate --- .../workflows/tmp-sarif-provenance-full-verifier.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tmp-sarif-provenance-full-verifier.yml b/.github/workflows/tmp-sarif-provenance-full-verifier.yml index 03af6ba04..8c1610bd7 100644 --- a/.github/workflows/tmp-sarif-provenance-full-verifier.yml +++ b/.github/workflows/tmp-sarif-provenance-full-verifier.yml @@ -35,12 +35,12 @@ jobs: - name: Install exact dependency graph run: pnpm install --frozen-lockfile - - name: Render canonical candidate diff + - name: Verify SARIF provenance candidate run: | set -euo pipefail - pnpm exec prettier --write \ - .github/workflows/appguardrail.yml \ - packages/commercial-readiness/src/source-verification-workflow.test.mjs - git diff -- \ - .github/workflows/appguardrail.yml \ + pnpm exec prettier --check \ packages/commercial-readiness/src/source-verification-workflow.test.mjs + pnpm --filter @life-os/commercial-readiness test + pnpm --filter @life-os/commercial-readiness typecheck + pnpm --filter @life-os/commercial-readiness build + git diff --check From e4a7ddcdf55a02ccbebd70f7654a529578b27db2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:32:18 +0900 Subject: [PATCH 010/180] style(ci): preserve AppGuardrail newline boundary --- .github/workflows/appguardrail.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/appguardrail.yml b/.github/workflows/appguardrail.yml index a57c185fe..e88fe15a9 100644 --- a/.github/workflows/appguardrail.yml +++ b/.github/workflows/appguardrail.yml @@ -224,4 +224,4 @@ jobs: if [ ! -s appguardrail-findings.json ] || [ ! -s appguardrail.sarif ]; then echo "::error::AppGuardrail evidence was not produced." exit 1 - fi \ No newline at end of file + fi From 76e352ed063bcbf48be6061e2f0bf9edaa47ece7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:34:00 +0900 Subject: [PATCH 011/180] ci: retire SARIF provenance full verifier --- .../tmp-sarif-provenance-full-verifier.yml | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 .github/workflows/tmp-sarif-provenance-full-verifier.yml diff --git a/.github/workflows/tmp-sarif-provenance-full-verifier.yml b/.github/workflows/tmp-sarif-provenance-full-verifier.yml deleted file mode 100644 index 8c1610bd7..000000000 --- a/.github/workflows/tmp-sarif-provenance-full-verifier.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Temporary SARIF provenance full verifier - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - name: Checkout exact candidate - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js 24 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false - - - name: Enable Corepack - run: corepack enable - - - name: Install exact dependency graph - run: pnpm install --frozen-lockfile - - - name: Verify SARIF provenance candidate - run: | - set -euo pipefail - pnpm exec prettier --check \ - packages/commercial-readiness/src/source-verification-workflow.test.mjs - pnpm --filter @life-os/commercial-readiness test - pnpm --filter @life-os/commercial-readiness typecheck - pnpm --filter @life-os/commercial-readiness build - git diff --check From 169f18bc4b2ab94bf61a41b16cc5dbf60f3a8cb2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:37:56 +0900 Subject: [PATCH 012/180] test(commercial): bound unnamed workflow sibling steps --- .../src/source-verification-workflow.test.mjs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index e5c923e19..03cbc1750 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -53,6 +53,28 @@ function stepBlock(job, stepName) { return lines.slice(start, end).join('\n'); } +test('step extraction does not borrow evidence from unnamed sibling steps', () => { + const job = [ + ' scan:', + ' steps:', + ' - name: Materialize AppGuardrail SARIF PR merge provenance', + ' run: echo target-step', + ' - run: echo sibling-sentinel', + ' - uses: actions/upload-artifact@example', + ].join('\n'); + + const block = stepBlock( + job, + 'Materialize AppGuardrail SARIF PR merge provenance', + ); + assert.ok(block.includes('target-step')); + assert.equal( + block.includes('sibling-sentinel'), + false, + 'a named step must not satisfy its contract from a later unnamed sibling step', + ); +}); + test('required source-verification jobs explicitly checkout the contributor head', () => { const ci = readWorkflow('ci.yml'); for (const jobName of [ From df0a74f0ccc639a0b01f35f3c6d909e9b7dd6ca8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:38:33 +0900 Subject: [PATCH 013/180] fix(commercial): bound workflow step extraction by indentation --- .../src/source-verification-workflow.test.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index 03cbc1750..1f1ab0b4d 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -43,9 +43,13 @@ function stepBlock(job, stepName) { (line) => line.trim() === `- name: ${stepName}`, ); assert.notEqual(start, -1, `missing step ${stepName}`); + const startMatch = /^(\s*)-\s/u.exec(lines[start]); + assert.ok(startMatch, `invalid step indentation for ${stepName}`); + const stepIndent = startMatch[1]; let end = lines.length; for (let index = start + 1; index < lines.length; index += 1) { - if (/^\s+- name: /u.test(lines[index])) { + const siblingStep = /^(\s*)-\s/u.exec(lines[index]); + if (siblingStep?.[1] === stepIndent) { end = index; break; } From 05179d5868f0730d21e9e31324181ed54c61d35c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:41:34 +0900 Subject: [PATCH 014/180] test(commercial): enforce SARIF provenance guard and order --- .../src/source-verification-workflow.test.mjs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index 1f1ab0b4d..d1e0231ee 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -109,6 +109,16 @@ test('required source-verification jobs explicitly checkout the contributor head appguardrail, 'Materialize AppGuardrail SARIF PR merge provenance', ); + assert.ok( + sarifProvenance.includes("github.event_name == 'pull_request'"), + 'AppGuardrail SARIF provenance must run only for pull_request events', + ); + assert.ok( + sarifProvenance.includes( + 'github.event.pull_request.head.repo.full_name == github.repository', + ), + 'AppGuardrail SARIF provenance must be limited to same-repository pull requests', + ); assert.ok( sarifProvenance.includes( 'PR_NUMBER: ${{ github.event.pull_request.number }}', @@ -150,6 +160,12 @@ test('required source-verification jobs explicitly checkout the contributor head appguardrail, 'Upload AppGuardrail SARIF to code scanning', ); + assert.ok( + appguardrail.indexOf( + '- name: Materialize AppGuardrail SARIF PR merge provenance', + ) < appguardrail.indexOf('- name: Upload AppGuardrail SARIF to code scanning'), + 'AppGuardrail SARIF provenance must precede SARIF upload', + ); assert.ok( sarifUpload.includes(SARIF_SOURCE_REF), 'AppGuardrail SARIF ref is not bound to the analyzed contributor head', From 4a0858a10c42f5f9afa521857eba26ccc2d72b7c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:43:48 +0900 Subject: [PATCH 015/180] test(commercial): use structural workflow step ordering --- .../src/source-verification-workflow.test.mjs | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index d1e0231ee..a9b1624f4 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -36,13 +36,20 @@ function jobBlock(workflow, jobName) { return lines.slice(start, end).join('\n'); } -/** Extracts one named workflow step from an already bounded job block. */ -function stepBlock(job, stepName) { +/** Finds one named workflow step by its exact YAML sequence entry. */ +function stepStartIndex(job, stepName) { const lines = job.split('\n'); const start = lines.findIndex( (line) => line.trim() === `- name: ${stepName}`, ); assert.notEqual(start, -1, `missing step ${stepName}`); + return start; +} + +/** Extracts one named workflow step from an already bounded job block. */ +function stepBlock(job, stepName) { + const lines = job.split('\n'); + const start = stepStartIndex(job, stepName); const startMatch = /^(\s*)-\s/u.exec(lines[start]); assert.ok(startMatch, `invalid step indentation for ${stepName}`); const stepIndent = startMatch[1]; @@ -57,6 +64,14 @@ function stepBlock(job, stepName) { return lines.slice(start, end).join('\n'); } +/** Requires one exact named workflow step to occur before another. */ +function assertStepPrecedes(job, earlierStepName, laterStepName) { + assert.ok( + stepStartIndex(job, earlierStepName) < stepStartIndex(job, laterStepName), + `${earlierStepName} must precede ${laterStepName}`, + ); +} + test('step extraction does not borrow evidence from unnamed sibling steps', () => { const job = [ ' scan:', @@ -79,6 +94,28 @@ test('step extraction does not borrow evidence from unnamed sibling steps', () = ); }); +test('step ordering ignores comments that only mention a step name', () => { + const job = [ + ' scan:', + ' steps:', + ' # stale note: - name: Materialize AppGuardrail SARIF PR merge provenance', + ' - name: Upload AppGuardrail SARIF to code scanning', + ' run: echo upload', + ' - name: Materialize AppGuardrail SARIF PR merge provenance', + ' run: echo provenance', + ].join('\n'); + + assert.throws( + () => + assertStepPrecedes( + job, + 'Materialize AppGuardrail SARIF PR merge provenance', + 'Upload AppGuardrail SARIF to code scanning', + ), + /must precede/u, + ); +}); + test('required source-verification jobs explicitly checkout the contributor head', () => { const ci = readWorkflow('ci.yml'); for (const jobName of [ @@ -160,11 +197,10 @@ test('required source-verification jobs explicitly checkout the contributor head appguardrail, 'Upload AppGuardrail SARIF to code scanning', ); - assert.ok( - appguardrail.indexOf( - '- name: Materialize AppGuardrail SARIF PR merge provenance', - ) < appguardrail.indexOf('- name: Upload AppGuardrail SARIF to code scanning'), - 'AppGuardrail SARIF provenance must precede SARIF upload', + assertStepPrecedes( + appguardrail, + 'Materialize AppGuardrail SARIF PR merge provenance', + 'Upload AppGuardrail SARIF to code scanning', ); assert.ok( sarifUpload.includes(SARIF_SOURCE_REF), From fc50d20ab36641c53b9018d9a73a4d82d9c8882f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 07:08:49 +0900 Subject: [PATCH 016/180] test(commercial): reject duplicate workflow step authority --- .../src/source-verification-workflow.test.mjs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index a9b1624f4..059c147c5 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -116,6 +116,23 @@ test('step ordering ignores comments that only mention a step name', () => { ); }); +test('step lookup rejects duplicate authority-bearing step names', () => { + const job = [ + ' scan:', + ' steps:', + ' - name: Upload AppGuardrail SARIF to code scanning', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + ' - name: Upload AppGuardrail SARIF to code scanning', + ' uses: github/codeql-action/upload-sarif@example', + ].join('\n'); + + assert.throws( + () => stepBlock(job, 'Upload AppGuardrail SARIF to code scanning'), + /exactly one step/u, + ); +}); + test('required source-verification jobs explicitly checkout the contributor head', () => { const ci = readWorkflow('ci.yml'); for (const jobName of [ @@ -293,4 +310,4 @@ test('merge-tree compatibility provisions the PostgreSQL contract required by th `merge_compatibility is missing ${variableName}`, ); } -}); +}); \ No newline at end of file From 02dd001daf30460f298d90d823775342ebef27a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 07:09:01 +0900 Subject: [PATCH 017/180] ci: verify SARIF step-name uniqueness regression --- .../workflows/tmp-sarif-step-uniqueness.yml | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/tmp-sarif-step-uniqueness.yml diff --git a/.github/workflows/tmp-sarif-step-uniqueness.yml b/.github/workflows/tmp-sarif-step-uniqueness.yml new file mode 100644 index 000000000..8f0ce65e3 --- /dev/null +++ b/.github/workflows/tmp-sarif-step-uniqueness.yml @@ -0,0 +1,27 @@ +name: Temporary SARIF Step Uniqueness Verifier + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Verify source-workflow contract + run: node --test packages/commercial-readiness/src/source-verification-workflow.test.mjs From 4f550baa80a8f2b5769b1e0391ec6af593ebbf8d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 07:10:13 +0900 Subject: [PATCH 018/180] test(commercial): require unique workflow step authority --- .../src/source-verification-workflow.test.mjs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index 059c147c5..445d6e457 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -36,14 +36,21 @@ function jobBlock(workflow, jobName) { return lines.slice(start, end).join('\n'); } -/** Finds one named workflow step by its exact YAML sequence entry. */ +/** Finds one unique named workflow step by its exact YAML sequence entry. */ function stepStartIndex(job, stepName) { const lines = job.split('\n'); - const start = lines.findIndex( - (line) => line.trim() === `- name: ${stepName}`, + const matches = []; + for (let index = 0; index < lines.length; index += 1) { + if (lines[index].trim() === `- name: ${stepName}`) { + matches.push(index); + } + } + assert.equal( + matches.length, + 1, + `expected exactly one step ${stepName}, found ${matches.length}`, ); - assert.notEqual(start, -1, `missing step ${stepName}`); - return start; + return matches[0]; } /** Extracts one named workflow step from an already bounded job block. */ From 37412c61c2436b2701c4c2783bae8afab2a66c43 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 07:15:36 +0900 Subject: [PATCH 019/180] ci: retire SARIF step uniqueness verifier --- .../workflows/tmp-sarif-step-uniqueness.yml | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 .github/workflows/tmp-sarif-step-uniqueness.yml diff --git a/.github/workflows/tmp-sarif-step-uniqueness.yml b/.github/workflows/tmp-sarif-step-uniqueness.yml deleted file mode 100644 index 8f0ce65e3..000000000 --- a/.github/workflows/tmp-sarif-step-uniqueness.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Temporary SARIF Step Uniqueness Verifier - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 5 - steps: - - name: Checkout exact candidate - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Verify source-workflow contract - run: node --test packages/commercial-readiness/src/source-verification-workflow.test.mjs From a8f61be914fa6ae1be99ea71ea095c217c0ffdb1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 08:07:16 +0900 Subject: [PATCH 020/180] test(ci): bind SARIF action authority to one reviewed step --- .../src/source-verification-workflow.test.mjs | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index 445d6e457..5ace56413 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -79,6 +79,36 @@ function assertStepPrecedes(job, earlierStepName, laterStepName) { ); } +/** Finds executable uses entries for one exact GitHub Action identity. */ +function actionUseLines(workflowBlock, actionName) { + const usesEntry = /^uses:\s*(['"]?)([^'"\s#]+)\1(?:\s+#.*)?$/u; + return workflowBlock + .split('\n') + .map((line) => line.trim()) + .filter((line) => { + if (line.startsWith('#')) { + return false; + } + const match = usesEntry.exec(line); + return match?.[2].startsWith(`${actionName}@`) ?? false; + }); +} + +/** Requires one action invocation and binds it to the reviewed named step. */ +function assertUniqueActionUseInStep(job, actionName, stepName) { + const jobUses = actionUseLines(job, actionName); + assert.equal( + jobUses.length, + 1, + `expected exactly one ${actionName} use, found ${jobUses.length}`, + ); + assert.equal( + actionUseLines(stepBlock(job, stepName), actionName).length, + 1, + `${actionName} must be owned by ${stepName}`, + ); +} + test('step extraction does not borrow evidence from unnamed sibling steps', () => { const job = [ ' scan:', @@ -140,6 +170,32 @@ test('step lookup rejects duplicate authority-bearing step names', () => { ); }); +test('SARIF upload authority rejects differently named duplicate action uses', () => { + const job = [ + ' scan:', + ' steps:', + ' - name: Upload AppGuardrail SARIF to code scanning', + ' uses: github/codeql-action/upload-sarif@reviewed-sha', + ' with:', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + ' - name: Upload alternate SARIF', + ' uses: "github/codeql-action/upload-sarif@unreviewed-sha"', + ' with:', + ' sarif_file: alternate.sarif', + ].join('\n'); + + assert.throws( + () => + assertUniqueActionUseInStep( + job, + 'github/codeql-action/upload-sarif', + 'Upload AppGuardrail SARIF to code scanning', + ), + /exactly one github\/codeql-action\/upload-sarif use/u, + ); +}); + test('required source-verification jobs explicitly checkout the contributor head', () => { const ci = readWorkflow('ci.yml'); for (const jobName of [ @@ -221,6 +277,11 @@ test('required source-verification jobs explicitly checkout the contributor head appguardrail, 'Upload AppGuardrail SARIF to code scanning', ); + assertUniqueActionUseInStep( + appguardrail, + 'github/codeql-action/upload-sarif', + 'Upload AppGuardrail SARIF to code scanning', + ); assertStepPrecedes( appguardrail, 'Materialize AppGuardrail SARIF PR merge provenance', @@ -317,4 +378,4 @@ test('merge-tree compatibility provisions the PostgreSQL contract required by th `merge_compatibility is missing ${variableName}`, ); } -}); \ No newline at end of file +}); From af22490e36ca925d74d3f606dfef5daa9af2fa6f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 09:08:27 +0900 Subject: [PATCH 021/180] test(ci): reject unnamed duplicate SARIF action authority --- .../src/source-verification-workflow.test.mjs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index 5ace56413..f6a96e25d 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -81,7 +81,7 @@ function assertStepPrecedes(job, earlierStepName, laterStepName) { /** Finds executable uses entries for one exact GitHub Action identity. */ function actionUseLines(workflowBlock, actionName) { - const usesEntry = /^uses:\s*(['"]?)([^'"\s#]+)\1(?:\s+#.*)?$/u; + const usesEntry = /^(?:-\s+)?uses:\s*(['"]?)([^'"\s#]+)\1(?:\s+#.*)?$/u; return workflowBlock .split('\n') .map((line) => line.trim()) @@ -196,6 +196,31 @@ test('SARIF upload authority rejects differently named duplicate action uses', ( ); }); +test('SARIF upload authority rejects unnamed duplicate action uses', () => { + const job = [ + ' scan:', + ' steps:', + ' - name: Upload AppGuardrail SARIF to code scanning', + ' uses: github/codeql-action/upload-sarif@reviewed-sha', + ' with:', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + ' - uses: "github/codeql-action/upload-sarif@unreviewed-sha"', + ' with:', + ' sarif_file: alternate.sarif', + ].join('\n'); + + assert.throws( + () => + assertUniqueActionUseInStep( + job, + 'github/codeql-action/upload-sarif', + 'Upload AppGuardrail SARIF to code scanning', + ), + /exactly one github\/codeql-action\/upload-sarif use/u, + ); +}); + test('required source-verification jobs explicitly checkout the contributor head', () => { const ci = readWorkflow('ci.yml'); for (const jobName of [ From 2a938abd15a284c939b843d96f36cca78a0378cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 09:12:40 +0900 Subject: [PATCH 022/180] test(ci): ignore SARIF authority text inside run blocks --- .../src/source-verification-workflow.test.mjs | 107 ++++++++++++++---- 1 file changed, 85 insertions(+), 22 deletions(-) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index f6a96e25d..25e0f3086 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -36,12 +36,28 @@ function jobBlock(workflow, jobName) { return lines.slice(start, end).join('\n'); } -/** Finds one unique named workflow step by its exact YAML sequence entry. */ +/** Resolves the exact sequence indentation for real workflow steps. */ +function stepSequenceIndent(job) { + const lines = job.split('\n'); + const jobEntry = /^(\s*)[A-Za-z0-9_-]+:\s*$/u.exec(lines[0]); + assert.ok(jobEntry, 'invalid bounded workflow job'); + const stepsLine = `${jobEntry[1]} steps:`; + assert.equal( + lines.filter((line) => line === stepsLine).length, + 1, + 'expected exactly one direct steps mapping in bounded workflow job', + ); + return `${jobEntry[1]} `; +} + +/** Finds one unique named workflow step at the real steps sequence depth. */ function stepStartIndex(job, stepName) { const lines = job.split('\n'); + const stepIndent = stepSequenceIndent(job); + const expected = `${stepIndent}- name: ${stepName}`; const matches = []; for (let index = 0; index < lines.length; index += 1) { - if (lines[index].trim() === `- name: ${stepName}`) { + if (lines[index] === expected) { matches.push(index); } } @@ -57,13 +73,10 @@ function stepStartIndex(job, stepName) { function stepBlock(job, stepName) { const lines = job.split('\n'); const start = stepStartIndex(job, stepName); - const startMatch = /^(\s*)-\s/u.exec(lines[start]); - assert.ok(startMatch, `invalid step indentation for ${stepName}`); - const stepIndent = startMatch[1]; + const stepIndent = stepSequenceIndent(job); let end = lines.length; for (let index = start + 1; index < lines.length; index += 1) { - const siblingStep = /^(\s*)-\s/u.exec(lines[index]); - if (siblingStep?.[1] === stepIndent) { + if (lines[index].startsWith(`${stepIndent}- `)) { end = index; break; } @@ -79,32 +92,57 @@ function assertStepPrecedes(job, earlierStepName, laterStepName) { ); } -/** Finds executable uses entries for one exact GitHub Action identity. */ -function actionUseLines(workflowBlock, actionName) { - const usesEntry = /^(?:-\s+)?uses:\s*(['"]?)([^'"\s#]+)\1(?:\s+#.*)?$/u; - return workflowBlock - .split('\n') - .map((line) => line.trim()) - .filter((line) => { - if (line.startsWith('#')) { - return false; +/** Finds executable action uses only from real step entries and direct step keys. */ +function actionUses(job, actionName) { + const lines = job.split('\n'); + const stepIndent = stepSequenceIndent(job); + const directKeyIndent = `${stepIndent} `; + const usesEntry = /^uses:\s*(['"]?)([^'"\s#]+)\1(?:\s+#.*)?$/u; + const uses = []; + + for (let index = 0; index < lines.length; index += 1) { + if (!lines[index].startsWith(`${stepIndent}- `)) { + continue; + } + + const inline = usesEntry.exec(lines[index].slice(`${stepIndent}- `.length)); + if (inline?.[2].startsWith(`${actionName}@`)) { + uses.push({ line: lines[index], stepStart: index }); + } + + let end = lines.length; + for (let sibling = index + 1; sibling < lines.length; sibling += 1) { + if (lines[sibling].startsWith(`${stepIndent}- `)) { + end = sibling; + break; } - const match = usesEntry.exec(line); - return match?.[2].startsWith(`${actionName}@`) ?? false; - }); + } + for (let child = index + 1; child < end; child += 1) { + const direct = lines[child].slice(directKeyIndent.length); + if (!lines[child].startsWith(directKeyIndent) || direct.startsWith(' ')) { + continue; + } + const match = usesEntry.exec(direct); + if (match?.[2].startsWith(`${actionName}@`)) { + uses.push({ line: lines[child], stepStart: index }); + } + } + } + + return uses; } /** Requires one action invocation and binds it to the reviewed named step. */ function assertUniqueActionUseInStep(job, actionName, stepName) { - const jobUses = actionUseLines(job, actionName); + const jobUses = actionUses(job, actionName); assert.equal( jobUses.length, 1, `expected exactly one ${actionName} use, found ${jobUses.length}`, ); assert.equal( - actionUseLines(stepBlock(job, stepName), actionName).length, - 1, + jobUses[0].stepStart, + stepStartIndex(job, stepName), `${actionName} must be owned by ${stepName}`, ); } @@ -221,6 +259,31 @@ test('SARIF upload authority rejects unnamed duplicate action uses', () => { ); }); +test('workflow-step authority ignores name and uses text inside run blocks', () => { + const job = [ + ' scan:', + ' steps:', + ' - name: Generate harmless evidence note', + ' run: |', + " cat <<'EOF' > note.txt", + ' - name: Upload AppGuardrail SARIF to code scanning', + ' uses: github/codeql-action/upload-sarif@fake-sha', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + ' EOF', + ].join('\n'); + + assert.throws( + () => + assertUniqueActionUseInStep( + job, + 'github/codeql-action/upload-sarif', + 'Upload AppGuardrail SARIF to code scanning', + ), + /expected exactly one github\/codeql-action\/upload-sarif use, found 0/u, + ); +}); + test('required source-verification jobs explicitly checkout the contributor head', () => { const ci = readWorkflow('ci.yml'); for (const jobName of [ From d3e5ad24eb22b53558e972251dab575156645b32 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:06:48 +0900 Subject: [PATCH 023/180] test(ci): reproduce misplaced SARIF source binding --- .../sarif-source-binding-contract.test.mjs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs new file mode 100644 index 000000000..9a5b8b9f1 --- /dev/null +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +const SARIF_SOURCE_REF = + "ref: ${{ github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}"; +const SARIF_SOURCE_SHA = + 'sha: ${{ github.event.pull_request.head.sha || github.sha }}'; + +/** Mirrors the current source-binding assertions so hostile placement can be exercised directly. */ +function assertSarifSourceBinding(uploadStep) { + assert.ok( + uploadStep.includes(SARIF_SOURCE_REF), + 'SARIF upload must bind ref to the analyzed contributor head', + ); + assert.ok( + uploadStep.includes(SARIF_SOURCE_SHA), + 'SARIF upload must bind sha to the analyzed contributor head', + ); +} + +test('SARIF source binding accepts the reviewed direct with entries', () => { + const uploadStep = [ + ' - name: Upload AppGuardrail SARIF to code scanning', + ' uses: github/codeql-action/upload-sarif@reviewed-sha', + ' with:', + ' sarif_file: appguardrail.sarif', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + ].join('\n'); + + assert.doesNotThrow(() => assertSarifSourceBinding(uploadStep)); +}); + +test('SARIF source binding rejects contributor markers moved outside direct with entries', () => { + const hostileUploadStep = [ + ' - name: Upload AppGuardrail SARIF to code scanning', + ' uses: github/codeql-action/upload-sarif@reviewed-sha', + ' env:', + ` SARIF_REF_MARKER: "${SARIF_SOURCE_REF}"`, + ` SARIF_SHA_MARKER: "${SARIF_SOURCE_SHA}"`, + ' with:', + ' sarif_file: appguardrail.sarif', + ].join('\n'); + + assert.throws( + () => assertSarifSourceBinding(hostileUploadStep), + /must bind/u, + 'authority-looking ref/sha text outside with: must not satisfy upload input binding', + ); +}); From 592ac36034dd489fd6f48819f87cae88ae1be281 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:06:59 +0900 Subject: [PATCH 024/180] ci(test): run focused SARIF source-binding verifier --- .../workflows/verify-sarif-source-binding.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/verify-sarif-source-binding.yml diff --git a/.github/workflows/verify-sarif-source-binding.yml b/.github/workflows/verify-sarif-source-binding.yml new file mode 100644 index 000000000..f8185e4e5 --- /dev/null +++ b/.github/workflows/verify-sarif-source-binding.yml @@ -0,0 +1,32 @@ +name: Verify SARIF source binding + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact pushed head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js 24 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Verify focused SARIF source-binding contract + run: node --test packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs From ad014b62133b65dd56d405cee68644cb3c401678 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:08:05 +0900 Subject: [PATCH 025/180] test(ci): bind SARIF source identity to with inputs --- .../sarif-source-binding-contract.test.mjs | 91 ++++++++++++++++--- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs index 9a5b8b9f1..aedb47462 100644 --- a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -1,32 +1,93 @@ import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; import test from 'node:test'; +const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); const SARIF_SOURCE_REF = "ref: ${{ github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}"; const SARIF_SOURCE_SHA = 'sha: ${{ github.event.pull_request.head.sha || github.sha }}'; +const UPLOAD_STEP_NAME = 'Upload AppGuardrail SARIF to code scanning'; -/** Mirrors the current source-binding assertions so hostile placement can be exercised directly. */ +/** Extracts one uniquely named workflow step at the direct steps-sequence depth. */ +function namedStep(workflow, stepName) { + const lines = workflow.split('\n'); + const stepsIndex = lines.findIndex((line) => /^\s+steps:\s*$/u.test(line)); + assert.notEqual(stepsIndex, -1, 'workflow must contain a steps mapping'); + const stepsIndent = /^(\s*)steps:\s*$/u.exec(lines[stepsIndex])?.[1]; + assert.notEqual(stepsIndent, undefined, 'workflow steps indentation is invalid'); + const stepIndent = `${stepsIndent} `; + const expected = `${stepIndent}- name: ${stepName}`; + const matches = []; + for (let index = stepsIndex + 1; index < lines.length; index += 1) { + if (lines[index] === expected) { + matches.push(index); + } + } + assert.equal(matches.length, 1, `expected exactly one step ${stepName}`); + + const start = matches[0]; + let end = lines.length; + for (let index = start + 1; index < lines.length; index += 1) { + if (lines[index].startsWith(`${stepIndent}- `)) { + end = index; + break; + } + } + return lines.slice(start, end).join('\n'); +} + +/** Extracts one direct child mapping from a workflow step without borrowing sibling mappings. */ +function directMapping(step, mappingName) { + const lines = step.split('\n'); + const stepMatch = /^(\s*)-\s/u.exec(lines[0]); + assert.ok(stepMatch, 'workflow step indentation is invalid'); + const directIndent = `${stepMatch[1]} `; + const mappingLine = `${directIndent}${mappingName}:`; + const matches = []; + for (let index = 1; index < lines.length; index += 1) { + if (lines[index] === mappingLine) { + matches.push(index); + } + } + assert.equal( + matches.length, + 1, + `expected exactly one direct ${mappingName} mapping`, + ); + + const start = matches[0] + 1; + let end = lines.length; + for (let index = start; index < lines.length; index += 1) { + if (!lines[index].startsWith(`${directIndent} `)) { + end = index; + break; + } + } + return lines.slice(start, end); +} + +/** Requires contributor-head ref and sha to be direct upload-sarif with inputs. */ function assertSarifSourceBinding(uploadStep) { + const withEntries = directMapping(uploadStep, 'with'); assert.ok( - uploadStep.includes(SARIF_SOURCE_REF), - 'SARIF upload must bind ref to the analyzed contributor head', + withEntries.includes(` ${SARIF_SOURCE_REF}`), + 'SARIF upload must bind ref to the analyzed contributor head in direct with inputs', ); assert.ok( - uploadStep.includes(SARIF_SOURCE_SHA), - 'SARIF upload must bind sha to the analyzed contributor head', + withEntries.includes(` ${SARIF_SOURCE_SHA}`), + 'SARIF upload must bind sha to the analyzed contributor head in direct with inputs', ); } -test('SARIF source binding accepts the reviewed direct with entries', () => { - const uploadStep = [ - ' - name: Upload AppGuardrail SARIF to code scanning', - ' uses: github/codeql-action/upload-sarif@reviewed-sha', - ' with:', - ' sarif_file: appguardrail.sarif', - ` ${SARIF_SOURCE_REF}`, - ` ${SARIF_SOURCE_SHA}`, - ].join('\n'); +test('AppGuardrail SARIF upload binds contributor identity through direct with inputs', () => { + const workflow = readFileSync( + join(REPOSITORY_ROOT, '.github/workflows/appguardrail.yml'), + 'utf8', + ); + const uploadStep = namedStep(workflow, UPLOAD_STEP_NAME); assert.doesNotThrow(() => assertSarifSourceBinding(uploadStep)); }); @@ -44,7 +105,7 @@ test('SARIF source binding rejects contributor markers moved outside direct with assert.throws( () => assertSarifSourceBinding(hostileUploadStep), - /must bind/u, + /must bind ref/u, 'authority-looking ref/sha text outside with: must not satisfy upload input binding', ); }); From e68d9b15ebdfae83994a696a3d3727e7758aa804 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:08:49 +0900 Subject: [PATCH 026/180] ci(test): verify Commercial Readiness suite after SARIF repair --- .github/workflows/verify-sarif-source-binding.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/verify-sarif-source-binding.yml b/.github/workflows/verify-sarif-source-binding.yml index f8185e4e5..f3dbb527e 100644 --- a/.github/workflows/verify-sarif-source-binding.yml +++ b/.github/workflows/verify-sarif-source-binding.yml @@ -30,3 +30,6 @@ jobs: - name: Verify focused SARIF source-binding contract run: node --test packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs + + - name: Verify Commercial Readiness test suite + run: node --test packages/commercial-readiness/src/*.test.mjs From 6e25408936af0948f160085f1db564a52eb551c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:09:34 +0900 Subject: [PATCH 027/180] ci(test): retire focused SARIF verifier --- .../workflows/verify-sarif-source-binding.yml | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 .github/workflows/verify-sarif-source-binding.yml diff --git a/.github/workflows/verify-sarif-source-binding.yml b/.github/workflows/verify-sarif-source-binding.yml deleted file mode 100644 index f3dbb527e..000000000 --- a/.github/workflows/verify-sarif-source-binding.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Verify SARIF source binding - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout exact pushed head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js 24 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - - - name: Verify focused SARIF source-binding contract - run: node --test packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs - - - name: Verify Commercial Readiness test suite - run: node --test packages/commercial-readiness/src/*.test.mjs From a6081749b3c91360610f344a54ef9b55a1511533 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:10:53 +0900 Subject: [PATCH 028/180] test(ci): reproduce duplicate SARIF source inputs --- .../sarif-source-binding-contract.test.mjs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs index aedb47462..3954f8482 100644 --- a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -109,3 +109,22 @@ test('SARIF source binding rejects contributor markers moved outside direct with 'authority-looking ref/sha text outside with: must not satisfy upload input binding', ); }); + +test('SARIF source binding rejects duplicate direct ref or sha inputs', () => { + const hostileUploadStep = [ + ' - name: Upload AppGuardrail SARIF to code scanning', + ' uses: github/codeql-action/upload-sarif@reviewed-sha', + ' with:', + ' sarif_file: appguardrail.sarif', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + " ref: ${{ format('refs/pull/{0}/merge', github.event.pull_request.number) }}", + ' sha: ${{ github.sha }}', + ].join('\n'); + + assert.throws( + () => assertSarifSourceBinding(hostileUploadStep), + /exactly one direct (ref|sha) input/u, + 'duplicate YAML keys must not override the reviewed contributor-head binding', + ); +}); From acb1084f68441fb6a36b5481a06a7051fcd2ac1d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:11:02 +0900 Subject: [PATCH 029/180] ci(test): verify duplicate SARIF source-input regression --- .../workflows/verify-sarif-source-binding.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/verify-sarif-source-binding.yml diff --git a/.github/workflows/verify-sarif-source-binding.yml b/.github/workflows/verify-sarif-source-binding.yml new file mode 100644 index 000000000..f8185e4e5 --- /dev/null +++ b/.github/workflows/verify-sarif-source-binding.yml @@ -0,0 +1,32 @@ +name: Verify SARIF source binding + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact pushed head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js 24 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Verify focused SARIF source-binding contract + run: node --test packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs From 63db720d255cfb7375a8c0425e585d0d4e7982c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:11:44 +0900 Subject: [PATCH 030/180] test(ci): reject duplicate SARIF source inputs --- .../sarif-source-binding-contract.test.mjs | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs index 3954f8482..2435fa193 100644 --- a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -45,6 +45,7 @@ function directMapping(step, mappingName) { const stepMatch = /^(\s*)-\s/u.exec(lines[0]); assert.ok(stepMatch, 'workflow step indentation is invalid'); const directIndent = `${stepMatch[1]} `; + const entryIndent = `${directIndent} `; const mappingLine = `${directIndent}${mappingName}:`; const matches = []; for (let index = 1; index < lines.length; index += 1) { @@ -61,27 +62,42 @@ function directMapping(step, mappingName) { const start = matches[0] + 1; let end = lines.length; for (let index = start; index < lines.length; index += 1) { - if (!lines[index].startsWith(`${directIndent} `)) { + if (!lines[index].startsWith(entryIndent)) { end = index; break; } } - return lines.slice(start, end); + return { lines: lines.slice(start, end), entryIndent }; } -/** Requires contributor-head ref and sha to be direct upload-sarif with inputs. */ -function assertSarifSourceBinding(uploadStep) { - const withEntries = directMapping(uploadStep, 'with'); - assert.ok( - withEntries.includes(` ${SARIF_SOURCE_REF}`), - 'SARIF upload must bind ref to the analyzed contributor head in direct with inputs', +/** Requires one unique direct mapping input with the reviewed exact value. */ +function assertUniqueDirectInput(mapping, key, expectedEntry) { + const prefix = `${mapping.entryIndent}${key}:`; + const directEntries = mapping.lines.filter((line) => { + if (!line.startsWith(prefix)) { + return false; + } + return !line.slice(mapping.entryIndent.length).startsWith(' '); + }); + assert.equal( + directEntries.length, + 1, + `expected exactly one direct ${key} input`, ); - assert.ok( - withEntries.includes(` ${SARIF_SOURCE_SHA}`), - 'SARIF upload must bind sha to the analyzed contributor head in direct with inputs', + assert.equal( + directEntries[0], + `${mapping.entryIndent}${expectedEntry}`, + `SARIF upload must bind ${key} to the analyzed contributor head in direct with inputs`, ); } +/** Requires contributor-head ref and sha to be unique direct upload-sarif with inputs. */ +function assertSarifSourceBinding(uploadStep) { + const withMapping = directMapping(uploadStep, 'with'); + assertUniqueDirectInput(withMapping, 'ref', SARIF_SOURCE_REF); + assertUniqueDirectInput(withMapping, 'sha', SARIF_SOURCE_SHA); +} + test('AppGuardrail SARIF upload binds contributor identity through direct with inputs', () => { const workflow = readFileSync( join(REPOSITORY_ROOT, '.github/workflows/appguardrail.yml'), @@ -105,7 +121,7 @@ test('SARIF source binding rejects contributor markers moved outside direct with assert.throws( () => assertSarifSourceBinding(hostileUploadStep), - /must bind ref/u, + /exactly one direct ref input/u, 'authority-looking ref/sha text outside with: must not satisfy upload input binding', ); }); @@ -124,7 +140,7 @@ test('SARIF source binding rejects duplicate direct ref or sha inputs', () => { assert.throws( () => assertSarifSourceBinding(hostileUploadStep), - /exactly one direct (ref|sha) input/u, + /exactly one direct ref input/u, 'duplicate YAML keys must not override the reviewed contributor-head binding', ); }); From 7fbb68fee0ad76ae0e33ee1d23fe2fa71b0c6862 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:12:19 +0900 Subject: [PATCH 031/180] ci(test): verify package after duplicate-input repair --- .github/workflows/verify-sarif-source-binding.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/verify-sarif-source-binding.yml b/.github/workflows/verify-sarif-source-binding.yml index f8185e4e5..f3dbb527e 100644 --- a/.github/workflows/verify-sarif-source-binding.yml +++ b/.github/workflows/verify-sarif-source-binding.yml @@ -30,3 +30,6 @@ jobs: - name: Verify focused SARIF source-binding contract run: node --test packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs + + - name: Verify Commercial Readiness test suite + run: node --test packages/commercial-readiness/src/*.test.mjs From ab72a882630b0b746d9e26d166346af1a9c9fd93 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:13:37 +0900 Subject: [PATCH 032/180] ci(test): retire duplicate-input SARIF verifier --- .../workflows/verify-sarif-source-binding.yml | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 .github/workflows/verify-sarif-source-binding.yml diff --git a/.github/workflows/verify-sarif-source-binding.yml b/.github/workflows/verify-sarif-source-binding.yml deleted file mode 100644 index f3dbb527e..000000000 --- a/.github/workflows/verify-sarif-source-binding.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Verify SARIF source binding - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout exact pushed head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js 24 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - - - name: Verify focused SARIF source-binding contract - run: node --test packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs - - - name: Verify Commercial Readiness test suite - run: node --test packages/commercial-readiness/src/*.test.mjs From a78ffc58a999a20aab287a8a24d6369e7d24f549 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 11:06:24 +0900 Subject: [PATCH 033/180] test(ci): prove SARIF upload job-boundary ambiguity --- .../sarif-source-binding-contract.test.mjs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs index 2435fa193..1177c2773 100644 --- a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -108,6 +108,29 @@ test('AppGuardrail SARIF upload binds contributor identity through direct with i assert.doesNotThrow(() => assertSarifSourceBinding(uploadStep)); }); +test('SARIF source binding rejects upload authority moved to another workflow job', () => { + const hostileWorkflow = [ + 'jobs:', + ' scan:', + ' steps:', + ' - name: Harmless scan step', + ' run: echo scan', + ' decoy:', + ' steps:', + ` - name: ${UPLOAD_STEP_NAME}`, + ' uses: github/codeql-action/upload-sarif@reviewed-sha', + ' with:', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + ].join('\n'); + + assert.throws( + () => namedStep(hostileWorkflow, UPLOAD_STEP_NAME), + /missing reviewed step from scan job/u, + 'a SARIF upload in another workflow job must not satisfy the scan-job authority contract', + ); +}); + test('SARIF source binding rejects contributor markers moved outside direct with entries', () => { const hostileUploadStep = [ ' - name: Upload AppGuardrail SARIF to code scanning', From db80a94ecccd166ce608a359ebe007bcf847cb92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 11:07:05 +0900 Subject: [PATCH 034/180] fix(ci): bind SARIF source verification to scan job --- .../sarif-source-binding-contract.test.mjs | 70 ++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs index 1177c2773..d5d6e7b35 100644 --- a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -11,22 +11,74 @@ const SARIF_SOURCE_SHA = 'sha: ${{ github.event.pull_request.head.sha || github.sha }}'; const UPLOAD_STEP_NAME = 'Upload AppGuardrail SARIF to code scanning'; -/** Extracts one uniquely named workflow step at the direct steps-sequence depth. */ -function namedStep(workflow, stepName) { +/** Extracts one uniquely named direct workflow job from the top-level jobs mapping. */ +function namedJob(workflow, jobName) { const lines = workflow.split('\n'); - const stepsIndex = lines.findIndex((line) => /^\s+steps:\s*$/u.test(line)); - assert.notEqual(stepsIndex, -1, 'workflow must contain a steps mapping'); - const stepsIndent = /^(\s*)steps:\s*$/u.exec(lines[stepsIndex])?.[1]; - assert.notEqual(stepsIndent, undefined, 'workflow steps indentation is invalid'); - const stepIndent = `${stepsIndent} `; + const jobsIndexes = []; + for (let index = 0; index < lines.length; index += 1) { + if (lines[index] === 'jobs:') { + jobsIndexes.push(index); + } + } + assert.equal(jobsIndexes.length, 1, 'workflow must contain exactly one jobs mapping'); + + const jobIndent = ' '; + const expected = `${jobIndent}${jobName}:`; + const matches = []; + for (let index = jobsIndexes[0] + 1; index < lines.length; index += 1) { + if (lines[index] === expected) { + matches.push(index); + } + } + assert.equal(matches.length, 1, `expected exactly one workflow job ${jobName}`); + + const start = matches[0]; + let end = lines.length; + for (let index = start + 1; index < lines.length; index += 1) { + if (/^ [A-Za-z0-9_-]+:\s*$/u.test(lines[index])) { + end = index; + break; + } + if (/^[^\s#]/u.test(lines[index])) { + end = index; + break; + } + } + return lines.slice(start, end).join('\n'); +} + +/** Extracts one uniquely named workflow step from the AppGuardrail scan job. */ +function namedStep(workflow, stepName) { + const scanJob = namedJob(workflow, 'scan'); + const lines = scanJob.split('\n'); + const jobMatch = /^(\s*)scan:\s*$/u.exec(lines[0]); + assert.ok(jobMatch, 'scan job indentation is invalid'); + const stepsLine = `${jobMatch[1]} steps:`; + const stepsIndexes = []; + for (let index = 1; index < lines.length; index += 1) { + if (lines[index] === stepsLine) { + stepsIndexes.push(index); + } + } + assert.equal( + stepsIndexes.length, + 1, + 'scan job must contain exactly one direct steps mapping', + ); + + const stepIndent = `${jobMatch[1]} `; const expected = `${stepIndent}- name: ${stepName}`; const matches = []; - for (let index = stepsIndex + 1; index < lines.length; index += 1) { + for (let index = stepsIndexes[0] + 1; index < lines.length; index += 1) { if (lines[index] === expected) { matches.push(index); } } - assert.equal(matches.length, 1, `expected exactly one step ${stepName}`); + assert.equal( + matches.length, + 1, + `missing reviewed step from scan job: expected exactly one ${stepName}, found ${matches.length}`, + ); const start = matches[0]; let end = lines.length; From df6b038d7a314e37bc1f87da5dca1c356919fcab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:05:01 +0900 Subject: [PATCH 035/180] test(ci): reject SARIF job authority outside jobs --- .../sarif-source-binding-contract.test.mjs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs index d5d6e7b35..15b6804ab 100644 --- a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -219,3 +219,28 @@ test('SARIF source binding rejects duplicate direct ref or sha inputs', () => { 'duplicate YAML keys must not override the reviewed contributor-head binding', ); }); + + +test('SARIF source binding rejects scan-looking mappings outside top-level jobs', () => { + const hostileWorkflow = [ + 'jobs:', + ' build:', + ' steps:', + ' - name: Build', + ' run: echo build', + 'env:', + ' scan:', + ' steps:', + ` - name: ${UPLOAD_STEP_NAME}`, + ' uses: github/codeql-action/upload-sarif@reviewed-sha', + ' with:', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + ].join('\n'); + + assert.throws( + () => namedStep(hostileWorkflow, UPLOAD_STEP_NAME), + /expected exactly one workflow job scan/u, + 'a scan-shaped mapping outside top-level jobs must not satisfy workflow job authority', + ); +}); From f8ab2bcdeb3bf1e7212607b05e288eca4aab2637 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:05:47 +0900 Subject: [PATCH 036/180] fix(ci): bound SARIF job lookup to top-level jobs --- .../sarif-source-binding-contract.test.mjs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs index 15b6804ab..dbb0db93f 100644 --- a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -22,10 +22,19 @@ function namedJob(workflow, jobName) { } assert.equal(jobsIndexes.length, 1, 'workflow must contain exactly one jobs mapping'); + const jobsStart = jobsIndexes[0]; + let jobsEnd = lines.length; + for (let index = jobsStart + 1; index < lines.length; index += 1) { + if (/^[^\s#]/u.test(lines[index])) { + jobsEnd = index; + break; + } + } + const jobIndent = ' '; const expected = `${jobIndent}${jobName}:`; const matches = []; - for (let index = jobsIndexes[0] + 1; index < lines.length; index += 1) { + for (let index = jobsStart + 1; index < jobsEnd; index += 1) { if (lines[index] === expected) { matches.push(index); } @@ -33,16 +42,12 @@ function namedJob(workflow, jobName) { assert.equal(matches.length, 1, `expected exactly one workflow job ${jobName}`); const start = matches[0]; - let end = lines.length; - for (let index = start + 1; index < lines.length; index += 1) { + let end = jobsEnd; + for (let index = start + 1; index < jobsEnd; index += 1) { if (/^ [A-Za-z0-9_-]+:\s*$/u.test(lines[index])) { end = index; break; } - if (/^[^\s#]/u.test(lines[index])) { - end = index; - break; - } } return lines.slice(start, end).join('\n'); } From 33d89f624807467f7fc7310ee743c873fb34f8ce Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 13:04:56 +0900 Subject: [PATCH 037/180] test(ci): reproduce SARIF provenance if-key false positive --- ...ail-provenance-condition-contract.test.mjs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs diff --git a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs new file mode 100644 index 000000000..a38f0d0c8 --- /dev/null +++ b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +const PULL_REQUEST_CONDITION = "github.event_name == 'pull_request'"; +const SAME_REPOSITORY_CONDITION = + 'github.event.pull_request.head.repo.full_name == github.repository'; + +/** Legacy text-wide guard check retained only to prove the review finding RED. */ +function assertProvenanceGuard(step) { + assert.ok( + step.includes(PULL_REQUEST_CONDITION), + 'provenance step must require pull_request events in its direct if guard', + ); + assert.ok( + step.includes(SAME_REPOSITORY_CONDITION), + 'provenance step must require same-repository pull requests in its direct if guard', + ); +} + +test('provenance condition rejects guard strings that exist only outside the if key', () => { + const hostileStep = [ + ' - name: Materialize AppGuardrail SARIF PR merge provenance', + ' if: always()', + ` # ${PULL_REQUEST_CONDITION}`, + ' run: |', + ` echo "${SAME_REPOSITORY_CONDITION}"`, + ].join('\n'); + + assert.throws( + () => assertProvenanceGuard(hostileStep), + /direct if guard/u, + 'comments or run-block text must not satisfy the provenance guard contract', + ); +}); From 3a39971101ac2f988920a0674eeb27b05b396c86 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 13:05:29 +0900 Subject: [PATCH 038/180] test(ci): bind SARIF provenance guard to direct if key --- ...ail-provenance-condition-contract.test.mjs | 116 ++++++++++++++++-- 1 file changed, 107 insertions(+), 9 deletions(-) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs index a38f0d0c8..1b335b5d3 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs @@ -1,25 +1,107 @@ import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; import test from 'node:test'; +const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); +const PROVENANCE_STEP_NAME = + 'Materialize AppGuardrail SARIF PR merge provenance'; const PULL_REQUEST_CONDITION = "github.event_name == 'pull_request'"; const SAME_REPOSITORY_CONDITION = 'github.event.pull_request.head.repo.full_name == github.repository'; +const EXPECTED_PROVENANCE_CONDITION = `${PULL_REQUEST_CONDITION} && ${SAME_REPOSITORY_CONDITION}`; -/** Legacy text-wide guard check retained only to prove the review finding RED. */ -function assertProvenanceGuard(step) { - assert.ok( - step.includes(PULL_REQUEST_CONDITION), - 'provenance step must require pull_request events in its direct if guard', +/** Extracts one uniquely named real workflow step at its YAML sequence indentation. */ +function namedStep(workflow, stepName) { + const lines = workflow.split('\n'); + const matches = []; + for (let index = 0; index < lines.length; index += 1) { + const match = /^(\s*)- name: (.+)$/u.exec(lines[index]); + if (match?.[2] === stepName) { + matches.push({ index, indent: match[1] }); + } + } + assert.equal( + matches.length, + 1, + `expected exactly one workflow step ${stepName}`, ); - assert.ok( - step.includes(SAME_REPOSITORY_CONDITION), - 'provenance step must require same-repository pull requests in its direct if guard', + + const { index: start, indent } = matches[0]; + let end = lines.length; + for (let index = start + 1; index < lines.length; index += 1) { + if (lines[index].startsWith(`${indent}- `)) { + end = index; + break; + } + } + return lines.slice(start, end).join('\n'); +} + +/** Reads one unique direct scalar key from a workflow step and normalizes folded block text. */ +function directScalar(step, key) { + const lines = step.split('\n'); + const stepMatch = /^(\s*)-\s/u.exec(lines[0]); + assert.ok(stepMatch, 'workflow step indentation is invalid'); + const directIndent = `${stepMatch[1]} `; + const prefix = `${directIndent}${key}:`; + const matches = []; + + for (let index = 1; index < lines.length; index += 1) { + if (!lines[index].startsWith(prefix)) { + continue; + } + const suffix = lines[index].slice(prefix.length); + if (suffix.length === 0 || /^\s/u.test(suffix)) { + matches.push({ index, suffix: suffix.trim() }); + } + } + assert.equal(matches.length, 1, `expected exactly one direct ${key} key`); + + const { index, suffix } = matches[0]; + if (!/^[>|][+-]?$/u.test(suffix)) { + return suffix; + } + + const body = []; + for (let bodyIndex = index + 1; bodyIndex < lines.length; bodyIndex += 1) { + const line = lines[bodyIndex]; + if (line.trim().length === 0) { + continue; + } + const leading = /^\s*/u.exec(line)?.[0].length ?? 0; + if (leading <= directIndent.length) { + break; + } + body.push(line.trim()); + } + assert.notEqual(body.length, 0, `direct ${key} block scalar must not be empty`); + return body.join(' '); +} + +/** Requires the provenance restriction to live in the step's direct if key. */ +function assertProvenanceGuard(step) { + assert.equal( + directScalar(step, 'if'), + EXPECTED_PROVENANCE_CONDITION, + 'provenance step must require pull_request and same-repository authority in its direct if guard', ); } +test('AppGuardrail provenance step owns the exact same-repository pull_request guard', () => { + const workflow = readFileSync( + join(REPOSITORY_ROOT, '.github/workflows/appguardrail.yml'), + 'utf8', + ); + assert.doesNotThrow(() => + assertProvenanceGuard(namedStep(workflow, PROVENANCE_STEP_NAME)), + ); +}); + test('provenance condition rejects guard strings that exist only outside the if key', () => { const hostileStep = [ - ' - name: Materialize AppGuardrail SARIF PR merge provenance', + ` - name: ${PROVENANCE_STEP_NAME}`, ' if: always()', ` # ${PULL_REQUEST_CONDITION}`, ' run: |', @@ -32,3 +114,19 @@ test('provenance condition rejects guard strings that exist only outside the if 'comments or run-block text must not satisfy the provenance guard contract', ); }); + +test('provenance condition rejects duplicate direct if authority', () => { + const hostileStep = [ + ` - name: ${PROVENANCE_STEP_NAME}`, + ' if: >-', + ` ${PULL_REQUEST_CONDITION}`, + ` && ${SAME_REPOSITORY_CONDITION}`, + ' if: always()', + ' run: echo duplicate-if', + ].join('\n'); + + assert.throws( + () => assertProvenanceGuard(hostileStep), + /exactly one direct if key/u, + ); +}); From 1e84e85ddb22acfcc4624792a32d0185740028ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 13:08:39 +0900 Subject: [PATCH 039/180] test(ci): reproduce provenance step-shape spoof --- ...ail-provenance-condition-contract.test.mjs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs index 1b335b5d3..a8af74ecb 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs @@ -130,3 +130,27 @@ test('provenance condition rejects duplicate direct if authority', () => { /exactly one direct if key/u, ); }); + +test('provenance step authority rejects step-shaped text inside a run block', () => { + const hostileWorkflow = [ + 'jobs:', + ' scan:', + ' steps:', + ' - name: Harmless generator', + ' run: |', + " cat <<'EOF' > note.yml", + ` - name: ${PROVENANCE_STEP_NAME}`, + ' if: >-', + ` ${PULL_REQUEST_CONDITION}`, + ` && ${SAME_REPOSITORY_CONDITION}`, + ' run: echo fake-authority', + ' EOF', + ].join('\n'); + + assert.throws( + () => + assertProvenanceGuard(namedStep(hostileWorkflow, PROVENANCE_STEP_NAME)), + /workflow step/u, + 'run-block text must not become provenance workflow-step authority', + ); +}); From 8f9ccbcbbe053ffafeb13af5643e69a9080757f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 13:09:07 +0900 Subject: [PATCH 040/180] test(ci): structurally bind provenance step authority --- ...ail-provenance-condition-contract.test.mjs | 77 ++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs index a8af74ecb..7754fac1f 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs @@ -12,26 +12,83 @@ const SAME_REPOSITORY_CONDITION = 'github.event.pull_request.head.repo.full_name == github.repository'; const EXPECTED_PROVENANCE_CONDITION = `${PULL_REQUEST_CONDITION} && ${SAME_REPOSITORY_CONDITION}`; -/** Extracts one uniquely named real workflow step at its YAML sequence indentation. */ -function namedStep(workflow, stepName) { +/** Extracts one uniquely named direct job from the workflow's top-level jobs mapping. */ +function namedJob(workflow, jobName) { const lines = workflow.split('\n'); - const matches = []; + const jobsIndexes = []; for (let index = 0; index < lines.length; index += 1) { - const match = /^(\s*)- name: (.+)$/u.exec(lines[index]); - if (match?.[2] === stepName) { - matches.push({ index, indent: match[1] }); + if (lines[index] === 'jobs:') { + jobsIndexes.push(index); + } + } + assert.equal(jobsIndexes.length, 1, 'workflow must contain exactly one jobs mapping'); + + const jobsStart = jobsIndexes[0]; + let jobsEnd = lines.length; + for (let index = jobsStart + 1; index < lines.length; index += 1) { + if (/^[^\s#]/u.test(lines[index])) { + jobsEnd = index; + break; + } + } + + const expected = ` ${jobName}:`; + const matches = []; + for (let index = jobsStart + 1; index < jobsEnd; index += 1) { + if (lines[index] === expected) { + matches.push(index); + } + } + assert.equal(matches.length, 1, `expected exactly one workflow job ${jobName}`); + + const start = matches[0]; + let end = jobsEnd; + for (let index = start + 1; index < jobsEnd; index += 1) { + if (/^ [A-Za-z0-9_-]+:\s*$/u.test(lines[index])) { + end = index; + break; + } + } + return lines.slice(start, end).join('\n'); +} + +/** Extracts one uniquely named real step from the direct steps sequence of jobs.scan. */ +function namedStep(workflow, stepName) { + const scanJob = namedJob(workflow, 'scan'); + const lines = scanJob.split('\n'); + const jobMatch = /^(\s*)scan:\s*$/u.exec(lines[0]); + assert.ok(jobMatch, 'scan job indentation is invalid'); + const stepsLine = `${jobMatch[1]} steps:`; + const stepsIndexes = []; + for (let index = 1; index < lines.length; index += 1) { + if (lines[index] === stepsLine) { + stepsIndexes.push(index); + } + } + assert.equal( + stepsIndexes.length, + 1, + 'scan job must contain exactly one direct steps mapping', + ); + + const stepIndent = `${jobMatch[1]} `; + const expected = `${stepIndent}- name: ${stepName}`; + const matches = []; + for (let index = stepsIndexes[0] + 1; index < lines.length; index += 1) { + if (lines[index] === expected) { + matches.push(index); } } assert.equal( matches.length, 1, - `expected exactly one workflow step ${stepName}`, + `expected exactly one direct workflow step ${stepName}`, ); - const { index: start, indent } = matches[0]; + const start = matches[0]; let end = lines.length; for (let index = start + 1; index < lines.length; index += 1) { - if (lines[index].startsWith(`${indent}- `)) { + if (lines[index].startsWith(`${stepIndent}- `)) { end = index; break; } @@ -150,7 +207,7 @@ test('provenance step authority rejects step-shaped text inside a run block', () assert.throws( () => assertProvenanceGuard(namedStep(hostileWorkflow, PROVENANCE_STEP_NAME)), - /workflow step/u, + /direct workflow step/u, 'run-block text must not become provenance workflow-step authority', ); }); From 779f2658eb3bd55c9c13a90ea0049ca26dba8765 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 14:06:00 +0900 Subject: [PATCH 041/180] test(ci): structurally bind provenance env authority --- ...guardrail-provenance-env-contract.test.mjs | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs diff --git a/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs new file mode 100644 index 000000000..d617a0c1f --- /dev/null +++ b/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs @@ -0,0 +1,200 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; +import test from 'node:test'; + +const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); +const PROVENANCE_STEP_NAME = + 'Materialize AppGuardrail SARIF PR merge provenance'; +const EXPECTED_PR_NUMBER = + 'PR_NUMBER: ${{ github.event.pull_request.number }}'; +const EXPECTED_MERGE_SHA = 'EXPECTED_MERGE_SHA: ${{ github.sha }}'; + +/** Extracts one uniquely named direct job from the workflow's top-level jobs mapping. */ +function namedJob(workflow, jobName) { + const lines = workflow.split('\n'); + const jobsIndexes = []; + for (let index = 0; index < lines.length; index += 1) { + if (lines[index] === 'jobs:') { + jobsIndexes.push(index); + } + } + assert.equal(jobsIndexes.length, 1, 'workflow must contain exactly one jobs mapping'); + + const jobsStart = jobsIndexes[0]; + let jobsEnd = lines.length; + for (let index = jobsStart + 1; index < lines.length; index += 1) { + if (/^[^\s#]/u.test(lines[index])) { + jobsEnd = index; + break; + } + } + + const expected = ` ${jobName}:`; + const matches = []; + for (let index = jobsStart + 1; index < jobsEnd; index += 1) { + if (lines[index] === expected) { + matches.push(index); + } + } + assert.equal(matches.length, 1, `expected exactly one workflow job ${jobName}`); + + const start = matches[0]; + let end = jobsEnd; + for (let index = start + 1; index < jobsEnd; index += 1) { + if (/^ [A-Za-z0-9_-]+:\s*$/u.test(lines[index])) { + end = index; + break; + } + } + return lines.slice(start, end).join('\n'); +} + +/** Extracts one uniquely named real step from the direct steps sequence of jobs.scan. */ +function namedStep(workflow, stepName) { + const scanJob = namedJob(workflow, 'scan'); + const lines = scanJob.split('\n'); + const jobMatch = /^(\s*)scan:\s*$/u.exec(lines[0]); + assert.ok(jobMatch, 'scan job indentation is invalid'); + const stepsLine = `${jobMatch[1]} steps:`; + const stepsIndexes = []; + for (let index = 1; index < lines.length; index += 1) { + if (lines[index] === stepsLine) { + stepsIndexes.push(index); + } + } + assert.equal( + stepsIndexes.length, + 1, + 'scan job must contain exactly one direct steps mapping', + ); + + const stepIndent = `${jobMatch[1]} `; + const expected = `${stepIndent}- name: ${stepName}`; + const matches = []; + for (let index = stepsIndexes[0] + 1; index < lines.length; index += 1) { + if (lines[index] === expected) { + matches.push(index); + } + } + assert.equal( + matches.length, + 1, + `expected exactly one direct workflow step ${stepName}`, + ); + + const start = matches[0]; + let end = lines.length; + for (let index = start + 1; index < lines.length; index += 1) { + if (lines[index].startsWith(`${stepIndent}- `)) { + end = index; + break; + } + } + return lines.slice(start, end).join('\n'); +} + +/** Extracts one direct child mapping without borrowing sibling keys or block text. */ +function directMapping(step, mappingName) { + const lines = step.split('\n'); + const stepMatch = /^(\s*)-\s/u.exec(lines[0]); + assert.ok(stepMatch, 'workflow step indentation is invalid'); + const directIndent = `${stepMatch[1]} `; + const entryIndent = `${directIndent} `; + const mappingLine = `${directIndent}${mappingName}:`; + const matches = []; + for (let index = 1; index < lines.length; index += 1) { + if (lines[index] === mappingLine) { + matches.push(index); + } + } + assert.equal( + matches.length, + 1, + `expected exactly one direct ${mappingName} mapping`, + ); + + const start = matches[0] + 1; + let end = lines.length; + for (let index = start; index < lines.length; index += 1) { + if (!lines[index].startsWith(entryIndent)) { + end = index; + break; + } + } + return { lines: lines.slice(start, end), entryIndent }; +} + +/** Requires one unique direct mapping entry with the reviewed exact value. */ +function assertUniqueDirectEntry(mapping, key, expectedEntry) { + const prefix = `${mapping.entryIndent}${key}:`; + const directEntries = mapping.lines.filter((line) => { + if (!line.startsWith(prefix)) { + return false; + } + return !line.slice(mapping.entryIndent.length).startsWith(' '); + }); + assert.equal( + directEntries.length, + 1, + `expected exactly one direct ${key} entry`, + ); + assert.equal( + directEntries[0], + `${mapping.entryIndent}${expectedEntry}`, + `provenance ${key} must use the reviewed GitHub event authority`, + ); +} + +/** Requires provenance identity inputs to live in the step's direct env mapping. */ +function assertProvenanceEnv(step) { + const env = directMapping(step, 'env'); + assertUniqueDirectEntry(env, 'PR_NUMBER', EXPECTED_PR_NUMBER); + assertUniqueDirectEntry(env, 'EXPECTED_MERGE_SHA', EXPECTED_MERGE_SHA); +} + +test('AppGuardrail provenance step owns exact pull-request identity through direct env', () => { + const workflow = readFileSync( + join(REPOSITORY_ROOT, '.github/workflows/appguardrail.yml'), + 'utf8', + ); + assert.doesNotThrow(() => + assertProvenanceEnv(namedStep(workflow, PROVENANCE_STEP_NAME)), + ); +}); + +test('provenance env rejects authority markers that exist only outside env', () => { + const hostileStep = [ + ` - name: ${PROVENANCE_STEP_NAME}`, + ' env:', + ' HARMLESS: one', + ' run: |', + ` # ${EXPECTED_PR_NUMBER}`, + ` echo '${EXPECTED_MERGE_SHA}'`, + ].join('\n'); + + assert.throws( + () => assertProvenanceEnv(hostileStep), + /exactly one direct PR_NUMBER entry/u, + 'comment or run-block markers must not satisfy provenance environment authority', + ); +}); + +test('provenance env rejects duplicate pull-request identity keys', () => { + const hostileStep = [ + ` - name: ${PROVENANCE_STEP_NAME}`, + ' env:', + ` ${EXPECTED_PR_NUMBER}`, + ' PR_NUMBER: 999', + ` ${EXPECTED_MERGE_SHA}`, + ' EXPECTED_MERGE_SHA: deadbeef', + ' run: echo duplicate-env', + ].join('\n'); + + assert.throws( + () => assertProvenanceEnv(hostileStep), + /exactly one direct PR_NUMBER entry/u, + 'duplicate YAML env keys must not override reviewed provenance identity', + ); +}); From a6f7266edd82527bcefc7033d0b540b41af77deb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:16:48 +0900 Subject: [PATCH 042/180] test(ci): bind source checkout authority to owning job --- ...workflow-source-checkout-contract.test.mjs | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs diff --git a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs new file mode 100644 index 000000000..70ea4f325 --- /dev/null +++ b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs @@ -0,0 +1,255 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; +import test from 'node:test'; + +const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); +const SOURCE_REF = + 'ref: ${{ github.event.pull_request.head.sha || github.sha }}'; + +/** Finds direct jobs while respecting quoted IDs and inline-comment boundaries. */ +function directJobEntries(lines, start, end) { + const entries = []; + const pattern = + /^ (?:([A-Za-z_][A-Za-z0-9_-]*)|"([A-Za-z_][A-Za-z0-9_-]*)"|'([A-Za-z_][A-Za-z0-9_-]*)'):\s*(?:#.*)?$/u; + for (let index = start; index < end; index += 1) { + const match = pattern.exec(lines[index]); + if (match) { + entries.push({ index, name: match[1] ?? match[2] ?? match[3] }); + } + } + return entries; +} + +/** Extracts one direct job only from the top-level jobs mapping. */ +function namedJob(workflow, jobName) { + const lines = workflow.split('\n'); + const jobsIndexes = []; + for (let index = 0; index < lines.length; index += 1) { + if (lines[index] === 'jobs:') { + jobsIndexes.push(index); + } + } + assert.equal( + jobsIndexes.length, + 1, + 'workflow must contain exactly one jobs mapping', + ); + + const jobsStart = jobsIndexes[0]; + let jobsEnd = lines.length; + for (let index = jobsStart + 1; index < lines.length; index += 1) { + if (/^[^\s#]/u.test(lines[index])) { + jobsEnd = index; + break; + } + } + + const entries = directJobEntries(lines, jobsStart + 1, jobsEnd); + const matches = entries.filter((entry) => entry.name === jobName); + assert.equal( + matches.length, + 1, + `expected exactly one workflow job ${jobName}`, + ); + const start = matches[0].index; + const position = entries.findIndex((entry) => entry.index === start); + const end = + position + 1 < entries.length ? entries[position + 1].index : jobsEnd; + return lines.slice(start, end).join('\n'); +} + +/** Bounds the direct steps sequence without accepting later job-level mappings. */ +function stepsSection(job) { + const lines = job.split('\n'); + const jobIndent = /^\s*/u.exec(lines[0])?.[0] ?? ''; + const keyIndent = `${jobIndent} `; + const stepsLine = `${keyIndent}steps:`; + const indexes = []; + for (let index = 1; index < lines.length; index += 1) { + if (lines[index] === stepsLine) { + indexes.push(index); + } + } + assert.equal( + indexes.length, + 1, + 'job must contain exactly one direct steps mapping', + ); + + const start = indexes[0] + 1; + let end = lines.length; + for (let index = start; index < lines.length; index += 1) { + const line = lines[index]; + if (line.trim().length === 0 || line.trimStart().startsWith('#')) { + continue; + } + const indent = /^\s*/u.exec(line)?.[0].length ?? 0; + if (indent <= keyIndent.length) { + end = index; + break; + } + } + return { lines: lines.slice(start, end), stepIndent: `${keyIndent} ` }; +} + +/** Returns real direct workflow-step blocks from one already bounded job. */ +function stepBlocks(job) { + const section = stepsSection(job); + const starts = []; + for (let index = 0; index < section.lines.length; index += 1) { + if (section.lines[index].startsWith(`${section.stepIndent}- `)) { + starts.push(index); + } + } + return starts.map((start, position) => { + const end = + position + 1 < starts.length ? starts[position + 1] : section.lines.length; + return section.lines.slice(start, end); + }); +} + +/** Reads one unique direct scalar, including an inline key on the sequence item. */ +function directStepScalar(stepLines, key, stepIndent) { + const directIndent = `${stepIndent} `; + const directPrefix = `${directIndent}${key}:`; + const inlinePrefix = `${stepIndent}- ${key}:`; + const values = []; + if (stepLines[0]?.startsWith(inlinePrefix)) { + values.push(stepLines[0].slice(inlinePrefix.length).trim()); + } + for (const line of stepLines.slice(1)) { + if (line.startsWith(directPrefix)) { + values.push(line.slice(directPrefix.length).trim()); + } + } + assert.ok(values.length <= 1, `step must not duplicate direct ${key}`); + return values[0]; +} + +/** Reads direct with inputs without borrowing nested or sibling text. */ +function directWithEntries(stepLines, stepIndent) { + const directIndent = `${stepIndent} `; + const entryIndent = `${directIndent} `; + const mappingLine = `${directIndent}with:`; + const indexes = []; + for (let index = 1; index < stepLines.length; index += 1) { + if (stepLines[index] === mappingLine) { + indexes.push(index); + } + } + if (indexes.length === 0) { + return []; + } + assert.equal( + indexes.length, + 1, + 'checkout step must not duplicate direct with mapping', + ); + + const entries = []; + for (let index = indexes[0] + 1; index < stepLines.length; index += 1) { + const line = stepLines[index]; + if (!line.startsWith(entryIndent)) { + break; + } + if (!line.slice(entryIndent.length).startsWith(' ')) { + entries.push(line.trim()); + } + } + return entries; +} + +/** Requires an exact-head checkout to be direct, unique, and credential-clean. */ +function assertExactContributorCheckout(workflow, jobName) { + const job = namedJob(workflow, jobName); + const section = stepsSection(job); + const matches = []; + + for (const step of stepBlocks(job)) { + const uses = directStepScalar(step, 'uses', section.stepIndent); + if (!uses?.startsWith('actions/checkout@')) { + continue; + } + const entries = directWithEntries(step, section.stepIndent); + const refs = entries.filter((entry) => entry.startsWith('ref:')); + if (refs.length === 1 && refs[0] === SOURCE_REF) { + matches.push(entries); + } + } + + assert.equal( + matches.length, + 1, + `${jobName} must own exactly one direct checkout bound to the contributor head`, + ); + assert.deepEqual( + matches[0].filter((entry) => entry.startsWith('persist-credentials:')), + ['persist-credentials: false'], + `${jobName} contributor checkout must disable credential persistence exactly once`, + ); +} + +const REQUIRED_JOBS = [ + [ + 'ci.yml', + ['compose_runtime', 'today-concurrency', 'validate', 'browser-acceptance'], + ], + ['appguardrail.yml', ['scan']], + ['commercial-readiness.yml', ['audit']], +]; + +test('source-verification jobs own direct exact contributor checkouts', () => { + for (const [workflowName, jobNames] of REQUIRED_JOBS) { + const workflow = readFileSync( + join(REPOSITORY_ROOT, '.github/workflows', workflowName), + 'utf8', + ); + for (const jobName of jobNames) { + assertExactContributorCheckout(workflow, jobName); + } + } +}); + +test('job extraction rejects checkout evidence borrowed from an inline-comment sibling', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - name: Checkout without reviewed ref', + ' uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' decoy: # valid sibling job boundary', + ' steps:', + ' - name: Decoy checkout', + ' uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ` ${SOURCE_REF}`, + ].join('\n'); + + assert.throws( + () => assertExactContributorCheckout(hostile, 'validate'), + /must own exactly one direct checkout bound to the contributor head/u, + ); +}); + +test('checkout source binding rejects duplicate direct ref authority', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ` ${SOURCE_REF}`, + ' ref: refs/pull/123/merge', + ].join('\n'); + + assert.throws( + () => assertExactContributorCheckout(hostile, 'validate'), + /must own exactly one direct checkout bound to the contributor head/u, + ); +}); From 1404ce686bdf658493f1f8f7f996d1f2faf54da7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:17:42 +0900 Subject: [PATCH 043/180] ci(test): verify source checkout authority contract --- .../tmp-source-checkout-contract-verifier.yml | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/tmp-source-checkout-contract-verifier.yml diff --git a/.github/workflows/tmp-source-checkout-contract-verifier.yml b/.github/workflows/tmp-source-checkout-contract-verifier.yml new file mode 100644 index 000000000..c16e466c8 --- /dev/null +++ b/.github/workflows/tmp-source-checkout-contract-verifier.yml @@ -0,0 +1,40 @@ +name: Temporary source checkout contract verifier + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact verifier head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + + - name: Enable Corepack + run: corepack enable + + - name: Install reproducible dependencies + run: pnpm install --frozen-lockfile + + - name: Verify source checkout authority contract + run: >- + pnpm --filter @life-os/commercial-readiness exec node --test + src/workflow-source-checkout-contract.test.mjs From 9cf61e30f8d461626124b38005ba052c8a59c9a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:19:32 +0900 Subject: [PATCH 044/180] test(ci): retire source checkout verifier --- .../tmp-source-checkout-contract-verifier.yml | 40 ------------------- 1 file changed, 40 deletions(-) delete mode 100644 .github/workflows/tmp-source-checkout-contract-verifier.yml diff --git a/.github/workflows/tmp-source-checkout-contract-verifier.yml b/.github/workflows/tmp-source-checkout-contract-verifier.yml deleted file mode 100644 index c16e466c8..000000000 --- a/.github/workflows/tmp-source-checkout-contract-verifier.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Temporary source checkout contract verifier - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout exact verifier head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 22 - - - name: Enable Corepack - run: corepack enable - - - name: Install reproducible dependencies - run: pnpm install --frozen-lockfile - - - name: Verify source checkout authority contract - run: >- - pnpm --filter @life-os/commercial-readiness exec node --test - src/workflow-source-checkout-contract.test.mjs From 75a44ff44cb274002744d95b714649a9943f41c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:22:34 +0900 Subject: [PATCH 045/180] ci(test): verify checkout contract package quality --- ...l-readiness-checkout-contract-verifier.yml | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml diff --git a/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml b/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml new file mode 100644 index 000000000..b5d6cce5c --- /dev/null +++ b/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml @@ -0,0 +1,49 @@ +name: Temporary commercial readiness checkout contract verifier + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact verifier head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + + - name: Enable Corepack + run: corepack enable + + - name: Install reproducible dependencies + run: pnpm install --frozen-lockfile + + - name: Check permanent contract formatting + run: >- + pnpm exec prettier --single-quote --check + packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs + + - name: Run complete Commercial Readiness suite + run: pnpm --filter @life-os/commercial-readiness test + + - name: Typecheck Commercial Readiness + run: pnpm --filter @life-os/commercial-readiness typecheck + + - name: Build Commercial Readiness + run: pnpm --filter @life-os/commercial-readiness build From b166eb5b10802f884cbd522827aec0bda756d74e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:23:45 +0900 Subject: [PATCH 046/180] ci(test): diagnose checkout contract formatting --- ...l-readiness-checkout-contract-verifier.yml | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml b/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml index b5d6cce5c..cd900724d 100644 --- a/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml +++ b/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml @@ -1,4 +1,4 @@ -name: Temporary commercial readiness checkout contract verifier +name: Temporary commercial readiness checkout contract formatter on: push: @@ -9,7 +9,7 @@ permissions: contents: read jobs: - verify: + format-diagnostic: runs-on: ubuntu-24.04 timeout-minutes: 10 steps: @@ -34,16 +34,12 @@ jobs: - name: Install reproducible dependencies run: pnpm install --frozen-lockfile - - name: Check permanent contract formatting - run: >- - pnpm exec prettier --single-quote --check - packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs - - - name: Run complete Commercial Readiness suite - run: pnpm --filter @life-os/commercial-readiness test - - - name: Typecheck Commercial Readiness - run: pnpm --filter @life-os/commercial-readiness typecheck - - - name: Build Commercial Readiness - run: pnpm --filter @life-os/commercial-readiness build + - name: Materialize canonical formatter output + run: | + set -euo pipefail + target='packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs' + pnpm exec prettier --single-quote --write "$target" + echo 'CANONICAL_FILE_BASE64_BEGIN' + base64 -w0 "$target" + echo + echo 'CANONICAL_FILE_BASE64_END' From aa0dc708a4aa187e20c9a04a90785420f40c87ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:24:32 +0900 Subject: [PATCH 047/180] ci(test): expose checkout contract formatter diff --- ...tmp-commercial-readiness-checkout-contract-verifier.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml b/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml index cd900724d..92ea2bcc8 100644 --- a/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml +++ b/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml @@ -34,12 +34,9 @@ jobs: - name: Install reproducible dependencies run: pnpm install --frozen-lockfile - - name: Materialize canonical formatter output + - name: Materialize canonical formatter diff run: | set -euo pipefail target='packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs' pnpm exec prettier --single-quote --write "$target" - echo 'CANONICAL_FILE_BASE64_BEGIN' - base64 -w0 "$target" - echo - echo 'CANONICAL_FILE_BASE64_END' + git --no-pager diff -- "$target" From 638f11be40619b7f5c9a1d255401b3c9e2a1e45d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:26:27 +0900 Subject: [PATCH 048/180] style(test): normalize checkout authority contract --- .../src/workflow-source-checkout-contract.test.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs index 70ea4f325..6bbdbee33 100644 --- a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs @@ -105,7 +105,9 @@ function stepBlocks(job) { } return starts.map((start, position) => { const end = - position + 1 < starts.length ? starts[position + 1] : section.lines.length; + position + 1 < starts.length + ? starts[position + 1] + : section.lines.length; return section.lines.slice(start, end); }); } From 6eae9501475cb9cc385913f325232f51084f3caf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:26:38 +0900 Subject: [PATCH 049/180] ci(test): verify normalized checkout contract package --- ...l-readiness-checkout-contract-verifier.yml | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml b/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml index 92ea2bcc8..697d46c67 100644 --- a/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml +++ b/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml @@ -1,4 +1,4 @@ -name: Temporary commercial readiness checkout contract formatter +name: Temporary commercial readiness checkout contract verifier on: push: @@ -9,9 +9,9 @@ permissions: contents: read jobs: - format-diagnostic: + verify: runs-on: ubuntu-24.04 - timeout-minutes: 10 + timeout-minutes: 15 steps: - name: Checkout exact verifier head uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -34,9 +34,19 @@ jobs: - name: Install reproducible dependencies run: pnpm install --frozen-lockfile - - name: Materialize canonical formatter diff - run: | - set -euo pipefail - target='packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs' - pnpm exec prettier --single-quote --write "$target" - git --no-pager diff -- "$target" + - name: Check permanent contract formatting + run: >- + pnpm exec prettier --single-quote --check + packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs + + - name: Run complete Commercial Readiness suite + run: pnpm --filter @life-os/commercial-readiness test + + - name: Typecheck Commercial Readiness + run: pnpm --filter @life-os/commercial-readiness typecheck + + - name: Build Commercial Readiness + run: pnpm --filter @life-os/commercial-readiness build + + - name: Check patch whitespace + run: git diff --check HEAD^ From c30c2f9ec8f1415f151849084b15b6aa0e9d94fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:27:30 +0900 Subject: [PATCH 050/180] ci(test): remove shallow-parent-only verifier check --- .../tmp-commercial-readiness-checkout-contract-verifier.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml b/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml index 697d46c67..2d94e7b4f 100644 --- a/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml +++ b/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml @@ -47,6 +47,3 @@ jobs: - name: Build Commercial Readiness run: pnpm --filter @life-os/commercial-readiness build - - - name: Check patch whitespace - run: git diff --check HEAD^ From 045f9dbc5fef1b4aa5959e79fe51dca4d1fea3b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:28:09 +0900 Subject: [PATCH 051/180] test(ci): retire commercial readiness checkout verifier --- ...l-readiness-checkout-contract-verifier.yml | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 .github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml diff --git a/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml b/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml deleted file mode 100644 index 2d94e7b4f..000000000 --- a/.github/workflows/tmp-commercial-readiness-checkout-contract-verifier.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Temporary commercial readiness checkout contract verifier - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - name: Checkout exact verifier head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 22 - - - name: Enable Corepack - run: corepack enable - - - name: Install reproducible dependencies - run: pnpm install --frozen-lockfile - - - name: Check permanent contract formatting - run: >- - pnpm exec prettier --single-quote --check - packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs - - - name: Run complete Commercial Readiness suite - run: pnpm --filter @life-os/commercial-readiness test - - - name: Typecheck Commercial Readiness - run: pnpm --filter @life-os/commercial-readiness typecheck - - - name: Build Commercial Readiness - run: pnpm --filter @life-os/commercial-readiness build From 661612294101e84ccd762c0e03299c6dbd834c7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:37:26 +0900 Subject: [PATCH 052/180] test(commercial): reject second source checkout --- ...workflow-source-checkout-contract.test.mjs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs index 6bbdbee33..9824b0b78 100644 --- a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs @@ -255,3 +255,24 @@ test('checkout source binding rejects duplicate direct ref authority', () => { /must own exactly one direct checkout bound to the contributor head/u, ); }); + +test('checkout source binding rejects a later current-repository checkout', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ` ${SOURCE_REF}`, + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'); + + assert.throws( + () => assertExactContributorCheckout(hostile, 'validate'), + /must own exactly one current-repository checkout/u, + ); +}); From 9caaf1b5a4e0f6deb07eca4af913ef6952bdca85 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:38:25 +0900 Subject: [PATCH 053/180] fix(commercial): bind source checkout authority --- ...workflow-source-checkout-contract.test.mjs | 94 +++++++++++++++++-- 1 file changed, 84 insertions(+), 10 deletions(-) diff --git a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs index 9824b0b78..549f5e23f 100644 --- a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs @@ -5,6 +5,7 @@ import { join } from 'node:path'; import test from 'node:test'; const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); +const SELF_REPOSITORY = 'ContextualWisdomLab/life-os'; const SOURCE_REF = 'ref: ${{ github.event.pull_request.head.sha || github.sha }}'; @@ -163,11 +164,40 @@ function directWithEntries(stepLines, stepIndent) { return entries; } -/** Requires an exact-head checkout to be direct, unique, and credential-clean. */ +/** Classifies checkout repository authority without accepting dynamic ambiguity. */ +function checkoutRepositoryKind(entries) { + const repositories = entries.filter((entry) => entry.startsWith('repository:')); + assert.ok( + repositories.length <= 1, + 'checkout step must not duplicate direct repository authority', + ); + if (repositories.length === 0) { + return 'self'; + } + + let repository = repositories[0].slice('repository:'.length).trim(); + if ( + (repository.startsWith('"') && repository.endsWith('"')) || + (repository.startsWith("'") && repository.endsWith("'")) + ) { + repository = repository.slice(1, -1); + } + if (repository === SELF_REPOSITORY || repository === '${{ github.repository }}') { + return 'self'; + } + assert.match( + repository, + /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u, + 'external checkout repository authority must be one static owner/name', + ); + return 'external'; +} + +/** Requires one exact self checkout while allowing explicit static external dependencies. */ function assertExactContributorCheckout(workflow, jobName) { const job = namedJob(workflow, jobName); const section = stepsSection(job); - const matches = []; + const selfCheckouts = []; for (const step of stepBlocks(job)) { const uses = directStepScalar(step, 'uses', section.stepIndent); @@ -175,19 +205,23 @@ function assertExactContributorCheckout(workflow, jobName) { continue; } const entries = directWithEntries(step, section.stepIndent); - const refs = entries.filter((entry) => entry.startsWith('ref:')); - if (refs.length === 1 && refs[0] === SOURCE_REF) { - matches.push(entries); + if (checkoutRepositoryKind(entries) === 'self') { + selfCheckouts.push(entries); } } assert.equal( - matches.length, + selfCheckouts.length, 1, - `${jobName} must own exactly one direct checkout bound to the contributor head`, + `${jobName} must own exactly one current-repository checkout`, + ); + assert.deepEqual( + selfCheckouts[0].filter((entry) => entry.startsWith('ref:')), + [SOURCE_REF], + `${jobName} current-repository checkout must bind exactly to the contributor head`, ); assert.deepEqual( - matches[0].filter((entry) => entry.startsWith('persist-credentials:')), + selfCheckouts[0].filter((entry) => entry.startsWith('persist-credentials:')), ['persist-credentials: false'], `${jobName} contributor checkout must disable credential persistence exactly once`, ); @@ -234,7 +268,7 @@ test('job extraction rejects checkout evidence borrowed from an inline-comment s assert.throws( () => assertExactContributorCheckout(hostile, 'validate'), - /must own exactly one direct checkout bound to the contributor head/u, + /current-repository checkout must bind exactly to the contributor head/u, ); }); @@ -252,7 +286,7 @@ test('checkout source binding rejects duplicate direct ref authority', () => { assert.throws( () => assertExactContributorCheckout(hostile, 'validate'), - /must own exactly one direct checkout bound to the contributor head/u, + /current-repository checkout must bind exactly to the contributor head/u, ); }); @@ -276,3 +310,43 @@ test('checkout source binding rejects a later current-repository checkout', () = /must own exactly one current-repository checkout/u, ); }); + +test('checkout source binding allows one explicit static external dependency checkout', () => { + const valid = [ + 'jobs:', + ' scan:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ` ${SOURCE_REF}`, + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' repository: ContextualWisdomLab/appguardrail', + ' ref: reviewed-appguardrail-sha', + ' persist-credentials: false', + ].join('\n'); + + assert.doesNotThrow(() => assertExactContributorCheckout(valid, 'scan')); +}); + +test('checkout source binding rejects ambiguous dynamic repository authority', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ` ${SOURCE_REF}`, + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' repository: ${{ matrix.repository }}', + ' ref: refs/heads/main', + ].join('\n'); + + assert.throws( + () => assertExactContributorCheckout(hostile, 'validate'), + /external checkout repository authority must be one static owner\/name/u, + ); +}); From 3a83d1b85504b99675413bc7ccb95dd7ad2ea7c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:39:41 +0900 Subject: [PATCH 054/180] ci: add read-only maintainer verifier --- .../workflows/lifeos-maintainer-verifier.yml | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/lifeos-maintainer-verifier.yml diff --git a/.github/workflows/lifeos-maintainer-verifier.yml b/.github/workflows/lifeos-maintainer-verifier.yml new file mode 100644 index 000000000..499fc0937 --- /dev/null +++ b/.github/workflows/lifeos-maintainer-verifier.yml @@ -0,0 +1,52 @@ +name: LifeOS maintainer verifier + +on: + push: + branches: [fix/appguardrail-sarif-provenance-v1] + +permissions: + contents: read + +jobs: + quality: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Checkout exact pushed head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + + - name: Enable Corepack + run: corepack enable + + - name: Install reproducible dependencies + run: pnpm install --frozen-lockfile + + - name: Verify canonical formatting with diagnostic diff + shell: bash + run: | + set -euo pipefail + target='packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs' + formatted="${RUNNER_TEMP}/workflow-source-checkout-contract.test.mjs" + pnpm exec prettier --single-quote "$target" > "$formatted" + diff -u "$target" "$formatted" + + - name: Test Commercial Readiness + run: pnpm --filter @life-os/commercial-readiness test + + - name: Typecheck Commercial Readiness + run: pnpm --filter @life-os/commercial-readiness typecheck + + - name: Build Commercial Readiness + run: pnpm --filter @life-os/commercial-readiness build From 0e579fbdd0cd3f487f51c32530c0bfaa96e32adf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:40:34 +0900 Subject: [PATCH 055/180] style(commercial): apply canonical verifier format --- .../src/workflow-source-checkout-contract.test.mjs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs index 549f5e23f..cf9611b7d 100644 --- a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs @@ -166,7 +166,9 @@ function directWithEntries(stepLines, stepIndent) { /** Classifies checkout repository authority without accepting dynamic ambiguity. */ function checkoutRepositoryKind(entries) { - const repositories = entries.filter((entry) => entry.startsWith('repository:')); + const repositories = entries.filter((entry) => + entry.startsWith('repository:'), + ); assert.ok( repositories.length <= 1, 'checkout step must not duplicate direct repository authority', @@ -182,7 +184,10 @@ function checkoutRepositoryKind(entries) { ) { repository = repository.slice(1, -1); } - if (repository === SELF_REPOSITORY || repository === '${{ github.repository }}') { + if ( + repository === SELF_REPOSITORY || + repository === '${{ github.repository }}' + ) { return 'self'; } assert.match( @@ -221,7 +226,9 @@ function assertExactContributorCheckout(workflow, jobName) { `${jobName} current-repository checkout must bind exactly to the contributor head`, ); assert.deepEqual( - selfCheckouts[0].filter((entry) => entry.startsWith('persist-credentials:')), + selfCheckouts[0].filter((entry) => + entry.startsWith('persist-credentials:'), + ), ['persist-credentials: false'], `${jobName} contributor checkout must disable credential persistence exactly once`, ); From 8c4dc506a839352b4c78b2a59dab8e24e5ad324f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:41:18 +0900 Subject: [PATCH 056/180] ci: retire maintainer verifier --- .../workflows/lifeos-maintainer-verifier.yml | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 .github/workflows/lifeos-maintainer-verifier.yml diff --git a/.github/workflows/lifeos-maintainer-verifier.yml b/.github/workflows/lifeos-maintainer-verifier.yml deleted file mode 100644 index 499fc0937..000000000 --- a/.github/workflows/lifeos-maintainer-verifier.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: LifeOS maintainer verifier - -on: - push: - branches: [fix/appguardrail-sarif-provenance-v1] - -permissions: - contents: read - -jobs: - quality: - runs-on: ubuntu-24.04 - timeout-minutes: 20 - steps: - - name: Checkout exact pushed head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 22 - - - name: Enable Corepack - run: corepack enable - - - name: Install reproducible dependencies - run: pnpm install --frozen-lockfile - - - name: Verify canonical formatting with diagnostic diff - shell: bash - run: | - set -euo pipefail - target='packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs' - formatted="${RUNNER_TEMP}/workflow-source-checkout-contract.test.mjs" - pnpm exec prettier --single-quote "$target" > "$formatted" - diff -u "$target" "$formatted" - - - name: Test Commercial Readiness - run: pnpm --filter @life-os/commercial-readiness test - - - name: Typecheck Commercial Readiness - run: pnpm --filter @life-os/commercial-readiness typecheck - - - name: Build Commercial Readiness - run: pnpm --filter @life-os/commercial-readiness build From 22d0e2175469aafdab8f5dc60c00180662dd0fce Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 16:07:52 +0900 Subject: [PATCH 057/180] test(ci): reject case-variant self checkout authority --- ...workflow-source-checkout-contract.test.mjs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs index cf9611b7d..d7327bd42 100644 --- a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs @@ -6,6 +6,7 @@ import test from 'node:test'; const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); const SELF_REPOSITORY = 'ContextualWisdomLab/life-os'; +const SELF_REPOSITORY_NORMALIZED = SELF_REPOSITORY.toLowerCase(); const SOURCE_REF = 'ref: ${{ github.event.pull_request.head.sha || github.sha }}'; @@ -185,7 +186,7 @@ function checkoutRepositoryKind(entries) { repository = repository.slice(1, -1); } if ( - repository === SELF_REPOSITORY || + repository.toLowerCase() === SELF_REPOSITORY_NORMALIZED || repository === '${{ github.repository }}' ) { return 'self'; @@ -318,6 +319,29 @@ test('checkout source binding rejects a later current-repository checkout', () = ); }); +test('checkout source binding treats case-variant current-repository authority as self', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ` ${SOURCE_REF}`, + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' repository: contextualwisdomlab/LIFE-OS', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'); + + assert.throws( + () => assertExactContributorCheckout(hostile, 'validate'), + /must own exactly one current-repository checkout/u, + 'GitHub repository owner/name matching is case-insensitive and must not create an external-checkout escape hatch', + ); +}); + test('checkout source binding allows one explicit static external dependency checkout', () => { const valid = [ 'jobs:', From 5b5674e092b7bf724054b669f09642683faf28f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 16:11:33 +0900 Subject: [PATCH 058/180] test(ci): reject quoted checkout authority bypass --- ...workflow-source-checkout-contract.test.mjs | 50 ++++++++++++++++--- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs index d7327bd42..68f6f07ca 100644 --- a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs @@ -132,6 +132,20 @@ function directStepScalar(stepLines, key, stepIndent) { return values[0]; } +/** Removes one YAML quote pair from a static scalar without evaluating expressions. */ +function unquoteStaticScalar(value) { + if (value === undefined) { + return undefined; + } + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + return value.slice(1, -1); + } + return value; +} + /** Reads direct with inputs without borrowing nested or sibling text. */ function directWithEntries(stepLines, stepIndent) { const directIndent = `${stepIndent} `; @@ -178,13 +192,9 @@ function checkoutRepositoryKind(entries) { return 'self'; } - let repository = repositories[0].slice('repository:'.length).trim(); - if ( - (repository.startsWith('"') && repository.endsWith('"')) || - (repository.startsWith("'") && repository.endsWith("'")) - ) { - repository = repository.slice(1, -1); - } + const repository = unquoteStaticScalar( + repositories[0].slice('repository:'.length).trim(), + ); if ( repository.toLowerCase() === SELF_REPOSITORY_NORMALIZED || repository === '${{ github.repository }}' @@ -206,7 +216,9 @@ function assertExactContributorCheckout(workflow, jobName) { const selfCheckouts = []; for (const step of stepBlocks(job)) { - const uses = directStepScalar(step, 'uses', section.stepIndent); + const uses = unquoteStaticScalar( + directStepScalar(step, 'uses', section.stepIndent), + ); if (!uses?.startsWith('actions/checkout@')) { continue; } @@ -319,6 +331,28 @@ test('checkout source binding rejects a later current-repository checkout', () = ); }); +test('checkout source binding rejects a quoted later current-repository checkout', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ` ${SOURCE_REF}`, + ' - uses: "actions/checkout@reviewed-sha"', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'); + + assert.throws( + () => assertExactContributorCheckout(hostile, 'validate'), + /must own exactly one current-repository checkout/u, + 'quoted YAML uses scalars must not hide a second current-repository checkout', + ); +}); + test('checkout source binding treats case-variant current-repository authority as self', () => { const hostile = [ 'jobs:', From b7ddafef73c258071f0505f367e4d1006449a579 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 16:13:04 +0900 Subject: [PATCH 059/180] test(ci): parse commented quoted checkout scalars --- ...workflow-source-checkout-contract.test.mjs | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs index 68f6f07ca..f7c12ef7b 100644 --- a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs @@ -132,18 +132,24 @@ function directStepScalar(stepLines, key, stepIndent) { return values[0]; } -/** Removes one YAML quote pair from a static scalar without evaluating expressions. */ -function unquoteStaticScalar(value) { +/** Parses one static YAML scalar while preserving fail-closed expression handling. */ +function staticScalarValue(value) { if (value === undefined) { return undefined; } - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - return value.slice(1, -1); + + const doubleQuoted = /^("(?:[^"\\]|\\.)*")(?:\s+#.*)?$/u.exec(value); + if (doubleQuoted) { + return JSON.parse(doubleQuoted[1]); } - return value; + + const singleQuoted = /^'((?:[^']|'')*)'(?:\s+#.*)?$/u.exec(value); + if (singleQuoted) { + return singleQuoted[1].replaceAll("''", "'"); + } + + const comment = /\s+#/u.exec(value); + return (comment ? value.slice(0, comment.index) : value).trim(); } /** Reads direct with inputs without borrowing nested or sibling text. */ @@ -192,7 +198,7 @@ function checkoutRepositoryKind(entries) { return 'self'; } - const repository = unquoteStaticScalar( + const repository = staticScalarValue( repositories[0].slice('repository:'.length).trim(), ); if ( @@ -216,7 +222,7 @@ function assertExactContributorCheckout(workflow, jobName) { const selfCheckouts = []; for (const step of stepBlocks(job)) { - const uses = unquoteStaticScalar( + const uses = staticScalarValue( directStepScalar(step, 'uses', section.stepIndent), ); if (!uses?.startsWith('actions/checkout@')) { @@ -353,6 +359,28 @@ test('checkout source binding rejects a quoted later current-repository checkout ); }); +test('checkout source binding rejects quoted checkout authority with an inline comment', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ` ${SOURCE_REF}`, + ' - uses: "actions/checkout@reviewed-sha" # still a checkout', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'); + + assert.throws( + () => assertExactContributorCheckout(hostile, 'validate'), + /must own exactly one current-repository checkout/u, + 'an inline YAML comment must not hide quoted checkout authority', + ); +}); + test('checkout source binding treats case-variant current-repository authority as self', () => { const hostile = [ 'jobs:', From ea72605db4337d44fd7062feab98b9eaf9065c26 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 17:08:28 +0900 Subject: [PATCH 060/180] test(ci): isolate external source checkouts --- ...w-external-checkout-path-contract.test.mjs | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs diff --git a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs new file mode 100644 index 000000000..1ff56ee38 --- /dev/null +++ b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs @@ -0,0 +1,257 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; +import test from 'node:test'; + +const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); +const SELF_REPOSITORY = 'contextualwisdomlab/life-os'; + +/** Reads one static YAML scalar without evaluating expressions. */ +function staticScalarValue(value) { + if (value === undefined) { + return undefined; + } + + const doubleQuoted = /^("(?:[^"\\]|\\.)*")(?:\s+#.*)?$/u.exec(value); + if (doubleQuoted) { + return JSON.parse(doubleQuoted[1]); + } + + const singleQuoted = /^'((?:[^']|'')*)'(?:\s+#.*)?$/u.exec(value); + if (singleQuoted) { + return singleQuoted[1].replaceAll("''", "'"); + } + + const comment = /\s+#/u.exec(value); + return (comment ? value.slice(0, comment.index) : value).trim(); +} + +/** Extracts exactly one direct job from the top-level jobs mapping. */ +function namedJob(workflow, jobName) { + const lines = workflow.split('\n'); + const jobs = lines + .map((line, index) => ({ line, index })) + .filter(({ line }) => line === 'jobs:'); + assert.equal(jobs.length, 1, 'workflow must contain exactly one jobs mapping'); + + const jobsStart = jobs[0].index; + let jobsEnd = lines.length; + for (let index = jobsStart + 1; index < lines.length; index += 1) { + if (/^[^\s#]/u.test(lines[index])) { + jobsEnd = index; + break; + } + } + + const jobPattern = /^ ([A-Za-z_][A-Za-z0-9_-]*):\s*(?:#.*)?$/u; + const starts = []; + for (let index = jobsStart + 1; index < jobsEnd; index += 1) { + const match = jobPattern.exec(lines[index]); + if (match) { + starts.push({ index, name: match[1] }); + } + } + const matches = starts.filter(({ name }) => name === jobName); + assert.equal(matches.length, 1, `expected exactly one workflow job ${jobName}`); + const start = matches[0].index; + const position = starts.findIndex(({ index }) => index === start); + const end = position + 1 < starts.length ? starts[position + 1].index : jobsEnd; + return lines.slice(start, end); +} + +/** Returns direct step blocks from one bounded job. */ +function stepBlocks(jobLines) { + const jobIndent = /^\s*/u.exec(jobLines[0])?.[0].length ?? 0; + const keyIndent = jobIndent + 2; + const stepIndent = keyIndent + 2; + const stepsIndexes = []; + for (let index = 1; index < jobLines.length; index += 1) { + if (jobLines[index] === `${' '.repeat(keyIndent)}steps:`) { + stepsIndexes.push(index); + } + } + assert.equal(stepsIndexes.length, 1, 'job must contain exactly one direct steps mapping'); + + const starts = []; + for (let index = stepsIndexes[0] + 1; index < jobLines.length; index += 1) { + const line = jobLines[index]; + if (line.trim().length === 0 || line.trimStart().startsWith('#')) { + continue; + } + const indent = /^\s*/u.exec(line)?.[0].length ?? 0; + if (indent <= keyIndent) { + break; + } + if (indent === stepIndent && line.startsWith(`${' '.repeat(stepIndent)}- `)) { + starts.push(index); + } + } + + return starts.map((start, position) => { + const end = position + 1 < starts.length ? starts[position + 1] : jobLines.length; + return { lines: jobLines.slice(start, end), stepIndent }; + }); +} + +/** Reads one direct scalar from a step, including an inline sequence key. */ +function directStepScalar(step, key) { + const directIndent = ' '.repeat(step.stepIndent + 2); + const inlinePrefix = `${' '.repeat(step.stepIndent)}- ${key}:`; + const directPrefix = `${directIndent}${key}:`; + const values = []; + if (step.lines[0]?.startsWith(inlinePrefix)) { + values.push(step.lines[0].slice(inlinePrefix.length).trim()); + } + for (const line of step.lines.slice(1)) { + if (line.startsWith(directPrefix)) { + values.push(line.slice(directPrefix.length).trim()); + } + } + assert.ok(values.length <= 1, `step must not duplicate direct ${key}`); + return values[0]; +} + +/** Reads direct with entries and rejects duplicate mapping authority. */ +function directWithMap(step) { + const directIndent = ' '.repeat(step.stepIndent + 2); + const entryIndent = ' '.repeat(step.stepIndent + 4); + const withLine = `${directIndent}with:`; + const indexes = []; + for (let index = 1; index < step.lines.length; index += 1) { + if (step.lines[index] === withLine) { + indexes.push(index); + } + } + assert.ok(indexes.length <= 1, 'checkout step must not duplicate direct with mapping'); + if (indexes.length === 0) { + return new Map(); + } + + const entries = new Map(); + for (let index = indexes[0] + 1; index < step.lines.length; index += 1) { + const line = step.lines[index]; + if (!line.startsWith(entryIndent)) { + break; + } + const relative = line.slice(entryIndent.length); + if (relative.startsWith(' ') || relative.trim().length === 0 || relative.trimStart().startsWith('#')) { + continue; + } + const separator = relative.indexOf(':'); + assert.ok(separator > 0, 'checkout with entry must be one direct key/value scalar'); + const key = relative.slice(0, separator).trim(); + const value = relative.slice(separator + 1).trim(); + assert.ok(!entries.has(key), `checkout step must not duplicate direct ${key} authority`); + entries.set(key, value); + } + return entries; +} + +/** Requires external dependency checkouts to stay below the current workspace root. */ +function assertExternalCheckoutIsolation(workflow, jobName) { + const job = namedJob(workflow, jobName); + for (const step of stepBlocks(job)) { + const uses = staticScalarValue(directStepScalar(step, 'uses')); + if (!uses?.startsWith('actions/checkout@')) { + continue; + } + + const withEntries = directWithMap(step); + const repositoryValue = withEntries.get('repository'); + if (repositoryValue === undefined) { + continue; + } + const repository = staticScalarValue(repositoryValue); + assert.ok(repository, 'checkout repository authority must be a non-empty static scalar'); + if (repository.toLowerCase() === SELF_REPOSITORY || repository === '${{ github.repository }}') { + continue; + } + assert.match( + repository, + /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u, + 'external checkout repository authority must be one static owner/name', + ); + + const pathValue = withEntries.get('path'); + assert.notEqual(pathValue, undefined, 'external checkout must use one direct isolated path'); + const path = staticScalarValue(pathValue); + assert.ok(path, 'external checkout path must be a non-empty static scalar'); + assert.doesNotMatch(path, /\$\{\{/u, 'external checkout path must not be dynamic'); + assert.doesNotMatch(path, /^[\\/]/u, 'external checkout path must be workspace-relative'); + assert.doesNotMatch(path, /\\/u, 'external checkout path must use portable forward slashes'); + const segments = path.split('/'); + assert.ok( + segments.every((segment) => segment.length > 0 && segment !== '.' && segment !== '..'), + 'external checkout path must remain in a non-root workspace subdirectory', + ); + } +} + +const actualWorkflow = readFileSync( + join(REPOSITORY_ROOT, '.github/workflows/appguardrail.yml'), + 'utf8', +); + +test('AppGuardrail external dependency checkout cannot replace the LifeOS workspace root', () => { + assert.doesNotThrow(() => assertExternalCheckoutIsolation(actualWorkflow, 'scan')); +}); + +test('external checkout without a path is rejected', () => { + const hostile = [ + 'jobs:', + ' scan:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' repository: ContextualWisdomLab/appguardrail', + ' ref: reviewed-appguardrail-sha', + ' persist-credentials: false', + ].join('\n'); + assert.throws(() => assertExternalCheckoutIsolation(hostile, 'scan'), /must use one direct isolated path/u); +}); + +test('external checkout cannot target the workspace root or escape it', () => { + for (const unsafePath of ['.', '..', '../appguardrail', 'nested/../appguardrail', '/tmp/appguardrail']) { + const hostile = [ + 'jobs:', + ' scan:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' repository: ContextualWisdomLab/appguardrail', + ` path: ${unsafePath}`, + ' ref: reviewed-appguardrail-sha', + ].join('\n'); + assert.throws(() => assertExternalCheckoutIsolation(hostile, 'scan')); + } +}); + +test('external checkout path cannot be runtime-dynamic', () => { + const hostile = [ + 'jobs:', + ' scan:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' repository: ContextualWisdomLab/appguardrail', + ' path: ${{ github.workspace }}', + ' ref: reviewed-appguardrail-sha', + ].join('\n'); + assert.throws(() => assertExternalCheckoutIsolation(hostile, 'scan'), /must not be dynamic/u); +}); + +test('external checkout may use one explicit isolated subdirectory', () => { + const valid = [ + 'jobs:', + ' scan:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' repository: ContextualWisdomLab/appguardrail', + ' path: _appguardrail', + ' ref: reviewed-appguardrail-sha', + ' persist-credentials: false', + ].join('\n'); + assert.doesNotThrow(() => assertExternalCheckoutIsolation(valid, 'scan')); +}); From 9b803580aaa326bdc19d5f2516c3a62402ee8e43 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 17:11:15 +0900 Subject: [PATCH 061/180] ci: verify external checkout path contract --- ...verify-external-checkout-path-contract.yml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/verify-external-checkout-path-contract.yml diff --git a/.github/workflows/verify-external-checkout-path-contract.yml b/.github/workflows/verify-external-checkout-path-contract.yml new file mode 100644 index 000000000..4d78e4a6a --- /dev/null +++ b/.github/workflows/verify-external-checkout-path-contract.yml @@ -0,0 +1,26 @@ +name: Verify external checkout path contract + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact verifier head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.sha }} + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + - name: Verify external checkout path authority + run: node --test packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs From bab51bb3525b1035b2ff52281a7285bf3a7d80b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 17:11:52 +0900 Subject: [PATCH 062/180] fix(ci): keep verifier checkout warning-free --- .github/workflows/verify-external-checkout-path-contract.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/verify-external-checkout-path-contract.yml b/.github/workflows/verify-external-checkout-path-contract.yml index 4d78e4a6a..0bb7f6c6e 100644 --- a/.github/workflows/verify-external-checkout-path-contract.yml +++ b/.github/workflows/verify-external-checkout-path-contract.yml @@ -15,6 +15,10 @@ jobs: steps: - name: Checkout exact verifier head uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main with: persist-credentials: false ref: ${{ github.sha }} From de53b8a6879db7e3974e1638b78b06c5fd1105ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 17:12:22 +0900 Subject: [PATCH 063/180] chore(ci): retire external checkout verifier --- ...verify-external-checkout-path-contract.yml | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/verify-external-checkout-path-contract.yml diff --git a/.github/workflows/verify-external-checkout-path-contract.yml b/.github/workflows/verify-external-checkout-path-contract.yml deleted file mode 100644 index 0bb7f6c6e..000000000 --- a/.github/workflows/verify-external-checkout-path-contract.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Verify external checkout path contract - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout exact verifier head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - - name: Verify external checkout path authority - run: node --test packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs From 31617bd63968b1d0fc033619b254fcfe506e0812 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 17:12:45 +0900 Subject: [PATCH 064/180] ci: verify commercial readiness path contract --- ...ify-commercial-readiness-path-contract.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/verify-commercial-readiness-path-contract.yml diff --git a/.github/workflows/verify-commercial-readiness-path-contract.yml b/.github/workflows/verify-commercial-readiness-path-contract.yml new file mode 100644 index 000000000..813b0ca9e --- /dev/null +++ b/.github/workflows/verify-commercial-readiness-path-contract.yml @@ -0,0 +1,32 @@ +name: Verify commercial readiness path contract + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact verifier head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + - name: Verify commercial readiness contracts + run: node --test packages/commercial-readiness/src/*.test.mjs + - name: Check new path contract syntax + run: node --check packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs From 4546c84ae55b1dc1b486b5af071bf5814e51c52e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 17:13:50 +0900 Subject: [PATCH 065/180] chore(ci): retire commercial readiness verifier --- ...ify-commercial-readiness-path-contract.yml | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 .github/workflows/verify-commercial-readiness-path-contract.yml diff --git a/.github/workflows/verify-commercial-readiness-path-contract.yml b/.github/workflows/verify-commercial-readiness-path-contract.yml deleted file mode 100644 index 813b0ca9e..000000000 --- a/.github/workflows/verify-commercial-readiness-path-contract.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Verify commercial readiness path contract - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout exact verifier head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - - name: Verify commercial readiness contracts - run: node --test packages/commercial-readiness/src/*.test.mjs - - name: Check new path contract syntax - run: node --check packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs From 10dc22e1fdeb6bdc869146d96efaec09dc8f2874 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:05:45 +0900 Subject: [PATCH 066/180] test(ci): add bounded external checkout verifier --- ...pguardrail-external-checkout-isolation.yml | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/verify-appguardrail-external-checkout-isolation.yml diff --git a/.github/workflows/verify-appguardrail-external-checkout-isolation.yml b/.github/workflows/verify-appguardrail-external-checkout-isolation.yml new file mode 100644 index 000000000..4f108d129 --- /dev/null +++ b/.github/workflows/verify-appguardrail-external-checkout-isolation.yml @@ -0,0 +1,31 @@ +name: Verify AppGuardrail external checkout isolation + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: false + - name: Use Node 24 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: '24.8.0' + - name: Verify external checkout isolation contract + run: node --test packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs From 7f9d9b9f2ed42e78dd54faac1ef0ac98ec51236b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:06:21 +0900 Subject: [PATCH 067/180] test(ci): reproduce external checkout source-subtree overwrite --- ...low-external-checkout-path-contract.test.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs index 1ff56ee38..8500715c4 100644 --- a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs @@ -241,6 +241,23 @@ test('external checkout path cannot be runtime-dynamic', () => { assert.throws(() => assertExternalCheckoutIsolation(hostile, 'scan'), /must not be dynamic/u); }); +test('external checkout cannot overwrite a LifeOS source subtree', () => { + const hostile = [ + 'jobs:', + ' scan:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' repository: ContextualWisdomLab/appguardrail', + ' path: packages/commercial-readiness', + ' ref: reviewed-appguardrail-sha', + ].join('\n'); + assert.throws( + () => assertExternalCheckoutIsolation(hostile, 'scan'), + /reviewed external checkout path/u, + ); +}); + test('external checkout may use one explicit isolated subdirectory', () => { const valid = [ 'jobs:', From 4a05f3a4c5632f307f21aa0b87b9a8c010e43293 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:08:26 +0900 Subject: [PATCH 068/180] test(ci): bind external checkout to reviewed isolation path --- ...low-external-checkout-path-contract.test.mjs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs index 8500715c4..0c82e9a17 100644 --- a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs @@ -6,6 +6,9 @@ import test from 'node:test'; const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); const SELF_REPOSITORY = 'contextualwisdomlab/life-os'; +const REVIEWED_EXTERNAL_CHECKOUT_PATHS = new Map([ + ['contextualwisdomlab/appguardrail', '_appguardrail'], +]); /** Reads one static YAML scalar without evaluating expressions. */ function staticScalarValue(value) { @@ -148,7 +151,7 @@ function directWithMap(step) { return entries; } -/** Requires external dependency checkouts to stay below the current workspace root. */ +/** Requires external dependency checkouts to stay in their reviewed isolated workspace path. */ function assertExternalCheckoutIsolation(workflow, jobName) { const job = namedJob(workflow, jobName); for (const step of stepBlocks(job)) { @@ -185,6 +188,18 @@ function assertExternalCheckoutIsolation(workflow, jobName) { segments.every((segment) => segment.length > 0 && segment !== '.' && segment !== '..'), 'external checkout path must remain in a non-root workspace subdirectory', ); + + const reviewedPath = REVIEWED_EXTERNAL_CHECKOUT_PATHS.get(repository.toLowerCase()); + assert.notEqual( + reviewedPath, + undefined, + 'external checkout repository must have one reviewed external checkout path', + ); + assert.equal( + path, + reviewedPath, + 'external checkout must use its reviewed external checkout path', + ); } } From cd5081986eb8bf93c01d619126d36c35ee220949 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 18:13:45 +0900 Subject: [PATCH 069/180] test(ci): retire external checkout verifier --- ...pguardrail-external-checkout-isolation.yml | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 .github/workflows/verify-appguardrail-external-checkout-isolation.yml diff --git a/.github/workflows/verify-appguardrail-external-checkout-isolation.yml b/.github/workflows/verify-appguardrail-external-checkout-isolation.yml deleted file mode 100644 index 4f108d129..000000000 --- a/.github/workflows/verify-appguardrail-external-checkout-isolation.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Verify AppGuardrail external checkout isolation - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout exact candidate - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - ref: ${{ github.sha }} - fetch-depth: 1 - persist-credentials: false - - name: Use Node 24 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: '24.8.0' - - name: Verify external checkout isolation contract - run: node --test packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs From 76471247ff52f5d1579e232e4efbe06ff29f8f5e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:09:22 +0900 Subject: [PATCH 070/180] test(commercial): expose YAML alias checkout authority gap --- ...rkflow-source-yaml-alias-contract.test.mjs | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs diff --git a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs new file mode 100644 index 000000000..a7fbc8b5f --- /dev/null +++ b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs @@ -0,0 +1,137 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; +import test from 'node:test'; + +const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); + +/** Extracts one direct job only from the workflow's top-level jobs mapping. */ +function namedJob(workflow, jobName) { + const lines = workflow.split('\n'); + const jobsIndexes = []; + for (let index = 0; index < lines.length; index += 1) { + if (lines[index] === 'jobs:') { + jobsIndexes.push(index); + } + } + assert.equal( + jobsIndexes.length, + 1, + 'workflow must contain exactly one jobs mapping', + ); + + const jobsStart = jobsIndexes[0]; + let jobsEnd = lines.length; + for (let index = jobsStart + 1; index < lines.length; index += 1) { + if (/^[^\s#]/u.test(lines[index])) { + jobsEnd = index; + break; + } + } + + const directJobPattern = + /^ (?:([A-Za-z_][A-Za-z0-9_-]*)|"([A-Za-z_][A-Za-z0-9_-]*)"|'([A-Za-z_][A-Za-z0-9_-]*)'):\s*(?:#.*)?$/u; + const jobs = []; + for (let index = jobsStart + 1; index < jobsEnd; index += 1) { + const match = directJobPattern.exec(lines[index]); + if (match) { + jobs.push({ index, name: match[1] ?? match[2] ?? match[3] }); + } + } + + const matches = jobs.filter(({ name }) => name === jobName); + assert.equal(matches.length, 1, `expected exactly one workflow job ${jobName}`); + const start = matches[0].index; + const position = jobs.findIndex(({ index }) => index === start); + const end = position + 1 < jobs.length ? jobs[position + 1].index : jobsEnd; + return lines.slice(start, end); +} + +/** Bounds the direct steps sequence of one job. */ +function directSteps(jobLines) { + const jobIndent = /^\s*/u.exec(jobLines[0])?.[0].length ?? 0; + const keyIndent = jobIndent + 2; + const stepIndent = keyIndent + 2; + const stepsLine = `${' '.repeat(keyIndent)}steps:`; + const indexes = []; + for (let index = 1; index < jobLines.length; index += 1) { + if (jobLines[index] === stepsLine) { + indexes.push(index); + } + } + assert.equal( + indexes.length, + 1, + 'job must contain exactly one direct steps mapping', + ); + + const lines = []; + for (let index = indexes[0] + 1; index < jobLines.length; index += 1) { + const line = jobLines[index]; + if (line.trim().length === 0 || line.trimStart().startsWith('#')) { + continue; + } + const indent = /^\s*/u.exec(line)?.[0].length ?? 0; + if (indent <= keyIndent) { + break; + } + if (indent === stepIndent && line.startsWith(`${' '.repeat(stepIndent)}- `)) { + lines.push(line); + } + } + return { lines, stepIndent }; +} + +/** + * Requires source-verification workflow steps to expose their authority directly. + * + * YAML aliases can replay a previously declared step after raw-text checkout + * verification has counted the declaration only once, so source-verification + * jobs must not use step-level YAML anchor/alias indirection. + */ +function assertDirectStepAuthority(workflow, jobName) { + const { lines } = directSteps(namedJob(workflow, jobName)); + assert.ok(lines.length > 0, `${jobName} must contain direct workflow steps`); +} + +const REQUIRED_JOBS = [ + [ + 'ci.yml', + ['compose_runtime', 'today-concurrency', 'validate', 'browser-acceptance'], + ], + ['appguardrail.yml', ['scan']], + ['commercial-readiness.yml', ['audit']], +]; + +test('source-verification jobs use direct non-aliased step authority', () => { + for (const [workflowName, jobNames] of REQUIRED_JOBS) { + const workflow = readFileSync( + join(REPOSITORY_ROOT, '.github/workflows', workflowName), + 'utf8', + ); + for (const jobName of jobNames) { + assertDirectStepAuthority(workflow, jobName); + } + } +}); + +test('source-verification rejects YAML alias replay of a checkout step', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - &contributor_checkout', + ' uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ' - *contributor_checkout', + ].join('\n'); + + assert.throws( + () => assertDirectStepAuthority(hostile, 'validate'), + /YAML anchor or alias/u, + 'an alias-replayed checkout must not evade exact checkout counting', + ); +}); From 1088438f58bfc8e1360e8f27cfe6b6499976b63c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:09:37 +0900 Subject: [PATCH 071/180] ci: verify source YAML alias regression --- .../verify-source-yaml-alias-contract.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/verify-source-yaml-alias-contract.yml diff --git a/.github/workflows/verify-source-yaml-alias-contract.yml b/.github/workflows/verify-source-yaml-alias-contract.yml new file mode 100644 index 000000000..46c19164a --- /dev/null +++ b/.github/workflows/verify-source-yaml-alias-contract.yml @@ -0,0 +1,32 @@ +name: Verify source YAML alias contract + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + ref: ${{ github.sha }} + persist-credentials: false + fetch-depth: 1 + + - name: Set up Node.js 24 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: '24' + + - name: Verify YAML alias source-authority contract + run: node --test packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs From 52cbaf68b7a08d6fcba52ed7a3ba56efc71f88eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:11:12 +0900 Subject: [PATCH 072/180] fix(commercial): reject YAML alias source authority --- .../src/workflow-source-yaml-alias-contract.test.mjs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs index a7fbc8b5f..044abeb92 100644 --- a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs @@ -91,8 +91,16 @@ function directSteps(jobLines) { * jobs must not use step-level YAML anchor/alias indirection. */ function assertDirectStepAuthority(workflow, jobName) { - const { lines } = directSteps(namedJob(workflow, jobName)); + const { lines, stepIndent } = directSteps(namedJob(workflow, jobName)); assert.ok(lines.length > 0, `${jobName} must contain direct workflow steps`); + for (const line of lines) { + const sequenceValue = line.slice(stepIndent + 2).trimStart(); + assert.doesNotMatch( + sequenceValue, + /^[&*]/u, + `${jobName} source-verification steps must not use YAML anchor or alias authority`, + ); + } } const REQUIRED_JOBS = [ From 7cf47dab27845254e11c92297de8d6c3e92b5990 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:12:26 +0900 Subject: [PATCH 073/180] test(commercial): expose noncanonical step sequence bypass --- ...rkflow-source-yaml-alias-contract.test.mjs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs index 044abeb92..d03336959 100644 --- a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs @@ -143,3 +143,39 @@ test('source-verification rejects YAML alias replay of a checkout step', () => { 'an alias-replayed checkout must not evade exact checkout counting', ); }); + +test('source-verification rejects noncanonical sequence forms that raw checkout scanning cannot parse', () => { + const hostileVariants = [ + [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ' -', + ' uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'), + [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ' - { uses: actions/checkout@reviewed-sha, with: { persist-credentials: false, ref: refs/heads/main } }', + ].join('\n'), + ]; + + for (const hostile of hostileVariants) { + assert.throws( + () => assertDirectStepAuthority(hostile, 'validate'), + /canonical direct mapping/u, + ); + } +}); From 0c83e6fc26e247ad408d74ee8133847d70f142f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:12:59 +0900 Subject: [PATCH 074/180] fix(commercial): require canonical direct source steps --- ...rkflow-source-yaml-alias-contract.test.mjs | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs index d03336959..67880524b 100644 --- a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs @@ -48,7 +48,7 @@ function namedJob(workflow, jobName) { return lines.slice(start, end); } -/** Bounds the direct steps sequence of one job. */ +/** Bounds every direct sequence item in one job's steps mapping. */ function directSteps(jobLines) { const jobIndent = /^\s*/u.exec(jobLines[0])?.[0].length ?? 0; const keyIndent = jobIndent + 2; @@ -67,6 +67,7 @@ function directSteps(jobLines) { ); const lines = []; + const sequenceMarker = `${' '.repeat(stepIndent)}-`; for (let index = indexes[0] + 1; index < jobLines.length; index += 1) { const line = jobLines[index]; if (line.trim().length === 0 || line.trimStart().startsWith('#')) { @@ -76,7 +77,10 @@ function directSteps(jobLines) { if (indent <= keyIndent) { break; } - if (indent === stepIndent && line.startsWith(`${' '.repeat(stepIndent)}- `)) { + if ( + indent === stepIndent && + (line === sequenceMarker || line.startsWith(`${sequenceMarker} `)) + ) { lines.push(line); } } @@ -86,20 +90,27 @@ function directSteps(jobLines) { /** * Requires source-verification workflow steps to expose their authority directly. * - * YAML aliases can replay a previously declared step after raw-text checkout - * verification has counted the declaration only once, so source-verification - * jobs must not use step-level YAML anchor/alias indirection. + * The existing source-verification scanners reason about block-style direct step + * mappings. YAML anchor/alias, bare-sequence, and flow-mapping forms would be + * executable YAML while escaping that authority model, so this boundary keeps + * those jobs in one canonical direct mapping form. */ function assertDirectStepAuthority(workflow, jobName) { const { lines, stepIndent } = directSteps(namedJob(workflow, jobName)); assert.ok(lines.length > 0, `${jobName} must contain direct workflow steps`); for (const line of lines) { - const sequenceValue = line.slice(stepIndent + 2).trimStart(); + const sequenceValue = line.slice(stepIndent + 1); + const trimmedValue = sequenceValue.trimStart(); assert.doesNotMatch( - sequenceValue, + trimmedValue, /^[&*]/u, `${jobName} source-verification steps must not use YAML anchor or alias authority`, ); + assert.match( + sequenceValue, + /^ [A-Za-z_][A-Za-z0-9_-]*:/u, + `${jobName} source-verification steps must use one canonical direct mapping sequence form`, + ); } } From 2e890dfe00a978288b638a4abd9ac910c4e17e47 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 19:14:01 +0900 Subject: [PATCH 075/180] ci: retire YAML alias verifier --- .../verify-source-yaml-alias-contract.yml | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 .github/workflows/verify-source-yaml-alias-contract.yml diff --git a/.github/workflows/verify-source-yaml-alias-contract.yml b/.github/workflows/verify-source-yaml-alias-contract.yml deleted file mode 100644 index 46c19164a..000000000 --- a/.github/workflows/verify-source-yaml-alias-contract.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Verify source YAML alias contract - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - steps: - - name: Checkout exact candidate - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - ref: ${{ github.sha }} - persist-credentials: false - fetch-depth: 1 - - - name: Set up Node.js 24 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: '24' - - - name: Verify YAML alias source-authority contract - run: node --test packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs From db36ec471ac92e4cdbac2cd7f395bdb6c46f5621 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 20:03:15 +0900 Subject: [PATCH 076/180] test(commercial): cover scalar YAML alias source authority --- ...rkflow-source-yaml-alias-contract.test.mjs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs index 67880524b..7c88eaf81 100644 --- a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs @@ -190,3 +190,25 @@ test('source-verification rejects noncanonical sequence forms that raw checkout ); } }); + +test('source-verification rejects YAML anchor and alias authority hidden in direct step scalar values', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: &checkout_action actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ' - uses: *checkout_action', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'); + + assert.throws( + () => assertDirectStepAuthority(hostile, 'validate'), + /YAML anchor or alias/u, + 'scalar anchor/alias authority must not evade source-verification step scanning', + ); +}); From 5ab4fd15ee4367641d4b36e2a4a2c4003046925a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 20:03:24 +0900 Subject: [PATCH 077/180] ci(commercial): verify scalar YAML alias regression --- .../verify-source-yaml-alias-contract.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/verify-source-yaml-alias-contract.yml diff --git a/.github/workflows/verify-source-yaml-alias-contract.yml b/.github/workflows/verify-source-yaml-alias-contract.yml new file mode 100644 index 000000000..46c19164a --- /dev/null +++ b/.github/workflows/verify-source-yaml-alias-contract.yml @@ -0,0 +1,32 @@ +name: Verify source YAML alias contract + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + ref: ${{ github.sha }} + persist-credentials: false + fetch-depth: 1 + + - name: Set up Node.js 24 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: '24' + + - name: Verify YAML alias source-authority contract + run: node --test packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs From a3f39a6a991380a4db26e6c2032da54e84f38239 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 20:04:26 +0900 Subject: [PATCH 078/180] fix(commercial): reject structural scalar YAML aliases --- ...rkflow-source-yaml-alias-contract.test.mjs | 78 +++++++++++++++++-- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs index 7c88eaf81..d70b5fa68 100644 --- a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs @@ -66,25 +66,86 @@ function directSteps(jobLines) { 'job must contain exactly one direct steps mapping', ); - const lines = []; + const sectionLines = []; const sequenceMarker = `${' '.repeat(stepIndent)}-`; for (let index = indexes[0] + 1; index < jobLines.length; index += 1) { const line = jobLines[index]; if (line.trim().length === 0 || line.trimStart().startsWith('#')) { + sectionLines.push(line); continue; } const indent = /^\s*/u.exec(line)?.[0].length ?? 0; if (indent <= keyIndent) { break; } + sectionLines.push(line); + } + + const starts = []; + for (let index = 0; index < sectionLines.length; index += 1) { + const line = sectionLines[index]; + const indent = /^\s*/u.exec(line)?.[0].length ?? 0; if ( indent === stepIndent && (line === sequenceMarker || line.startsWith(`${sequenceMarker} `)) ) { - lines.push(line); + starts.push(index); + } + } + + const blocks = starts.map((start, position) => { + const end = + position + 1 < starts.length ? starts[position + 1] : sectionLines.length; + return sectionLines.slice(start, end); + }); + return { blocks, stepIndent }; +} + +/** Rejects YAML anchor/alias authority from structural step mappings. */ +function assertNoStructuralYamlReferences(stepLines, stepIndent, jobName) { + const directIndent = stepIndent + 2; + let blockScalarIndent; + + for (let index = 0; index < stepLines.length; index += 1) { + const line = stepLines[index]; + if (line.trim().length === 0 || line.trimStart().startsWith('#')) { + continue; + } + + const indent = /^\s*/u.exec(line)?.[0].length ?? 0; + if (blockScalarIndent !== undefined) { + if (indent > blockScalarIndent) { + continue; + } + blockScalarIndent = undefined; + } + + const structural = + index === 0 + ? line.slice(stepIndent + 1) + : indent >= directIndent + ? line.slice(indent) + : undefined; + if (structural === undefined) { + continue; + } + + const mapping = /^(?:-\s+)?[A-Za-z_][A-Za-z0-9_-]*:\s*(.*)$/u.exec( + structural, + ); + if (!mapping) { + continue; + } + const value = mapping[1].trimStart(); + assert.doesNotMatch( + value, + /^[&*][A-Za-z0-9_-]+(?:\s|$)/u, + `${jobName} source-verification structural scalars must not use YAML anchor or alias authority`, + ); + if (/^[|>][+-]?(?:\s+#.*)?$/u.test(value)) { + blockScalarIndent = indent; } } - return { lines, stepIndent }; } /** @@ -96,10 +157,10 @@ function directSteps(jobLines) { * those jobs in one canonical direct mapping form. */ function assertDirectStepAuthority(workflow, jobName) { - const { lines, stepIndent } = directSteps(namedJob(workflow, jobName)); - assert.ok(lines.length > 0, `${jobName} must contain direct workflow steps`); - for (const line of lines) { - const sequenceValue = line.slice(stepIndent + 1); + const { blocks, stepIndent } = directSteps(namedJob(workflow, jobName)); + assert.ok(blocks.length > 0, `${jobName} must contain direct workflow steps`); + for (const block of blocks) { + const sequenceValue = block[0].slice(stepIndent + 1); const trimmedValue = sequenceValue.trimStart(); assert.doesNotMatch( trimmedValue, @@ -111,6 +172,7 @@ function assertDirectStepAuthority(workflow, jobName) { /^ [A-Za-z_][A-Za-z0-9_-]*:/u, `${jobName} source-verification steps must use one canonical direct mapping sequence form`, ); + assertNoStructuralYamlReferences(block, stepIndent, jobName); } } @@ -208,7 +270,7 @@ test('source-verification rejects YAML anchor and alias authority hidden in dire assert.throws( () => assertDirectStepAuthority(hostile, 'validate'), - /YAML anchor or alias/u, + /structural scalars must not use YAML anchor or alias authority/u, 'scalar anchor/alias authority must not evade source-verification step scanning', ); }); From 30b9745c4c0f99abc8f2d83841bc58b411cd3da9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 20:05:57 +0900 Subject: [PATCH 079/180] test(commercial): exercise nested scalar YAML aliases --- .../src/workflow-source-yaml-alias-contract.test.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs index d70b5fa68..84378e740 100644 --- a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs @@ -258,11 +258,13 @@ test('source-verification rejects YAML anchor and alias authority hidden in dire 'jobs:', ' validate:', ' steps:', - ' - uses: &checkout_action actions/checkout@reviewed-sha', + ' - name: Contributor checkout', + ' uses: &checkout_action actions/checkout@reviewed-sha', ' with:', ' persist-credentials: false', ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', - ' - uses: *checkout_action', + ' - name: Hidden second checkout', + ' uses: *checkout_action', ' with:', ' persist-credentials: false', ' ref: refs/heads/main', From 348a0496cdce4f3c42beb21ede3eec5427ce3fd3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 20:07:12 +0900 Subject: [PATCH 080/180] fix(commercial): inspect first-line scalar YAML authority --- ...rkflow-source-yaml-alias-contract.test.mjs | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs index 84378e740..4bd583faf 100644 --- a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs @@ -122,7 +122,7 @@ function assertNoStructuralYamlReferences(stepLines, stepIndent, jobName) { const structural = index === 0 - ? line.slice(stepIndent + 1) + ? line.slice(stepIndent) : indent >= directIndent ? line.slice(indent) : undefined; @@ -254,25 +254,42 @@ test('source-verification rejects noncanonical sequence forms that raw checkout }); test('source-verification rejects YAML anchor and alias authority hidden in direct step scalar values', () => { - const hostile = [ - 'jobs:', - ' validate:', - ' steps:', - ' - name: Contributor checkout', - ' uses: &checkout_action actions/checkout@reviewed-sha', - ' with:', - ' persist-credentials: false', - ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', - ' - name: Hidden second checkout', - ' uses: *checkout_action', - ' with:', - ' persist-credentials: false', - ' ref: refs/heads/main', - ].join('\n'); + const hostileVariants = [ + [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: &checkout_action actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ' - uses: *checkout_action', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'), + [ + 'jobs:', + ' validate:', + ' steps:', + ' - name: Contributor checkout', + ' uses: &checkout_action actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ' - name: Hidden second checkout', + ' uses: *checkout_action', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'), + ]; - assert.throws( - () => assertDirectStepAuthority(hostile, 'validate'), - /structural scalars must not use YAML anchor or alias authority/u, - 'scalar anchor/alias authority must not evade source-verification step scanning', - ); + for (const hostile of hostileVariants) { + assert.throws( + () => assertDirectStepAuthority(hostile, 'validate'), + /structural scalars must not use YAML anchor or alias authority/u, + 'scalar anchor/alias authority must not evade source-verification step scanning', + ); + } }); From d617e8a4a4a35c1db2e6ef37256a0f22d44d0dce Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 20:12:12 +0900 Subject: [PATCH 081/180] ci(commercial): retire scalar YAML alias verifier --- .../verify-source-yaml-alias-contract.yml | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 .github/workflows/verify-source-yaml-alias-contract.yml diff --git a/.github/workflows/verify-source-yaml-alias-contract.yml b/.github/workflows/verify-source-yaml-alias-contract.yml deleted file mode 100644 index 46c19164a..000000000 --- a/.github/workflows/verify-source-yaml-alias-contract.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Verify source YAML alias contract - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - steps: - - name: Checkout exact candidate - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - ref: ${{ github.sha }} - persist-credentials: false - fetch-depth: 1 - - - name: Set up Node.js 24 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: '24' - - - name: Verify YAML alias source-authority contract - run: node --test packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs From 6e8f80cc8750e666a65d065919793992291acb81 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 21:07:42 +0900 Subject: [PATCH 082/180] test(ci): bound provenance condition to direct job entries --- ...ail-provenance-condition-contract.test.mjs | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs index 7754fac1f..28798a59e 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs @@ -12,6 +12,20 @@ const SAME_REPOSITORY_CONDITION = 'github.event.pull_request.head.repo.full_name == github.repository'; const EXPECTED_PROVENANCE_CONDITION = `${PULL_REQUEST_CONDITION} && ${SAME_REPOSITORY_CONDITION}`; +/** Finds direct jobs while respecting quoted IDs and inline-comment boundaries. */ +function directJobEntries(lines, start, end) { + const entries = []; + const pattern = + /^ (?:([A-Za-z_][A-Za-z0-9_-]*)|"([A-Za-z_][A-Za-z0-9_-]*)"|'([A-Za-z_][A-Za-z0-9_-]*)'):\s*(?:#.*)?$/u; + for (let index = start; index < end; index += 1) { + const match = pattern.exec(lines[index]); + if (match) { + entries.push({ index, name: match[1] ?? match[2] ?? match[3] }); + } + } + return entries; +} + /** Extracts one uniquely named direct job from the workflow's top-level jobs mapping. */ function namedJob(workflow, jobName) { const lines = workflow.split('\n'); @@ -32,23 +46,14 @@ function namedJob(workflow, jobName) { } } - const expected = ` ${jobName}:`; - const matches = []; - for (let index = jobsStart + 1; index < jobsEnd; index += 1) { - if (lines[index] === expected) { - matches.push(index); - } - } + const entries = directJobEntries(lines, jobsStart + 1, jobsEnd); + const matches = entries.filter((entry) => entry.name === jobName); assert.equal(matches.length, 1, `expected exactly one workflow job ${jobName}`); - const start = matches[0]; - let end = jobsEnd; - for (let index = start + 1; index < jobsEnd; index += 1) { - if (/^ [A-Za-z0-9_-]+:\s*$/u.test(lines[index])) { - end = index; - break; - } - } + const start = matches[0].index; + const position = entries.findIndex((entry) => entry.index === start); + const end = + position + 1 < entries.length ? entries[position + 1].index : jobsEnd; return lines.slice(start, end).join('\n'); } @@ -211,3 +216,26 @@ test('provenance step authority rejects step-shaped text inside a run block', () 'run-block text must not become provenance workflow-step authority', ); }); + +test('provenance condition does not borrow authority from quoted or commented sibling jobs', () => { + for (const sibling of [' decoy: # sibling', ' "decoy":']) { + const hostileWorkflow = [ + 'jobs:', + ' scan:', + sibling, + ' steps:', + ` - name: ${PROVENANCE_STEP_NAME}`, + ' if: >-', + ` ${PULL_REQUEST_CONDITION}`, + ` && ${SAME_REPOSITORY_CONDITION}`, + ' run: echo sibling-authority', + ].join('\n'); + + assert.throws( + () => + assertProvenanceGuard(namedStep(hostileWorkflow, PROVENANCE_STEP_NAME)), + /direct steps mapping/u, + 'a sibling job must not lend provenance guard authority to jobs.scan', + ); + } +}); From b91bf3b52dc92037394153825e2ad7f7ffb5abca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 21:08:10 +0900 Subject: [PATCH 083/180] test(ci): bound provenance env to direct job entries --- ...guardrail-provenance-env-contract.test.mjs | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs index d617a0c1f..c0a6e1b0e 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs @@ -11,6 +11,20 @@ const EXPECTED_PR_NUMBER = 'PR_NUMBER: ${{ github.event.pull_request.number }}'; const EXPECTED_MERGE_SHA = 'EXPECTED_MERGE_SHA: ${{ github.sha }}'; +/** Finds direct jobs while respecting quoted IDs and inline-comment boundaries. */ +function directJobEntries(lines, start, end) { + const entries = []; + const pattern = + /^ (?:([A-Za-z_][A-Za-z0-9_-]*)|"([A-Za-z_][A-Za-z0-9_-]*)"|'([A-Za-z_][A-Za-z0-9_-]*)'):\s*(?:#.*)?$/u; + for (let index = start; index < end; index += 1) { + const match = pattern.exec(lines[index]); + if (match) { + entries.push({ index, name: match[1] ?? match[2] ?? match[3] }); + } + } + return entries; +} + /** Extracts one uniquely named direct job from the workflow's top-level jobs mapping. */ function namedJob(workflow, jobName) { const lines = workflow.split('\n'); @@ -31,23 +45,14 @@ function namedJob(workflow, jobName) { } } - const expected = ` ${jobName}:`; - const matches = []; - for (let index = jobsStart + 1; index < jobsEnd; index += 1) { - if (lines[index] === expected) { - matches.push(index); - } - } + const entries = directJobEntries(lines, jobsStart + 1, jobsEnd); + const matches = entries.filter((entry) => entry.name === jobName); assert.equal(matches.length, 1, `expected exactly one workflow job ${jobName}`); - const start = matches[0]; - let end = jobsEnd; - for (let index = start + 1; index < jobsEnd; index += 1) { - if (/^ [A-Za-z0-9_-]+:\s*$/u.test(lines[index])) { - end = index; - break; - } - } + const start = matches[0].index; + const position = entries.findIndex((entry) => entry.index === start); + const end = + position + 1 < entries.length ? entries[position + 1].index : jobsEnd; return lines.slice(start, end).join('\n'); } @@ -198,3 +203,25 @@ test('provenance env rejects duplicate pull-request identity keys', () => { 'duplicate YAML env keys must not override reviewed provenance identity', ); }); + +test('provenance env does not borrow identity from quoted or commented sibling jobs', () => { + for (const sibling of [' decoy: # sibling', ' "decoy":']) { + const hostileWorkflow = [ + 'jobs:', + ' scan:', + sibling, + ' steps:', + ` - name: ${PROVENANCE_STEP_NAME}`, + ' env:', + ` ${EXPECTED_PR_NUMBER}`, + ` ${EXPECTED_MERGE_SHA}`, + ' run: echo sibling-authority', + ].join('\n'); + + assert.throws( + () => assertProvenanceEnv(namedStep(hostileWorkflow, PROVENANCE_STEP_NAME)), + /direct steps mapping/u, + 'a sibling job must not lend provenance environment authority to jobs.scan', + ); + } +}); From dac0537715f366e9afff51528c42be1977caeacd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 21:08:59 +0900 Subject: [PATCH 084/180] test(ci): close YAML structural authority gaps --- ...rkflow-source-yaml-alias-contract.test.mjs | 65 ++++++++++++++++++- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs index 4bd583faf..235c98ccd 100644 --- a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs @@ -130,8 +130,15 @@ function assertNoStructuralYamlReferences(stepLines, stepIndent, jobName) { continue; } - const mapping = /^(?:-\s+)?[A-Za-z_][A-Za-z0-9_-]*:\s*(.*)$/u.exec( - structural, + const mappingCandidate = structural.replace(/^-\s+/u, ''); + assert.doesNotMatch( + mappingCandidate, + /^[&*][A-Za-z0-9_-]+(?:\s|$)/u, + `${jobName} source-verification structural mapping keys must not use YAML anchor or alias authority`, + ); + + const mapping = /^[A-Za-z_][A-Za-z0-9_-]*:\s*(.*)$/u.exec( + mappingCandidate, ); if (!mapping) { continue; @@ -142,7 +149,7 @@ function assertNoStructuralYamlReferences(stepLines, stepIndent, jobName) { /^[&*][A-Za-z0-9_-]+(?:\s|$)/u, `${jobName} source-verification structural scalars must not use YAML anchor or alias authority`, ); - if (/^[|>][+-]?(?:\s+#.*)?$/u.test(value)) { + if (/^[|>](?:[1-9][+-]?|[+-][1-9]?)?(?:\s+#.*)?$/u.test(value)) { blockScalarIndent = indent; } } @@ -293,3 +300,55 @@ test('source-verification rejects YAML anchor and alias authority hidden in dire ); } }); + +test('source-verification rejects YAML anchor or alias authority attached to structural mapping keys', () => { + const hostileVariants = [ + [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' &repository_key repository: attacker/example', + ' persist-credentials: false', + ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ].join('\n'), + [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' repository: &repository_value attacker/example', + ' *repository_value : attacker/example', + ' persist-credentials: false', + ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ].join('\n'), + ]; + + for (const hostile of hostileVariants) { + assert.throws( + () => assertDirectStepAuthority(hostile, 'validate'), + /structural mapping keys must not use YAML anchor or alias authority/u, + 'mapping-key composition must not evade direct checkout authority scanning', + ); + } +}); + +test('source-verification ignores structural-looking text inside block scalars with indentation indicators', () => { + for (const header of ['|2', '>-2', '|+2']) { + const valid = [ + 'jobs:', + ' validate:', + ' steps:', + ' - name: Safe shell', + ` run: ${header}`, + ' repository: *not-yaml-authority', + ].join('\n'); + + assert.doesNotThrow( + () => assertDirectStepAuthority(valid, 'validate'), + `block scalar ${header} payload must not become structural authority`, + ); + } +}); From 9edf731015a589e9e0a2f3cc944fc5d8a3ad912e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 21:09:33 +0900 Subject: [PATCH 085/180] test(ci): pin external checkout path failure modes --- ...w-external-checkout-path-contract.test.mjs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs index 0c82e9a17..897b85ea1 100644 --- a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs @@ -185,7 +185,11 @@ function assertExternalCheckoutIsolation(workflow, jobName) { assert.doesNotMatch(path, /\\/u, 'external checkout path must use portable forward slashes'); const segments = path.split('/'); assert.ok( - segments.every((segment) => segment.length > 0 && segment !== '.' && segment !== '..'), + !segments.includes('..'), + 'external checkout path must be workspace-relative', + ); + assert.ok( + segments.every((segment) => segment.length > 0 && segment !== '.'), 'external checkout path must remain in a non-root workspace subdirectory', ); @@ -227,7 +231,15 @@ test('external checkout without a path is rejected', () => { }); test('external checkout cannot target the workspace root or escape it', () => { - for (const unsafePath of ['.', '..', '../appguardrail', 'nested/../appguardrail', '/tmp/appguardrail']) { + const unsafePaths = [ + ['.', /non-root workspace subdirectory/u], + ['..', /workspace-relative/u], + ['../appguardrail', /workspace-relative/u], + ['nested/../appguardrail', /workspace-relative/u], + ['/tmp/appguardrail', /workspace-relative/u], + ]; + + for (const [unsafePath, expectedError] of unsafePaths) { const hostile = [ 'jobs:', ' scan:', @@ -238,7 +250,10 @@ test('external checkout cannot target the workspace root or escape it', () => { ` path: ${unsafePath}`, ' ref: reviewed-appguardrail-sha', ].join('\n'); - assert.throws(() => assertExternalCheckoutIsolation(hostile, 'scan')); + assert.throws( + () => assertExternalCheckoutIsolation(hostile, 'scan'), + expectedError, + ); } }); From 1db2110c344b0af8969685ad7f4f421443939ace Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 21:11:05 +0900 Subject: [PATCH 086/180] ci: verify PR279 review repairs --- .github/workflows/pr279-focused-verifier.yml | 50 ++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/pr279-focused-verifier.yml diff --git a/.github/workflows/pr279-focused-verifier.yml b/.github/workflows/pr279-focused-verifier.yml new file mode 100644 index 000000000..ae9fcb400 --- /dev/null +++ b/.github/workflows/pr279-focused-verifier.yml @@ -0,0 +1,50 @@ +name: PR279 Focused Source Verification + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + paths: + - .github/workflows/pr279-focused-verifier.yml + - packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs + - packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs + - packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs + - packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact verifier head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22.16.0' + + - name: Verify focused source contracts + run: >- + node --test + packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs + packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs + packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs + packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs + + - name: Syntax-check focused source contracts + run: >- + node --check packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs && + node --check packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs && + node --check packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs && + node --check packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs From 7be7d9a02bd3f5a813bc8ac080f6256a39615212 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 21:17:15 +0900 Subject: [PATCH 087/180] test(ci): isolate mapping-key alias regression --- .../src/workflow-source-yaml-alias-contract.test.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs index 235c98ccd..5cc2d8514 100644 --- a/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs @@ -314,13 +314,14 @@ test('source-verification rejects YAML anchor or alias authority attached to str ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', ].join('\n'), [ + 'env:', + ' REPOSITORY_KEY: &repository_key repository', 'jobs:', ' validate:', ' steps:', ' - uses: actions/checkout@reviewed-sha', ' with:', - ' repository: &repository_value attacker/example', - ' *repository_value : attacker/example', + ' *repository_key : attacker/example', ' persist-credentials: false', ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', ].join('\n'), From 0f42fb2289a85d3a202533afde1e1678e0951fa8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 21:17:29 +0900 Subject: [PATCH 088/180] ci: use Node 24 setup action in PR279 verifier --- .github/workflows/pr279-focused-verifier.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr279-focused-verifier.yml b/.github/workflows/pr279-focused-verifier.yml index ae9fcb400..e7e984858 100644 --- a/.github/workflows/pr279-focused-verifier.yml +++ b/.github/workflows/pr279-focused-verifier.yml @@ -30,7 +30,7 @@ jobs: ref: ${{ github.sha }} - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '22.16.0' From 6eb637af529debb89f48e3d14d3563d2cefb2fa7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 21:21:46 +0900 Subject: [PATCH 089/180] ci: retire PR279 focused verifier --- .github/workflows/pr279-focused-verifier.yml | 50 -------------------- 1 file changed, 50 deletions(-) delete mode 100644 .github/workflows/pr279-focused-verifier.yml diff --git a/.github/workflows/pr279-focused-verifier.yml b/.github/workflows/pr279-focused-verifier.yml deleted file mode 100644 index e7e984858..000000000 --- a/.github/workflows/pr279-focused-verifier.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: PR279 Focused Source Verification - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - paths: - - .github/workflows/pr279-focused-verifier.yml - - packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs - - packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs - - packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs - - packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout exact verifier head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '22.16.0' - - - name: Verify focused source contracts - run: >- - node --test - packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs - packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs - packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs - packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs - - - name: Syntax-check focused source contracts - run: >- - node --check packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs && - node --check packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs && - node --check packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs && - node --check packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs From f5dec48892461db159ae621a238317052eb2b2fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 22:10:33 +0900 Subject: [PATCH 090/180] test(commercial): reproduce quoted workflow-key escape --- ...orkflow-source-plain-key-contract.test.mjs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs diff --git a/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs new file mode 100644 index 000000000..a066e5475 --- /dev/null +++ b/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +/** Mirrors the current structural-key recognition used by source verification. */ +function assertCurrentStructuralAuthority(stepLines, stepIndent) { + const directIndent = stepIndent + 2; + let blockScalarIndent; + + for (let index = 0; index < stepLines.length; index += 1) { + const line = stepLines[index]; + if (line.trim().length === 0 || line.trimStart().startsWith('#')) { + continue; + } + + const indent = /^\s*/u.exec(line)?.[0].length ?? 0; + if (blockScalarIndent !== undefined) { + if (indent > blockScalarIndent) { + continue; + } + blockScalarIndent = undefined; + } + + const structural = + index === 0 + ? line.slice(stepIndent) + : indent >= directIndent + ? line.slice(indent) + : undefined; + if (structural === undefined) { + continue; + } + + const mappingCandidate = structural.replace(/^-\s+/u, ''); + const mapping = /^[A-Za-z_][A-Za-z0-9_-]*:\s*(.*)$/u.exec( + mappingCandidate, + ); + if (!mapping) { + continue; + } + + const value = mapping[1].trimStart(); + if (/^[|>](?:[1-9][+-]?|[+-][1-9]?)?(?:\s+#.*)?$/u.test(value)) { + blockScalarIndent = indent; + } + } +} + +test('source verification rejects a quoted structural uses key that can hide a second checkout', () => { + const hostileStep = [ + ' - name: Hidden second checkout', + ' "uses": actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ]; + + assert.throws( + () => assertCurrentStructuralAuthority(hostileStep, 6), + /canonical plain mapping keys/u, + 'quoted structural keys must not hide executable checkout authority from source verification', + ); +}); From 6946fb588669a0fb7eff96723fd6bb7102116b3e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 22:11:19 +0900 Subject: [PATCH 091/180] test(commercial): reject quoted workflow authority keys --- ...orkflow-source-plain-key-contract.test.mjs | 203 ++++++++++++++++-- 1 file changed, 183 insertions(+), 20 deletions(-) diff --git a/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs index a066e5475..cc9793ce1 100644 --- a/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs @@ -1,8 +1,115 @@ import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; import test from 'node:test'; -/** Mirrors the current structural-key recognition used by source verification. */ -function assertCurrentStructuralAuthority(stepLines, stepIndent) { +const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); +const PLAIN_MAPPING = /^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/u; +const QUOTED_MAPPING_KEY = + /^(?:"(?:[^"\\]|\\.)*"|'(?:[^']|'')*')\s*:/u; +const BLOCK_SCALAR = /^[|>](?:[1-9][+-]?|[+-][1-9]?)?(?:\s+#.*)?$/u; + +/** Extracts one direct job only from the workflow's top-level jobs mapping. */ +function namedJob(workflow, jobName) { + const lines = workflow.split('\n'); + const jobsIndexes = []; + for (let index = 0; index < lines.length; index += 1) { + if (lines[index] === 'jobs:') { + jobsIndexes.push(index); + } + } + assert.equal( + jobsIndexes.length, + 1, + 'workflow must contain exactly one jobs mapping', + ); + + const jobsStart = jobsIndexes[0]; + let jobsEnd = lines.length; + for (let index = jobsStart + 1; index < lines.length; index += 1) { + if (/^[^\s#]/u.test(lines[index])) { + jobsEnd = index; + break; + } + } + + const directJobPattern = + /^ (?:([A-Za-z_][A-Za-z0-9_-]*)|"([A-Za-z_][A-Za-z0-9_-]*)"|'([A-Za-z_][A-Za-z0-9_-]*)'):\s*(?:#.*)?$/u; + const jobs = []; + for (let index = jobsStart + 1; index < jobsEnd; index += 1) { + const match = directJobPattern.exec(lines[index]); + if (match) { + jobs.push({ index, name: match[1] ?? match[2] ?? match[3] }); + } + } + + const matches = jobs.filter(({ name }) => name === jobName); + assert.equal(matches.length, 1, `expected exactly one workflow job ${jobName}`); + const start = matches[0].index; + const position = jobs.findIndex(({ index }) => index === start); + const end = position + 1 < jobs.length ? jobs[position + 1].index : jobsEnd; + return lines.slice(start, end); +} + +/** Bounds every direct sequence item in one job's direct steps mapping. */ +function directSteps(jobLines) { + const jobIndent = /^\s*/u.exec(jobLines[0])?.[0].length ?? 0; + const keyIndent = jobIndent + 2; + const stepIndent = keyIndent + 2; + const stepsLine = `${' '.repeat(keyIndent)}steps:`; + const indexes = []; + for (let index = 1; index < jobLines.length; index += 1) { + if (jobLines[index] === stepsLine) { + indexes.push(index); + } + } + assert.equal( + indexes.length, + 1, + 'job must contain exactly one direct steps mapping', + ); + + const sectionLines = []; + const sequenceMarker = `${' '.repeat(stepIndent)}-`; + for (let index = indexes[0] + 1; index < jobLines.length; index += 1) { + const line = jobLines[index]; + if (line.trim().length === 0 || line.trimStart().startsWith('#')) { + sectionLines.push(line); + continue; + } + const indent = /^\s*/u.exec(line)?.[0].length ?? 0; + if (indent <= keyIndent) { + break; + } + sectionLines.push(line); + } + + const starts = []; + for (let index = 0; index < sectionLines.length; index += 1) { + const line = sectionLines[index]; + const indent = /^\s*/u.exec(line)?.[0].length ?? 0; + if ( + indent === stepIndent && + (line === sequenceMarker || line.startsWith(`${sequenceMarker} `)) + ) { + starts.push(index); + } + } + + const blocks = starts.map((start, position) => { + const end = + position + 1 < starts.length ? starts[position + 1] : sectionLines.length; + return sectionLines.slice(start, end); + }); + return { blocks, stepIndent }; +} + +/** + * Keeps structural workflow authority in the plain-key form consumed by the + * source-verification scanners while leaving quoted scalar values unrestricted. + */ +function assertPlainStructuralKeys(stepLines, stepIndent, jobName) { const directIndent = stepIndent + 2; let blockScalarIndent; @@ -31,32 +138,88 @@ function assertCurrentStructuralAuthority(stepLines, stepIndent) { } const mappingCandidate = structural.replace(/^-\s+/u, ''); - const mapping = /^[A-Za-z_][A-Za-z0-9_-]*:\s*(.*)$/u.exec( + assert.doesNotMatch( mappingCandidate, + QUOTED_MAPPING_KEY, + `${jobName} source-verification structural mapping keys must use canonical plain identifiers`, ); + + const mapping = PLAIN_MAPPING.exec(mappingCandidate); if (!mapping) { continue; } - - const value = mapping[1].trimStart(); - if (/^[|>](?:[1-9][+-]?|[+-][1-9]?)?(?:\s+#.*)?$/u.test(value)) { + if (BLOCK_SCALAR.test(mapping[2].trimStart())) { blockScalarIndent = indent; } } } -test('source verification rejects a quoted structural uses key that can hide a second checkout', () => { - const hostileStep = [ - ' - name: Hidden second checkout', - ' "uses": actions/checkout@reviewed-sha', - ' with:', - ' persist-credentials: false', - ' ref: refs/heads/main', - ]; - - assert.throws( - () => assertCurrentStructuralAuthority(hostileStep, 6), - /canonical plain mapping keys/u, - 'quoted structural keys must not hide executable checkout authority from source verification', - ); +function assertJobUsesPlainStructuralKeys(workflow, jobName) { + const { blocks, stepIndent } = directSteps(namedJob(workflow, jobName)); + assert.ok(blocks.length > 0, `${jobName} must contain direct workflow steps`); + for (const block of blocks) { + assertPlainStructuralKeys(block, stepIndent, jobName); + } +} + +const REQUIRED_JOBS = [ + [ + 'ci.yml', + ['compose_runtime', 'today-concurrency', 'validate', 'browser-acceptance'], + ], + ['appguardrail.yml', ['scan']], + ['commercial-readiness.yml', ['audit']], +]; + +test('source-verification jobs expose structural authority through plain mapping keys', () => { + for (const [workflowName, jobNames] of REQUIRED_JOBS) { + const workflow = readFileSync( + join(REPOSITORY_ROOT, '.github/workflows', workflowName), + 'utf8', + ); + for (const jobName of jobNames) { + assertJobUsesPlainStructuralKeys(workflow, jobName); + } + } +}); + +test('source verification rejects quoted structural keys that can hide a second checkout', () => { + for (const quotedUses of [ + '"uses": actions/checkout@reviewed-sha', + "'uses': actions/checkout@reviewed-sha", + ]) { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ' - name: Hidden second checkout', + ` ${quotedUses}`, + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'); + + assert.throws( + () => assertJobUsesPlainStructuralKeys(hostile, 'validate'), + /structural mapping keys must use canonical plain identifiers/u, + 'quoted structural keys must not hide executable checkout authority from source verification', + ); + } +}); + +test('quoted mapping-looking text inside a block scalar is not workflow authority', () => { + const valid = [ + 'jobs:', + ' validate:', + ' steps:', + ' - name: Safe shell', + ' run: |2', + ' "uses": actions/checkout@not-workflow-authority', + ].join('\n'); + + assert.doesNotThrow(() => assertJobUsesPlainStructuralKeys(valid, 'validate')); }); From a1283b3d9cf3f097c7cb09175a98fe91e67536bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 22:15:25 +0900 Subject: [PATCH 092/180] test(commercial): reproduce explicit workflow-key escape --- ...orkflow-source-plain-key-contract.test.mjs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs index cc9793ce1..0a84fbec4 100644 --- a/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs @@ -211,6 +211,30 @@ test('source verification rejects quoted structural keys that can hide a second } }); +test('source verification rejects explicit mapping-key syntax that can hide a second checkout', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ' - name: Hidden second checkout', + ' ? uses', + ' : actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'); + + assert.throws( + () => assertJobUsesPlainStructuralKeys(hostile, 'validate'), + /structural mapping keys must use canonical plain identifiers/u, + 'explicit mapping-key syntax must not hide executable checkout authority from source verification', + ); +}); + test('quoted mapping-looking text inside a block scalar is not workflow authority', () => { const valid = [ 'jobs:', From f149bf7935602c399d9176a592cce45d5371508e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 22:15:49 +0900 Subject: [PATCH 093/180] test(commercial): reject explicit workflow authority keys --- .../src/workflow-source-plain-key-contract.test.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs index 0a84fbec4..e178d4879 100644 --- a/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs @@ -8,6 +8,7 @@ const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); const PLAIN_MAPPING = /^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/u; const QUOTED_MAPPING_KEY = /^(?:"(?:[^"\\]|\\.)*"|'(?:[^']|'')*')\s*:/u; +const EXPLICIT_MAPPING_KEY = /^\?\s/u; const BLOCK_SCALAR = /^[|>](?:[1-9][+-]?|[+-][1-9]?)?(?:\s+#.*)?$/u; /** Extracts one direct job only from the workflow's top-level jobs mapping. */ @@ -143,6 +144,11 @@ function assertPlainStructuralKeys(stepLines, stepIndent, jobName) { QUOTED_MAPPING_KEY, `${jobName} source-verification structural mapping keys must use canonical plain identifiers`, ); + assert.doesNotMatch( + mappingCandidate, + EXPLICIT_MAPPING_KEY, + `${jobName} source-verification structural mapping keys must use canonical plain identifiers`, + ); const mapping = PLAIN_MAPPING.exec(mappingCandidate); if (!mapping) { From 74921ab057bf7465e6af3a72ad92fa8d9876235c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 23:07:08 +0900 Subject: [PATCH 094/180] test(commercial): reproduce case-variant checkout action bypass --- ...workflow-source-checkout-contract.test.mjs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs index f7c12ef7b..db7ef3873 100644 --- a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs @@ -404,6 +404,28 @@ test('checkout source binding treats case-variant current-repository authority a ); }); +test('checkout source binding rejects case-variant checkout action identity', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ` ${SOURCE_REF}`, + ' - uses: Actions/Checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'); + + assert.throws( + () => assertExactContributorCheckout(hostile, 'validate'), + /must own exactly one current-repository checkout/u, + 'GitHub repository identity is case-insensitive, so action-path casing must not hide a second checkout', + ); +}); + test('checkout source binding allows one explicit static external dependency checkout', () => { const valid = [ 'jobs:', From 3dce644004c2a0ae788396c3e452a8b45c6b71ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 23:07:24 +0900 Subject: [PATCH 095/180] ci(commercial): add bounded checkout identity verifier --- .../verify-checkout-action-identity.yml | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/verify-checkout-action-identity.yml diff --git a/.github/workflows/verify-checkout-action-identity.yml b/.github/workflows/verify-checkout-action-identity.yml new file mode 100644 index 000000000..3060b5c43 --- /dev/null +++ b/.github/workflows/verify-checkout-action-identity.yml @@ -0,0 +1,29 @@ +name: Verify checkout action identity + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + steps: + - name: Checkout exact candidate with case-variant action identity + uses: Actions/Checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + - name: Set up Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: 22.16.0 + - name: Reproduce checkout action identity regression + run: node --test packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs From 4a9ef75dd922401e545b777b5de8a9a5503da911 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 23:10:00 +0900 Subject: [PATCH 096/180] fix(commercial): normalize checkout action repository identity --- .../workflow-source-checkout-contract.test.mjs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs index db7ef3873..efc4c6f01 100644 --- a/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs @@ -7,6 +7,7 @@ import test from 'node:test'; const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); const SELF_REPOSITORY = 'ContextualWisdomLab/life-os'; const SELF_REPOSITORY_NORMALIZED = SELF_REPOSITORY.toLowerCase(); +const CHECKOUT_ACTION_REPOSITORY = 'actions/checkout'; const SOURCE_REF = 'ref: ${{ github.event.pull_request.head.sha || github.sha }}'; @@ -215,6 +216,18 @@ function checkoutRepositoryKind(entries) { return 'external'; } +/** Matches the checkout action by case-insensitive repository identity while preserving the ref. */ +function isCheckoutAction(uses) { + if (uses === undefined) { + return false; + } + const refSeparator = uses.indexOf('@'); + return ( + refSeparator > 0 && + uses.slice(0, refSeparator).toLowerCase() === CHECKOUT_ACTION_REPOSITORY + ); +} + /** Requires one exact self checkout while allowing explicit static external dependencies. */ function assertExactContributorCheckout(workflow, jobName) { const job = namedJob(workflow, jobName); @@ -225,7 +238,7 @@ function assertExactContributorCheckout(workflow, jobName) { const uses = staticScalarValue( directStepScalar(step, 'uses', section.stepIndent), ); - if (!uses?.startsWith('actions/checkout@')) { + if (!isCheckoutAction(uses)) { continue; } const entries = directWithEntries(step, section.stepIndent); From ac8a5637e1a1c582a2a875191a56ba4dbfe2f6c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 23:12:08 +0900 Subject: [PATCH 097/180] chore(ci): retire checkout identity verifier --- .../verify-checkout-action-identity.yml | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 .github/workflows/verify-checkout-action-identity.yml diff --git a/.github/workflows/verify-checkout-action-identity.yml b/.github/workflows/verify-checkout-action-identity.yml deleted file mode 100644 index 3060b5c43..000000000 --- a/.github/workflows/verify-checkout-action-identity.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Verify checkout action identity - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - steps: - - name: Checkout exact candidate with case-variant action identity - uses: Actions/Checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - name: Set up Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: 22.16.0 - - name: Reproduce checkout action identity regression - run: node --test packages/commercial-readiness/src/workflow-source-checkout-contract.test.mjs From c0ab38acbd3f2de559f9cae1c391391780786249 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 01:08:00 +0900 Subject: [PATCH 098/180] test(commercial): cover spaced YAML structural keys --- ...orkflow-source-plain-key-contract.test.mjs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs index e178d4879..571a54aff 100644 --- a/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs @@ -241,6 +241,29 @@ test('source verification rejects explicit mapping-key syntax that can hide a se ); }); +test('source verification rejects whitespace before a structural mapping separator', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ' - name: Hidden second checkout', + ' uses : actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'); + + assert.throws( + () => assertJobUsesPlainStructuralKeys(hostile, 'validate'), + /structural mapping keys must keep the key adjacent to its colon/u, + 'YAML permits whitespace before the mapping separator, so source verification must reject that alternate structural spelling', + ); +}); + test('quoted mapping-looking text inside a block scalar is not workflow authority', () => { const valid = [ 'jobs:', From 6b18188c4def4699366634fb9bb38d826cdc806f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 01:08:14 +0900 Subject: [PATCH 099/180] ci(commercial): verify spaced YAML structural-key regression --- .github/workflows/verify-spaced-yaml-key.yml | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/verify-spaced-yaml-key.yml diff --git a/.github/workflows/verify-spaced-yaml-key.yml b/.github/workflows/verify-spaced-yaml-key.yml new file mode 100644 index 000000000..8f7f1f104 --- /dev/null +++ b/.github/workflows/verify-spaced-yaml-key.yml @@ -0,0 +1,30 @@ +name: Verify spaced YAML structural keys + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + ref: ${{ github.sha }} + persist-credentials: false + fetch-depth: 1 + - name: Set up Node 24 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: '24' + - name: Verify structural-key contract + run: node --test packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs From d412afb2ce8f9b3087a10b49def2be111bec0c35 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 01:08:56 +0900 Subject: [PATCH 100/180] fix(commercial): reject spaced YAML structural keys --- .../src/workflow-source-plain-key-contract.test.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs index 571a54aff..bbb8c7a63 100644 --- a/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs @@ -6,6 +6,7 @@ import test from 'node:test'; const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); const PLAIN_MAPPING = /^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/u; +const SPACED_PLAIN_MAPPING_KEY = /^[A-Za-z_][A-Za-z0-9_-]*\s+:/u; const QUOTED_MAPPING_KEY = /^(?:"(?:[^"\\]|\\.)*"|'(?:[^']|'')*')\s*:/u; const EXPLICIT_MAPPING_KEY = /^\?\s/u; @@ -149,6 +150,11 @@ function assertPlainStructuralKeys(stepLines, stepIndent, jobName) { EXPLICIT_MAPPING_KEY, `${jobName} source-verification structural mapping keys must use canonical plain identifiers`, ); + assert.doesNotMatch( + mappingCandidate, + SPACED_PLAIN_MAPPING_KEY, + `${jobName} source-verification structural mapping keys must keep the key adjacent to its colon`, + ); const mapping = PLAIN_MAPPING.exec(mappingCandidate); if (!mapping) { From e5a8e95ffd323ed0d48edb19b7ceef0ef5fe1b6b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 01:09:31 +0900 Subject: [PATCH 101/180] ci(commercial): prove spaced YAML key runtime semantics --- .../verify-github-yaml-spaced-key.yml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/verify-github-yaml-spaced-key.yml diff --git a/.github/workflows/verify-github-yaml-spaced-key.yml b/.github/workflows/verify-github-yaml-spaced-key.yml new file mode 100644 index 000000000..709b10216 --- /dev/null +++ b/.github/workflows/verify-github-yaml-spaced-key.yml @@ -0,0 +1,26 @@ +name: Verify GitHub YAML spaced key semantics + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + steps: + - name: Exercise YAML mapping separator spacing + uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + ref: ${{ github.sha }} + persist-credentials: false + fetch-depth: 1 + - name: Confirm checkout materialized candidate + run: test "$(git rev-parse HEAD)" = "${GITHUB_SHA}" From f2d59018341ab1e14222a0fed0105c63ad3a1be3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 01:09:52 +0900 Subject: [PATCH 102/180] chore(ci): retire spaced-key runtime probe --- .../verify-github-yaml-spaced-key.yml | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 .github/workflows/verify-github-yaml-spaced-key.yml diff --git a/.github/workflows/verify-github-yaml-spaced-key.yml b/.github/workflows/verify-github-yaml-spaced-key.yml deleted file mode 100644 index 709b10216..000000000 --- a/.github/workflows/verify-github-yaml-spaced-key.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Verify GitHub YAML spaced key semantics - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - steps: - - name: Exercise YAML mapping separator spacing - uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - ref: ${{ github.sha }} - persist-credentials: false - fetch-depth: 1 - - name: Confirm checkout materialized candidate - run: test "$(git rev-parse HEAD)" = "${GITHUB_SHA}" From 5d175a019f34a82656284420a98c2d41d67744c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 01:10:03 +0900 Subject: [PATCH 103/180] chore(ci): retire spaced-key contract verifier --- .github/workflows/verify-spaced-yaml-key.yml | 30 -------------------- 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/verify-spaced-yaml-key.yml diff --git a/.github/workflows/verify-spaced-yaml-key.yml b/.github/workflows/verify-spaced-yaml-key.yml deleted file mode 100644 index 8f7f1f104..000000000 --- a/.github/workflows/verify-spaced-yaml-key.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Verify spaced YAML structural keys - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - steps: - - name: Checkout exact candidate - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - ref: ${{ github.sha }} - persist-credentials: false - fetch-depth: 1 - - name: Set up Node 24 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: '24' - - name: Verify structural-key contract - run: node --test packages/commercial-readiness/src/workflow-source-plain-key-contract.test.mjs From 3c66a035d2736753eaa2ebd91580d4977694f324 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 02:08:50 +0900 Subject: [PATCH 104/180] test(commercial): cover case-variant external checkout action --- ...flow-external-checkout-path-contract.test.mjs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs index 897b85ea1..440131276 100644 --- a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs @@ -230,6 +230,22 @@ test('external checkout without a path is rejected', () => { assert.throws(() => assertExternalCheckoutIsolation(hostile, 'scan'), /must use one direct isolated path/u); }); +test('case-variant checkout action cannot bypass external checkout isolation', () => { + const hostile = [ + 'jobs:', + ' scan:', + ' steps:', + ' - uses: Actions/Checkout@reviewed-sha', + ' with:', + ' repository: attacker/example', + ' ref: reviewed-external-sha', + ].join('\n'); + assert.throws( + () => assertExternalCheckoutIsolation(hostile, 'scan'), + /must use one direct isolated path/u, + ); +}); + test('external checkout cannot target the workspace root or escape it', () => { const unsafePaths = [ ['.', /non-root workspace subdirectory/u], From ef0fabeab42e999d469041929bdc37e1e6d90b4d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 02:09:09 +0900 Subject: [PATCH 105/180] test(ci): verify external checkout action identity --- ...rify-external-checkout-action-identity.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/verify-external-checkout-action-identity.yml diff --git a/.github/workflows/verify-external-checkout-action-identity.yml b/.github/workflows/verify-external-checkout-action-identity.yml new file mode 100644 index 000000000..fd18607f7 --- /dev/null +++ b/.github/workflows/verify-external-checkout-action-identity.yml @@ -0,0 +1,32 @@ +name: Verify external checkout action identity + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + steps: + - name: Checkout exact verifier head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: false + - name: Set up Node 22.16.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: 22.16.0 + - name: Verify external checkout isolation contract + run: node --test packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs + - name: Verify contract syntax + run: node --check packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs From 76492f1e977d576ab82cd4ac41347fc261725655 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 02:10:10 +0900 Subject: [PATCH 106/180] fix(commercial): normalize checkout action repository identity --- ...rkflow-external-checkout-path-contract.test.mjs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs index 440131276..809ee5bc3 100644 --- a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs @@ -30,6 +30,18 @@ function staticScalarValue(value) { return (comment ? value.slice(0, comment.index) : value).trim(); } +/** Recognizes checkout by case-insensitive action repository identity while preserving its ref. */ +function isCheckoutAction(uses) { + if (!uses) { + return false; + } + const separator = uses.indexOf('@'); + return ( + separator > 0 && + uses.slice(0, separator).toLowerCase() === 'actions/checkout' + ); +} + /** Extracts exactly one direct job from the top-level jobs mapping. */ function namedJob(workflow, jobName) { const lines = workflow.split('\n'); @@ -156,7 +168,7 @@ function assertExternalCheckoutIsolation(workflow, jobName) { const job = namedJob(workflow, jobName); for (const step of stepBlocks(job)) { const uses = staticScalarValue(directStepScalar(step, 'uses')); - if (!uses?.startsWith('actions/checkout@')) { + if (!isCheckoutAction(uses)) { continue; } From 7e798cb6d584f73a2d2bf788b59422f81e92271c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 02:10:52 +0900 Subject: [PATCH 107/180] chore(ci): retire external checkout identity verifier --- ...rify-external-checkout-action-identity.yml | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 .github/workflows/verify-external-checkout-action-identity.yml diff --git a/.github/workflows/verify-external-checkout-action-identity.yml b/.github/workflows/verify-external-checkout-action-identity.yml deleted file mode 100644 index fd18607f7..000000000 --- a/.github/workflows/verify-external-checkout-action-identity.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Verify external checkout action identity - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - steps: - - name: Checkout exact verifier head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - ref: ${{ github.sha }} - fetch-depth: 1 - persist-credentials: false - - name: Set up Node 22.16.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: 22.16.0 - - name: Verify external checkout isolation contract - run: node --test packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs - - name: Verify contract syntax - run: node --check packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs From 96718d669d0280436f15d6173561b49fc23f853f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:07:05 +0900 Subject: [PATCH 108/180] test(ci): verify explicit YAML tag semantics --- .github/workflows/verify-tagged-uses.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/verify-tagged-uses.yml diff --git a/.github/workflows/verify-tagged-uses.yml b/.github/workflows/verify-tagged-uses.yml new file mode 100644 index 000000000..564f8ae95 --- /dev/null +++ b/.github/workflows/verify-tagged-uses.yml @@ -0,0 +1,21 @@ +name: Verify tagged uses semantics + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Execute checkout through an explicit YAML string tag + uses: !!str actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Verify checkout executed + run: test -f packages/commercial-readiness/package.json From b20a5a85c55f09d313ba0742920220ba1deac1b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:08:19 +0900 Subject: [PATCH 109/180] test(ci): reproduce YAML tag authority bypass --- .github/workflows/verify-tagged-uses.yml | 43 ++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify-tagged-uses.yml b/.github/workflows/verify-tagged-uses.yml index 564f8ae95..b32096c39 100644 --- a/.github/workflows/verify-tagged-uses.yml +++ b/.github/workflows/verify-tagged-uses.yml @@ -17,5 +17,44 @@ jobs: uses: !!str actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: Verify checkout executed - run: test -f packages/commercial-readiness/package.json + - name: Reproduce source-verification YAML-tag bypass + run: | + set -euo pipefail + node --input-type=module <<'NODE' + import { readFileSync, rmSync, writeFileSync } from 'node:fs'; + import { spawnSync } from 'node:child_process'; + + const sourcePath = 'packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs'; + const probePath = 'packages/commercial-readiness/src/workflow-source-yaml-tag-red.test.mjs'; + const source = readFileSync(sourcePath, 'utf8'); + const hostile = String.raw` + +test('explicit YAML tags cannot hide checkout authority from source verification', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ' - name: Hidden tagged checkout', + ' uses: !!str actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\\n'); + + assert.throws( + () => assertDirectStepAuthority(hostile, 'validate'), + /YAML tag authority/u, + ); +}); +`; + writeFileSync(probePath, source + hostile, 'utf8'); + const result = spawnSync(process.execPath, ['--test', probePath], { + stdio: 'inherit', + }); + rmSync(probePath, { force: true }); + process.exit(result.status ?? 1); + NODE From 5ec554c89adadf43b7519388abc71bab392c662b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:08:58 +0900 Subject: [PATCH 110/180] test(ci): isolate YAML tag bypass RED --- .github/workflows/verify-tagged-uses.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-tagged-uses.yml b/.github/workflows/verify-tagged-uses.yml index b32096c39..639766d2c 100644 --- a/.github/workflows/verify-tagged-uses.yml +++ b/.github/workflows/verify-tagged-uses.yml @@ -37,7 +37,7 @@ test('explicit YAML tags cannot hide checkout authority from source verification ' - uses: actions/checkout@reviewed-sha', ' with:', ' persist-credentials: false', - ' ref: ${{ github.event.pull_request.head.sha || github.sha }}', + ' ref: reviewed-contributor-sha', ' - name: Hidden tagged checkout', ' uses: !!str actions/checkout@reviewed-sha', ' with:', From 20cb930bb28791a57643ad156f1794a0fe280aa8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:09:25 +0900 Subject: [PATCH 111/180] test(ci): run YAML tag bypass RED --- .github/workflows/verify-tagged-uses.yml | 55 +++++++++++------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/.github/workflows/verify-tagged-uses.yml b/.github/workflows/verify-tagged-uses.yml index 639766d2c..8db673429 100644 --- a/.github/workflows/verify-tagged-uses.yml +++ b/.github/workflows/verify-tagged-uses.yml @@ -23,38 +23,35 @@ jobs: node --input-type=module <<'NODE' import { readFileSync, rmSync, writeFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; - const sourcePath = 'packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs'; const probePath = 'packages/commercial-readiness/src/workflow-source-yaml-tag-red.test.mjs'; const source = readFileSync(sourcePath, 'utf8'); - const hostile = String.raw` - -test('explicit YAML tags cannot hide checkout authority from source verification', () => { - const hostile = [ - 'jobs:', - ' validate:', - ' steps:', - ' - uses: actions/checkout@reviewed-sha', - ' with:', - ' persist-credentials: false', - ' ref: reviewed-contributor-sha', - ' - name: Hidden tagged checkout', - ' uses: !!str actions/checkout@reviewed-sha', - ' with:', - ' persist-credentials: false', - ' ref: refs/heads/main', - ].join('\\n'); - - assert.throws( - () => assertDirectStepAuthority(hostile, 'validate'), - /YAML tag authority/u, - ); -}); -`; - writeFileSync(probePath, source + hostile, 'utf8'); - const result = spawnSync(process.execPath, ['--test', probePath], { - stdio: 'inherit', - }); + const hostileTest = [ + '', + "test('explicit YAML tags cannot hide checkout authority from source verification', () => {", + ' const hostile = [', + " 'jobs:',", + " ' validate:',", + " ' steps:',", + " ' - uses: actions/checkout@reviewed-sha',", + " ' with:',", + " ' persist-credentials: false',", + " ' ref: reviewed-contributor-sha',", + " ' - name: Hidden tagged checkout',", + " ' uses: !!str actions/checkout@reviewed-sha',", + " ' with:',", + " ' persist-credentials: false',", + " ' ref: refs/heads/main',", + " ].join('\\\\n');", + ' assert.throws(', + " () => assertDirectStepAuthority(hostile, 'validate'),", + ' /YAML tag authority/u,', + ' );', + '});', + '', + ].join('\n'); + writeFileSync(probePath, source + hostileTest, 'utf8'); + const result = spawnSync(process.execPath, ['--test', probePath], { stdio: 'inherit' }); rmSync(probePath, { force: true }); process.exit(result.status ?? 1); NODE From ccc03214dcf3741b3353b10ed415b86efe281695 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:11:02 +0900 Subject: [PATCH 112/180] test(ci): correct YAML tag RED fixture --- .github/workflows/verify-tagged-uses.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/verify-tagged-uses.yml b/.github/workflows/verify-tagged-uses.yml index 8db673429..69b10326a 100644 --- a/.github/workflows/verify-tagged-uses.yml +++ b/.github/workflows/verify-tagged-uses.yml @@ -15,6 +15,10 @@ jobs: steps: - name: Execute checkout through an explicit YAML string tag uses: !!str actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main with: persist-credentials: false - name: Reproduce source-verification YAML-tag bypass @@ -42,7 +46,7 @@ jobs: " ' with:',", " ' persist-credentials: false',", " ' ref: refs/heads/main',", - " ].join('\\\\n');", + " ].join('\\n');", ' assert.throws(', " () => assertDirectStepAuthority(hostile, 'validate'),", ' /YAML tag authority/u,', From 52eb46929484b66185eb02315e97aae2ae2e8bb1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:13:01 +0900 Subject: [PATCH 113/180] fix(ci): reject YAML tag authority in source verification --- ...workflow-source-yaml-tag-contract.test.mjs | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 packages/commercial-readiness/src/workflow-source-yaml-tag-contract.test.mjs diff --git a/packages/commercial-readiness/src/workflow-source-yaml-tag-contract.test.mjs b/packages/commercial-readiness/src/workflow-source-yaml-tag-contract.test.mjs new file mode 100644 index 000000000..bf80f5b68 --- /dev/null +++ b/packages/commercial-readiness/src/workflow-source-yaml-tag-contract.test.mjs @@ -0,0 +1,240 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; +import test from 'node:test'; + +const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); +const TAG_PROPERTY = /^!(?:!|<[^>]+>|[^\s]+)?(?:\s|$)/u; +const PLAIN_MAPPING = /^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/u; +const BLOCK_SCALAR = /^[|>](?:[1-9][+-]?|[+-][1-9]?)?(?:\s+#.*)?$/u; + +/** Extracts one direct job only from the workflow's top-level jobs mapping. */ +function namedJob(workflow, jobName) { + const lines = workflow.split('\n'); + const jobsIndexes = []; + for (let index = 0; index < lines.length; index += 1) { + if (lines[index] === 'jobs:') { + jobsIndexes.push(index); + } + } + assert.equal( + jobsIndexes.length, + 1, + 'workflow must contain exactly one jobs mapping', + ); + + const jobsStart = jobsIndexes[0]; + let jobsEnd = lines.length; + for (let index = jobsStart + 1; index < lines.length; index += 1) { + if (/^[^\s#]/u.test(lines[index])) { + jobsEnd = index; + break; + } + } + + const directJobPattern = + /^ (?:([A-Za-z_][A-Za-z0-9_-]*)|"([A-Za-z_][A-Za-z0-9_-]*)"|'([A-Za-z_][A-Za-z0-9_-]*)'):\s*(?:#.*)?$/u; + const jobs = []; + for (let index = jobsStart + 1; index < jobsEnd; index += 1) { + const match = directJobPattern.exec(lines[index]); + if (match) { + jobs.push({ index, name: match[1] ?? match[2] ?? match[3] }); + } + } + + const matches = jobs.filter(({ name }) => name === jobName); + assert.equal(matches.length, 1, `expected exactly one workflow job ${jobName}`); + const start = matches[0].index; + const position = jobs.findIndex(({ index }) => index === start); + const end = position + 1 < jobs.length ? jobs[position + 1].index : jobsEnd; + return lines.slice(start, end); +} + +/** Bounds every direct sequence item in one job's direct steps mapping. */ +function directSteps(jobLines) { + const jobIndent = /^\s*/u.exec(jobLines[0])?.[0].length ?? 0; + const keyIndent = jobIndent + 2; + const stepIndent = keyIndent + 2; + const stepsLine = `${' '.repeat(keyIndent)}steps:`; + const indexes = []; + for (let index = 1; index < jobLines.length; index += 1) { + if (jobLines[index] === stepsLine) { + indexes.push(index); + } + } + assert.equal( + indexes.length, + 1, + 'job must contain exactly one direct steps mapping', + ); + + const sectionLines = []; + const sequenceMarker = `${' '.repeat(stepIndent)}-`; + for (let index = indexes[0] + 1; index < jobLines.length; index += 1) { + const line = jobLines[index]; + if (line.trim().length === 0 || line.trimStart().startsWith('#')) { + sectionLines.push(line); + continue; + } + const indent = /^\s*/u.exec(line)?.[0].length ?? 0; + if (indent <= keyIndent) { + break; + } + sectionLines.push(line); + } + + const starts = []; + for (let index = 0; index < sectionLines.length; index += 1) { + const line = sectionLines[index]; + const indent = /^\s*/u.exec(line)?.[0].length ?? 0; + if ( + indent === stepIndent && + (line === sequenceMarker || line.startsWith(`${sequenceMarker} `)) + ) { + starts.push(index); + } + } + + const blocks = starts.map((start, position) => { + const end = + position + 1 < starts.length ? starts[position + 1] : sectionLines.length; + return sectionLines.slice(start, end); + }); + return { blocks, stepIndent }; +} + +/** Rejects explicit YAML tag properties from structural step authority. */ +function assertNoStructuralYamlTags(stepLines, stepIndent, jobName) { + const directIndent = stepIndent + 2; + let blockScalarIndent; + + for (let index = 0; index < stepLines.length; index += 1) { + const line = stepLines[index]; + if (line.trim().length === 0 || line.trimStart().startsWith('#')) { + continue; + } + + const indent = /^\s*/u.exec(line)?.[0].length ?? 0; + if (blockScalarIndent !== undefined) { + if (indent > blockScalarIndent) { + continue; + } + blockScalarIndent = undefined; + } + + const structural = + index === 0 + ? line.slice(stepIndent) + : indent >= directIndent + ? line.slice(indent) + : undefined; + if (structural === undefined) { + continue; + } + + const mappingCandidate = structural.replace(/^-\s+/u, ''); + assert.doesNotMatch( + mappingCandidate, + TAG_PROPERTY, + `${jobName} source-verification structural mapping keys must not use YAML tag authority`, + ); + + const mapping = PLAIN_MAPPING.exec(mappingCandidate); + if (!mapping) { + continue; + } + const value = mapping[2].trimStart(); + assert.doesNotMatch( + value, + TAG_PROPERTY, + `${jobName} source-verification structural scalar values must not use YAML tag authority`, + ); + if (BLOCK_SCALAR.test(value)) { + blockScalarIndent = indent; + } + } +} + +function assertJobHasNoStructuralYamlTags(workflow, jobName) { + const { blocks, stepIndent } = directSteps(namedJob(workflow, jobName)); + assert.ok(blocks.length > 0, `${jobName} must contain direct workflow steps`); + for (const block of blocks) { + assertNoStructuralYamlTags(block, stepIndent, jobName); + } +} + +const REQUIRED_JOBS = [ + [ + 'ci.yml', + ['compose_runtime', 'today-concurrency', 'validate', 'browser-acceptance'], + ], + ['appguardrail.yml', ['scan']], + ['commercial-readiness.yml', ['audit']], +]; + +test('source-verification jobs do not use explicit YAML tag authority', () => { + for (const [workflowName, jobNames] of REQUIRED_JOBS) { + const workflow = readFileSync( + join(REPOSITORY_ROOT, '.github/workflows', workflowName), + 'utf8', + ); + for (const jobName of jobNames) { + assertJobHasNoStructuralYamlTags(workflow, jobName); + } + } +}); + +test('explicit YAML tag cannot hide checkout authority from source verification', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: reviewed-contributor-sha', + ' - name: Hidden tagged checkout', + ' uses: !!str actions/checkout@reviewed-sha', + ' with:', + ' persist-credentials: false', + ' ref: refs/heads/main', + ].join('\n'); + + assert.throws( + () => assertJobHasNoStructuralYamlTags(hostile, 'validate'), + /structural scalar values must not use YAML tag authority/u, + ); +}); + +test('explicit YAML tag cannot compose a structural mapping key', () => { + const hostile = [ + 'jobs:', + ' validate:', + ' steps:', + ' - name: Hidden tagged key', + ' !!str uses: actions/checkout@reviewed-sha', + ].join('\n'); + + assert.throws( + () => assertJobHasNoStructuralYamlTags(hostile, 'validate'), + /structural mapping keys must not use YAML tag authority/u, + ); +}); + +test('tag-looking text inside a block scalar is not workflow authority', () => { + for (const header of ['|2', '>-2', '|+2']) { + const valid = [ + 'jobs:', + ' validate:', + ' steps:', + ' - name: Safe shell', + ` run: ${header}`, + ' uses: !!str actions/checkout@not-workflow-authority', + ].join('\n'); + + assert.doesNotThrow(() => + assertJobHasNoStructuralYamlTags(valid, 'validate'), + ); + } +}); From c7053e37f356070ae13d04f8b2eca0c8a1074459 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:13:11 +0900 Subject: [PATCH 114/180] test(ci): verify YAML tag authority repair --- .github/workflows/verify-tagged-uses.yml | 39 ++---------------------- 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/.github/workflows/verify-tagged-uses.yml b/.github/workflows/verify-tagged-uses.yml index 69b10326a..536f741d3 100644 --- a/.github/workflows/verify-tagged-uses.yml +++ b/.github/workflows/verify-tagged-uses.yml @@ -21,41 +21,8 @@ jobs: GIT_CONFIG_VALUE_0: main with: persist-credentials: false - - name: Reproduce source-verification YAML-tag bypass + - name: Verify permanent YAML-tag source contract run: | set -euo pipefail - node --input-type=module <<'NODE' - import { readFileSync, rmSync, writeFileSync } from 'node:fs'; - import { spawnSync } from 'node:child_process'; - const sourcePath = 'packages/commercial-readiness/src/workflow-source-yaml-alias-contract.test.mjs'; - const probePath = 'packages/commercial-readiness/src/workflow-source-yaml-tag-red.test.mjs'; - const source = readFileSync(sourcePath, 'utf8'); - const hostileTest = [ - '', - "test('explicit YAML tags cannot hide checkout authority from source verification', () => {", - ' const hostile = [', - " 'jobs:',", - " ' validate:',", - " ' steps:',", - " ' - uses: actions/checkout@reviewed-sha',", - " ' with:',", - " ' persist-credentials: false',", - " ' ref: reviewed-contributor-sha',", - " ' - name: Hidden tagged checkout',", - " ' uses: !!str actions/checkout@reviewed-sha',", - " ' with:',", - " ' persist-credentials: false',", - " ' ref: refs/heads/main',", - " ].join('\\n');", - ' assert.throws(', - " () => assertDirectStepAuthority(hostile, 'validate'),", - ' /YAML tag authority/u,', - ' );', - '});', - '', - ].join('\n'); - writeFileSync(probePath, source + hostileTest, 'utf8'); - const result = spawnSync(process.execPath, ['--test', probePath], { stdio: 'inherit' }); - rmSync(probePath, { force: true }); - process.exit(result.status ?? 1); - NODE + node --test packages/commercial-readiness/src/workflow-source-yaml-tag-contract.test.mjs + node --check packages/commercial-readiness/src/workflow-source-yaml-tag-contract.test.mjs From 882f344c2469b234ed43c78de5f806cd030ca958 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:13:41 +0900 Subject: [PATCH 115/180] chore(ci): retire YAML tag verifier --- .github/workflows/verify-tagged-uses.yml | 28 ------------------------ 1 file changed, 28 deletions(-) delete mode 100644 .github/workflows/verify-tagged-uses.yml diff --git a/.github/workflows/verify-tagged-uses.yml b/.github/workflows/verify-tagged-uses.yml deleted file mode 100644 index 536f741d3..000000000 --- a/.github/workflows/verify-tagged-uses.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Verify tagged uses semantics - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 5 - steps: - - name: Execute checkout through an explicit YAML string tag - uses: !!str actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - - name: Verify permanent YAML-tag source contract - run: | - set -euo pipefail - node --test packages/commercial-readiness/src/workflow-source-yaml-tag-contract.test.mjs - node --check packages/commercial-readiness/src/workflow-source-yaml-tag-contract.test.mjs From 324dc17491e802f77a6d137ab3abff0f3cf166b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:14:55 +0900 Subject: [PATCH 116/180] test(ci): run broad item 32 verification --- .github/workflows/verify-item32-broad.yml | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/verify-item32-broad.yml diff --git a/.github/workflows/verify-item32-broad.yml b/.github/workflows/verify-item32-broad.yml new file mode 100644 index 000000000..51e42c08c --- /dev/null +++ b/.github/workflows/verify-item32-broad.yml @@ -0,0 +1,29 @@ +name: Verify item 32 broad commercial readiness + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact branch head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + - name: Run commercial-readiness contracts + run: node --test packages/commercial-readiness/src/*.test.mjs + - name: Verify changed-source syntax and whitespace + run: | + node --check packages/commercial-readiness/src/workflow-source-yaml-tag-contract.test.mjs + git diff --check 7973277a4c97db6eaa071ff2726187f6b33009f5...HEAD From 729052468436e56a8af921389a507acfcf6cb9be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:15:27 +0900 Subject: [PATCH 117/180] test(ci): repair broad verifier base materialization --- .github/workflows/verify-item32-broad.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/verify-item32-broad.yml b/.github/workflows/verify-item32-broad.yml index 51e42c08c..0d8881ee7 100644 --- a/.github/workflows/verify-item32-broad.yml +++ b/.github/workflows/verify-item32-broad.yml @@ -25,5 +25,8 @@ jobs: run: node --test packages/commercial-readiness/src/*.test.mjs - name: Verify changed-source syntax and whitespace run: | + set -euo pipefail node --check packages/commercial-readiness/src/workflow-source-yaml-tag-contract.test.mjs - git diff --check 7973277a4c97db6eaa071ff2726187f6b33009f5...HEAD + git fetch --no-tags --depth=1 origin 7973277a4c97db6eaa071ff2726187f6b33009f5 + test "$(git rev-parse FETCH_HEAD)" = "7973277a4c97db6eaa071ff2726187f6b33009f5" + git diff --check 7973277a4c97db6eaa071ff2726187f6b33009f5 HEAD From e7738877dade39432dc579a88f5d594a2454e36e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:15:57 +0900 Subject: [PATCH 118/180] chore(ci): retire item 32 broad verifier --- .github/workflows/verify-item32-broad.yml | 32 ----------------------- 1 file changed, 32 deletions(-) delete mode 100644 .github/workflows/verify-item32-broad.yml diff --git a/.github/workflows/verify-item32-broad.yml b/.github/workflows/verify-item32-broad.yml deleted file mode 100644 index 0d8881ee7..000000000 --- a/.github/workflows/verify-item32-broad.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Verify item 32 broad commercial readiness - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout exact branch head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - - name: Run commercial-readiness contracts - run: node --test packages/commercial-readiness/src/*.test.mjs - - name: Verify changed-source syntax and whitespace - run: | - set -euo pipefail - node --check packages/commercial-readiness/src/workflow-source-yaml-tag-contract.test.mjs - git fetch --no-tags --depth=1 origin 7973277a4c97db6eaa071ff2726187f6b33009f5 - test "$(git rev-parse FETCH_HEAD)" = "7973277a4c97db6eaa071ff2726187f6b33009f5" - git diff --check 7973277a4c97db6eaa071ff2726187f6b33009f5 HEAD From 19608e93028ef2ad3f7f448958878d57eb59694b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:20:10 +0900 Subject: [PATCH 119/180] test(ci): expose final-step boundary contamination --- ...w-external-checkout-path-contract.test.mjs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs index 809ee5bc3..7bb4d832d 100644 --- a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs @@ -242,6 +242,28 @@ test('external checkout without a path is rejected', () => { assert.throws(() => assertExternalCheckoutIsolation(hostile, 'scan'), /must use one direct isolated path/u); }); +test('post-steps mappings cannot contaminate the final checkout step boundary', () => { + const hostile = [ + 'jobs:', + ' scan:', + ' steps:', + ' - uses: actions/checkout@reviewed-sha', + ' with:', + ' repository: ContextualWisdomLab/appguardrail', + ' ref: reviewed-appguardrail-sha', + ' persist-credentials: false', + ' strategy:', + ' matrix:', + ' with:', + ' path: _appguardrail', + ].join('\n'); + assert.throws( + () => assertExternalCheckoutIsolation(hostile, 'scan'), + /must use one direct isolated path/u, + 'post-steps job mappings must not become authority for the final checkout step', + ); +}); + test('case-variant checkout action cannot bypass external checkout isolation', () => { const hostile = [ 'jobs:', From 3522695c797994986db62700cb4b4302bf36526c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:20:18 +0900 Subject: [PATCH 120/180] test(ci): host item 33 boundary RED --- .github/workflows/verify-item33-boundary.yml | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/verify-item33-boundary.yml diff --git a/.github/workflows/verify-item33-boundary.yml b/.github/workflows/verify-item33-boundary.yml new file mode 100644 index 000000000..f002409a6 --- /dev/null +++ b/.github/workflows/verify-item33-boundary.yml @@ -0,0 +1,28 @@ +name: Verify item 33 checkout-step boundary + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout exact branch head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + - name: Reproduce final-step boundary contamination + run: | + set -euo pipefail + node --test packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs + node --check packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs From 3d219a1ebd3d0864218d111d352197ca5e113756 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:21:03 +0900 Subject: [PATCH 121/180] fix(ci): bound final checkout step to steps section --- .../src/workflow-external-checkout-path-contract.test.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs index 7bb4d832d..d6d83942b 100644 --- a/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs +++ b/packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs @@ -89,6 +89,7 @@ function stepBlocks(jobLines) { assert.equal(stepsIndexes.length, 1, 'job must contain exactly one direct steps mapping'); const starts = []; + let stepsEnd = jobLines.length; for (let index = stepsIndexes[0] + 1; index < jobLines.length; index += 1) { const line = jobLines[index]; if (line.trim().length === 0 || line.trimStart().startsWith('#')) { @@ -96,6 +97,7 @@ function stepBlocks(jobLines) { } const indent = /^\s*/u.exec(line)?.[0].length ?? 0; if (indent <= keyIndent) { + stepsEnd = index; break; } if (indent === stepIndent && line.startsWith(`${' '.repeat(stepIndent)}- `)) { @@ -104,7 +106,7 @@ function stepBlocks(jobLines) { } return starts.map((start, position) => { - const end = position + 1 < starts.length ? starts[position + 1] : jobLines.length; + const end = position + 1 < starts.length ? starts[position + 1] : stepsEnd; return { lines: jobLines.slice(start, end), stepIndent }; }); } From 6d166bceb18fba218b02a80ae9b6f333eaee1246 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:21:25 +0900 Subject: [PATCH 122/180] test(ci): broaden item 33 exact-head verification --- .github/workflows/verify-item33-boundary.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/verify-item33-boundary.yml b/.github/workflows/verify-item33-boundary.yml index f002409a6..4d27d81c9 100644 --- a/.github/workflows/verify-item33-boundary.yml +++ b/.github/workflows/verify-item33-boundary.yml @@ -11,7 +11,7 @@ permissions: jobs: verify: runs-on: ubuntu-24.04 - timeout-minutes: 5 + timeout-minutes: 10 steps: - name: Checkout exact branch head uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -21,8 +21,12 @@ jobs: GIT_CONFIG_VALUE_0: main with: persist-credentials: false - - name: Reproduce final-step boundary contamination + - name: Run commercial-readiness contracts + run: node --test packages/commercial-readiness/src/*.test.mjs + - name: Verify changed-source syntax and whitespace run: | set -euo pipefail - node --test packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs node --check packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs + git fetch --no-tags --depth=1 origin 7973277a4c97db6eaa071ff2726187f6b33009f5 + test "$(git rev-parse FETCH_HEAD)" = "7973277a4c97db6eaa071ff2726187f6b33009f5" + git diff --check 7973277a4c97db6eaa071ff2726187f6b33009f5 HEAD From cc89f564d3df299a466460af3dc0e03d8e2f2255 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:21:55 +0900 Subject: [PATCH 123/180] chore(ci): retire item 33 boundary verifier --- .github/workflows/verify-item33-boundary.yml | 32 -------------------- 1 file changed, 32 deletions(-) delete mode 100644 .github/workflows/verify-item33-boundary.yml diff --git a/.github/workflows/verify-item33-boundary.yml b/.github/workflows/verify-item33-boundary.yml deleted file mode 100644 index 4d27d81c9..000000000 --- a/.github/workflows/verify-item33-boundary.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Verify item 33 checkout-step boundary - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout exact branch head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - - name: Run commercial-readiness contracts - run: node --test packages/commercial-readiness/src/*.test.mjs - - name: Verify changed-source syntax and whitespace - run: | - set -euo pipefail - node --check packages/commercial-readiness/src/workflow-external-checkout-path-contract.test.mjs - git fetch --no-tags --depth=1 origin 7973277a4c97db6eaa071ff2726187f6b33009f5 - test "$(git rev-parse FETCH_HEAD)" = "7973277a4c97db6eaa071ff2726187f6b33009f5" - git diff --check 7973277a4c97db6eaa071ff2726187f6b33009f5 HEAD From 7a45a5c3878b0f9ef5810e73eaf351fb74241205 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 04:09:16 +0900 Subject: [PATCH 124/180] test(ci): bound source verification at sibling jobs --- .../src/source-verification-workflow.test.mjs | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index 25e0f3086..bf7e740f8 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -21,6 +21,13 @@ function readWorkflow(name) { return readFileSync(join(REPOSITORY_ROOT, '.github/workflows', name), 'utf8'); } +/** Recognizes direct top-level job entries, including quoted IDs and inline comments. */ +function isDirectJobEntry(line) { + return /^ (?:[A-Za-z_][A-Za-z0-9_-]*|"[A-Za-z_][A-Za-z0-9_-]*"|'[A-Za-z_][A-Za-z0-9_-]*'):\s*(?:#.*)?$/u.test( + line, + ); +} + /** Extracts one top-level workflow job without requiring a YAML parser. */ function jobBlock(workflow, jobName) { const lines = workflow.split('\n'); @@ -28,7 +35,7 @@ function jobBlock(workflow, jobName) { assert.notEqual(start, -1, `missing job ${jobName}`); let end = lines.length; for (let index = start + 1; index < lines.length; index += 1) { - if (/^ [A-Za-z0-9_-]+:\s*$/u.test(lines[index])) { + if (isDirectJobEntry(lines[index])) { end = index; break; } @@ -147,6 +154,29 @@ function assertUniqueActionUseInStep(job, actionName, stepName) { ); } +test('job extraction does not borrow authority from quoted or commented sibling jobs', () => { + for (const sibling of [ + ' "decoy":', + " 'decoy': # sibling", + ' decoy: # sibling', + ]) { + const workflow = [ + 'jobs:', + ' scan:', + sibling, + ' steps:', + ' - name: Upload AppGuardrail SARIF to code scanning', + ' uses: github/codeql-action/upload-sarif@fake-sha', + ].join('\n'); + + assert.equal( + jobBlock(workflow, 'scan'), + ' scan:', + 'a bounded job must stop before every direct sibling job spelling', + ); + } +}); + test('step extraction does not borrow evidence from unnamed sibling steps', () => { const job = [ ' scan:', @@ -466,4 +496,4 @@ test('merge-tree compatibility provisions the PostgreSQL contract required by th `merge_compatibility is missing ${variableName}`, ); } -}); +}); \ No newline at end of file From 40f5b86abdb01fb734cc44788116dc9af5d7fa6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 04:09:43 +0900 Subject: [PATCH 125/180] ci: verify item 34 source job boundary --- .../workflows/verify-item34-job-boundary.yml | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/verify-item34-job-boundary.yml diff --git a/.github/workflows/verify-item34-job-boundary.yml b/.github/workflows/verify-item34-job-boundary.yml new file mode 100644 index 000000000..4deaf857e --- /dev/null +++ b/.github/workflows/verify-item34-job-boundary.yml @@ -0,0 +1,34 @@ +name: Verify item 34 source job boundary + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Pin Git default branch naming + run: git config --global init.defaultBranch main + + - name: Checkout exact pushed head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + fetch-depth: 1 + + - name: Verify exact source-verification repair + shell: bash + run: | + set -euo pipefail + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" + node --version + cd packages/commercial-readiness + node --test src/*.test.mjs + node --check src/source-verification-workflow.test.mjs + git diff --check From 9524892d572bacbdc503928d35aa029670bf349b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 04:10:15 +0900 Subject: [PATCH 126/180] ci: retire item 34 verifier --- .../workflows/verify-item34-job-boundary.yml | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 .github/workflows/verify-item34-job-boundary.yml diff --git a/.github/workflows/verify-item34-job-boundary.yml b/.github/workflows/verify-item34-job-boundary.yml deleted file mode 100644 index 4deaf857e..000000000 --- a/.github/workflows/verify-item34-job-boundary.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Verify item 34 source job boundary - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Pin Git default branch naming - run: git config --global init.defaultBranch main - - - name: Checkout exact pushed head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - persist-credentials: false - fetch-depth: 1 - - - name: Verify exact source-verification repair - shell: bash - run: | - set -euo pipefail - test "$(git rev-parse HEAD)" = "$GITHUB_SHA" - node --version - cd packages/commercial-readiness - node --test src/*.test.mjs - node --check src/source-verification-workflow.test.mjs - git diff --check From be476080e6a054f5614d84120106c9f6061167b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:07:44 +0900 Subject: [PATCH 127/180] test(ci): reproduce SARIF sibling job authority leak --- .../sarif-source-binding-contract.test.mjs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs index dbb0db93f..49662e313 100644 --- a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -188,6 +188,29 @@ test('SARIF source binding rejects upload authority moved to another workflow jo ); }); +for (const siblingJobLine of [' "decoy":', " 'decoy': # sibling"]) { + test(`SARIF source binding rejects authority borrowed across sibling boundary ${siblingJobLine}`, () => { + const hostileWorkflow = [ + 'jobs:', + ' scan:', + ' runs-on: ubuntu-latest', + siblingJobLine, + ' steps:', + ` - name: ${UPLOAD_STEP_NAME}`, + ' uses: github/codeql-action/upload-sarif@reviewed-sha', + ' with:', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + ].join('\n'); + + assert.throws( + () => namedStep(hostileWorkflow, UPLOAD_STEP_NAME), + /scan job must contain exactly one direct steps mapping/u, + 'quoted or commented sibling jobs must terminate scan authority before their steps', + ); + }); +} + test('SARIF source binding rejects contributor markers moved outside direct with entries', () => { const hostileUploadStep = [ ' - name: Upload AppGuardrail SARIF to code scanning', @@ -225,7 +248,6 @@ test('SARIF source binding rejects duplicate direct ref or sha inputs', () => { ); }); - test('SARIF source binding rejects scan-looking mappings outside top-level jobs', () => { const hostileWorkflow = [ 'jobs:', From a22218208614d40a49f2a2c1d27c4cad419a6311 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:08:24 +0900 Subject: [PATCH 128/180] fix(ci): bound SARIF authority to direct scan job --- .../sarif-source-binding-contract.test.mjs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs index 49662e313..a2a4cf947 100644 --- a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -10,6 +10,14 @@ const SARIF_SOURCE_REF = const SARIF_SOURCE_SHA = 'sha: ${{ github.event.pull_request.head.sha || github.sha }}'; const UPLOAD_STEP_NAME = 'Upload AppGuardrail SARIF to code scanning'; +const DIRECT_JOB_ENTRY = + /^ (?:([A-Za-z_][A-Za-z0-9_-]*)|"([A-Za-z_][A-Za-z0-9_-]*)"|'([A-Za-z_][A-Za-z0-9_-]*)'):\s*(?:#.*)?$/u; + +/** Returns the canonical job ID for one direct job entry, or null for non-job lines. */ +function directJobName(line) { + const match = DIRECT_JOB_ENTRY.exec(line); + return match ? (match[1] ?? match[2] ?? match[3]) : null; +} /** Extracts one uniquely named direct workflow job from the top-level jobs mapping. */ function namedJob(workflow, jobName) { @@ -31,11 +39,9 @@ function namedJob(workflow, jobName) { } } - const jobIndent = ' '; - const expected = `${jobIndent}${jobName}:`; const matches = []; for (let index = jobsStart + 1; index < jobsEnd; index += 1) { - if (lines[index] === expected) { + if (directJobName(lines[index]) === jobName) { matches.push(index); } } @@ -44,7 +50,7 @@ function namedJob(workflow, jobName) { const start = matches[0]; let end = jobsEnd; for (let index = start + 1; index < jobsEnd; index += 1) { - if (/^ [A-Za-z0-9_-]+:\s*$/u.test(lines[index])) { + if (directJobName(lines[index]) !== null) { end = index; break; } @@ -188,7 +194,11 @@ test('SARIF source binding rejects upload authority moved to another workflow jo ); }); -for (const siblingJobLine of [' "decoy":', " 'decoy': # sibling"]) { +for (const siblingJobLine of [ + ' "decoy":', + " 'decoy': # sibling", + ' decoy: # sibling', +]) { test(`SARIF source binding rejects authority borrowed across sibling boundary ${siblingJobLine}`, () => { const hostileWorkflow = [ 'jobs:', From 416b24059565854064c66244979f0791846b1218 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 06:04:09 +0900 Subject: [PATCH 129/180] test(ci): verify PR279 item35 exact contracts --- .github/workflows/verify-pr279-item35.yml | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/verify-pr279-item35.yml diff --git a/.github/workflows/verify-pr279-item35.yml b/.github/workflows/verify-pr279-item35.yml new file mode 100644 index 000000000..5be6373de --- /dev/null +++ b/.github/workflows/verify-pr279-item35.yml @@ -0,0 +1,36 @@ +name: Verify PR279 Item35 + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Checkout exact verifier head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + fetch-depth: 1 + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: 22.16.0 + + - name: Verify Commercial Readiness contracts + run: node --test packages/commercial-readiness/src/*.test.mjs + + - name: Verify repaired SARIF source-binding syntax + run: node --check packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs From 72cb17202eb8176a57576413a471b96486227aae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 06:06:17 +0900 Subject: [PATCH 130/180] test(ci): retire PR279 item35 verifier --- .github/workflows/verify-pr279-item35.yml | 36 ----------------------- 1 file changed, 36 deletions(-) delete mode 100644 .github/workflows/verify-pr279-item35.yml diff --git a/.github/workflows/verify-pr279-item35.yml b/.github/workflows/verify-pr279-item35.yml deleted file mode 100644 index 5be6373de..000000000 --- a/.github/workflows/verify-pr279-item35.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Verify PR279 Item35 - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - name: Checkout exact verifier head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - fetch-depth: 1 - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: 22.16.0 - - - name: Verify Commercial Readiness contracts - run: node --test packages/commercial-readiness/src/*.test.mjs - - - name: Verify repaired SARIF source-binding syntax - run: node --check packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs From 56fbca11f8f93874c4362015c3ca417f86a97bdc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 07:04:22 +0900 Subject: [PATCH 131/180] test(ci): add bounded verifier for PR 279 item 36 --- .github/workflows/verify-pr279-item36.yml | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/verify-pr279-item36.yml diff --git a/.github/workflows/verify-pr279-item36.yml b/.github/workflows/verify-pr279-item36.yml new file mode 100644 index 000000000..5a00140f2 --- /dev/null +++ b/.github/workflows/verify-pr279-item36.yml @@ -0,0 +1,35 @@ +name: Verify PR 279 Item 36 + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + steps: + - name: Checkout exact source head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: 22.16.0 + + - name: Verify source-verification contract + run: node --test packages/commercial-readiness/src/source-verification-workflow.test.mjs + + - name: Verify syntax + run: node --check packages/commercial-readiness/src/source-verification-workflow.test.mjs From 07544942097e40bfea72580e0b7d1511159221a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 07:05:03 +0900 Subject: [PATCH 132/180] test(ci): reproduce case-aliased SARIF action bypass --- .../src/source-verification-workflow.test.mjs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index bf7e740f8..6d617efae 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -289,6 +289,32 @@ test('SARIF upload authority rejects unnamed duplicate action uses', () => { ); }); +test('SARIF upload authority rejects case-aliased duplicate action repository identity', () => { + const job = [ + ' scan:', + ' steps:', + ' - name: Upload AppGuardrail SARIF to code scanning', + ' uses: github/codeql-action/upload-sarif@reviewed-sha', + ' with:', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + ' - name: Upload case-aliased SARIF', + ' uses: GitHub/CodeQL-Action/upload-sarif@unreviewed-sha', + ' with:', + ' sarif_file: alternate.sarif', + ].join('\n'); + + assert.throws( + () => + assertUniqueActionUseInStep( + job, + 'github/codeql-action/upload-sarif', + 'Upload AppGuardrail SARIF to code scanning', + ), + /expected exactly one github\/codeql-action\/upload-sarif use, found 2/u, + ); +}); + test('workflow-step authority ignores name and uses text inside run blocks', () => { const job = [ ' scan:', From 7260b616cfcffe718a9cec88d588dc757adefc19 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 07:06:07 +0900 Subject: [PATCH 133/180] fix(ci): bind SARIF action repository identity case-insensitively --- .../src/source-verification-workflow.test.mjs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index 6d617efae..576d14530 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -99,6 +99,26 @@ function assertStepPrecedes(job, earlierStepName, laterStepName) { ); } +/** Matches GitHub Action repository identity case-insensitively while preserving subpath spelling. */ +function matchesActionIdentity(uses, actionName) { + const refSeparator = uses.indexOf('@'); + if (refSeparator <= 0) { + return false; + } + + const candidate = uses.slice(0, refSeparator).split('/'); + const reviewed = actionName.split('/'); + if (candidate.length !== reviewed.length || candidate.length < 2) { + return false; + } + + return ( + candidate[0].toLowerCase() === reviewed[0].toLowerCase() && + candidate[1].toLowerCase() === reviewed[1].toLowerCase() && + candidate.slice(2).every((segment, index) => segment === reviewed[index + 2]) + ); +} + /** Finds executable action uses only from real step entries and direct step keys. */ function actionUses(job, actionName) { const lines = job.split('\n'); @@ -113,7 +133,7 @@ function actionUses(job, actionName) { } const inline = usesEntry.exec(lines[index].slice(`${stepIndent}- `.length)); - if (inline?.[2].startsWith(`${actionName}@`)) { + if (inline?.[2] && matchesActionIdentity(inline[2], actionName)) { uses.push({ line: lines[index], stepStart: index }); } @@ -130,7 +150,7 @@ function actionUses(job, actionName) { continue; } const match = usesEntry.exec(direct); - if (match?.[2].startsWith(`${actionName}@`)) { + if (match?.[2] && matchesActionIdentity(match[2], actionName)) { uses.push({ line: lines[child], stepStart: index }); } } From 91ef583d92ebd85e82f8a8c6041cb017e7ab1676 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 07:06:24 +0900 Subject: [PATCH 134/180] chore(ci): remove completed PR 279 item 36 verifier --- .github/workflows/verify-pr279-item36.yml | 35 ----------------------- 1 file changed, 35 deletions(-) delete mode 100644 .github/workflows/verify-pr279-item36.yml diff --git a/.github/workflows/verify-pr279-item36.yml b/.github/workflows/verify-pr279-item36.yml deleted file mode 100644 index 5a00140f2..000000000 --- a/.github/workflows/verify-pr279-item36.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Verify PR 279 Item 36 - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - steps: - - name: Checkout exact source head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - env: - GIT_CONFIG_COUNT: 1 - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - ref: ${{ github.sha }} - fetch-depth: 1 - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: 22.16.0 - - - name: Verify source-verification contract - run: node --test packages/commercial-readiness/src/source-verification-workflow.test.mjs - - - name: Verify syntax - run: node --check packages/commercial-readiness/src/source-verification-workflow.test.mjs From 4b6ed63449e608a2e2cd873b501a841e7f41aa1a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 08:03:32 +0900 Subject: [PATCH 135/180] test(ci): reproduce SARIF upload action identity bypass --- .../src/sarif-source-binding-contract.test.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs index a2a4cf947..ed0a3bbd3 100644 --- a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -281,3 +281,20 @@ test('SARIF source binding rejects scan-looking mappings outside top-level jobs' 'a scan-shaped mapping outside top-level jobs must not satisfy workflow job authority', ); }); + +test('SARIF source binding rejects a correctly named step backed by the wrong action', () => { + const hostileUploadStep = [ + ` - name: ${UPLOAD_STEP_NAME}`, + ' uses: attacker/example@reviewed-sha', + ' with:', + ' sarif_file: appguardrail.sarif', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + ].join('\n'); + + assert.throws( + () => assertSarifSourceBinding(hostileUploadStep), + /must use github\/codeql-action\/upload-sarif/u, + 'step name and contributor bindings must not substitute for reviewed upload action identity', + ); +}); From 453b8871857b4ea5c24cd1145527f3489e7873e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 08:03:40 +0900 Subject: [PATCH 136/180] test(ci): verify SARIF upload action identity regression --- .github/workflows/verify-pr279-item37.yml | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/verify-pr279-item37.yml diff --git a/.github/workflows/verify-pr279-item37.yml b/.github/workflows/verify-pr279-item37.yml new file mode 100644 index 000000000..9c21dc7e4 --- /dev/null +++ b/.github/workflows/verify-pr279-item37.yml @@ -0,0 +1,34 @@ +name: Verify PR 279 Item 37 + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + steps: + - name: Checkout exact source head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: '22.16.0' + + - name: Verify SARIF source binding contract + run: node --test packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs + + - name: Verify syntax + run: node --check packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs From c750a0b8b4021cc7b7f7e2b2c48b9db6233b9d29 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 08:04:42 +0900 Subject: [PATCH 137/180] fix(ci): bind SARIF source evidence to upload action identity --- .../sarif-source-binding-contract.test.mjs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs index ed0a3bbd3..afa1aa562 100644 --- a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -154,8 +154,35 @@ function assertUniqueDirectInput(mapping, key, expectedEntry) { ); } +/** Requires the named upload step to invoke the reviewed CodeQL SARIF action identity. */ +function assertSarifUploadAction(uploadStep) { + const lines = uploadStep.split('\n'); + const stepMatch = /^(\s*)-\s/u.exec(lines[0]); + assert.ok(stepMatch, 'workflow step indentation is invalid'); + const directPrefix = `${stepMatch[1]} uses:`; + const directUses = lines.filter((line) => line.startsWith(directPrefix)); + assert.equal(directUses.length, 1, 'expected exactly one direct uses authority'); + + const scalar = directUses[0] + .slice(directPrefix.length) + .trim() + .replace(/\s+#.*$/u, ''); + const atIndex = scalar.lastIndexOf('@'); + assert.ok(atIndex > 0, 'SARIF upload action must carry an explicit ref'); + + const segments = scalar.slice(0, atIndex).split('/'); + assert.ok( + segments.length === 3 && + segments[0].toLowerCase() === 'github' && + segments[1].toLowerCase() === 'codeql-action' && + segments[2] === 'upload-sarif', + 'SARIF upload must use github/codeql-action/upload-sarif', + ); +} + /** Requires contributor-head ref and sha to be unique direct upload-sarif with inputs. */ function assertSarifSourceBinding(uploadStep) { + assertSarifUploadAction(uploadStep); const withMapping = directMapping(uploadStep, 'with'); assertUniqueDirectInput(withMapping, 'ref', SARIF_SOURCE_REF); assertUniqueDirectInput(withMapping, 'sha', SARIF_SOURCE_SHA); @@ -298,3 +325,16 @@ test('SARIF source binding rejects a correctly named step backed by the wrong ac 'step name and contributor bindings must not substitute for reviewed upload action identity', ); }); + +test('SARIF source binding resolves GitHub action repository identity case-insensitively', () => { + const uploadStep = [ + ` - name: ${UPLOAD_STEP_NAME}`, + ' uses: GitHub/CodeQL-Action/upload-sarif@reviewed-sha', + ' with:', + ' sarif_file: appguardrail.sarif', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + ].join('\n'); + + assert.doesNotThrow(() => assertSarifSourceBinding(uploadStep)); +}); From 3e4defaf4e52fff86f75f1a843dd579f74418a2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 08:05:07 +0900 Subject: [PATCH 138/180] chore(ci): remove completed item 37 verifier --- .github/workflows/verify-pr279-item37.yml | 34 ----------------------- 1 file changed, 34 deletions(-) delete mode 100644 .github/workflows/verify-pr279-item37.yml diff --git a/.github/workflows/verify-pr279-item37.yml b/.github/workflows/verify-pr279-item37.yml deleted file mode 100644 index 9c21dc7e4..000000000 --- a/.github/workflows/verify-pr279-item37.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Verify PR 279 Item 37 - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - steps: - - name: Checkout exact source head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: '22.16.0' - - - name: Verify SARIF source binding contract - run: node --test packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs - - - name: Verify syntax - run: node --check packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs From 78b5fb32b876e4a4f96402bc7757e341a370e93d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 09:05:15 +0900 Subject: [PATCH 139/180] test(ci): bound SARIF upload lookup to direct steps --- .../sarif-source-binding-contract.test.mjs | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs index afa1aa562..3e9b75411 100644 --- a/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs +++ b/packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs @@ -77,10 +77,28 @@ function namedStep(workflow, stepName) { 'scan job must contain exactly one direct steps mapping', ); + const stepsStart = stepsIndexes[0]; + const directJobMemberIndent = `${jobMatch[1]} `; const stepIndent = `${jobMatch[1]} `; + let stepsEnd = lines.length; + for (let index = stepsStart + 1; index < lines.length; index += 1) { + const line = lines[index]; + const trimmed = line.trim(); + if (trimmed === '' || trimmed.startsWith('#')) { + continue; + } + if ( + line.startsWith(directJobMemberIndent) && + !line.startsWith(stepIndent) + ) { + stepsEnd = index; + break; + } + } + const expected = `${stepIndent}- name: ${stepName}`; const matches = []; - for (let index = stepsIndexes[0] + 1; index < lines.length; index += 1) { + for (let index = stepsStart + 1; index < stepsEnd; index += 1) { if (lines[index] === expected) { matches.push(index); } @@ -92,8 +110,8 @@ function namedStep(workflow, stepName) { ); const start = matches[0]; - let end = lines.length; - for (let index = start + 1; index < lines.length; index += 1) { + let end = stepsEnd; + for (let index = start + 1; index < stepsEnd; index += 1) { if (lines[index].startsWith(`${stepIndent}- `)) { end = index; break; @@ -221,6 +239,31 @@ test('SARIF source binding rejects upload authority moved to another workflow jo ); }); +test('SARIF source binding rejects upload-looking text after the direct steps sequence', () => { + const hostileWorkflow = [ + 'jobs:', + ' scan:', + ' steps:', + ' - name: Harmless scan step', + ' run: echo scan', + ' name: |', + ` - name: ${UPLOAD_STEP_NAME}`, + ' uses: github/codeql-action/upload-sarif@reviewed-sha', + ' with:', + ` ${SARIF_SOURCE_REF}`, + ` ${SARIF_SOURCE_SHA}`, + ].join('\n'); + + assert.throws( + () => { + const uploadStep = namedStep(hostileWorkflow, UPLOAD_STEP_NAME); + assertSarifSourceBinding(uploadStep); + }, + /missing reviewed step from scan job/u, + 'job-level mappings after steps must not lend step authority to SARIF source binding', + ); +}); + for (const siblingJobLine of [ ' "decoy":', " 'decoy': # sibling", From b9c62e2885707af11a0bcf16292aaa813dd9e4ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 09:05:42 +0900 Subject: [PATCH 140/180] ci: verify PR279 item38 exact head --- .github/workflows/verify-pr279-item38.yml | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/verify-pr279-item38.yml diff --git a/.github/workflows/verify-pr279-item38.yml b/.github/workflows/verify-pr279-item38.yml new file mode 100644 index 000000000..8227921ba --- /dev/null +++ b/.github/workflows/verify-pr279-item38.yml @@ -0,0 +1,39 @@ +name: Verify PR279 Item38 + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + fetch-depth: 1 + + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + + - name: Verify commercial-readiness contracts + run: node --test packages/commercial-readiness/src/*.test.mjs + + - name: Verify Item 38 syntax + run: node --check packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs + + - name: Verify clean diff + run: git diff --check From ed0eb47b9e28b71f42b36c926d0e9facfe9bc0e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 09:06:27 +0900 Subject: [PATCH 141/180] ci: retire PR279 item38 verifier --- .github/workflows/verify-pr279-item38.yml | 39 ----------------------- 1 file changed, 39 deletions(-) delete mode 100644 .github/workflows/verify-pr279-item38.yml diff --git a/.github/workflows/verify-pr279-item38.yml b/.github/workflows/verify-pr279-item38.yml deleted file mode 100644 index 8227921ba..000000000 --- a/.github/workflows/verify-pr279-item38.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Verify PR279 Item38 - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout exact head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - fetch-depth: 1 - - - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: 22 - - - name: Verify commercial-readiness contracts - run: node --test packages/commercial-readiness/src/*.test.mjs - - - name: Verify Item 38 syntax - run: node --check packages/commercial-readiness/src/sarif-source-binding-contract.test.mjs - - - name: Verify clean diff - run: git diff --check From 9567333a66de6ea39b718d1f02effded4e806275 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:04:13 +0900 Subject: [PATCH 142/180] test(ci): reproduce post-steps provenance guard authority leak --- ...ail-provenance-condition-contract.test.mjs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs index 28798a59e..d62a325d5 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs @@ -217,6 +217,29 @@ test('provenance step authority rejects step-shaped text inside a run block', () ); }); +test('provenance condition rejects step-shaped text after the direct steps sequence', () => { + const hostileWorkflow = [ + 'jobs:', + ' scan:', + ' steps:', + ' - name: Harmless scan step', + ' run: echo scan', + ' name: |', + ` - name: ${PROVENANCE_STEP_NAME}`, + ' if: >-', + ` ${PULL_REQUEST_CONDITION}`, + ` && ${SAME_REPOSITORY_CONDITION}`, + ' run: echo fake-authority', + ].join('\n'); + + assert.throws( + () => + assertProvenanceGuard(namedStep(hostileWorkflow, PROVENANCE_STEP_NAME)), + /direct workflow step/u, + 'job-level mappings after steps must not lend provenance-step authority', + ); +}); + test('provenance condition does not borrow authority from quoted or commented sibling jobs', () => { for (const sibling of [' decoy: # sibling', ' "decoy":']) { const hostileWorkflow = [ From 5ae28a10c1bc29edbbb42baefad5f3c1642f3a38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:04:37 +0900 Subject: [PATCH 143/180] ci: add purpose verifier for PR279 item39 --- .github/workflows/verify-pr279-item39.yml | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/verify-pr279-item39.yml diff --git a/.github/workflows/verify-pr279-item39.yml b/.github/workflows/verify-pr279-item39.yml new file mode 100644 index 000000000..ce1c94de0 --- /dev/null +++ b/.github/workflows/verify-pr279-item39.yml @@ -0,0 +1,36 @@ +name: Verify PR279 Item39 + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + steps: + - name: Checkout exact head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: 22 + + - name: Verify provenance condition contract + run: node --test packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs + + - name: Verify syntax and diff hygiene + run: | + node --check packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs + git diff --check HEAD^ From f57d5fb251ff686ce0c5f5d07b73f45934ce8a40 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:06:03 +0900 Subject: [PATCH 144/180] test(ci): bound provenance guard to direct steps --- ...ail-provenance-condition-contract.test.mjs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs index d62a325d5..e7d1ce49f 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs @@ -76,10 +76,28 @@ function namedStep(workflow, stepName) { 'scan job must contain exactly one direct steps mapping', ); + const stepsStart = stepsIndexes[0]; + const directJobMemberIndent = `${jobMatch[1]} `; const stepIndent = `${jobMatch[1]} `; + let stepsEnd = lines.length; + for (let index = stepsStart + 1; index < lines.length; index += 1) { + const line = lines[index]; + const trimmed = line.trim(); + if (trimmed === '' || trimmed.startsWith('#')) { + continue; + } + if ( + line.startsWith(directJobMemberIndent) && + !line.startsWith(stepIndent) + ) { + stepsEnd = index; + break; + } + } + const expected = `${stepIndent}- name: ${stepName}`; const matches = []; - for (let index = stepsIndexes[0] + 1; index < lines.length; index += 1) { + for (let index = stepsStart + 1; index < stepsEnd; index += 1) { if (lines[index] === expected) { matches.push(index); } @@ -91,8 +109,8 @@ function namedStep(workflow, stepName) { ); const start = matches[0]; - let end = lines.length; - for (let index = start + 1; index < lines.length; index += 1) { + let end = stepsEnd; + for (let index = start + 1; index < stepsEnd; index += 1) { if (lines[index].startsWith(`${stepIndent}- `)) { end = index; break; From 42c332196a4662fa976f531e467d52ae2dd81f9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:06:58 +0900 Subject: [PATCH 145/180] ci: make PR279 item39 verifier shallow-checkout safe --- .github/workflows/verify-pr279-item39.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-pr279-item39.yml b/.github/workflows/verify-pr279-item39.yml index ce1c94de0..66a6415b4 100644 --- a/.github/workflows/verify-pr279-item39.yml +++ b/.github/workflows/verify-pr279-item39.yml @@ -33,4 +33,4 @@ jobs: - name: Verify syntax and diff hygiene run: | node --check packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs - git diff --check HEAD^ + git show --check --format= HEAD From 90998d044032a9032cb1a3ae1b02b346b05c5503 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:07:28 +0900 Subject: [PATCH 146/180] ci: make PR279 item39 hygiene check history independent --- .github/workflows/verify-pr279-item39.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify-pr279-item39.yml b/.github/workflows/verify-pr279-item39.yml index 66a6415b4..13839c70a 100644 --- a/.github/workflows/verify-pr279-item39.yml +++ b/.github/workflows/verify-pr279-item39.yml @@ -30,7 +30,9 @@ jobs: - name: Verify provenance condition contract run: node --test packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs - - name: Verify syntax and diff hygiene + - name: Verify syntax and source hygiene run: | node --check packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs - git show --check --format= HEAD + if grep -nE '[[:blank:]]+$' packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs; then + exit 1 + fi From ba96e165e5dbcbec4be3caa5a5273514ad25887a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:07:52 +0900 Subject: [PATCH 147/180] ci: retire PR279 item39 verifier --- .github/workflows/verify-pr279-item39.yml | 38 ----------------------- 1 file changed, 38 deletions(-) delete mode 100644 .github/workflows/verify-pr279-item39.yml diff --git a/.github/workflows/verify-pr279-item39.yml b/.github/workflows/verify-pr279-item39.yml deleted file mode 100644 index 13839c70a..000000000 --- a/.github/workflows/verify-pr279-item39.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Verify PR279 Item39 - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - steps: - - name: Checkout exact head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: 22 - - - name: Verify provenance condition contract - run: node --test packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs - - - name: Verify syntax and source hygiene - run: | - node --check packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs - if grep -nE '[[:blank:]]+$' packages/commercial-readiness/src/appguardrail-provenance-condition-contract.test.mjs; then - exit 1 - fi From f012c7e0412783e7a0140aecbb2c2e2c29e7e2f1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:08:20 +0900 Subject: [PATCH 148/180] test(ci): reproduce post-steps provenance env authority leak --- ...guardrail-provenance-env-contract.test.mjs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs index c0a6e1b0e..9f707057b 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs @@ -204,6 +204,28 @@ test('provenance env rejects duplicate pull-request identity keys', () => { ); }); +test('provenance env rejects step-shaped text after the direct steps sequence', () => { + const hostileWorkflow = [ + 'jobs:', + ' scan:', + ' steps:', + ' - name: Harmless scan step', + ' run: echo scan', + ' name: |', + ` - name: ${PROVENANCE_STEP_NAME}`, + ' env:', + ` ${EXPECTED_PR_NUMBER}`, + ` ${EXPECTED_MERGE_SHA}`, + ' run: echo fake-authority', + ].join('\n'); + + assert.throws( + () => assertProvenanceEnv(namedStep(hostileWorkflow, PROVENANCE_STEP_NAME)), + /direct workflow step/u, + 'job-level mappings after steps must not lend provenance environment authority', + ); +}); + test('provenance env does not borrow identity from quoted or commented sibling jobs', () => { for (const sibling of [' decoy: # sibling', ' "decoy":']) { const hostileWorkflow = [ From 83982dcf143c272d52aa7c4595fb381efb465167 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:08:28 +0900 Subject: [PATCH 149/180] ci: add purpose verifier for PR279 item40 --- .github/workflows/verify-pr279-item40.yml | 38 +++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/verify-pr279-item40.yml diff --git a/.github/workflows/verify-pr279-item40.yml b/.github/workflows/verify-pr279-item40.yml new file mode 100644 index 000000000..3e62e1f4a --- /dev/null +++ b/.github/workflows/verify-pr279-item40.yml @@ -0,0 +1,38 @@ +name: Verify PR279 Item40 + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + steps: + - name: Checkout exact head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: 22 + + - name: Verify provenance env contract + run: node --test packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs + + - name: Verify syntax and source hygiene + run: | + node --check packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs + if grep -nE '[[:blank:]]+$' packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs; then + exit 1 + fi From 80b3ea4d841376f057a6a1ab03f3e4b8f47e5981 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:09:48 +0900 Subject: [PATCH 150/180] test(ci): bound provenance env to direct steps --- ...guardrail-provenance-env-contract.test.mjs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs index 9f707057b..99a63fac0 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs @@ -75,10 +75,28 @@ function namedStep(workflow, stepName) { 'scan job must contain exactly one direct steps mapping', ); + const stepsStart = stepsIndexes[0]; + const directJobMemberIndent = `${jobMatch[1]} `; const stepIndent = `${jobMatch[1]} `; + let stepsEnd = lines.length; + for (let index = stepsStart + 1; index < lines.length; index += 1) { + const line = lines[index]; + const trimmed = line.trim(); + if (trimmed === '' || trimmed.startsWith('#')) { + continue; + } + if ( + line.startsWith(directJobMemberIndent) && + !line.startsWith(stepIndent) + ) { + stepsEnd = index; + break; + } + } + const expected = `${stepIndent}- name: ${stepName}`; const matches = []; - for (let index = stepsIndexes[0] + 1; index < lines.length; index += 1) { + for (let index = stepsStart + 1; index < stepsEnd; index += 1) { if (lines[index] === expected) { matches.push(index); } @@ -90,8 +108,8 @@ function namedStep(workflow, stepName) { ); const start = matches[0]; - let end = lines.length; - for (let index = start + 1; index < lines.length; index += 1) { + let end = stepsEnd; + for (let index = start + 1; index < stepsEnd; index += 1) { if (lines[index].startsWith(`${stepIndent}- `)) { end = index; break; From c4049ee414d50aa6fa0d967aaa4dfc43da4ca36e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:10:19 +0900 Subject: [PATCH 151/180] ci: retire PR279 item40 verifier --- .github/workflows/verify-pr279-item40.yml | 38 ----------------------- 1 file changed, 38 deletions(-) delete mode 100644 .github/workflows/verify-pr279-item40.yml diff --git a/.github/workflows/verify-pr279-item40.yml b/.github/workflows/verify-pr279-item40.yml deleted file mode 100644 index 3e62e1f4a..000000000 --- a/.github/workflows/verify-pr279-item40.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Verify PR279 Item40 - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - steps: - - name: Checkout exact head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 - with: - node-version: 22 - - - name: Verify provenance env contract - run: node --test packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs - - - name: Verify syntax and source hygiene - run: | - node --check packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs - if grep -nE '[[:blank:]]+$' packages/commercial-readiness/src/appguardrail-provenance-env-contract.test.mjs; then - exit 1 - fi From c9db578bb7a11c1154df2c16fc17c0aa9cd93eff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:11:22 +0900 Subject: [PATCH 152/180] test(ci): reproduce source verifier steps-boundary borrowing --- .github/workflows/verify-pr279-item41.yml | 57 +++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/verify-pr279-item41.yml diff --git a/.github/workflows/verify-pr279-item41.yml b/.github/workflows/verify-pr279-item41.yml new file mode 100644 index 000000000..292562fc1 --- /dev/null +++ b/.github/workflows/verify-pr279-item41.yml @@ -0,0 +1,57 @@ +name: Verify PR 279 item 41 + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Configure Git initial branch + run: git config --global init.defaultBranch main + - name: Checkout exact verifier head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.sha }} + persist-credentials: false + fetch-depth: 1 + - name: Add hostile post-steps authority regression + shell: bash + run: | + cat >> packages/commercial-readiness/src/source-verification-workflow.test.mjs <<'EOF' + + test('source verifier does not borrow named-step authority from job-level block scalar text', () => { + const job = [ + ' scan:', + ' name: |', + ' - name: Materialize AppGuardrail SARIF PR merge provenance', + " if: github.event_name == 'pull_request'", + ' env:', + ' PR_NUMBER: ${{ github.event.pull_request.number }}', + ' EXPECTED_MERGE_SHA: ${{ github.sha }}', + ' run: echo borrowed-provenance', + ' steps:', + ' - name: Upload AppGuardrail SARIF to code scanning', + ' uses: github/codeql-action/upload-sarif@reviewed-sha', + ].join('\n'); + + assert.throws( + () => + stepBlock( + job, + 'Materialize AppGuardrail SARIF PR merge provenance', + ), + /expected exactly one step/u, + ); + }); + EOF + - name: Verify source workflow authority boundary + run: node --test packages/commercial-readiness/src/source-verification-workflow.test.mjs + - name: Verify syntax + run: node --check packages/commercial-readiness/src/source-verification-workflow.test.mjs From 26ed27a6808951485ef96b5d77cab0949a03a6c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:13:22 +0900 Subject: [PATCH 153/180] fix(ci): bound source verifier to direct steps section --- .../src/source-verification-workflow.test.mjs | 88 +++++++++++++++---- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index 576d14530..860ba5465 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -43,27 +43,49 @@ function jobBlock(workflow, jobName) { return lines.slice(start, end).join('\n'); } -/** Resolves the exact sequence indentation for real workflow steps. */ -function stepSequenceIndent(job) { +/** Resolves the bounded direct steps sequence inside one workflow job. */ +function stepsSection(job) { const lines = job.split('\n'); const jobEntry = /^(\s*)[A-Za-z0-9_-]+:\s*$/u.exec(lines[0]); assert.ok(jobEntry, 'invalid bounded workflow job'); - const stepsLine = `${jobEntry[1]} steps:`; + const directJobMemberIndent = `${jobEntry[1]} `; + const stepIndent = `${jobEntry[1]} `; + const stepsLine = `${directJobMemberIndent}steps:`; + const stepsIndexes = []; + for (let index = 1; index < lines.length; index += 1) { + if (lines[index] === stepsLine) { + stepsIndexes.push(index); + } + } assert.equal( - lines.filter((line) => line === stepsLine).length, + stepsIndexes.length, 1, 'expected exactly one direct steps mapping in bounded workflow job', ); - return `${jobEntry[1]} `; + + const start = stepsIndexes[0]; + let end = lines.length; + for (let index = start + 1; index < lines.length; index += 1) { + const line = lines[index]; + const trimmed = line.trim(); + if (trimmed === '' || trimmed.startsWith('#')) { + continue; + } + if (line.startsWith(directJobMemberIndent) && !line.startsWith(stepIndent)) { + end = index; + break; + } + } + + return { lines, start, end, stepIndent }; } /** Finds one unique named workflow step at the real steps sequence depth. */ function stepStartIndex(job, stepName) { - const lines = job.split('\n'); - const stepIndent = stepSequenceIndent(job); + const { lines, start, end, stepIndent } = stepsSection(job); const expected = `${stepIndent}- name: ${stepName}`; const matches = []; - for (let index = 0; index < lines.length; index += 1) { + for (let index = start + 1; index < end; index += 1) { if (lines[index] === expected) { matches.push(index); } @@ -78,11 +100,10 @@ function stepStartIndex(job, stepName) { /** Extracts one named workflow step from an already bounded job block. */ function stepBlock(job, stepName) { - const lines = job.split('\n'); + const { lines, end: stepsEnd, stepIndent } = stepsSection(job); const start = stepStartIndex(job, stepName); - const stepIndent = stepSequenceIndent(job); - let end = lines.length; - for (let index = start + 1; index < lines.length; index += 1) { + let end = stepsEnd; + for (let index = start + 1; index < stepsEnd; index += 1) { if (lines[index].startsWith(`${stepIndent}- `)) { end = index; break; @@ -121,13 +142,17 @@ function matchesActionIdentity(uses, actionName) { /** Finds executable action uses only from real step entries and direct step keys. */ function actionUses(job, actionName) { - const lines = job.split('\n'); - const stepIndent = stepSequenceIndent(job); + const { + lines, + start: stepsStart, + end: stepsEnd, + stepIndent, + } = stepsSection(job); const directKeyIndent = `${stepIndent} `; const usesEntry = /^uses:\s*(['"]?)([^'"\s#]+)\1(?:\s+#.*)?$/u; const uses = []; - for (let index = 0; index < lines.length; index += 1) { + for (let index = stepsStart + 1; index < stepsEnd; index += 1) { if (!lines[index].startsWith(`${stepIndent}- `)) { continue; } @@ -137,8 +162,8 @@ function actionUses(job, actionName) { uses.push({ line: lines[index], stepStart: index }); } - let end = lines.length; - for (let sibling = index + 1; sibling < lines.length; sibling += 1) { + let end = stepsEnd; + for (let sibling = index + 1; sibling < stepsEnd; sibling += 1) { if (lines[sibling].startsWith(`${stepIndent}- `)) { end = sibling; break; @@ -197,6 +222,35 @@ test('job extraction does not borrow authority from quoted or commented sibling } }); +test('step extraction does not borrow authority from job-level block scalar text', () => { + const job = [ + ' scan:', + ' name: |', + ' - name: Materialize AppGuardrail SARIF PR merge provenance', + " if: github.event_name == 'pull_request'", + ' env:', + ' PR_NUMBER: ${{ github.event.pull_request.number }}', + ' EXPECTED_MERGE_SHA: ${{ github.sha }}', + ' run: echo borrowed-provenance', + ' - name: Upload AppGuardrail SARIF to code scanning', + ' uses: github/codeql-action/upload-sarif@fake-sha', + ' steps:', + ' - name: Harmless real step', + ' run: echo real-step', + ].join('\n'); + + assert.throws( + () => + stepBlock(job, 'Materialize AppGuardrail SARIF PR merge provenance'), + /expected exactly one step/u, + ); + assert.equal( + actionUses(job, 'github/codeql-action/upload-sarif').length, + 0, + 'action authority must come only from the direct steps sequence', + ); +}); + test('step extraction does not borrow evidence from unnamed sibling steps', () => { const job = [ ' scan:', From d3df282bc72d799d66a229a9ef572d38976f5fc5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:15:24 +0900 Subject: [PATCH 154/180] test(ci): broaden item 41 verifier --- .github/workflows/verify-pr279-item41.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/verify-pr279-item41.yml b/.github/workflows/verify-pr279-item41.yml index 292562fc1..80be06e11 100644 --- a/.github/workflows/verify-pr279-item41.yml +++ b/.github/workflows/verify-pr279-item41.yml @@ -51,7 +51,11 @@ jobs: ); }); EOF - - name: Verify source workflow authority boundary + - name: Verify focused source workflow authority boundary run: node --test packages/commercial-readiness/src/source-verification-workflow.test.mjs + - name: Verify all Commercial Readiness contracts + run: node --test packages/commercial-readiness/src/*.test.mjs - name: Verify syntax run: node --check packages/commercial-readiness/src/source-verification-workflow.test.mjs + - name: Verify source hygiene + run: git diff --check From ca2616aa9033477abf9ed278ae1ad87fc7a9cf80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:17:18 +0900 Subject: [PATCH 155/180] test(ci): reproduce top-level job authority borrowing --- .github/workflows/verify-pr279-item41.yml | 38 +++++++++-------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/.github/workflows/verify-pr279-item41.yml b/.github/workflows/verify-pr279-item41.yml index 80be06e11..4cab5f8fa 100644 --- a/.github/workflows/verify-pr279-item41.yml +++ b/.github/workflows/verify-pr279-item41.yml @@ -1,4 +1,4 @@ -name: Verify PR 279 item 41 +name: Verify PR 279 verifier boundaries on: push: @@ -21,41 +21,31 @@ jobs: ref: ${{ github.sha }} persist-credentials: false fetch-depth: 1 - - name: Add hostile post-steps authority regression + - name: Add hostile top-level job regression shell: bash run: | cat >> packages/commercial-readiness/src/source-verification-workflow.test.mjs <<'EOF' - test('source verifier does not borrow named-step authority from job-level block scalar text', () => { - const job = [ + test('job extraction ignores job-shaped text outside the top-level jobs mapping', () => { + const workflow = [ + 'name: |', ' scan:', - ' name: |', + ' steps:', ' - name: Materialize AppGuardrail SARIF PR merge provenance', - " if: github.event_name == 'pull_request'", - ' env:', - ' PR_NUMBER: ${{ github.event.pull_request.number }}', - ' EXPECTED_MERGE_SHA: ${{ github.sha }}', - ' run: echo borrowed-provenance', + ' run: echo fake-job-authority', + 'jobs:', + ' scan:', ' steps:', - ' - name: Upload AppGuardrail SARIF to code scanning', - ' uses: github/codeql-action/upload-sarif@reviewed-sha', + ' - name: Harmless real step', + ' run: echo real-job-authority', ].join('\n'); - assert.throws( - () => - stepBlock( - job, - 'Materialize AppGuardrail SARIF PR merge provenance', - ), - /expected exactly one step/u, - ); + const block = jobBlock(workflow, 'scan'); + assert.ok(block.includes('Harmless real step')); + assert.equal(block.includes('fake-job-authority'), false); }); EOF - name: Verify focused source workflow authority boundary run: node --test packages/commercial-readiness/src/source-verification-workflow.test.mjs - - name: Verify all Commercial Readiness contracts - run: node --test packages/commercial-readiness/src/*.test.mjs - name: Verify syntax run: node --check packages/commercial-readiness/src/source-verification-workflow.test.mjs - - name: Verify source hygiene - run: git diff --check From 861e5cb2b14771632ff68af30fe493785738df8a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:21:27 +0900 Subject: [PATCH 156/180] fix(ci): bound source verifier to top-level jobs mapping --- .../src/source-verification-workflow.test.mjs | 83 ++++++++++++++++--- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/packages/commercial-readiness/src/source-verification-workflow.test.mjs b/packages/commercial-readiness/src/source-verification-workflow.test.mjs index 860ba5465..5a27494a1 100644 --- a/packages/commercial-readiness/src/source-verification-workflow.test.mjs +++ b/packages/commercial-readiness/src/source-verification-workflow.test.mjs @@ -15,27 +15,60 @@ const SARIF_SOURCE_REF = "ref: ${{ github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}"; const SARIF_SOURCE_SHA = 'sha: ${{ github.event.pull_request.head.sha || github.sha }}'; +const DIRECT_JOB_ENTRY = + /^ (?:([A-Za-z_][A-Za-z0-9_-]*)|"([A-Za-z_][A-Za-z0-9_-]*)"|'([A-Za-z_][A-Za-z0-9_-]*)'):\s*(?:#.*)?$/u; /** Reads one repository workflow as UTF-8 text. */ function readWorkflow(name) { return readFileSync(join(REPOSITORY_ROOT, '.github/workflows', name), 'utf8'); } -/** Recognizes direct top-level job entries, including quoted IDs and inline comments. */ -function isDirectJobEntry(line) { - return /^ (?:[A-Za-z_][A-Za-z0-9_-]*|"[A-Za-z_][A-Za-z0-9_-]*"|'[A-Za-z_][A-Za-z0-9_-]*'):\s*(?:#.*)?$/u.test( - line, - ); +/** Returns the canonical job ID for one direct job entry, or null for non-job lines. */ +function directJobName(line) { + const match = DIRECT_JOB_ENTRY.exec(line); + return match ? (match[1] ?? match[2] ?? match[3]) : null; } -/** Extracts one top-level workflow job without requiring a YAML parser. */ +/** Extracts one direct job only from the workflow's top-level jobs mapping. */ function jobBlock(workflow, jobName) { const lines = workflow.split('\n'); - const start = lines.findIndex((line) => line === ` ${jobName}:`); - assert.notEqual(start, -1, `missing job ${jobName}`); - let end = lines.length; - for (let index = start + 1; index < lines.length; index += 1) { - if (isDirectJobEntry(lines[index])) { + const jobsIndexes = []; + for (let index = 0; index < lines.length; index += 1) { + if (lines[index] === 'jobs:') { + jobsIndexes.push(index); + } + } + assert.equal( + jobsIndexes.length, + 1, + 'workflow must contain exactly one top-level jobs mapping', + ); + + const jobsStart = jobsIndexes[0]; + let jobsEnd = lines.length; + for (let index = jobsStart + 1; index < lines.length; index += 1) { + if (/^[^\s#]/u.test(lines[index])) { + jobsEnd = index; + break; + } + } + + const matches = []; + for (let index = jobsStart + 1; index < jobsEnd; index += 1) { + if (directJobName(lines[index]) === jobName) { + matches.push(index); + } + } + assert.equal( + matches.length, + 1, + `expected exactly one workflow job ${jobName}`, + ); + + const start = matches[0]; + let end = jobsEnd; + for (let index = start + 1; index < jobsEnd; index += 1) { + if (directJobName(lines[index]) !== null) { end = index; break; } @@ -71,7 +104,10 @@ function stepsSection(job) { if (trimmed === '' || trimmed.startsWith('#')) { continue; } - if (line.startsWith(directJobMemberIndent) && !line.startsWith(stepIndent)) { + if ( + line.startsWith(directJobMemberIndent) && + !line.startsWith(stepIndent) + ) { end = index; break; } @@ -222,6 +258,29 @@ test('job extraction does not borrow authority from quoted or commented sibling } }); +test('job extraction ignores job-shaped text outside the top-level jobs mapping', () => { + const workflow = [ + 'name: |', + ' scan:', + ' steps:', + ' - name: Materialize AppGuardrail SARIF PR merge provenance', + ' run: echo fake-job-authority', + 'jobs:', + ' scan:', + ' steps:', + ' - name: Harmless real step', + ' run: echo real-job-authority', + ].join('\n'); + + const block = jobBlock(workflow, 'scan'); + assert.ok(block.includes('Harmless real step')); + assert.equal( + block.includes('fake-job-authority'), + false, + 'job authority must come only from the top-level jobs mapping', + ); +}); + test('step extraction does not borrow authority from job-level block scalar text', () => { const job = [ ' scan:', From 21d5a9c911ab5ba915fb7d8abb12cba898ac02be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:23:02 +0900 Subject: [PATCH 157/180] test(ci): broaden item 42 verifier --- .github/workflows/verify-pr279-item41.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/verify-pr279-item41.yml b/.github/workflows/verify-pr279-item41.yml index 4cab5f8fa..90161bf05 100644 --- a/.github/workflows/verify-pr279-item41.yml +++ b/.github/workflows/verify-pr279-item41.yml @@ -47,5 +47,9 @@ jobs: EOF - name: Verify focused source workflow authority boundary run: node --test packages/commercial-readiness/src/source-verification-workflow.test.mjs + - name: Verify all Commercial Readiness contracts + run: node --test packages/commercial-readiness/src/*.test.mjs - name: Verify syntax run: node --check packages/commercial-readiness/src/source-verification-workflow.test.mjs + - name: Verify source hygiene + run: git diff --check From 1dff787ea847054b153409e3def43c1c2bfdbf1d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:27:02 +0900 Subject: [PATCH 158/180] chore(ci): remove completed PR 279 verifier --- .github/workflows/verify-pr279-item41.yml | 55 ----------------------- 1 file changed, 55 deletions(-) delete mode 100644 .github/workflows/verify-pr279-item41.yml diff --git a/.github/workflows/verify-pr279-item41.yml b/.github/workflows/verify-pr279-item41.yml deleted file mode 100644 index 90161bf05..000000000 --- a/.github/workflows/verify-pr279-item41.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Verify PR 279 verifier boundaries - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Configure Git initial branch - run: git config --global init.defaultBranch main - - name: Checkout exact verifier head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - with: - ref: ${{ github.sha }} - persist-credentials: false - fetch-depth: 1 - - name: Add hostile top-level job regression - shell: bash - run: | - cat >> packages/commercial-readiness/src/source-verification-workflow.test.mjs <<'EOF' - - test('job extraction ignores job-shaped text outside the top-level jobs mapping', () => { - const workflow = [ - 'name: |', - ' scan:', - ' steps:', - ' - name: Materialize AppGuardrail SARIF PR merge provenance', - ' run: echo fake-job-authority', - 'jobs:', - ' scan:', - ' steps:', - ' - name: Harmless real step', - ' run: echo real-job-authority', - ].join('\n'); - - const block = jobBlock(workflow, 'scan'); - assert.ok(block.includes('Harmless real step')); - assert.equal(block.includes('fake-job-authority'), false); - }); - EOF - - name: Verify focused source workflow authority boundary - run: node --test packages/commercial-readiness/src/source-verification-workflow.test.mjs - - name: Verify all Commercial Readiness contracts - run: node --test packages/commercial-readiness/src/*.test.mjs - - name: Verify syntax - run: node --check packages/commercial-readiness/src/source-verification-workflow.test.mjs - - name: Verify source hygiene - run: git diff --check From 7db2335353269b681b5316704a612bcd3ecb2d32 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 12:10:21 +0900 Subject: [PATCH 159/180] test(ci): expose provenance run marker false acceptance --- ...guardrail-provenance-run-contract.test.mjs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs diff --git a/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs new file mode 100644 index 000000000..16b572775 --- /dev/null +++ b/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs @@ -0,0 +1,81 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; +import test from 'node:test'; + +const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); +const PROVENANCE_STEP_NAME = + 'Materialize AppGuardrail SARIF PR merge provenance'; +const FETCH_COMMAND = 'git fetch --no-tags --depth=1 origin "$merge_ref"'; +const REV_PARSE_COMMAND = 'git rev-parse FETCH_HEAD'; +const CAT_FILE_COMMAND = 'git cat-file -e "${EXPECTED_MERGE_SHA}^{commit}"'; + +/** Extracts the reviewed provenance step from the direct scan steps sequence. */ +function provenanceStep(workflow) { + const lines = workflow.split('\n'); + const jobsIndex = lines.indexOf('jobs:'); + assert.notEqual(jobsIndex, -1, 'workflow must contain jobs'); + const scanIndex = lines.indexOf(' scan:', jobsIndex + 1); + assert.notEqual(scanIndex, -1, 'workflow must contain jobs.scan'); + const stepsIndex = lines.indexOf(' steps:', scanIndex + 1); + assert.notEqual(stepsIndex, -1, 'jobs.scan must contain direct steps'); + + const expected = ` - name: ${PROVENANCE_STEP_NAME}`; + const start = lines.indexOf(expected, stepsIndex + 1); + assert.notEqual(start, -1, 'scan must contain the reviewed provenance step'); + let end = lines.length; + for (let index = start + 1; index < lines.length; index += 1) { + if (lines[index].startsWith(' - ')) { + end = index; + break; + } + if (/^ [A-Za-z_][A-Za-z0-9_-]*:/u.test(lines[index])) { + end = index; + break; + } + } + return lines.slice(start, end).join('\n'); +} + +/** + * Mirrors the current aggregate verifier's provenance-run assertions. + * The hostile regression below proves this text-presence model is insufficient. + */ +function assertProvenanceRun(step) { + assert.ok(step.includes(FETCH_COMMAND), 'provenance must fetch the bounded merge ref'); + assert.ok(step.includes(REV_PARSE_COMMAND), 'provenance must inspect FETCH_HEAD'); + assert.ok(step.includes(CAT_FILE_COMMAND), 'provenance must materialize the merge commit'); + assert.equal(step.includes('fetch-depth: 0'), false, 'provenance must stay shallow'); + assert.equal(step.includes('git checkout'), false, 'provenance must not replace the contributor checkout'); +} + +test('AppGuardrail provenance owns the reviewed bounded run authority', () => { + const workflow = readFileSync( + join(REPOSITORY_ROOT, '.github/workflows/appguardrail.yml'), + 'utf8', + ); + assert.doesNotThrow(() => assertProvenanceRun(provenanceStep(workflow))); +}); + +test('provenance run authority rejects marker text that exists only in shell comments', () => { + const hostileStep = [ + ` - name: ${PROVENANCE_STEP_NAME}`, + " if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository", + ' env:', + ' PR_NUMBER: ${{ github.event.pull_request.number }}', + ' EXPECTED_MERGE_SHA: ${{ github.sha }}', + ' run: |', + ' set -euo pipefail', + ` # ${FETCH_COMMAND}`, + ` # fetched_merge_sha="$(${REV_PARSE_COMMAND})"`, + ` # ${CAT_FILE_COMMAND}`, + ' echo "provenance materialization skipped"', + ].join('\n'); + + assert.throws( + () => assertProvenanceRun(hostileStep), + /reviewed direct run authority/u, + 'comment-only marker text must not satisfy executable provenance authority', + ); +}); From bf2adb33933b2149cd39eeb7414c2c1133705f7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 12:10:34 +0900 Subject: [PATCH 160/180] ci(test): verify PR 279 item 43 regression --- .github/workflows/verify-pr279-item43.yml | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/verify-pr279-item43.yml diff --git a/.github/workflows/verify-pr279-item43.yml b/.github/workflows/verify-pr279-item43.yml new file mode 100644 index 000000000..aaad37524 --- /dev/null +++ b/.github/workflows/verify-pr279-item43.yml @@ -0,0 +1,35 @@ +name: Verify PR 279 item 43 + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout exact verifier head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Verify provenance run authority contract + run: node --test packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs + + - name: Verify contract syntax + run: node --check packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs From 3d7b53c07f0cbdae07ca7c1516fa1826c6fd76bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 12:12:03 +0900 Subject: [PATCH 161/180] fix(ci): bind provenance to exact direct run authority --- ...guardrail-provenance-run-contract.test.mjs | 85 +++++++++++++++---- 1 file changed, 70 insertions(+), 15 deletions(-) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs index 16b572775..27926d7c7 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs @@ -7,9 +7,26 @@ import test from 'node:test'; const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); const PROVENANCE_STEP_NAME = 'Materialize AppGuardrail SARIF PR merge provenance'; -const FETCH_COMMAND = 'git fetch --no-tags --depth=1 origin "$merge_ref"'; -const REV_PARSE_COMMAND = 'git rev-parse FETCH_HEAD'; -const CAT_FILE_COMMAND = 'git cat-file -e "${EXPECTED_MERGE_SHA}^{commit}"'; +const EXPECTED_PROVENANCE_RUN = [ + 'set -euo pipefail', + 'if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then', + ' echo "::error::Pull request number is not a positive integer."', + ' exit 1', + 'fi', + 'if ! [[ "$EXPECTED_MERGE_SHA" =~ ^[0-9a-f]{40}$ ]]; then', + ' echo "::error::Advertised pull request merge SHA is not a full commit SHA."', + ' exit 1', + 'fi', + '', + 'merge_ref="refs/pull/${PR_NUMBER}/merge"', + 'git fetch --no-tags --depth=1 origin "$merge_ref"', + 'fetched_merge_sha="$(git rev-parse FETCH_HEAD)"', + 'if [ "$fetched_merge_sha" != "$EXPECTED_MERGE_SHA" ]; then', + ' echo "::error::Fetched pull request merge provenance does not match github.sha."', + ' exit 1', + 'fi', + 'git cat-file -e "${EXPECTED_MERGE_SHA}^{commit}"', +].join('\n'); /** Extracts the reviewed provenance step from the direct scan steps sequence. */ function provenanceStep(workflow) { @@ -38,16 +55,54 @@ function provenanceStep(workflow) { return lines.slice(start, end).join('\n'); } -/** - * Mirrors the current aggregate verifier's provenance-run assertions. - * The hostile regression below proves this text-presence model is insufficient. - */ +/** Reads the step's unique direct literal run block without accepting marker-only text. */ +function directLiteralRun(step) { + const lines = step.split('\n'); + const stepMatch = /^(\s*)-\s/u.exec(lines[0]); + assert.ok(stepMatch, 'workflow step indentation is invalid'); + const directIndent = `${stepMatch[1]} `; + const runPrefix = `${directIndent}run:`; + const matches = []; + for (let index = 1; index < lines.length; index += 1) { + if (lines[index].startsWith(runPrefix)) { + matches.push(index); + } + } + assert.equal( + matches.length, + 1, + 'provenance step must own exactly one reviewed direct run authority', + ); + + const runIndex = matches[0]; + assert.equal( + lines[runIndex], + `${runPrefix} |`, + 'provenance step must use the reviewed direct literal run authority', + ); + const bodyIndent = `${directIndent} `; + const body = []; + for (let index = runIndex + 1; index < lines.length; index += 1) { + const line = lines[index]; + if (line === '') { + body.push(''); + continue; + } + if (!line.startsWith(bodyIndent)) { + break; + } + body.push(line.slice(bodyIndent.length)); + } + return body.join('\n'); +} + +/** Requires the provenance shell to remain exactly the reviewed bounded materialization program. */ function assertProvenanceRun(step) { - assert.ok(step.includes(FETCH_COMMAND), 'provenance must fetch the bounded merge ref'); - assert.ok(step.includes(REV_PARSE_COMMAND), 'provenance must inspect FETCH_HEAD'); - assert.ok(step.includes(CAT_FILE_COMMAND), 'provenance must materialize the merge commit'); - assert.equal(step.includes('fetch-depth: 0'), false, 'provenance must stay shallow'); - assert.equal(step.includes('git checkout'), false, 'provenance must not replace the contributor checkout'); + assert.equal( + directLiteralRun(step), + EXPECTED_PROVENANCE_RUN, + 'provenance step must retain the reviewed direct run authority', + ); } test('AppGuardrail provenance owns the reviewed bounded run authority', () => { @@ -67,9 +122,9 @@ test('provenance run authority rejects marker text that exists only in shell com ' EXPECTED_MERGE_SHA: ${{ github.sha }}', ' run: |', ' set -euo pipefail', - ` # ${FETCH_COMMAND}`, - ` # fetched_merge_sha="$(${REV_PARSE_COMMAND})"`, - ` # ${CAT_FILE_COMMAND}`, + ' # git fetch --no-tags --depth=1 origin "$merge_ref"', + ' # fetched_merge_sha="$(git rev-parse FETCH_HEAD)"', + ' # git cat-file -e "${EXPECTED_MERGE_SHA}^{commit}"', ' echo "provenance materialization skipped"', ].join('\n'); From 070f29bd23d33a7321d3039495729f85653816e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 12:14:13 +0900 Subject: [PATCH 162/180] ci(test): broaden PR 279 item 43 verification --- .github/workflows/verify-pr279-item43.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/verify-pr279-item43.yml b/.github/workflows/verify-pr279-item43.yml index aaad37524..0fc509ab7 100644 --- a/.github/workflows/verify-pr279-item43.yml +++ b/.github/workflows/verify-pr279-item43.yml @@ -28,8 +28,8 @@ jobs: with: node-version: '24' - - name: Verify provenance run authority contract - run: node --test packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs + - name: Verify full commercial-readiness contract suite + run: node --test packages/commercial-readiness/src/*.test.mjs - - name: Verify contract syntax + - name: Verify new contract syntax run: node --check packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs From 45259ab6efa1d69f4e4fb39e53ae622d8581d79a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 12:14:24 +0900 Subject: [PATCH 163/180] chore(ci): remove completed PR 279 item 43 verifier --- .github/workflows/verify-pr279-item43.yml | 35 ----------------------- 1 file changed, 35 deletions(-) delete mode 100644 .github/workflows/verify-pr279-item43.yml diff --git a/.github/workflows/verify-pr279-item43.yml b/.github/workflows/verify-pr279-item43.yml deleted file mode 100644 index 0fc509ab7..000000000 --- a/.github/workflows/verify-pr279-item43.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Verify PR 279 item 43 - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 5 - steps: - - name: Checkout exact verifier head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - - - name: Verify full commercial-readiness contract suite - run: node --test packages/commercial-readiness/src/*.test.mjs - - - name: Verify new contract syntax - run: node --check packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs From 4ef8d98ec3d226c5289b9d18687387f2933d5ce1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 13:04:00 +0900 Subject: [PATCH 164/180] fix(ci): honor provenance run scalar separator blanks --- .../appguardrail-provenance-run-contract.test.mjs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs index 27926d7c7..1a91c1887 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs @@ -93,6 +93,9 @@ function directLiteralRun(step) { } body.push(line.slice(bodyIndent.length)); } + while (body.at(-1) === '') { + body.pop(); + } return body.join('\n'); } @@ -113,6 +116,17 @@ test('AppGuardrail provenance owns the reviewed bounded run authority', () => { assert.doesNotThrow(() => assertProvenanceRun(provenanceStep(workflow))); }); +test('provenance run authority treats separator blank lines as outside the reviewed body', () => { + const validStep = [ + ` - name: ${PROVENANCE_STEP_NAME}`, + ' run: |', + ...EXPECTED_PROVENANCE_RUN.split('\n').map((line) => ` ${line}`), + '', + ].join('\n'); + + assert.doesNotThrow(() => assertProvenanceRun(validStep)); +}); + test('provenance run authority rejects marker text that exists only in shell comments', () => { const hostileStep = [ ` - name: ${PROVENANCE_STEP_NAME}`, From 682f092f914aa352f14bf8814a4c2c25b7d98169 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 13:04:28 +0900 Subject: [PATCH 165/180] ci(test): verify PR 279 item 43 repair --- .github/workflows/verify-pr279-item43.yml | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/verify-pr279-item43.yml diff --git a/.github/workflows/verify-pr279-item43.yml b/.github/workflows/verify-pr279-item43.yml new file mode 100644 index 000000000..83aeab387 --- /dev/null +++ b/.github/workflows/verify-pr279-item43.yml @@ -0,0 +1,41 @@ +name: Verify PR 279 item 43 + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout exact verifier head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Verify provenance run authority contract + run: node --test packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs + + - name: Verify full commercial-readiness contract suite + run: node --test packages/commercial-readiness/src/*.test.mjs + + - name: Verify new contract syntax + run: node --check packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs + + - name: Verify source hygiene + run: git diff --check HEAD^ HEAD From 85e23cd23e56e2979135b8a1a01c3c2b31211e24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 13:05:17 +0900 Subject: [PATCH 166/180] ci(test): retire PR 279 item 43 verifier --- .github/workflows/verify-pr279-item43.yml | 41 ----------------------- 1 file changed, 41 deletions(-) delete mode 100644 .github/workflows/verify-pr279-item43.yml diff --git a/.github/workflows/verify-pr279-item43.yml b/.github/workflows/verify-pr279-item43.yml deleted file mode 100644 index 83aeab387..000000000 --- a/.github/workflows/verify-pr279-item43.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Verify PR 279 item 43 - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 5 - steps: - - name: Checkout exact verifier head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - - - name: Verify provenance run authority contract - run: node --test packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs - - - name: Verify full commercial-readiness contract suite - run: node --test packages/commercial-readiness/src/*.test.mjs - - - name: Verify new contract syntax - run: node --check packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs - - - name: Verify source hygiene - run: git diff --check HEAD^ HEAD From 5ba263d4913d40769cc468a1b9495a74f2d73ac9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 14:05:26 +0900 Subject: [PATCH 167/180] ci(test): verify item 43 with bounded hygiene history --- .github/workflows/verify-pr279-item43-v2.yml | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/verify-pr279-item43-v2.yml diff --git a/.github/workflows/verify-pr279-item43-v2.yml b/.github/workflows/verify-pr279-item43-v2.yml new file mode 100644 index 000000000..0fbdc9366 --- /dev/null +++ b/.github/workflows/verify-pr279-item43-v2.yml @@ -0,0 +1,52 @@ +name: Verify PR 279 item 43 v2 + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout exact verifier head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Verify provenance run authority contract + run: node --test packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs + + - name: Verify full commercial-readiness contract suite + run: node --test packages/commercial-readiness/src/*.test.mjs + + - name: Verify contract syntax + run: node --check packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs + + - name: Verify exact repair source hygiene + env: + BASE_SHA: 3d7b53c07f0cbdae07ca7c1516fa1826c6fd76bd + REPAIR_SHA: 4ef8d98ec3d226c5289b9d18687387f2933d5ce1 + run: | + set -euo pipefail + git fetch --no-tags --depth=1 origin "$BASE_SHA" + fetched_base="$(git rev-parse FETCH_HEAD)" + test "$fetched_base" = "$BASE_SHA" + git fetch --no-tags --depth=1 origin "$REPAIR_SHA" + fetched_repair="$(git rev-parse FETCH_HEAD)" + test "$fetched_repair" = "$REPAIR_SHA" + git diff --check "$BASE_SHA" "$REPAIR_SHA" From 305fb99ff32ae69b3ce5ea6f7af36de36a9ee62e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 14:06:45 +0900 Subject: [PATCH 168/180] ci(test): retire item 43 bounded verifier --- .github/workflows/verify-pr279-item43-v2.yml | 52 -------------------- 1 file changed, 52 deletions(-) delete mode 100644 .github/workflows/verify-pr279-item43-v2.yml diff --git a/.github/workflows/verify-pr279-item43-v2.yml b/.github/workflows/verify-pr279-item43-v2.yml deleted file mode 100644 index 0fbdc9366..000000000 --- a/.github/workflows/verify-pr279-item43-v2.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Verify PR 279 item 43 v2 - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 5 - steps: - - name: Checkout exact verifier head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - - - name: Verify provenance run authority contract - run: node --test packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs - - - name: Verify full commercial-readiness contract suite - run: node --test packages/commercial-readiness/src/*.test.mjs - - - name: Verify contract syntax - run: node --check packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs - - - name: Verify exact repair source hygiene - env: - BASE_SHA: 3d7b53c07f0cbdae07ca7c1516fa1826c6fd76bd - REPAIR_SHA: 4ef8d98ec3d226c5289b9d18687387f2933d5ce1 - run: | - set -euo pipefail - git fetch --no-tags --depth=1 origin "$BASE_SHA" - fetched_base="$(git rev-parse FETCH_HEAD)" - test "$fetched_base" = "$BASE_SHA" - git fetch --no-tags --depth=1 origin "$REPAIR_SHA" - fetched_repair="$(git rev-parse FETCH_HEAD)" - test "$fetched_repair" = "$REPAIR_SHA" - git diff --check "$BASE_SHA" "$REPAIR_SHA" From 7e5d39fd2564046ed81fc601526a182814993d0a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:10:07 +0900 Subject: [PATCH 169/180] test(ci): reproduce provenance run step impersonation --- ...guardrail-provenance-run-contract.test.mjs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs index 1a91c1887..41d3e4ba2 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs @@ -148,3 +148,26 @@ test('provenance run authority rejects marker text that exists only in shell com 'comment-only marker text must not satisfy executable provenance authority', ); }); + +test('provenance run authority rejects job-level block scalar step impersonation', () => { + const hostileWorkflow = [ + 'name: Hostile workflow', + 'on: workflow_dispatch', + 'jobs:', + ' scan:', + ' runs-on: ubuntu-24.04', + ' steps:', + ' - name: Harmless scan work', + ' run: echo harmless', + ' "name": |', + ` - name: ${PROVENANCE_STEP_NAME}`, + ' run: |', + ...EXPECTED_PROVENANCE_RUN.split('\n').map((line) => ` ${line}`), + ].join('\n'); + + assert.throws( + () => assertProvenanceRun(provenanceStep(hostileWorkflow)), + /scan must contain the reviewed provenance step/u, + 'job-level scalar payload must not impersonate a direct scan step', + ); +}); From 22d728d6109d7e378af86bbf497371379cb6d9e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:10:28 +0900 Subject: [PATCH 170/180] ci: verify PR 279 item 44 regression --- .github/workflows/verify-pr279-item44.yml | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/verify-pr279-item44.yml diff --git a/.github/workflows/verify-pr279-item44.yml b/.github/workflows/verify-pr279-item44.yml new file mode 100644 index 000000000..133fad313 --- /dev/null +++ b/.github/workflows/verify-pr279-item44.yml @@ -0,0 +1,35 @@ +name: Verify PR 279 Item 44 + +on: + push: + branches: + - fix/appguardrail-sarif-provenance-v1 + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact source head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22.16.0 + + - name: Verify provenance run authority contract + run: node --test packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs + + - name: Verify contract syntax + run: node --check packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs From 92b553b43516d84c4033d277da6a7d7cf0733bdc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:11:56 +0900 Subject: [PATCH 171/180] test(ci): bound provenance run step authority to scan steps --- ...guardrail-provenance-run-contract.test.mjs | 102 +++++++++++++++--- 1 file changed, 87 insertions(+), 15 deletions(-) diff --git a/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs index 41d3e4ba2..e89d6628c 100644 --- a/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs +++ b/packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs @@ -7,6 +7,8 @@ import test from 'node:test'; const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); const PROVENANCE_STEP_NAME = 'Materialize AppGuardrail SARIF PR merge provenance'; +const DIRECT_JOB_ENTRY = + /^ (?:([A-Za-z_][A-Za-z0-9_-]*)|"([A-Za-z_][A-Za-z0-9_-]*)"|'([A-Za-z_][A-Za-z0-9_-]*)'):\s*(?:#.*)?$/u; const EXPECTED_PROVENANCE_RUN = [ 'set -euo pipefail', 'if ! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then', @@ -28,26 +30,96 @@ const EXPECTED_PROVENANCE_RUN = [ 'git cat-file -e "${EXPECTED_MERGE_SHA}^{commit}"', ].join('\n'); -/** Extracts the reviewed provenance step from the direct scan steps sequence. */ +/** Returns the canonical job ID for one direct jobs mapping entry. */ +function directJobName(line) { + const match = DIRECT_JOB_ENTRY.exec(line); + return match ? (match[1] ?? match[2] ?? match[3]) : null; +} + +/** Extracts the reviewed provenance step only from the direct scan steps sequence. */ function provenanceStep(workflow) { const lines = workflow.split('\n'); - const jobsIndex = lines.indexOf('jobs:'); - assert.notEqual(jobsIndex, -1, 'workflow must contain jobs'); - const scanIndex = lines.indexOf(' scan:', jobsIndex + 1); - assert.notEqual(scanIndex, -1, 'workflow must contain jobs.scan'); - const stepsIndex = lines.indexOf(' steps:', scanIndex + 1); - assert.notEqual(stepsIndex, -1, 'jobs.scan must contain direct steps'); + const jobsIndexes = []; + for (let index = 0; index < lines.length; index += 1) { + if (lines[index] === 'jobs:') { + jobsIndexes.push(index); + } + } + assert.equal( + jobsIndexes.length, + 1, + 'workflow must contain exactly one top-level jobs mapping', + ); - const expected = ` - name: ${PROVENANCE_STEP_NAME}`; - const start = lines.indexOf(expected, stepsIndex + 1); - assert.notEqual(start, -1, 'scan must contain the reviewed provenance step'); - let end = lines.length; - for (let index = start + 1; index < lines.length; index += 1) { - if (lines[index].startsWith(' - ')) { - end = index; + const jobsStart = jobsIndexes[0]; + let jobsEnd = lines.length; + for (let index = jobsStart + 1; index < lines.length; index += 1) { + if (/^[^\s#]/u.test(lines[index])) { + jobsEnd = index; + break; + } + } + + const scanIndexes = []; + for (let index = jobsStart + 1; index < jobsEnd; index += 1) { + if (directJobName(lines[index]) === 'scan') { + scanIndexes.push(index); + } + } + assert.equal(scanIndexes.length, 1, 'workflow must contain exactly one jobs.scan'); + + const scanStart = scanIndexes[0]; + let scanEnd = jobsEnd; + for (let index = scanStart + 1; index < jobsEnd; index += 1) { + if (directJobName(lines[index]) !== null) { + scanEnd = index; break; } - if (/^ [A-Za-z_][A-Za-z0-9_-]*:/u.test(lines[index])) { + } + + const stepsIndexes = []; + for (let index = scanStart + 1; index < scanEnd; index += 1) { + if (lines[index] === ' steps:') { + stepsIndexes.push(index); + } + } + assert.equal( + stepsIndexes.length, + 1, + 'jobs.scan must contain exactly one direct steps sequence', + ); + + const stepsStart = stepsIndexes[0]; + let stepsEnd = scanEnd; + for (let index = stepsStart + 1; index < scanEnd; index += 1) { + const line = lines[index]; + const trimmed = line.trim(); + if (trimmed === '' || trimmed.startsWith('#')) { + continue; + } + if (line.startsWith(' ') && !line.startsWith(' ')) { + stepsEnd = index; + break; + } + } + + const expected = ` - name: ${PROVENANCE_STEP_NAME}`; + const matches = []; + for (let index = stepsStart + 1; index < stepsEnd; index += 1) { + if (lines[index] === expected) { + matches.push(index); + } + } + assert.equal( + matches.length, + 1, + 'scan must contain the reviewed provenance step exactly once', + ); + + const start = matches[0]; + let end = stepsEnd; + for (let index = start + 1; index < stepsEnd; index += 1) { + if (lines[index].startsWith(' - ')) { end = index; break; } From cbf486d2d1217d5af34cf05228ec7727b039d7f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:12:38 +0900 Subject: [PATCH 172/180] ci: broaden PR 279 item 44 verification --- .github/workflows/verify-pr279-item44.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/verify-pr279-item44.yml b/.github/workflows/verify-pr279-item44.yml index 133fad313..71cb5d35a 100644 --- a/.github/workflows/verify-pr279-item44.yml +++ b/.github/workflows/verify-pr279-item44.yml @@ -31,5 +31,11 @@ jobs: - name: Verify provenance run authority contract run: node --test packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs + - name: Verify complete Commercial Readiness contract suite + run: node --test packages/commercial-readiness/src/*.test.mjs + - name: Verify contract syntax run: node --check packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs + + - name: Verify clean verifier execution + run: test -z "$(git status --porcelain)" From b1c6550e85223e874f1ab9e3f8cb634a7168fd1f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:14:26 +0900 Subject: [PATCH 173/180] ci: remove completed PR 279 item 44 verifier --- .github/workflows/verify-pr279-item44.yml | 41 ----------------------- 1 file changed, 41 deletions(-) delete mode 100644 .github/workflows/verify-pr279-item44.yml diff --git a/.github/workflows/verify-pr279-item44.yml b/.github/workflows/verify-pr279-item44.yml deleted file mode 100644 index 71cb5d35a..000000000 --- a/.github/workflows/verify-pr279-item44.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Verify PR 279 Item 44 - -on: - push: - branches: - - fix/appguardrail-sarif-provenance-v1 - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout exact source head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 22.16.0 - - - name: Verify provenance run authority contract - run: node --test packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs - - - name: Verify complete Commercial Readiness contract suite - run: node --test packages/commercial-readiness/src/*.test.mjs - - - name: Verify contract syntax - run: node --check packages/commercial-readiness/src/appguardrail-provenance-run-contract.test.mjs - - - name: Verify clean verifier execution - run: test -z "$(git status --porcelain)" From cd6c5c3ce7cf3cd0da32e79c16d25d2ffd6d0079 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 17:07:20 +0900 Subject: [PATCH 174/180] test(ci): prove SARIF upload provenance gate --- ...l-provenance-upload-gate-contract.test.mjs | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 packages/commercial-readiness/src/appguardrail-provenance-upload-gate-contract.test.mjs diff --git a/packages/commercial-readiness/src/appguardrail-provenance-upload-gate-contract.test.mjs b/packages/commercial-readiness/src/appguardrail-provenance-upload-gate-contract.test.mjs new file mode 100644 index 000000000..d0a269655 --- /dev/null +++ b/packages/commercial-readiness/src/appguardrail-provenance-upload-gate-contract.test.mjs @@ -0,0 +1,171 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; +import test from 'node:test'; + +const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url)); +const PROVENANCE_STEP_NAME = + 'Materialize AppGuardrail SARIF PR merge provenance'; +const UPLOAD_STEP_NAME = 'Upload AppGuardrail SARIF to code scanning'; +const PROVENANCE_STEP_ID = 'materialize_pr_provenance'; +const DIRECT_JOB_ENTRY = + /^ (?:([A-Za-z_][A-Za-z0-9_-]*)|"([A-Za-z_][A-Za-z0-9_-]*)"|'([A-Za-z_][A-Za-z0-9_-]*)'):\s*(?:#.*)?$/u; +const EXPECTED_UPLOAD_CONDITION = [ + 'always()', + "&& hashFiles('appguardrail.sarif') != ''", + "&& (github.event_name != 'pull_request'", + '|| (github.event.pull_request.head.repo.full_name == github.repository', + `&& steps.${PROVENANCE_STEP_ID}.outcome == 'success'))`, +].join(' '); + +/** Returns the canonical job ID for one direct jobs mapping entry. */ +function directJobName(line) { + const match = DIRECT_JOB_ENTRY.exec(line); + return match ? (match[1] ?? match[2] ?? match[3]) : null; +} + +/** Extracts one uniquely named step from the direct jobs.scan steps sequence. */ +function namedScanStep(workflow, stepName) { + const lines = workflow.split('\n'); + const jobsIndexes = []; + for (let index = 0; index < lines.length; index += 1) { + if (lines[index] === 'jobs:') { + jobsIndexes.push(index); + } + } + assert.equal( + jobsIndexes.length, + 1, + 'workflow must contain exactly one top-level jobs mapping', + ); + + const jobsStart = jobsIndexes[0]; + let jobsEnd = lines.length; + for (let index = jobsStart + 1; index < lines.length; index += 1) { + if (/^[^\s#]/u.test(lines[index])) { + jobsEnd = index; + break; + } + } + + const jobs = []; + for (let index = jobsStart + 1; index < jobsEnd; index += 1) { + const name = directJobName(lines[index]); + if (name !== null) { + jobs.push({ index, name }); + } + } + const scanMatches = jobs.filter(({ name }) => name === 'scan'); + assert.equal(scanMatches.length, 1, 'workflow must contain exactly one jobs.scan'); + + const scanStart = scanMatches[0].index; + const scanPosition = jobs.findIndex(({ index }) => index === scanStart); + const scanEnd = + scanPosition + 1 < jobs.length ? jobs[scanPosition + 1].index : jobsEnd; + + const stepsIndexes = []; + for (let index = scanStart + 1; index < scanEnd; index += 1) { + if (lines[index] === ' steps:') { + stepsIndexes.push(index); + } + } + assert.equal( + stepsIndexes.length, + 1, + 'jobs.scan must contain exactly one direct steps sequence', + ); + + const stepsStart = stepsIndexes[0]; + let stepsEnd = scanEnd; + for (let index = stepsStart + 1; index < scanEnd; index += 1) { + const line = lines[index]; + const trimmed = line.trim(); + if (trimmed === '' || trimmed.startsWith('#')) { + continue; + } + if (line.startsWith(' ') && !line.startsWith(' ')) { + stepsEnd = index; + break; + } + } + + const expected = ` - name: ${stepName}`; + const matches = []; + for (let index = stepsStart + 1; index < stepsEnd; index += 1) { + if (lines[index] === expected) { + matches.push(index); + } + } + assert.equal(matches.length, 1, `scan must contain exactly one ${stepName} step`); + + const start = matches[0]; + let end = stepsEnd; + for (let index = start + 1; index < stepsEnd; index += 1) { + if (lines[index].startsWith(' - ')) { + end = index; + break; + } + } + return lines.slice(start, end).join('\n'); +} + +/** Reads one unique direct plain scalar from a step. */ +function directPlainScalar(step, key) { + const lines = step.split('\n'); + const prefix = ` ${key}:`; + const matches = lines + .slice(1) + .filter((line) => line.startsWith(prefix)); + assert.equal(matches.length, 1, `step must contain exactly one direct ${key} key`); + const line = matches[0]; + assert.match(line, new RegExp(`^ ${key}: [A-Za-z_][A-Za-z0-9_-]*$`, 'u')); + return line.slice(prefix.length).trim(); +} + +/** Reads one unique direct folded if scalar without borrowing marker text from other keys. */ +function directFoldedIf(step) { + const lines = step.split('\n'); + const matches = []; + for (let index = 1; index < lines.length; index += 1) { + if (lines[index].startsWith(' if:')) { + matches.push(index); + } + } + assert.equal(matches.length, 1, 'step must contain exactly one direct if key'); + + const index = matches[0]; + assert.equal(lines[index], ' if: >-', 'upload step must use the reviewed folded if scalar'); + const parts = []; + for (let cursor = index + 1; cursor < lines.length; cursor += 1) { + const line = lines[cursor]; + if (line === '') { + continue; + } + if (!line.startsWith(' ')) { + break; + } + parts.push(line.trim()); + } + return parts.join(' '); +} + +test('AppGuardrail SARIF upload is fail-closed on PR provenance materialization', () => { + const workflow = readFileSync( + join(REPOSITORY_ROOT, '.github/workflows/appguardrail.yml'), + 'utf8', + ); + const provenance = namedScanStep(workflow, PROVENANCE_STEP_NAME); + const upload = namedScanStep(workflow, UPLOAD_STEP_NAME); + + assert.equal( + directPlainScalar(provenance, 'id'), + PROVENANCE_STEP_ID, + 'provenance materialization must publish the reviewed step outcome authority', + ); + assert.equal( + directFoldedIf(upload), + EXPECTED_UPLOAD_CONDITION, + 'same-repository PR SARIF upload must require successful provenance materialization', + ); +}); From 6afd31544cdbbbd7f9a1bf0b977173d5734595a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 17:07:38 +0900 Subject: [PATCH 175/180] test(ci): run PR279 Item45 regression --- .github/workflows/verify-pr279-item45.yml | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/verify-pr279-item45.yml diff --git a/.github/workflows/verify-pr279-item45.yml b/.github/workflows/verify-pr279-item45.yml new file mode 100644 index 000000000..9c3067298 --- /dev/null +++ b/.github/workflows/verify-pr279-item45.yml @@ -0,0 +1,33 @@ +name: Verify PR279 Item45 regression + +on: + push: + branches: [fix/appguardrail-sarif-provenance-v1] + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout regression exact + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + with: + persist-credentials: false + ref: cd6c5c3ce7cf3cd0da32e79c16d25d2ffd6d0079 + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + + - name: Verify SARIF provenance upload gate contract + run: | + set -euo pipefail + node --test packages/commercial-readiness/src/appguardrail-provenance-upload-gate-contract.test.mjs From 1f9f37b48cef7cda750274830f56ad06b362dd3f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 17:08:27 +0900 Subject: [PATCH 176/180] fix(ci): fail closed before SARIF upload --- .github/workflows/appguardrail.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/appguardrail.yml b/.github/workflows/appguardrail.yml index e88fe15a9..a6f99c91c 100644 --- a/.github/workflows/appguardrail.yml +++ b/.github/workflows/appguardrail.yml @@ -125,6 +125,7 @@ jobs: PY - name: Materialize AppGuardrail SARIF PR merge provenance + id: materialize_pr_provenance if: >- github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository @@ -156,7 +157,8 @@ jobs: always() && hashFiles('appguardrail.sarif') != '' && (github.event_name != 'pull_request' - || github.event.pull_request.head.repo.full_name == github.repository) + || (github.event.pull_request.head.repo.full_name == github.repository + && steps.materialize_pr_provenance.outcome == 'success')) uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3 with: sarif_file: appguardrail.sarif From 697f331b282781cc973699620e03c1496bfa6fd0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 17:09:31 +0900 Subject: [PATCH 177/180] test(ci): verify PR279 Item45 repair --- .github/workflows/verify-pr279-item45.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/verify-pr279-item45.yml b/.github/workflows/verify-pr279-item45.yml index 9c3067298..e2c14d540 100644 --- a/.github/workflows/verify-pr279-item45.yml +++ b/.github/workflows/verify-pr279-item45.yml @@ -1,4 +1,4 @@ -name: Verify PR279 Item45 regression +name: Verify PR279 Item45 repair on: push: @@ -10,9 +10,9 @@ permissions: jobs: verify: runs-on: ubuntu-24.04 - timeout-minutes: 10 + timeout-minutes: 15 steps: - - name: Checkout regression exact + - name: Checkout repair exact uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 env: GIT_CONFIG_COUNT: '1' @@ -20,14 +20,25 @@ jobs: GIT_CONFIG_VALUE_0: main with: persist-credentials: false - ref: cd6c5c3ce7cf3cd0da32e79c16d25d2ffd6d0079 + ref: 1f9f37b48cef7cda750274830f56ad06b362dd3f - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 - - name: Verify SARIF provenance upload gate contract + - name: Verify focused SARIF provenance upload gate contract run: | set -euo pipefail node --test packages/commercial-readiness/src/appguardrail-provenance-upload-gate-contract.test.mjs + + - name: Verify complete Commercial Readiness contracts + run: | + set -euo pipefail + node --test packages/commercial-readiness/src/*.test.mjs + + - name: Verify changed source syntax and whitespace + run: | + set -euo pipefail + node --check packages/commercial-readiness/src/appguardrail-provenance-upload-gate-contract.test.mjs + git diff --check From 04ac6bed279ab668704fdec46084bc4bac4e9287 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 17:10:45 +0900 Subject: [PATCH 178/180] chore(ci): retire PR279 Item45 verifier --- .github/workflows/verify-pr279-item45.yml | 44 ----------------------- 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/verify-pr279-item45.yml diff --git a/.github/workflows/verify-pr279-item45.yml b/.github/workflows/verify-pr279-item45.yml deleted file mode 100644 index e2c14d540..000000000 --- a/.github/workflows/verify-pr279-item45.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Verify PR279 Item45 repair - -on: - push: - branches: [fix/appguardrail-sarif-provenance-v1] - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - name: Checkout repair exact - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - env: - GIT_CONFIG_COUNT: '1' - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - with: - persist-credentials: false - ref: 1f9f37b48cef7cda750274830f56ad06b362dd3f - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 22 - - - name: Verify focused SARIF provenance upload gate contract - run: | - set -euo pipefail - node --test packages/commercial-readiness/src/appguardrail-provenance-upload-gate-contract.test.mjs - - - name: Verify complete Commercial Readiness contracts - run: | - set -euo pipefail - node --test packages/commercial-readiness/src/*.test.mjs - - - name: Verify changed source syntax and whitespace - run: | - set -euo pipefail - node --check packages/commercial-readiness/src/appguardrail-provenance-upload-gate-contract.test.mjs - git diff --check From a087f11cac19ac6e09e03e9f285ec0cd01ebe09d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:12:02 +0900 Subject: [PATCH 179/180] chore(stack): inherit checkout-step authority contract --- .../src/node24-actions-contract.test.mjs | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/commercial-development-agent/src/node24-actions-contract.test.mjs b/packages/commercial-development-agent/src/node24-actions-contract.test.mjs index 8dc66b2bb..634591120 100644 --- a/packages/commercial-development-agent/src/node24-actions-contract.test.mjs +++ b/packages/commercial-development-agent/src/node24-actions-contract.test.mjs @@ -38,10 +38,35 @@ function expectReviewedActionPins(path, workflow) { } } +function isDirectStepUses(lines, lineIndex, reviewedAction) { + const line = lines[lineIndex]; + const trimmed = line.trimStart(); + const authority = `uses: ${reviewedAction}`; + if (trimmed !== authority && !trimmed.startsWith(`${authority} #`)) { + return false; + } + + const usesIndent = line.length - trimmed.length; + if (usesIndent < 2) return false; + const stepIndent = usesIndent - 2; + + for (let index = lineIndex - 1; index >= 0; index -= 1) { + const candidate = lines[index]; + if (candidate.trim() === '') continue; + const candidateIndent = candidate.length - candidate.trimStart().length; + if (candidateIndent < stepIndent) return false; + if (candidateIndent === stepIndent) { + return candidate.trimStart().startsWith('- '); + } + } + + return false; +} + function expectCheckoutInitialBranchAuthority(path, workflow) { const lines = workflow.split(String.fromCharCode(10)); const checkoutLineIndexes = lines.flatMap((line, index) => - line.includes(`uses: ${checkoutNode24}`) ? [index] : [], + isDirectStepUses(lines, index, checkoutNode24) ? [index] : [], ); expect(checkoutLineIndexes.length, `${path} checkout count`).toBeGreaterThan( @@ -139,6 +164,26 @@ describe('persistent GitHub Action runtime authority', () => { ).toThrow(); }); + it('rejects a block scalar impersonating an executable checkout step', () => { + const hostileWorkflow = [ + 'steps:', + ' - name: Hostile shell scalar', + ' run: |', + ` uses: ${checkoutNode24}`, + ' env:', + " GIT_CONFIG_COUNT: '1'", + ' GIT_CONFIG_KEY_0: init.defaultBranch', + ' GIT_CONFIG_VALUE_0: main', + ].join(String.fromCharCode(10)); + + expect(() => + expectCheckoutInitialBranchAuthority( + 'hostile-scalar-checkout.yml', + hostileWorkflow, + ), + ).toThrow(); + }); + it('preserves AppGuardrail steps at the scan job boundary', () => { const appguardrail = workflows['.github/workflows/appguardrail.yml']; const stepsLines = appguardrail From e2807eba2710bf5f9cc565f3e7f4f773cd586be8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:25:02 +0900 Subject: [PATCH 180/180] chore(stack): inherit block-scalar checkout authority repair --- .../src/node24-actions-contract.test.mjs | 65 +++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/packages/commercial-development-agent/src/node24-actions-contract.test.mjs b/packages/commercial-development-agent/src/node24-actions-contract.test.mjs index 634591120..fe4ed26ba 100644 --- a/packages/commercial-development-agent/src/node24-actions-contract.test.mjs +++ b/packages/commercial-development-agent/src/node24-actions-contract.test.mjs @@ -38,7 +38,43 @@ function expectReviewedActionPins(path, workflow) { } } +function lineIndent(line) { + return line.length - line.trimStart().length; +} + +function isBlockScalarHeader(line) { + return /(?:^|:\s+|-\s+)[|>](?:[1-9][+-]?|[+-][1-9]?)?\s*(?:#.*)?$/.test( + line.trimStart(), + ); +} + +function isInsideBlockScalar(lines, lineIndex) { + const targetIndent = lineIndent(lines[lineIndex]); + + for (let headerIndex = lineIndex - 1; headerIndex >= 0; headerIndex -= 1) { + const header = lines[headerIndex]; + if (header.trim() === '') continue; + const headerIndent = lineIndent(header); + if (headerIndent >= targetIndent || !isBlockScalarHeader(header)) continue; + + let boundedByHeader = true; + for (let index = headerIndex + 1; index <= lineIndex; index += 1) { + const candidate = lines[index]; + if (candidate.trim() === '') continue; + if (lineIndent(candidate) <= headerIndent) { + boundedByHeader = false; + break; + } + } + if (boundedByHeader) return true; + } + + return false; +} + function isDirectStepUses(lines, lineIndex, reviewedAction) { + if (isInsideBlockScalar(lines, lineIndex)) return false; + const line = lines[lineIndex]; const trimmed = line.trimStart(); const authority = `uses: ${reviewedAction}`; @@ -46,14 +82,14 @@ function isDirectStepUses(lines, lineIndex, reviewedAction) { return false; } - const usesIndent = line.length - trimmed.length; + const usesIndent = lineIndent(line); if (usesIndent < 2) return false; const stepIndent = usesIndent - 2; for (let index = lineIndex - 1; index >= 0; index -= 1) { const candidate = lines[index]; if (candidate.trim() === '') continue; - const candidateIndent = candidate.length - candidate.trimStart().length; + const candidateIndent = lineIndent(candidate); if (candidateIndent < stepIndent) return false; if (candidateIndent === stepIndent) { return candidate.trimStart().startsWith('- '); @@ -75,14 +111,14 @@ function expectCheckoutInitialBranchAuthority(path, workflow) { for (const checkoutLineIndex of checkoutLineIndexes) { const checkoutLine = lines[checkoutLineIndex]; - const usesIndent = checkoutLine.length - checkoutLine.trimStart().length; + const usesIndent = lineIndent(checkoutLine); const stepIndent = Math.max(0, usesIndent - 2); let stepEnd = lines.length; for (let index = checkoutLineIndex + 1; index < lines.length; index += 1) { const candidate = lines[index]; if (candidate.trim() === '') continue; - const candidateIndent = candidate.length - candidate.trimStart().length; + const candidateIndent = lineIndent(candidate); if ( candidateIndent === stepIndent && candidate.trimStart().startsWith('- ') @@ -184,6 +220,27 @@ describe('persistent GitHub Action runtime authority', () => { ).toThrow(); }); + it('rejects a nested scalar sequence impersonating a checkout step boundary', () => { + const hostileWorkflow = [ + 'steps:', + ' - name: Hostile shell scalar', + ' run: |', + ' - fake checkout', + ` uses: ${checkoutNode24}`, + ' env:', + " GIT_CONFIG_COUNT: '1'", + ' GIT_CONFIG_KEY_0: init.defaultBranch', + ' GIT_CONFIG_VALUE_0: main', + ].join(String.fromCharCode(10)); + + expect(() => + expectCheckoutInitialBranchAuthority( + 'hostile-nested-scalar-checkout.yml', + hostileWorkflow, + ), + ).toThrow(); + }); + it('preserves AppGuardrail steps at the scan job boundary', () => { const appguardrail = workflows['.github/workflows/appguardrail.yml']; const stepsLines = appguardrail