Skip to content
Draft
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
47 changes: 46 additions & 1 deletion .github/workflows/codeql-scan-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -589,11 +589,56 @@ jobs:
id: gate
run: python3 "$RUNNER_TEMP/codeql_sarif_gate.py" codeql-results-dispatch

- name: Select target CodeQL analysis-read credential
id: ghas_analysis_token
if: steps.gate.outcome == 'success'
env:
TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }}
TARGET_APP_TOKEN: ${{ steps.target_app_token.outputs.token || '' }}
PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }}
OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }}
WORKFLOW_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

probe_analysis_read() {
token_label="$1"
token="$2"
if [ -z "$token" ]; then
return 1
fi
if GH_TOKEN="$token" gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"repos/${TARGET_REPOSITORY}/code-scanning/analyses?per_page=1&tool_name=CodeQL" \
>/dev/null 2>&1; then
echo "::add-mask::$token"
{
printf 'token=%s\n' "$token"
printf 'source=%s\n' "$token_label"
} >>"$GITHUB_OUTPUT"
echo "Selected ${token_label} after proving target CodeQL analysis-read access."
return 0
fi
echo "::notice::${token_label} cannot read target CodeQL analyses; trying the next configured credential."
return 1
}

if probe_analysis_read "target-app-token" "$TARGET_APP_TOKEN" ||
probe_analysis_read "pr-review-merge-token" "$PR_REVIEW_MERGE_TOKEN" ||
probe_analysis_read "opencode-approve-token" "$OPENCODE_APPROVE_TOKEN" ||
probe_analysis_read "github-token" "$WORKFLOW_TOKEN"; then
exit 0
fi

echo "::error::no configured credential can read target CodeQL analyses; GHAS configuration identity cannot be proven."
exit 1

- name: Verify GHAS base/head CodeQL configuration identity
id: ghas_configuration_identity
if: steps.gate.outcome == 'success'
env:
GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }}
GH_TOKEN: ${{ steps.ghas_analysis_token.outputs.token }}
TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }}
PR_NUMBER: ${{ needs.validate-dispatch.outputs.pr_number }}
BASE_REF: ${{ needs.validate-dispatch.outputs.base_ref }}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# CodeQL GHAS analysis credential routing — 2026-09-19

## Symptom

OriginWeave PR #229 dispatch run `35303205858` reached the central scan jobs after `validate-dispatch` succeeded. The `actions` job `105600898203`, `javascript-typescript` job `105600898234`, and Python job `105600898461` all completed CodeQL analysis and the Medium+ SARIF gate successfully, then failed specifically at `Verify GHAS base/head CodeQL configuration identity`.

The identity step failed in about one second in all three shards. That is materially different from the bounded identity-continuity wait: `codeql_ghas_configuration_identity.py` polls up to 30 times with a 20-second interval when a legitimate base identity is merely not present on the exact head yet. An immediate failure therefore indicates that the analyses API could not be read, rather than that the intended continuity poll exhausted its budget.

This supersedes the earlier runner-queue diagnosis and the interim observation that the Python shard was still queued.

## Root cause

The scan job exchanges an OpenCode GitHub App token for target repository **content reads** and uses it successfully for live pull-request metadata and head materialization. The GHAS identity step reused this precedence expression:

```text
steps.target_app_token.outputs.token || PR_REVIEW_MERGE_TOKEN || OPENCODE_APPROVE_TOKEN || github.token
```

GitHub expression fallback is based on token presence, not API capability. A non-empty content-capable target token therefore masked every later credential even when it could not read the target repository's `code-scanning/analyses` endpoint. The job-level `security-events: read` permission applies to the workflow repository's `github.token`; it does not by itself grant that token cross-repository GHAS access.

This is a credential-capability selection defect, not a CodeQL analysis defect: all three failed shards had successful `Perform CodeQL Analysis`, successful Medium+ SARIF gates, and preserved SARIF evidence before the identity-read failure.

## Ownership and stacking

The canonical repair is central because cross-repository CodeQL evidence identity is owned by `ContextualWisdomLab/.github`. It is intentionally stacked on PR #2271 (`fix/codeql-dispatch-repository-identity`) because #2271 already changes `codeql-scan-dispatch.yml`; the successor preserves that admission repair instead of opening a conflicting parallel writer against `main`.

OriginWeave must not copy this workflow or synthesize a success status.

## RED

Commit `8e93ae226b52a4d0456137ae36191241d5c58fe7` adds an executable shell contract that requires:

- a content-only first credential to fall through when the target analyses endpoint rejects it;
- the next credential with proven target CodeQL analysis-read access to be selected;
- all configured candidates failing to read the endpoint to remain a hard failure;
- the GHAS identity step to consume only the credential that passed the capability probe, rather than repeating the presence-only precedence chain.

The test executes the workflow's real selector `run:` block with a fake `gh` boundary; it does not duplicate the production selection algorithm in Python.

## Repair

Commit `4c4fff284e6bb58fe738389a647e2a7d1031dd54` adds one preflight step immediately after the Medium+ SARIF gate. It probes the exact target repository's CodeQL analyses endpoint with the already-configured credentials in existing precedence order:

1. exchanged target app token;
2. `PR_REVIEW_MERGE_TOKEN`;
3. `OPENCODE_APPROVE_TOKEN`;
4. workflow token.

The first credential that **actually succeeds** against `repos/<target>/code-scanning/analyses` is masked and passed as a step output to the identity verifier. If no configured credential can read that endpoint, the job fails closed with an explicit capability diagnostic. No permission is broadened, no secret is printed, and the GHAS identity proof itself is unchanged.

Compared with #2271 exact `2b849c874122961e025c29f7fa0bb697863c3d68`, the repair generation is ordinary-forward and changes only the CodeQL dispatch workflow, focused credential-routing contract, and this doctoring record.

## Review finding and correction

CodeRabbit correctly found that the failure-closed contract initially inspected `result.stderr` even though the selector emits the `::error::no configured credential can read target CodeQL analyses...` diagnostic with plain `echo`, which `subprocess.run(..., capture_output=True)` captures on stdout. Commit `a900ec17f1e4a7db384e3052c23d3cf7440f0651` changes only that assertion to inspect `result.stdout`; the review thread was answered and resolved after verifying the production selector.

A focused local execution of the exact selector semantics confirms both branches: a rejected `content-token` falls through to `security-token`, writes `token=security-token` / `source=pr-review-merge-token`, and returns zero; when every configured credential is rejected, all four candidates are attempted in order, the failure diagnostic appears on stdout, and the selector exits non-zero. This is local contract evidence only and does not replace exact-head hosted checks or the required end-to-end dispatch.

## Alternatives rejected

Using the first non-empty token remained rejected because that is the defect reproduced by the live run.

Skipping GHAS identity verification when the analyses API is unreadable was rejected because dispatch SARIF cannot impersonate GitHub Default setup configuration identity. The existing #2133 invariant remains mandatory.

Granting broader permissions blindly to the exchanged token was not assumed. The exchange endpoint currently returns an installation token without a workflow-side permission request contract. Capability is therefore proven at the target endpoint rather than inferred from the token's provenance or label.

Retrying the failed OriginWeave head before the central owner repair is accepted was rejected: it would repeat the same deterministic credential selection and create noise rather than new evidence.

## Acceptance

Before merge, the stacked successor needs:

- focused RED→GREEN contract execution;
- existing CodeQL dispatch workflow/shell contracts;
- exact-head hosted security/quality checks;
- independent review;
- an end-to-end dispatch proving that the selected credential can read target GHAS analyses and that the existing base/head identity proof reaches its normal terminal verdict.

Only after the central repair is accepted should OriginWeave #229 receive a normal exact-head CodeQL rerun. No predecessor run, SARIF artifact, review, or status is transferable to a changed successor head.
111 changes: 111 additions & 0 deletions tests/test_codeql_scan_dispatch_ghas_credential_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Credential-routing contract for cross-repository GHAS CodeQL analysis reads."""

from __future__ import annotations

import os
import subprocess
from pathlib import Path

from tests.test_opencode_workflow_shell_syntax import _extract_run_block


REPO_ROOT = Path(__file__).resolve().parents[1]
WORKFLOW_PATH = REPO_ROOT / ".github/workflows/codeql-scan-dispatch.yml"
SELECT_STEP_NAME = "Select target CodeQL analysis-read credential"
VERIFY_STEP_NAME = "Verify GHAS base/head CodeQL configuration identity"


def _run_selector(tmp_path: Path, *, succeeding_token: str | None) -> subprocess.CompletedProcess[str]:
"""Execute the extracted selector with fixed Bash identity and a fake ``gh`` boundary."""
assert Path("/bin/bash").is_file(), "/bin/bash is required to run this workflow-contract test"

workflow_text = WORKFLOW_PATH.read_text(encoding="utf-8")
script = _extract_run_block(workflow_text, SELECT_STEP_NAME)

fake_bin = tmp_path / "bin"
fake_bin.mkdir(parents=True)
call_log = tmp_path / "calls"
fake_gh = fake_bin / "gh"
fake_gh.write_text(
"#!/usr/bin/env bash\n"
"set -euo pipefail\n"
'printf \'%s\\n\' "${GH_TOKEN:-<empty>}" >>"$FAKE_CALL_LOG"\n'
'test "$1" = api\n'
'test "$#" -eq 6\n'
'test "$2" = -H\n'
'test "$3" = "Accept: application/vnd.github+json"\n'
'test "$4" = -H\n'
'test "$5" = "X-GitHub-Api-Version: 2022-11-28"\n'
'test "$6" = "repos/ContextualWisdomLab/OriginWeave/code-scanning/analyses?per_page=1&tool_name=CodeQL"\n'
'if [ -n "${SUCCEEDING_TOKEN:-}" ] && [ "${GH_TOKEN:-}" = "$SUCCEEDING_TOKEN" ]; then\n'
" printf '[]\\n'\n"
" exit 0\n"
"fi\n"
"exit 1\n",
encoding="utf-8",
)
fake_gh.chmod(0o755)

output = tmp_path / "github-output"
env = {
**os.environ,
"PATH": f"{fake_bin}:{os.environ['PATH']}",
"GITHUB_OUTPUT": str(output),
"FAKE_CALL_LOG": str(call_log),
"SUCCEEDING_TOKEN": succeeding_token or "",
"TARGET_REPOSITORY": "ContextualWisdomLab/OriginWeave",
"TARGET_APP_TOKEN": "content-token",
"PR_REVIEW_MERGE_TOKEN": "security-token",
"OPENCODE_APPROVE_TOKEN": "approve-token",
"WORKFLOW_TOKEN": "workflow-token",
}
result = subprocess.run(
["/bin/bash"],
input=script,
text=True,
capture_output=True,
check=False,
env=env,
)
result.output_path = output # type: ignore[attr-defined]
result.call_log = call_log # type: ignore[attr-defined]
return result


def test_ghas_analysis_read_falls_through_content_only_target_app_token(tmp_path: Path) -> None:
"""A content-capable app token must not mask a later GHAS-capable credential."""
result = _run_selector(tmp_path, succeeding_token="security-token")

assert result.returncode == 0, result.stdout + result.stderr
output = result.output_path.read_text(encoding="utf-8")
assert "token=security-token" in output
assert "source=pr-review-merge-token" in output
assert result.call_log.read_text(encoding="utf-8").splitlines() == [
"content-token",
"security-token",
]


def test_ghas_analysis_read_fails_closed_when_no_candidate_can_read_target(tmp_path: Path) -> None:
"""Missing target code-scanning read authority must remain a hard prerequisite failure."""
result = _run_selector(tmp_path, succeeding_token=None)

assert result.returncode != 0
assert "no configured credential can read target CodeQL analyses" in result.stdout
assert result.call_log.read_text(encoding="utf-8").splitlines() == [
"content-token",
"security-token",
"approve-token",
"workflow-token",
]


def test_ghas_identity_step_consumes_only_probed_analysis_read_token() -> None:
"""The identity proof must not repeat the unprobed content-token precedence chain."""
workflow = WORKFLOW_PATH.read_text(encoding="utf-8")
verify_script = _extract_run_block(workflow, VERIFY_STEP_NAME)
verify_prefix = workflow.split(f" - name: {VERIFY_STEP_NAME}\n", 1)[1].split(" run: |", 1)[0]

assert "GH_TOKEN: ${{ steps.ghas_analysis_token.outputs.token }}" in verify_prefix
assert "steps.target_app_token.outputs.token ||" not in verify_prefix
assert "codeql_ghas_configuration_identity.py" in verify_script
Loading