From 6e20ef33c664431de36be077c92a6df215f71f71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:52:58 +0900 Subject: [PATCH 1/7] test(security): require literal PR-head scanner checkout --- tests/test_security_scan_exact_head.py | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_security_scan_exact_head.py diff --git a/tests/test_security_scan_exact_head.py b/tests/test_security_scan_exact_head.py new file mode 100644 index 000000000..3b6e34a57 --- /dev/null +++ b/tests/test_security_scan_exact_head.py @@ -0,0 +1,57 @@ +"""Exact-head contracts for the organization security scanner workflow.""" + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "security-scan.yml" + + +def workflow_job(workflow: str, job_name: str) -> str: + """Return one top-level job block from the central security workflow. + + The workflow uses two-space-indented job identifiers. Normalizing line + endings keeps this contract deterministic on Windows and Unix checkouts. + """ + + normalized = workflow.replace("\r\n", "\n").replace("\r", "\n") + marker = f"\n {job_name}:\n" + start = normalized.index(marker) + len(marker) + remaining = normalized[start:] + candidates = [ + offset + for line in remaining.splitlines(keepends=True) + if (offset := remaining.find(line)) >= 0 + and line.startswith(" ") + and not line.startswith(" ") + and line.rstrip().endswith(":") + ] + if not candidates: + return remaining + first = min(offset for offset in candidates if offset > 0) + return remaining[:first] + + +def test_repository_scanners_checkout_the_literal_pull_request_head() -> None: + """Trivy and Scorecard must never scan GitHub's synthetic merge ref.""" + + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + exact_repository = "repository: ${{ github.event.pull_request.head.repo.full_name }}" + exact_head = "ref: ${{ github.event.pull_request.head.sha }}" + + for job_name in ("trivy-fs", "scorecard"): + job = workflow_job(workflow, job_name) + assert exact_repository in job + assert exact_head in job + assert "persist-credentials: false" in job + + +def test_dependency_review_checkout_is_bound_to_the_same_exact_head() -> None: + """Supporting checkout evidence must match the API comparison head.""" + + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + job = workflow_job(workflow, "dependency-review") + + assert "repository: ${{ github.event.pull_request.head.repo.full_name }}" in job + assert "ref: ${{ github.event.pull_request.head.sha }}" in job + assert "HEAD_SHA: ${{ github.event.pull_request.head.sha }}" in job From 3a1f49a6d5e07b2a3aedd42627579d3e4d3213ce Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:55:43 +0900 Subject: [PATCH 2/7] ci(security): execute exact-head scanner contract --- .../security-scan-exact-head-quality-ci.yml | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/security-scan-exact-head-quality-ci.yml diff --git a/.github/workflows/security-scan-exact-head-quality-ci.yml b/.github/workflows/security-scan-exact-head-quality-ci.yml new file mode 100644 index 000000000..67169bf7e --- /dev/null +++ b/.github/workflows/security-scan-exact-head-quality-ci.yml @@ -0,0 +1,37 @@ +name: Security Scan Exact-Head Quality CI + +on: + pull_request: + paths: + - ".github/workflows/security-scan.yml" + - ".github/workflows/security-scan-exact-head-quality-ci.yml" + - "tests/test_security_scan_exact_head.py" + +concurrency: + group: security-scan-exact-head-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + exact-head-contract: + runs-on: ubuntu-24.04 + steps: + - name: Checkout literal pull request head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + - name: Compile exact-head contract + run: python3 -m py_compile tests/test_security_scan_exact_head.py + - name: Execute dependency-free exact-head contract + run: | + python3 - <<'PY' + from tests import test_security_scan_exact_head as contract + + contract.test_repository_scanners_checkout_the_literal_pull_request_head() + contract.test_dependency_review_checkout_is_bound_to_the_same_exact_head() + print("security scan exact-head contract passed") + PY From e735feb779dd20bc1dc20d40d1df60d1cd7d3dd3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:56:47 +0900 Subject: [PATCH 3/7] test(security): require literal-head SARIF attribution --- tests/test_security_scan_sarif_exact_head.py | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/test_security_scan_sarif_exact_head.py diff --git a/tests/test_security_scan_sarif_exact_head.py b/tests/test_security_scan_sarif_exact_head.py new file mode 100644 index 000000000..78f3e8bc3 --- /dev/null +++ b/tests/test_security_scan_sarif_exact_head.py @@ -0,0 +1,40 @@ +"""Durable exact-head SARIF contracts for central repository scanners.""" + +from pathlib import Path + + +WORKFLOW_PATH = ( + Path(__file__).resolve().parents[1] + / ".github" + / "workflows" + / "security-scan.yml" +) + + +def _job_block(workflow: str, job_name: str) -> str: + """Return one two-space-indented GitHub Actions job block.""" + + normalized = workflow.replace("\r\n", "\n").replace("\r", "\n") + marker = f"\n {job_name}:\n" + start = normalized.index(marker) + len(marker) + remaining = normalized[start:] + offset = 0 + for line in remaining.splitlines(keepends=True): + if offset and line.startswith(" ") and not line.startswith(" "): + if line.rstrip().endswith(":"): + return remaining[:offset] + offset += len(line) + return remaining + + +def test_repository_scanner_sarif_is_attributed_to_the_literal_head() -> None: + """Trivy and Scorecard SARIF must identify the exact scanned head SHA.""" + + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + expected_ref = "ref: refs/pull/${{ github.event.pull_request.number }}/head" + expected_sha = "sha: ${{ github.event.pull_request.head.sha }}" + + for job_name in ("trivy-fs", "scorecard"): + job = _job_block(workflow, job_name) + assert expected_ref in job + assert expected_sha in job From 3a1a026e11ab539eb0f418174a5e6467ee115074 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:58:04 +0900 Subject: [PATCH 4/7] fix(security): scan and publish literal PR-head evidence --- .github/workflows/security-scan.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index c3b8fa5db..4314ad3f1 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -257,9 +257,11 @@ jobs: contents: read pull-requests: read steps: - - name: Checkout + - name: Checkout exact head uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - name: Check dependency review support id: dependency_review_support @@ -311,9 +313,11 @@ jobs: security-events: write actions: read steps: - - name: Checkout + - name: Checkout exact head uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - name: Trivy filesystem scan uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0 @@ -387,6 +391,8 @@ jobs: with: sarif_file: trivy-results.sarif category: trivy-fs + ref: refs/pull/${{ github.event.pull_request.number }}/head + sha: ${{ github.event.pull_request.head.sha }} wait-for-processing: false - name: Report Trivy SARIF upload failure if: steps.upload_trivy_sarif.outcome == 'failure' @@ -403,9 +409,11 @@ jobs: contents: read actions: read steps: - - name: Checkout + - name: Checkout exact head uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - name: Run Scorecard uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 @@ -461,6 +469,8 @@ jobs: with: sarif_file: results.sarif category: scorecard + ref: refs/pull/${{ github.event.pull_request.number }}/head + sha: ${{ github.event.pull_request.head.sha }} wait-for-processing: false - name: Report Scorecard SARIF upload failure if: steps.upload_scorecard_sarif.outcome == 'failure' From ad2d960a75a7bf61c61211f9ea324e3560cad415 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:58:31 +0900 Subject: [PATCH 5/7] ci(security): verify exact-head checkout and SARIF contracts --- .../security-scan-exact-head-quality-ci.yml | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/security-scan-exact-head-quality-ci.yml b/.github/workflows/security-scan-exact-head-quality-ci.yml index 67169bf7e..e0b30fb16 100644 --- a/.github/workflows/security-scan-exact-head-quality-ci.yml +++ b/.github/workflows/security-scan-exact-head-quality-ci.yml @@ -6,6 +6,7 @@ on: - ".github/workflows/security-scan.yml" - ".github/workflows/security-scan-exact-head-quality-ci.yml" - "tests/test_security_scan_exact_head.py" + - "tests/test_security_scan_sarif_exact_head.py" concurrency: group: security-scan-exact-head-${{ github.event.pull_request.number }} @@ -24,14 +25,19 @@ jobs: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false - - name: Compile exact-head contract - run: python3 -m py_compile tests/test_security_scan_exact_head.py - - name: Execute dependency-free exact-head contract + - name: Compile exact-head contracts + run: >- + python3 -m py_compile + tests/test_security_scan_exact_head.py + tests/test_security_scan_sarif_exact_head.py + - name: Execute dependency-free exact-head contracts run: | python3 - <<'PY' - from tests import test_security_scan_exact_head as contract + from tests import test_security_scan_exact_head as checkout_contract + from tests import test_security_scan_sarif_exact_head as sarif_contract - contract.test_repository_scanners_checkout_the_literal_pull_request_head() - contract.test_dependency_review_checkout_is_bound_to_the_same_exact_head() - print("security scan exact-head contract passed") + checkout_contract.test_repository_scanners_checkout_the_literal_pull_request_head() + checkout_contract.test_dependency_review_checkout_is_bound_to_the_same_exact_head() + sarif_contract.test_repository_scanner_sarif_is_attributed_to_the_literal_head() + print("security scan exact-head contracts passed") PY From d6d067ad29c7bd09f07e7792b3d52d25975ba86e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:59:10 +0900 Subject: [PATCH 6/7] docs(security): record literal-head scanner evidence --- docs/doctoring/security-scan-exact-head.md | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/doctoring/security-scan-exact-head.md diff --git a/docs/doctoring/security-scan-exact-head.md b/docs/doctoring/security-scan-exact-head.md new file mode 100644 index 000000000..dedad3a41 --- /dev/null +++ b/docs/doctoring/security-scan-exact-head.md @@ -0,0 +1,49 @@ +# Security scan exact-head evidence + +## Decision + +The central `Security Scan` workflow treats the literal pull-request head as the only valid repository-scanner input. GitHub's `pull_request` event normally exposes a generated merge revision through `GITHUB_SHA`; that revision is useful for integration testing but cannot prove that Trivy or Scorecard scanned the exact current contributor head required by CWL authorization policy. + +The dependency-review support checkout, Trivy filesystem scan, and Scorecard posture scan therefore set both: + +```yaml +repository: ${{ github.event.pull_request.head.repo.full_name }} +ref: ${{ github.event.pull_request.head.sha }} +``` + +Persisted checkout credentials remain disabled. Fork pull requests are read through their explicit head repository and immutable commit SHA; no write credential is added. + +## Durable SARIF identity + +Scanning the head is insufficient when durable code-scanning evidence is attributed to a different revision. Trivy and Scorecard uploads explicitly bind: + +```yaml +ref: refs/pull/${{ github.event.pull_request.number }}/head +sha: ${{ github.event.pull_request.head.sha }} +``` + +GitHub's code-scanning API requires both a full Git reference and the commit SHA to which an uploaded analysis relates. The pair above states that the SARIF describes the pull-request head, not the generated merge commit. + +## Preserved security behavior + +This change does not alter scanner versions, vulnerability severities, Trivy's fixable Medium-or-higher hard gate, dependency-review thresholds, Scorecard's soft posture role, SARIF sanitation, permissions, or the existing OSV base-versus-head comparison. It only makes scanner input and result identity consistent. + +The workflow remains fail closed for absent scanner output and actionable findings. SARIF upload failures remain separately visible without suppressing the repository-local Trivy finding gate. A queued, cancelled, skipped, failed, missing, or predecessor-head run is not current-head evidence. + +## Verification + +`tests/test_security_scan_exact_head.py` verifies literal-head checkout for all three affected jobs. `tests/test_security_scan_sarif_exact_head.py` verifies durable Trivy and Scorecard SARIF attribution. The dedicated read-only quality workflow checks out the literal PR head, compiles both contracts, and executes them without package installation. + +The initiating DiskSage evidence was Security Scan run `31070907732`, whose Trivy job log checked out `refs/remotes/pull/137/merge` rather than DiskSage PR #137 head `87ac0e08cceed3d1a766da13a8f8123912178192`. That result remains historical merge-tree evidence and is not reclassified as exact-head proof. + +## Rollback + +Rollback requires an independently reviewed revert and fresh exact-head security evidence. Do not restore implicit checkout or automatic SARIF revision detection unless an equally strict mechanism proves that the scanned filesystem, SARIF `ref`, and SARIF `sha` all identify the same current pull-request head. + +## APA 7th references + +GitHub. (n.d.). *Events that trigger workflows*. GitHub Docs. Retrieved August 6, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows + +GitHub. (n.d.). *REST API endpoints for code scanning*. GitHub Docs. Retrieved August 6, 2026, from https://docs.github.com/en/rest/code-scanning/code-scanning + +GitHub. (n.d.). *Uploading CodeQL analysis results to GitHub*. GitHub Docs. Retrieved August 6, 2026, from https://docs.github.com/en/enterprise-cloud@latest/code-security/tutorials/customize-code-scanning/upload-results From aee317b3ecbfaad7cdb3898e603dfe5e69b8f7ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 6 Aug 2026 15:59:29 +0900 Subject: [PATCH 7/7] docs: record exact-head security scanner repair --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e601de81b..9b4e84e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,5 +12,6 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Bound dependency-review support, Trivy, and Scorecard checkouts to the literal pull-request head repository and SHA; bound Trivy and Scorecard SARIF uploads to the matching `refs/pull//head` identity; and added permanent dependency-free exact-head regression evidence. - Bound both trusted-uv quality jobs to `github.event.pull_request.head.sha` and added a permanent two-checkout regression contract so exact-head compatibility, coverage, docstring, and compilation claims cannot silently measure GitHub's generated pull-request merge revision. - Made Strix treat only a single LiteLLM provider-error line containing NVIDIA NIM context and model-catalog 404 evidence as cross-model fallback evidence, rejecting cross-line signal assembly and provider-like target source literals; moved the public default to Nemotron 3 Super 120B and added a second NVIDIA hosted candidate before GitHub Models without neutralizing reported vulnerabilities.