Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
cb07b8b
test(security): require non-200 dependency review fail-closed
seonghobae Sep 2, 2026
31f60e5
fix(security): fail closed on dependency graph non-200
seonghobae Sep 2, 2026
ee0f1ce
test(security): require caller permission envelope
seonghobae Sep 2, 2026
ca3bdbd
fix(security): preserve dependency-review caller permissions
seonghobae Sep 2, 2026
6d288ff
docs(security): record dependency-review authority boundary
seonghobae Sep 2, 2026
403ca1c
merge(main): preserve concurrent control-plane repairs
seonghobae Sep 2, 2026
ca7cb12
merge(main): preserve dependency-review control-plane fixes
seonghobae Sep 2, 2026
2595e24
test(security): preserve latest reusable dependency-review contract
seonghobae Sep 2, 2026
754121d
merge(main): preserve dependency-review owner and current controls
seonghobae Sep 2, 2026
3736634
test(security): require immutable dependency-review identities
seonghobae Sep 2, 2026
b1e6263
fix(security): validate immutable dependency-review identity
seonghobae Sep 2, 2026
8b86c0d
fix(security): carry exact identity validation into bundled scan
seonghobae Sep 2, 2026
ae12837
docs(security): record authenticated dependency-review evidence
seonghobae Sep 2, 2026
58a0b4c
test(security): preserve bundled dependency-review identity gate
seonghobae Sep 2, 2026
6e8998f
Merge branch 'main' into fix/dependency-review-non200-fail-closed
seonghobae Sep 3, 2026
04aad03
fix(security): reconcile immutable compare preflight with protected main
seonghobae Sep 6, 2026
c2e8ab0
fix(security): repair restack workflow composition
seonghobae Sep 6, 2026
0bb8f7c
chore(security): adopt protected main directly
seonghobae Sep 7, 2026
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
78 changes: 46 additions & 32 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Reusable Dependency Review (workflow_call), consolidating the near-
# identical dependency-review.yml files argos, mightyETL, newsdom-api,
# scopeweave, and naruon each carried independently. See
# identical dependency-review.yml files argos, mightyETL, naruon, newsdom-api,
# and scopeweave each carried independently. See
# docs/adr/0024-dependency-review-reusable-workflow-consolidation.md and
# docs/doctoring/dependency-review-reusable-workflow-consolidation.md for the
# per-repo field audit behind these inputs.
Expand All @@ -9,34 +9,41 @@
# calling repo's own thin workflow file -- a workflow_call target cannot also
# be the thing GitHub triggers directly on pull_request.
#
# Dependency Review requires GitHub Dependency Graph (and, on private repos
# without GitHub Advanced Security, it is unavailable regardless of a repo's
# own settings). scopeweave's original workflow already detected this
# dynamically via the dependency-graph compare API instead of assuming from
# public/private repository status (mightyETL's original approach, which is
# wrong for a private repo that does have GHAS). This reusable workflow
# adopts the dynamic detection as the common, more-correct behavior for
# every caller, so no per-repo public/private input is needed.
# Dependency Review requires a successful Dependency Graph comparison for the
# exact pull-request base/head. Repository visibility is not a sufficient
# capability signal, and HTTP 403/404 are not safe availability signals:
# GitHub can use those statuses for authorization/policy denials as well as
# unavailable resources. For a pull_request, only exact immutable base/head
# object IDs plus a valid owner/name repository identity may reach transport,
# and only HTTP 200 authorizes running the pinned action. Every non-200
# comparison fails closed. Non-pull_request triggers may skip because they do
# not carry the exact PR base/head identity.
#
# Reusable-workflow permissions can only be maintained or reduced through the
# call chain. Every thin caller therefore must declare at least `contents:
# read` and `pull-requests: read`; this workflow cannot elevate a caller token
# that omitted those scopes.
#
# Example caller (.github/workflows/dependency-review.yml in a product repo).
# Pin `uses:` to this file's exact commit SHA, not @main: an unpinned mutable
# ref would run an unreviewed central change against every PR check in the
# calling repo (Devin flagged this on the first four callers; fixed in all of
# them). If the calling repo's branch protection requires a status check
# literally named after the old standalone job, converting to `uses:` here
# will rename the published check to "<calling job> / dependency-review" and
# silently break that required check -- update the branch protection's
# required-check name to match before or immediately after merging a caller.
# Pin `uses:` to the exact protected-main commit SHA carrying this workflow,
# never `@main`: a mutable central ref could run an unreviewed workflow change
# against the caller's PR checks. If branch protection requires the old
# standalone job name, note that reusable workflow adoption publishes the
# combined check name `<calling job> / dependency-review`; update the required
# check name to the exact published context without weakening the gate.
#
# name: Dependency Review
# on:
# pull_request:
# permissions:
# contents: read
# pull-requests: read
# concurrency:
# group: dependency-review-${{ github.event.pull_request.number || github.ref }}
# cancel-in-progress: true
# jobs:
# dependency-review:
# uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@<commit-sha>
# uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@<protected-main-commit-sha>
# with:
# fail_on_severity: high
# allow_ghsas: "GHSA-69w3-r845-3855"
Expand Down Expand Up @@ -121,6 +128,23 @@ jobs:
exit 0
fi

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: 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: 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: 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

api_url="${GITHUB_API_URL:-https://api.github.com}"
response_file="$(mktemp)"
status="$(
Expand All @@ -137,14 +161,10 @@ jobs:
exit 0
fi

if [ "$status" = "403" ] || [ "$status" = "404" ]; then
echo "::warning::Dependency graph compare returned HTTP ${status} for ${REPOSITORY}; skipping the dependency-review hard gate (GitHub Dependency Graph, or GitHub Advanced Security on a private repository, is unavailable)."
echo "available=false" >>"$GITHUB_OUTPUT"
exit 0
echo "::error::Dependency graph comparison failed with HTTP ${status}. For a pull_request, non-200 responses are ambiguous between feature availability and authorization/policy/transport failure, so Dependency Review fails closed instead of being skipped."
if [ -s "$response_file" ]; then
cat "$response_file"
fi

echo "::error::Dependency graph availability check failed with HTTP ${status}. This is not a 'graph unavailable' response (403/404) -- treating it as a genuine failure instead of silently skipping the security gate."
cat "$response_file"
exit 1

- name: Dependency review
Expand All @@ -155,9 +175,3 @@ jobs:
fail-on-severity: ${{ inputs.fail_on_severity }}
allow-ghsas: ${{ inputs.allow_ghsas }}
comment-summary-in-pr: ${{ inputs.comment_summary_in_pr }}

- name: Dependency graph unavailable note
if: steps.dependency_graph.outputs.available != 'true' && github.event_name == 'pull_request'
run: |
echo "Dependency Review requires GitHub Dependency Graph to be enabled for this repository (and, on private repositories, GitHub Advanced Security)."
echo "Other required dependency-vulnerability gates (OSV-Scanner, Scorecard) remain the blocking coverage until Dependency Graph is available here."
17 changes: 17 additions & 0 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,23 @@ jobs:
set -euo pipefail

api_url="${GITHUB_API_URL:-https://api.github.com}"
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: 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: 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: 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 -sS --connect-timeout 10 --max-time 30 \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# ADR-0025: Fail closed on ambiguous Dependency Review authority and preserve caller permissions

- **Status:** Proposed
- **Date:** 2026-09-02
- **Scope:** `.github/workflows/dependency-review.yml`, its thin product callers, and the reusable-workflow security contract
- **Supersedes:** ADR-0024 only where ADR-0024 treated HTTP 403/404 as confirmed Dependency Graph unavailability or showed callers without an explicit permission envelope

## Problem

The protected-main consolidation from #1724 exposed two independent security-contract defects.

First, the reusable workflow classified Dependency Graph compare HTTP 403/404 as `available=false` and therefore skipped the blocking Dependency Review action. Those responses are not authoritative proof that the feature is unavailable: they can also be authorization or policy failures. Turning an ambiguous authorization-shaped response into success silently removes a security gate.

Second, the migration examples and thin callers omitted the permission envelope that the original repository-local workflows carried. GitHub reusable workflows cannot elevate `GITHUB_TOKEN` permissions through the call chain. A called workflow may maintain or reduce permissions granted by the caller, but it cannot manufacture `pull-requests: read` when the caller did not grant it. The result is a workflow-level `startup_failure` before any job is created.

## Constraints

1. Dependency Review remains a distinct hard gate; OSV-Scanner, Scorecard, or another scanner cannot substitute for an ambiguous Dependency Review failure.
2. The called workflow needs only `contents: read` and `pull-requests: read`; no write permission is introduced.
3. Product callers remain thin and repository-owned. They keep repository-specific trigger, severity, allowlist, and `continue_on_error` policy.
4. Consumers pin the reusable workflow to an immutable protected-main commit after this proposal is merged. `@main`, branch URLs, and unmerged PR heads are not production authority.
5. Non-`pull_request` invocations may skip because they lack an exact PR base/head pair; pull requests fail closed unless the comparison endpoint returns HTTP 200.

## Considered alternatives

### Treat 403/404 as feature unavailable

Rejected. The status code alone cannot distinguish a genuinely unavailable Dependency Graph from denied authorization/policy. A false negative here converts a required security control into a warning.

### Infer support from repository visibility or GHAS assumptions

Rejected. Visibility is not a capability proof and was already the weaker design ADR-0024 replaced.

### Rely on the called workflow's `permissions:` block

Rejected as insufficient. GitHub does not let a reusable workflow elevate permissions beyond the caller's grant. The called workflow still declares its least-privilege ceiling, but each thin caller must explicitly grant the same read scopes.

### Grant broader token permissions globally

Rejected. It increases blast radius and hides a caller-contract defect instead of repairing it.

## Decision

1. For `pull_request`, the Dependency Graph compare preflight sets `available=true` only on HTTP 200. Every other HTTP status is emitted with an error and terminates the job nonzero.
2. Remove the pull-request "Dependency graph unavailable" success path. No alternate scanner is described as replacement authority.
3. Keep the called workflow at `contents: read` + `pull-requests: read` and require every thin caller to declare at least those same scopes explicitly.
4. Make the executable central contract fail when the canonical caller example omits either required scope.
5. Replace the mutable `@main` caller example with an immutable `<protected-main-commit-sha>` placeholder. After merge, consumers pin the resulting protected-main SHA.

## Exact evidence

- #1725 first RED: `cb07b8bb28ef9d3a147cc966a0c70654d132da1d`; first production repair: `31f60e532e135008cabd09fcddd46a53062b0ea0`.
- Permission-envelope RED: `ee0f1ce544965772775b590050e40476df4ea8f6`; it changes only the contract and requires the missing caller permission example.
- Permission-envelope production repair: `ca3bdbd210de988ccd31f7fb96d3a97adfdb9bff`.
- `newsdom-api#784@1623977e6c37c78cb1a94a7a48c48f6d02cac86c`: run `33622976911`, `startup_failure`, zero jobs, reusable workflow immutably resolved to `.github@0bcd22d8bb07650aafb0a8f116e4c2bbb8744f03`.
- `mightyETL#330@65efdf7b4064df5b9811c0403defb707e6efbc02`: run `33623035969`, `startup_failure`, zero jobs.
- Consumer permission repairs then produced materialized current-head runs: newsdom-api `9a798d5ac7b9b295a1accb2327fc76611352290f` run `33623818000`; mightyETL `4576f863ede9fca0673d6cce5ae8a4093246f5ab` run `33623854807`; scopeweave `db8b8ed6d36a6dc6cc1d07255a7a9a86bc88bf4f` run `33623761776`; Argos #557 `ee4c5dd326977407435b0f2425fdecebc34a810f` run `33623867278`.

Hosted exact-current-head Checks and independent review remain required before this ADR may become Accepted.

## Consequences and follow-up

- A missing or denied Dependency Graph comparison is visible as a blocking failure instead of silent coverage loss.
- Caller permission omissions become an executable contract defect rather than an undocumented deployment prerequisite.
- The central workflow still cannot repair a consumer's omitted permissions by itself; each consumer must carry the explicit read-only envelope and later bump its immutable reusable-workflow pin to the protected-main SHA that contains this decision.
- #1643 remains a separate diagnostic lane for the required Security Scan path and is not evidence transfer for this reusable Dependency Review gate.

## References

GitHub. (n.d.). *Reusing workflow configurations*. GitHub Docs. https://docs.github.com/actions/using-workflows/reusing-workflows

GitHub. (n.d.). *Use GITHUB_TOKEN for authentication in workflows*. GitHub Docs. https://docs.github.com/actions/security-guides/automatic-token-authentication
Loading
Loading