Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/security-scan-exact-head-quality-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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"
- "tests/test_security_scan_sarif_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 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 checkout_contract
from tests import test_security_scan_sarif_exact_head as sarif_contract

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
16 changes: 13 additions & 3 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<number>/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.
49 changes: 49 additions & 0 deletions docs/doctoring/security-scan-exact-head.md
Original file line number Diff line number Diff line change
@@ -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
57 changes: 57 additions & 0 deletions tests/test_security_scan_exact_head.py
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions tests/test_security_scan_sarif_exact_head.py
Original file line number Diff line number Diff line change
@@ -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
Loading