Skip to content
Merged
53 changes: 38 additions & 15 deletions .github/workflows/codeql-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
# runner, then one coordinator POSTs repository_dispatch to
# codeql-scan-dispatch.yml (native, unrestricted, in
# ContextualWisdomLab/.github) with the remaining language matrix. The
# handler publishes codeql-dispatch/<language> and reruns only that exact
# failed job. On rerun the shard reads the terminal status once. Design:
# handler publishes a base/run/source-bound codeql-dispatch receipt and reruns
# only that exact failed job. On rerun the shard reads the terminal status once. Design:
# docs/adr/0025-codeql-required-workflow-dispatch-architecture.md. The
# merge-preview scan (analyze-merge) is required nowhere (PR #1766) and was
# dropped, not migrated.
Expand Down Expand Up @@ -164,7 +164,7 @@ jobs:
steps:
- name: Read current-head CodeQL dispatch verdict
# Shards never dispatch. They re-check the live head, consume an
# authenticated codeql-dispatch/<language> verdict when one exists,
# authenticated base/run/source-bound CodeQL verdict when one exists,
# and otherwise fail pending so the runner is released. One
# coordinator job POSTs the remaining language matrix after every
# shard has a job id.
Expand All @@ -183,6 +183,7 @@ jobs:
live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"
live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')"
live_base="$(printf '%s' "$live_pr" | jq -r '.base.sha // empty')"
live_merge="$(printf '%s' "$live_pr" | jq -r '.merge_commit_sha // empty')"
live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')"
if ! [[ "$PR_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ && "$live_head" =~ ^[0-9a-fA-F]{40}$ ]] || [[ "$live_state" != "open" && "$live_state" != "closed" ]]; then
echo "::error::Could not validate live pull request state before CodeQL dispatch."
Expand All @@ -207,8 +208,9 @@ jobs:
echo "verdict=obsolete" >>"$GITHUB_OUTPUT"
exit 0
fi
if ! [[ "$live_base" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::Could not validate live pull request base SHA before CodeQL verdict read."
if ! [[ "$live_base" =~ ^[0-9a-fA-F]{40}$ ]] ||
! [[ "$live_merge" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::Could not validate live pull request base/source SHA before CodeQL verdict read."
exit 1
fi
if ! [[ "$REQUIRED_RUN_ID" =~ ^[1-9][0-9]*$ ]]; then
Expand All @@ -217,13 +219,17 @@ jobs:
fi

statuses="$(gh api "repos/${TARGET_REPOSITORY}/commits/${PR_HEAD_SHA}/statuses")"
verdict_state="$(printf '%s' "$statuses" | jq -r --arg ctx "codeql-dispatch/${LANGUAGE}" '
expected_context="codeql-dispatch/${LANGUAGE}/${live_base}"
expected_description="cwl1;h=${PR_HEAD_SHA};w=codeql-scan-dispatch;r=${REQUIRED_RUN_ID};s=${live_merge}"
verdict_state="$(printf '%s' "$statuses" | jq -r --arg ctx "$expected_context" --arg description "$expected_description" '
[
.[]
| select(.context == $ctx)
| select(.description == $description)
| select(
(.creator.login // "" | ascii_downcase) as $creator
| $creator == "opencode-agent" or $creator == "opencode-agent[bot]"
or $creator == "cwl-noema-review" or $creator == "cwl-noema-review[bot]"
)
]
| first // {} | .state // empty
Expand All @@ -236,7 +242,7 @@ jobs:
;;
esac

expected_title="CodeQL Scan Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${PR_HEAD_SHA}/${live_base}/${REQUIRED_RUN_ID}"
expected_title="CodeQL Scan Dispatch ${TARGET_REPOSITORY}#${PR_NUMBER}@${PR_HEAD_SHA}/${live_base}/${REQUIRED_RUN_ID}/${live_merge}"
expected_job="CodeQL dispatch scan (${LANGUAGE})"
# A dispatch bound to this required run cannot predate its creation.
required_created_at="$(gh api "repos/${TARGET_REPOSITORY}/actions/runs/${REQUIRED_RUN_ID}" --jq .created_at)"
Expand Down Expand Up @@ -266,11 +272,20 @@ jobs:
gate_conclusion="$(printf '%s' "$dispatch_job" | jq -r '
(.steps[]? | select(.name == "Enforce CodeQL Medium+ SARIF gate") | .conclusion) // empty
')"
ghas_identity_conclusion="$(printf '%s' "$dispatch_job" | jq -r '
(.steps[]? | select(.name == "Verify GHAS base/head CodeQL configuration identity") | .conclusion) // empty
')"
sarif_upload_conclusion="$(printf '%s' "$dispatch_job" | jq -r '
(.steps[]? | select(.name == "Preserve CodeQL SARIF evidence") | .conclusion) // empty
')"
case "$gate_conclusion" in
success)
echo "verdict=success" >>"$GITHUB_OUTPUT"
echo "Found completed CodeQL dispatch scan gate for ${LANGUAGE}: success."
exit 0
if [ "$ghas_identity_conclusion" = "success" ] &&
[ "$sarif_upload_conclusion" = "success" ]; then
echo "verdict=success" >>"$GITHUB_OUTPUT"
echo "Found completed CodeQL dispatch proof for ${LANGUAGE}: gate, GHAS identity, and SARIF evidence succeeded."
exit 0
fi
;;
failure|cancelled|skipped)
echo "verdict=failure" >>"$GITHUB_OUTPUT"
Expand All @@ -290,7 +305,7 @@ jobs:
fi

if [ "$RUN_ATTEMPT" != "1" ]; then
echo "::error::Exact CodeQL job was rerun without an authenticated terminal verdict."
echo "::error::Exact CodeQL job was rerun without an authenticated terminal verdict; GHAS identity and preserved SARIF are required for authenticated terminal proof."
exit 1
fi
echo "verdict=pending" >>"$GITHUB_OUTPUT"
Expand Down Expand Up @@ -363,6 +378,7 @@ jobs:
live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"
live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')"
live_base="$(printf '%s' "$live_pr" | jq -r '.base.sha // empty')"
live_merge="$(printf '%s' "$live_pr" | jq -r '.merge_commit_sha // empty')"
live_base_ref="$(printf '%s' "$live_pr" | jq -r '.base.ref // empty')"
live_head_ref="$(printf '%s' "$live_pr" | jq -r '.head.ref // empty')"
live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')"
Expand All @@ -382,8 +398,10 @@ jobs:
echo "::error::CodeQL dispatch requires a canonical current run id."
exit 1
fi
if ! [[ "$live_base" =~ ^[0-9a-fA-F]{40}$ ]] || [ -z "$live_base_ref" ] || [ -z "$live_head_ref" ]; then
echo "::error::Could not validate live pull request base identity before CodeQL dispatch."
if ! [[ "$live_base" =~ ^[0-9a-fA-F]{40}$ ]] ||
! [[ "$live_merge" =~ ^[0-9a-fA-F]{40}$ ]] ||
[ -z "$live_base_ref" ] || [ -z "$live_head_ref" ]; then
echo "::error::Could not validate live pull request base/source identity before CodeQL dispatch."
exit 1
fi

Expand Down Expand Up @@ -420,13 +438,17 @@ jobs:
pending_matrix='[]'
while IFS= read -r entry; do
language="$(printf '%s' "$entry" | jq -r '.language // empty')"
verdict_state="$(printf '%s' "$statuses" | jq -r --arg ctx "codeql-dispatch/${language}" '
expected_context="codeql-dispatch/${language}/${live_base}"
expected_description="cwl1;h=${PR_HEAD_SHA};w=codeql-scan-dispatch;r=${REQUIRED_RUN_ID};s=${live_merge}"
verdict_state="$(printf '%s' "$statuses" | jq -r --arg ctx "$expected_context" --arg description "$expected_description" '
[
.[]
| select(.context == $ctx)
| select(.description == $description)
| select(
(.creator.login // "" | ascii_downcase) as $creator
| $creator == "opencode-agent" or $creator == "opencode-agent[bot]"
or $creator == "cwl-noema-review" or $creator == "cwl-noema-review[bot]"
)
]
| first // {} | .state // empty
Expand Down Expand Up @@ -484,5 +506,6 @@ jobs:
--argjson matrix "$pending_matrix" \
--arg required_run_id "$REQUIRED_RUN_ID" \
--argjson required_jobs "$required_jobs" \
'{event_type:"codeql-scan",client_payload:{target_repository:$target_repository,pr_number:$pr_number,pr_base_ref:$pr_base_ref,pr_base_sha:$pr_base_sha,pr_head_ref:$pr_head_ref,pr_head_sha:$pr_head_sha,matrix:$matrix,required_run_id:$required_run_id,required_jobs:$required_jobs}}' |
--arg producer_source_sha "$live_merge" \
'{event_type:"codeql-scan-v2",client_payload:{target_repository:$target_repository,pr_number:$pr_number,pr_base_ref:$pr_base_ref,pr_base_sha:$pr_base_sha,pr_head:{schema:"1",ref:$pr_head_ref,sha:$pr_head_sha},producer_source_sha:$producer_source_sha,matrix:$matrix,required_run_id:$required_run_id,required_jobs:$required_jobs}}' |
GH_TOKEN="$app_token" gh api -X POST repos/ContextualWisdomLab/.github/dispatches --input -
73 changes: 71 additions & 2 deletions .github/workflows/codeql-scan-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -701,10 +701,23 @@ jobs:
if-no-files-found: error
retention-days: 7

- name: Mint target-scoped Noema CodeQL status token
id: noema_status_token
if: always() && steps.noema_analysis_config.outputs.available == 'true'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }}
owner: ContextualWisdomLab
repositories: ${{ steps.noema_analysis_config.outputs.repository }}
permission-statuses: write

- name: Publish CodeQL dispatch status
id: publish_status
if: always() && steps.live_metadata.outcome == 'success'
env:
NOEMA_STATUS_TOKEN: ${{ steps.noema_status_token.outputs.token || '' }}
TARGET_APP_STATUS_TOKEN: ${{ steps.target_app_token.outputs.token || '' }}
GITHUB_STATUS_READ_TOKEN: ${{ github.token }}
PR_REVIEW_MERGE_STATUS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }}
Expand Down Expand Up @@ -783,6 +796,11 @@ jobs:
actual_creator="$(jq -r '.creator.login // "" | ascii_downcase' "$status_response" 2>/dev/null || true)"
creator_trusted=false
case "$token_label" in
noema-status-token)
case "$actual_creator" in
cwl-noema-review|cwl-noema-review\[bot\]) creator_trusted=true ;;
esac
;;
target-app-token|pr-review-merge-token|opencode-approve-token)
case "$actual_creator" in
opencode-agent|opencode-agent\[bot\]) creator_trusted=true ;;
Expand Down Expand Up @@ -815,6 +833,9 @@ jobs:
return 1
}

if post_status "noema-status-token" "${NOEMA_STATUS_TOKEN:-}"; then
exit 0
fi
if post_status "target-app-token" "$TARGET_APP_STATUS_TOKEN"; then
exit 0
fi
Expand Down Expand Up @@ -920,8 +941,34 @@ jobs:
echo "token=$app_token"
} >>"$GITHUB_OUTPUT"

- name: Resolve Noema settlement token configuration
id: noema_settlement_config
env:
NOEMA_APP_CLIENT_ID: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID || '' }}
NOEMA_APP_PRIVATE_KEY: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY || '' }}
TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }}
run: |
set -euo pipefail
if [ -n "$NOEMA_APP_CLIENT_ID" ] && [ -n "$NOEMA_APP_PRIVATE_KEY" ]; then
printf 'repository=%s\n' "${TARGET_REPOSITORY#*/}" >>"$GITHUB_OUTPUT"
echo "available=true" >>"$GITHUB_OUTPUT"
fi

- name: Mint target-scoped Noema CodeQL settlement token
id: noema_settlement_token
if: steps.noema_settlement_config.outputs.available == 'true'
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }}
owner: ContextualWisdomLab
repositories: ${{ steps.noema_settlement_config.outputs.repository }}
permission-actions: write

- name: Settle exact CodeQL required run
env:
NOEMA_WAKE_TOKEN: ${{ steps.noema_settlement_token.outputs.token || '' }}
TARGET_APP_WAKE_TOKEN: ${{ steps.target_app_token.outputs.token || '' }}
PR_REVIEW_MERGE_WAKE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }}
OPENCODE_APPROVE_WAKE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }}
Expand Down Expand Up @@ -960,7 +1007,8 @@ jobs:
}

github_api() {
run_api "target-app-token" "$TARGET_APP_WAKE_TOKEN" "$@" ||
run_api "noema-settlement-token" "${NOEMA_WAKE_TOKEN:-}" "$@" ||
run_api "target-app-token" "$TARGET_APP_WAKE_TOKEN" "$@" ||
run_api "pr-review-merge-token" "$PR_REVIEW_MERGE_WAKE_TOKEN" "$@" ||
run_api "opencode-approve-token" "$OPENCODE_APPROVE_WAKE_TOKEN" "$@" ||
run_api "github-token" "$GITHUB_WAKE_TOKEN" "$@"
Expand Down Expand Up @@ -1074,6 +1122,26 @@ jobs:
echo "::error::CodeQL settlement rejected incomplete handler gate or SARIF evidence for ${language}."
exit 1
fi
clean_gate_count="$(printf '%s' "$handler_jobs" | jq --arg name "$expected_job_name" --argjson attempt "$GITHUB_RUN_ATTEMPT" '
[.[] | select(
.name == $name
and .status == "completed"
and .run_attempt == $attempt
and ([.steps[]? | select(.name == "Enforce CodeQL Medium+ SARIF gate" and .conclusion == "success")] | length) == 1
)] | length
')"
ghas_identity_count="$(printf '%s' "$handler_jobs" | jq --arg name "$expected_job_name" --argjson attempt "$GITHUB_RUN_ATTEMPT" '
[.[] | select(
.name == $name
and .status == "completed"
and .run_attempt == $attempt
and ([.steps[]? | select(.name == "Verify GHAS base/head CodeQL configuration identity" and .conclusion == "success")] | length) == 1
)] | length
')"
if [ "$clean_gate_count" -eq 1 ] && [ "$ghas_identity_count" -ne 1 ]; then
echo "::error::CodeQL settlement rejected missing GHAS configuration identity proof for ${language}."
exit 1
fi
done < <(printf '%s' "$REQUIRED_JOBS" | jq -c '.[]')

case "$RERUN_MODE" in
Expand All @@ -1099,7 +1167,8 @@ jobs:
return 1
}

if post_wake "target-app-token" "$TARGET_APP_WAKE_TOKEN" ||
if post_wake "noema-settlement-token" "${NOEMA_WAKE_TOKEN:-}" ||
post_wake "target-app-token" "$TARGET_APP_WAKE_TOKEN" ||
post_wake "pr-review-merge-token" "$PR_REVIEW_MERGE_WAKE_TOKEN" ||
post_wake "opencode-approve-token" "$OPENCODE_APPROVE_WAKE_TOKEN" ||
post_wake "github-token" "$GITHUB_WAKE_TOKEN"; then
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.d/20260927-codeql-terminal-proof.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Fixed

- Require a successful GHAS base/head configuration-identity proof and preserved
SARIF before a clean central CodeQL gate may settle or satisfy an exact required
run. A failed post-gate identity check can no longer be promoted to GREEN by a
wake-only fallback.
- Bind CodeQL terminal receipts to the live base, required run, head, and merge
source through the v2 dispatch protocol, preventing a trusted but stale commit
status from satisfying a retargeted or later required run.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: "ADR-0032: Owned CodeQL status and settlement authority"
status: Proposed
date: "2026-09-27"
authors: "Codex"
tags: [architecture, ci, security]
supersedes: ""
superseded_by: ""
---

# ADR-0032: Owned CodeQL status and settlement authority

## Status

Proposed. Requires #2405 complete terminal-proof foundation, owned-app installation permission acceptance, and an unchanged-head live canary before protected deployment is accepted.

## Context

DiskSage #473 dispatch 36305375849 encountered cross-repository HTTP403 during status publication and required-run settlement. Public app and organization installation metadata confirm opencode-agent is owned by anomalyco and has Actions/read and statuses/read. A consumer cannot change the external owner's app permissions. The organization-owned cwl-noema-review (app4291520) is already installed on all repositories with security_events/read; its private-key organization secret is available to central workflows. The existing target-scoped analysis-read token remains the GHAS reader.

## Decision

Use the existing owned Noema app for separate target-repository tokens: statuses/write solely for authenticated CodeQL receipt publication, and Actions/write solely for exact required-run settlement. Keep security_events/read in its existing separate read token. The installation must authorize those two write permissions; credentials cannot mint permissions the installation lacks. Optional mint failures retain existing fallback credentials and never create validation success.

The owned status writer must publish as cwl-noema-review or cwl-noema-review[bot]; another returned creator is rejected. No arbitrary actor is added. Complete base/head/run/source/workflow receipt and terminal SARIF/GHAS proof from #2405 remain prerequisites; do not deploy the new receiver trust before that foundation. Preserve exact-run identity, supersession, rerun budget, SARIF preservation and Medium+ gates.

## Consequences

- POS-001: Removes dependence on an external app owner's unavailable write grants.
- POS-002: Reuses an installed app and keeps analysis, publication and lifecycle tokens separate and target scoped.
- NEG-001: Expands the owned installation's capabilities and therefore the impact of its private-key compromise. Restrict key access and retain the trusted default-branch workflow boundary; never export keys into reviewed source or logs.
- NEG-002: Needs owner-authenticated app settings and installation acceptance plus live verification. Unit contracts do not prove deployment or permission availability.

## Alternatives Considered

- ALT-001: Change the external OpenCode app. Rejected because anomalyco owns that app and its current grants cannot satisfy writes.
- ALT-002: Transfer a user's CLI token into CI. Rejected: broad personal credentials are unnecessary and not copied.
- ALT-003: Bypass identity/receipt checks or synthesize success. Rejected because that removes the security proof.
- ALT-004: Reuse the analysis-read token for mutations. Rejected because its read-only contract must remain unchanged.

## Implementation Notes

- IMP-001: Pin the existing create-github-app-token action and request exactly one target repository and one write permission per writer token.
- IMP-002: Grant Actions/write and Commit statuses/write to the owned app and accept the installation update; do not add Code Scanning writes.
- IMP-003: Accept deployment only after real current-head scan, GHAS identity, receipt creator, one exact run-wide wake and terminal required verdict are verified. References: ContextualWisdomLab/.github#2276, #1929 and #2405.

## References

GitHub. (n.d.). *Create GitHub App token*. https://github.com/actions/create-github-app-token
GitHub. (n.d.). *Choosing permissions for a GitHub App*. https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/choosing-permissions-for-a-github-app
Loading
Loading