Skip to content
66 changes: 45 additions & 21 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
# pull_request workflows upload to refs/pull/N/merge, so no single ref ever holds
# all tools. Bundling at the workflow/check level is ref-independent.
#
# NOTE on dependency-review: dependency graph can be unavailable on some repos.
# Treat that as "not enforceable here" instead of making the required workflow
# unsatisfiable; keep medium-or-higher dependency findings hard-failing where the
# API is supported.
# NOTE on dependency-review: unavailable evidence is not a clean result. Only
# an exact base/head comparison returning HTTP 200 may reach the pinned hard
# gate. Every other probe outcome fails closed without printing the response
# body. Diagnostics include allowlisted repository visibility. See
# docs/doctoring/dependency-review-fail-closed.md.
#
# NOTE on trivy-fs: it scans the whole repo, so a pre-existing FIXABLE
# MEDIUM/HIGH/CRITICAL finding blocks every PR in that repo until it is fixed.
Expand Down Expand Up @@ -257,9 +258,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 All @@ -268,36 +271,57 @@ jobs:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPOSITORY: ${{ github.repository }}
REPOSITORY_VISIBILITY: ${{ github.event.repository.visibility }}
run: |
set -euo pipefail

api_url="${GITHUB_API_URL:-https://api.github.com}"
response_file="$(mktemp)"
case "${REPOSITORY_VISIBILITY}" in
public|private|internal) visibility="${REPOSITORY_VISIBILITY}" ;;
*) visibility="unknown" ;;
esac
git_object_id='^[0-9a-f]{40}([0-9a-f]{24})?$'
repository_identity='^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'
if ! [[ "${BASE_SHA}" =~ $git_object_id ]] || ! [[ "${HEAD_SHA}" =~ $git_object_id ]]; then
echo "::error::Dependency review evidence unavailable for the allowlisted repository (visibility ${visibility}): exact 40- or 64-character hexadecimal base and head revisions are required before any compare request. Named refs are not evidence. Verify the pull-request event SHAs, then rerun. Failing closed."
exit 1
fi
if ! [[ "${REPOSITORY}" =~ $repository_identity ]]; then
echo "::error::Dependency review evidence unavailable (visibility ${visibility}): owner/name repository identity is required before any compare request. Verify the pull-request repository, then rerun. Failing closed."
exit 1
fi
repository_owner="${REPOSITORY%%/*}"
repository_name="${REPOSITORY#*/}"
if [ "${repository_owner}" = "." ] || [ "${repository_owner}" = ".." ] || [ "${repository_name}" = "." ] || [ "${repository_name}" = ".." ]; then
echo "::error::Dependency review evidence unavailable (visibility ${visibility}): owner/name repository identity is required before any compare request. Dot or parent-directory path components are not evidence. Verify the pull-request repository, then rerun. Failing closed."
exit 1
fi
set +e
status="$(
curl -fsS -o "$response_file" -w '%{http_code}' \
curl -sS --connect-timeout 10 --max-time 30 \
-o /dev/null \
-w '%{http_code}' \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"${api_url}/repos/${REPOSITORY}/dependency-graph/compare/${BASE_SHA}...${HEAD_SHA}" \
|| true
"${api_url}/repos/${REPOSITORY}/dependency-graph/compare/${BASE_SHA}...${HEAD_SHA}"
)"
curl_status=$?
set -e

if [ "$status" = "200" ]; then
echo "supported=true" >>"$GITHUB_OUTPUT"
exit 0
fi
case "$status" in
000|"") http_status="unavailable" ;;
[0-9][0-9][0-9]) http_status="$status" ;;
*) http_status="malformed" ;;
esac

if [ "$status" = "403" ] || [ "$status" = "404" ]; then
echo "::warning::Dependency review is unavailable for ${REPOSITORY}; skipping dependency-review hard gate."
echo "supported=false" >>"$GITHUB_OUTPUT"
exit 0
if [ "$curl_status" -ne 0 ] || [ "$http_status" != "200" ]; then
echo "::error::Dependency review evidence unavailable for ${REPOSITORY} (visibility ${visibility}) at exact base ${BASE_SHA} and head ${HEAD_SHA}: HTTP ${http_status}; curl exit ${curl_status}. Verify dependency-graph/security configuration and GitHub service behavior, then rerun. Failing closed."
exit 1
fi

echo "::error::Dependency review support check failed with HTTP ${status}."
cat "$response_file"
exit 1
echo "supported=true" >>"$GITHUB_OUTPUT"
- name: Dependency review
if: steps.dependency_review_support.outputs.supported == 'true'
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: moderate
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include (
Conflict-scope roots fail closed when the immediate parent directory is a symbolic link.
OriginWeave hourly NVIDIA NIM repair is a thin caller at minute 10. See [`docs/doctoring/originweave-hourly-review-caller.md`](docs/doctoring/originweave-hourly-review-caller.md).
nonnest2 hourly NVIDIA NIM repair is a thin caller at minute 16. See [`docs/doctoring/nonnest2-hourly-review-caller.md`](docs/doctoring/nonnest2-hourly-review-caller.md).
Dependency-review unavailability fails closed; diagnostics include allowlisted repository visibility. Named refs, non-`owner/name` repository values, and `.`/`..` path components are rejected before the compare request. See [`docs/doctoring/dependency-review-fail-closed.md`](docs/doctoring/dependency-review-fail-closed.md).
30 changes: 30 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,36 @@ sequenceDiagram
- Rust remains the psychometric arithmetic owner. Repair never substitutes
Python for scoring math.

## Dependency-review evidence

The central `Security Scan` job treats GitHub's exact `BASE_SHA...HEAD_SHA`
comparison as a hard supply-chain evidence boundary. The probe rejects named
refs, non-`owner/name` repository values, and `.`/`..` path components before
it calls the compare API, because GitHub would otherwise resolve `main` to a
moving HEAD and RFC 3986 would collapse `../.github` out of `/repos`. Only an
exact 40- or 64-character hexadecimal object ID pair plus transport exit `0`
plus HTTP `200` may reach the immutably pinned dependency-review action. A
`403`, `404`, timeout, truncated transfer, curl `000` sentinel, or malformed
status fails closed and records allowlisted repository visibility with the
exact revisions. Other scanners are complementary; they are not substitutes.

```mermaid
flowchart TD
Identity{"owner/name, no . or .., and 40- or 64-hex SHAs?"}
Probe["Exact base/head compare probe"]
Transport{"curl exit 0 and HTTP 200?"}
Action["Pinned dependency-review action"]
Fail["Fail closed with repo, visibility, SHAs, status"]

Identity -->|"yes"| Probe
Identity -->|"no"| Fail
Probe --> Transport
Transport -->|"yes"| Action
Transport -->|"no"| Fail
```

See [`docs/doctoring/dependency-review-fail-closed.md`](docs/doctoring/dependency-review-fail-closed.md).

## Quality gates

`scripts/ci/` ships with 100% statement/branch coverage and 100% docstrings.
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ Semantic Versioning where the repository publishes a release.
### Security

- Keep the Quarantine Sandbox Runtime caller read-only and model-secret-free, grant only job-scoped OIDC to the reusable scheduler, and preserve the product boundary in which the sandbox returns artifact-analysis evidence while hosts retain WAF/IDS, admission, final verdict, incident, and retention authority.
- Fail closed when GitHub dependency-review evidence is unavailable (non-200, transport failure, truncated compare, or curl's `000` no-status sentinel) instead of treating HTTP 403/404 as a clean skip; the probe records allowlisted repository visibility with the exact head SHA and never prints the API body or raw visibility strings.
- Reject named Git revisions, non-`owner/name` repository values, and `.`/`..` path components before the dependency-review compare request so GitHub cannot resolve `main` to a moving HEAD and so RFC 3986 remove-dot-segments cannot turn `../.github` into a compare URL. The organization `.github` special repository remains a legal name.

- Reject `.github/` and `scripts/ci/` from review-thread-derived autofix path authority so an untrusted inline reviewer cannot authorize the write-capable repair agent to modify workflows, CODEOWNERS, actions, scheduler code, or CI helpers that govern its own control plane.
- Require the model-write snapshot and exact-path allowlist to remain outside the pull-request worktree, checking both absolute and resolved locations so repository-local controls and outside-looking symlinks resolving into the repository fail closed before they can authorize or verify model changes.
- Snapshot the complete pre-model worktree for ordinary and conflict repair and reject every model-caused created, deleted, modified, mode-changed, retargeted, ignored, dangling, directory-backed, external-link, metadata-race, or out-of-scope path before staging or push.
Expand Down
78 changes: 78 additions & 0 deletions docs/doctoring/dependency-review-fail-closed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Dependency review fail-closed operations

Status: `active_pr` until the matching workflow and regression contract are present on protected `main`; thereafter `implemented_on_protected_main`.

## Decision

Dependency review is a hard supply-chain gate. The central workflow accepts only HTTP `200` from GitHub's exact `BASE_SHA...HEAD_SHA` comparison before invoking the immutably pinned dependency-review action. A `403`, `404`, `400`, `500`, `503`, empty or malformed status, curl `000` sentinel, timeout, transport failure, truncated exchange, or other unexpected outcome is unavailable evidence and fails closed.

The support probe has a 10-second connection limit and 30-second total limit. It preserves curl's transport exit code separately from the bounded HTTP status and requires transport exit `0` plus exact HTTP `200`. It discards the response body and logs only repository identity, allowlisted visibility (`public`, `private`, `internal`, or `unknown`), exact base/head revisions, the normalized HTTP status, and the numeric transport exit. Credentials, response bodies, raw untrusted visibility strings, named refs, and raw invalid repository paths are never diagnostic output. After a successful probe the pinned action is not independently skippable.

Before any compare request, the probe requires an `owner/name` repository identity and exact Git object IDs: 40 hexadecimal characters for SHA-1 or 64 hexadecimal characters for SHA-256 (Chacon & Straub, 2014; National Institute of Standards and Technology, 2015). GitHub's compare API resolves named revisions such as `main` to the current HEAD of that name (GitHub, n.d.). That moving target is not the pull-request head and is unavailable evidence. A single-slash `owner/name` whose owner or name is `.` or `..` is still refused: RFC 3986 remove-dot-segments would turn `/repos/../.github/...` into `/.github/...` (Berners-Lee et al., 2005; MITRE, 2026a, 2026b). The organization `.github` special repository remains a legal name; only the path-segment sentinels are rejected. Rejecting these values before interpolation prevents path injection into `/repos/{owner}/{repo}/dependency-graph/compare/{basehead}`.

RFC 9110 §15.3.1 defines `200` as a completed successful representation, not as a status that can be inferred after a truncated transfer (Fielding et al., 2022). curl's `%{http_code}` write-out is the numeric status from the last retrieved transfer; when no HTTP status was received it emits `000` (Stenberg, n.d.). That sentinel is unavailable evidence, not an HTTP status. NIST SP 800-53 Rev. 5 RA-5 and SA-12 require that vulnerability and supply-chain evidence be obtained, not assumed absent (National Institute of Standards and Technology, 2020). SLSA v1.0 likewise treats missing provenance as unverified rather than passing (SLSA, 2023). An HTTP `403` or `404` is therefore unavailable evidence, not a clean skip. GitHub documents `403` as the private-repository response when GitHub Advanced Security is not enabled, or when the comparison targets a fork (GitHub, n.d.). Record the allowlisted visibility and exact revisions, then verify dependency-graph or Advanced Security configuration. Do not infer `not-applicable` from `403`.

## Identity and authority

The dependency-review job checks out the pull request's explicit head repository and immutable head SHA with persisted credentials disabled. The API comparison independently binds the event's exact base and head Git object IDs after those values pass the hexadecimal length check. The job retains `contents: read` and `pull-requests: read`; it receives no write, OIDC, model, release, package, or deployment authority.

Checks, status contexts, review submissions, and merge authorization remain separate evidence classes. OSV, Trivy, CodeQL, Semgrep, Secret Scan, Scorecard, and Dependabot are complementary controls and are not semantic substitutes for dependency review.

## Failure classification and remediation

- Identity rejected (named ref, empty or non-hex revision, non-`owner/name` repository, or a `.`/`..` path component): fail the job before curl. Use the pull-request event's exact hexadecimal SHAs and `owner/name`, then rerun. Do not retry a named ref or a dotted path component.
- Transport exit `0` plus HTTP `200`: proceed to the pinned dependency-review action.
- Any other result: fail the job and retain exact repository, allowlisted visibility, base/head, status, and transport-exit evidence. An HTTP `200` emitted by a failed or partial transfer is unavailable evidence. curl `000` is recorded as `unavailable`. Do not infer a root cause from HTTP `403` or `404`.
- Public repository failure: verify dependency graph and security configuration, organization policy, token read access, and GitHub service health.
- HTTP `403` on a private or internal repository: verify whether GitHub Advanced Security / dependency review is entitled for that repository. Keep the job failed until a separately reviewed organization exception with compensating controls exists. Never infer `not-applicable` from an unavailable response.

Retries are operator-initiated only after the capability or service condition changes. Do not rerun unchanged evidence repeatedly and do not convert an unavailable endpoint into a green skip.

## Known canary

ContextualWisdomLab/EgressWeave#66, Security Scan run `31108241013`, job `92638903658`, compared `10d0c51daf2ad278d66f43be479df8cf6b08ba6d...c038a9509d1a8eae8561cc9081e67e12bd373d42` and received HTTP `403`. The required workflow printed the skip warning, omitted `actions/dependency-review-action`, and still concluded success. Downstream tracking: ContextualWisdomLab/EgressWeave#76. Keep ContextualWisdomLab/.github#810 open until a protected-main public consumer run proves a non-200 or failed-transfer comparison cannot green this job.

## Acceptance and rollback

Acceptance requires the permanent queue contract to reject the former `supported=false` path, require bounded probing and discarded bodies, require exact-head checkout, reject named refs, non-`owner/name` repository values, and `.`/`..` path components before any compare request, and prove that only `200` reaches the action. Exact-head CI/security evidence, current review, protected integration, and a real protected-main consumer run remain required.

Rollback requires an independently reviewed revert and fresh exact-head evidence. A rollback must not restore the `403`/`404` success path, accept named revisions as compare evidence, or print an API response body.

## References

Berners-Lee, T., Fielding, R., & Masinter, L. (2005). *Uniform Resource
Identifier (URI): Generic syntax* (RFC 3986). Internet Engineering Task
Force. https://doi.org/10.17487/RFC3986

Chacon, S., & Straub, B. (2014). *Pro Git* (2nd ed.). Apress.
https://git-scm.com/book/en/v2/Git-Internals-Git-Objects

Fielding, R., Nottingham, M., & Reschke, J. (Eds.). (2022). *HTTP semantics*
(RFC 9110). Internet Engineering Task Force. https://doi.org/10.17487/RFC9110

GitHub. (n.d.). *Dependency review*. GitHub Docs. Retrieved August 9, 2026, from https://docs.github.com/en/code-security/concepts/supply-chain-security/dependency-review

GitHub. (n.d.). *REST API endpoints for dependency review*. GitHub Docs. Retrieved August 16, 2026, from https://docs.github.com/en/rest/dependency-graph/dependency-review

GitHub. (n.d.). *Dependency graph*. GitHub Docs. Retrieved August 9, 2026, from https://docs.github.com/en/code-security/concepts/supply-chain-security/dependency-graph

GitHub. (n.d.). *Webhook events and payloads*. GitHub Docs. Retrieved August 16, 2026, from https://docs.github.com/en/webhooks/webhook-events-and-payloads#repository

MITRE. (2026a). *CWE-20: Improper input validation*.
https://cwe.mitre.org/data/definitions/20.html

MITRE. (2026b). *CWE-22: Improper limitation of a pathname to a restricted
directory ('Path Traversal')*.
https://cwe.mitre.org/data/definitions/22.html

National Institute of Standards and Technology. (2015). *Secure hash
standard (SHS)* (FIPS 180-4). https://doi.org/10.6028/NIST.FIPS.180-4

National Institute of Standards and Technology. (2020). *Security and
privacy controls for information systems and organizations* (NIST SP
800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5

SLSA. (2023). *SLSA v1.0: Supply-chain Levels for Software Artifacts*.
Open Source Security Foundation. https://slsa.dev/spec/v1.0/

Stenberg, D. (n.d.). *curl -- write out variables*. curl. Retrieved August 16, 2026, from https://curl.se/docs/manpage.html
Loading
Loading