From af7655d5398500411127db019ea8356edfc02ec4 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Agent Date: Sat, 14 Feb 2026 18:55:09 +0100 Subject: [PATCH 1/6] ci(codeql): Guardrail gegen Default-Setup Drift --- .../codeql-default-setup-guardrail.yml | 43 +++++++++ .../010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD | 18 ++++ tools/ci/bin/run.sh | 1 + tools/ci/check-codeql-default-setup.sh | 90 +++++++++++++++++++ .../ensure_issue.sh | 78 ++++++++++++++++ .../get_state.sh | 48 ++++++++++ 6 files changed, 278 insertions(+) create mode 100644 .github/workflows/codeql-default-setup-guardrail.yml create mode 100644 docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD create mode 100755 tools/ci/check-codeql-default-setup.sh create mode 100755 tools/ci/codeql-default-setup-guardrail/ensure_issue.sh create mode 100755 tools/ci/codeql-default-setup-guardrail/get_state.sh diff --git a/.github/workflows/codeql-default-setup-guardrail.yml b/.github/workflows/codeql-default-setup-guardrail.yml new file mode 100644 index 0000000..2a6d6f4 --- /dev/null +++ b/.github/workflows/codeql-default-setup-guardrail.yml @@ -0,0 +1,43 @@ +name: codeql-default-setup-guardrail + +on: + schedule: + # Daily drift detection for security settings that cannot be protected by branch rules. + - cron: "17 4 * * *" + workflow_dispatch: + +permissions: + contents: read + issues: write + +concurrency: + group: codeql-default-setup-guardrail + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + guardrail: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Read CodeQL Default Setup State + id: state + env: + GH_TOKEN: ${{ github.token }} + run: bash tools/ci/codeql-default-setup-guardrail/get_state.sh + + - name: Open Issue On Drift (idempotent) + if: steps.state.outputs.drift == 'true' + env: + GH_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: bash tools/ci/codeql-default-setup-guardrail/ensure_issue.sh + + - name: Fail Run On Drift (fail-closed) + if: steps.state.outputs.drift == 'true' + run: exit 1 diff --git a/docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD b/docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD new file mode 100644 index 0000000..a31cb55 --- /dev/null +++ b/docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD @@ -0,0 +1,18 @@ +# CodeQL Default Setup Guardrail + +## Ziel +Dieses Repository nutzt ein **CodeQL Advanced Setup** (`.github/workflows/codeql.yml`) fuer C# (build-mode `manual`). +GitHub **CodeQL Default Setup** muss deshalb **deaktiviert** sein (`state=not-configured`), sonst werden Advanced-SARIF Uploads abgelehnt. + +## Verifikation +```bash +gh api /repos/tomtastisch/FileClassifier/code-scanning/default-setup --jq '{state,updated_at,schedule}' +``` + +Erwartung: +- `state: "not-configured"` + +## Guardrails +- Merge-Gate (fail-closed): `tools/ci/check-codeql-default-setup.sh` ist Teil von `preflight`. +- Drift Detection: `.github/workflows/codeql-default-setup-guardrail.yml` laeuft taeglich und erstellt bei Drift ein Issue. + diff --git a/tools/ci/bin/run.sh b/tools/ci/bin/run.sh index e19635b..4b5d57b 100755 --- a/tools/ci/bin/run.sh +++ b/tools/ci/bin/run.sh @@ -179,6 +179,7 @@ run_preflight() { run_or_fail "CI-PREFLIGHT-001" "Label engine tests" node "${ROOT_DIR}/tools/versioning/test-compute-pr-labels.js" run_or_fail "CI-PREFLIGHT-001" "PR governance checks" bash "${ROOT_DIR}/tools/ci/check-pr-governance.sh" run_or_fail "CI-PREFLIGHT-001" "Code scanning tools open alerts must be zero" bash "${ROOT_DIR}/tools/ci/check-code-scanning-tools-zero.sh" + run_or_fail "CI-CODEQL-001" "CodeQL default setup must be not-configured" bash "${ROOT_DIR}/tools/ci/check-codeql-default-setup.sh" run_or_fail "CI-PREFLIGHT-001" "Doc consistency drift guard" python3 "${ROOT_DIR}/tools/check-doc-consistency.py" run_or_fail "CI-PREFLIGHT-001" "Doc shell compatibility guard" python3 "${ROOT_DIR}/tools/check-doc-shell-compat.py" run_or_fail "CI-PREFLIGHT-001" "Docs checker unit tests" python3 -m unittest discover -s "${ROOT_DIR}/tools/tests" -p "test_*.py" -v diff --git a/tools/ci/check-codeql-default-setup.sh b/tools/ci/check-codeql-default-setup.sh new file mode 100755 index 0000000..3c9b457 --- /dev/null +++ b/tools/ci/check-codeql-default-setup.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +export LC_ALL=C + +ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" +OUT_DIR="${ROOT_DIR}/artifacts/ci/preflight/codeql-default-setup-guardrail" +RAW_LOG="${OUT_DIR}/raw.log" +SUMMARY_MD="${OUT_DIR}/summary.md" +RESULT_JSON="${OUT_DIR}/result.json" +DEFAULT_SETUP_JSON="${OUT_DIR}/default-setup.json" + +mkdir -p "${OUT_DIR}" +: > "${RAW_LOG}" + +log() { + printf '%s\n' "$*" | tee -a "${RAW_LOG}" >/dev/null +} + +fail() { + local reason="$1" + log "FAIL: ${reason}" + { + echo "# CodeQL Default Setup Guardrail" + echo + echo "- status: fail" + echo "- reason: ${reason}" + } > "${SUMMARY_MD}" + jq -n --arg reason "${reason}" \ + '{schema_version:1,check_id:"codeql-default-setup-guardrail",status:"fail",reason:$reason,evidence_paths:["artifacts/ci/preflight/codeql-default-setup-guardrail/raw.log","artifacts/ci/preflight/codeql-default-setup-guardrail/summary.md"]}' \ + > "${RESULT_JSON}" + exit 1 +} + +if ! command -v gh >/dev/null 2>&1; then + fail "gh fehlt" +fi +if ! command -v jq >/dev/null 2>&1; then + fail "jq fehlt" +fi + +REPO="${GITHUB_REPOSITORY:-}" +if [[ -z "${REPO}" ]]; then + origin_url="$(git -C "${ROOT_DIR}" remote get-url origin 2>/dev/null || true)" + if [[ "${origin_url}" =~ github.com[:/]([^/]+/[^/.]+)(\.git)?$ ]]; then + REPO="${BASH_REMATCH[1]}" + fi +fi +if [[ -z "${REPO}" ]]; then + fail "Repository-Slug konnte nicht bestimmt werden" +fi + +attempt=1 +delay=2 +max_attempts=3 +while true; do + if gh api "repos/${REPO}/code-scanning/default-setup" > "${DEFAULT_SETUP_JSON}" 2>> "${RAW_LOG}"; then + break + fi + if (( attempt >= max_attempts )); then + fail "GitHub API fuer CodeQL Default Setup fehlgeschlagen" + fi + log "WARN: API-Fehler, retry ${attempt}/${max_attempts}" + sleep "${delay}" + attempt=$((attempt + 1)) + delay=$((delay * 2)) +done + +state="$(jq -r '.state // empty' "${DEFAULT_SETUP_JSON}")" +if [[ -z "${state}" ]]; then + fail "Ungueltige API-Antwort: state fehlt" +fi + +if [[ "${state}" != "not-configured" ]]; then + fail "CodeQL Default Setup ist aktiv (state=${state}). Advanced-Setup erfordert state=not-configured." +fi + +{ + echo "# CodeQL Default Setup Guardrail" + echo + echo "- status: pass" + echo "- repo: ${REPO}" + echo "- state: ${state}" +} > "${SUMMARY_MD}" + +jq -n \ + '{schema_version:1,check_id:"codeql-default-setup-guardrail",status:"pass",state:"not-configured",evidence_paths:["artifacts/ci/preflight/codeql-default-setup-guardrail/raw.log","artifacts/ci/preflight/codeql-default-setup-guardrail/default-setup.json","artifacts/ci/preflight/codeql-default-setup-guardrail/summary.md"]}' \ + > "${RESULT_JSON}" + +log "PASS: CodeQL Default Setup ist not-configured." diff --git a/tools/ci/codeql-default-setup-guardrail/ensure_issue.sh b/tools/ci/codeql-default-setup-guardrail/ensure_issue.sh new file mode 100755 index 0000000..098f3a5 --- /dev/null +++ b/tools/ci/codeql-default-setup-guardrail/ensure_issue.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo="${GITHUB_REPOSITORY:-${REPO:-}}" +if [[ -z "${repo}" ]]; then + echo "ERROR: missing GITHUB_REPOSITORY/REPO env (expected owner/name)." >&2 + exit 2 +fi + +retry() { + local -r max="${1}"; shift + local -r base_sleep="${1}"; shift + local attempt=1 + local sleep_s="${base_sleep}" + while true; do + if "$@"; then + return 0 + fi + if [[ "${attempt}" -ge "${max}" ]]; then + return 1 + fi + echo "WARN: command failed (attempt ${attempt}/${max}); retrying in ${sleep_s}s..." >&2 + sleep "${sleep_s}" + attempt="$((attempt + 1))" + sleep_s="$((sleep_s * 2))" + done +} + +state="" +if ! state="$(retry 4 1 gh api "repos/${repo}/code-scanning/default-setup" --jq .state)"; then + echo "ERROR: failed to query CodeQL default-setup state via GitHub API." >&2 + exit 3 +fi + +if [[ "${state}" == "not-configured" ]]; then + echo "INFO: state is not-configured; no drift issue required." + exit 0 +fi + +title="SECURITY: CodeQL Default Setup ist aktiviert (Guardrail)" +marker="" + +body=$'Der CI-Guardrail hat festgestellt, dass **GitHub CodeQL Default Setup** aktiv ist.\n\n' +body+=$'Impact:\n- Advanced CodeQL Workflow (`.github/workflows/codeql.yml`) kann dadurch keine SARIF-Ergebnisse wie erwartet verarbeiten.\n\n' +body+=$'Fix:\n- In GitHub UI: Settings -> Code security and analysis -> CodeQL -> Default setup deaktivieren\n- Oder per API: `PATCH /repos/{owner}/{repo}/code-scanning/default-setup` mit `state=not-configured`\n\n' +body+=$'Evidence:\n- Siehe Workflow-Logs und `artifacts/ci/preflight/codeql-default-setup-guardrail/`.\n\n' +body+=$"Observed state: \`${state}\`\n\n" +body+="${marker}"$'\n' + +existing="$(gh api "repos/${repo}/issues?state=open&per_page=100" --paginate | jq -r --arg t "${title}" '[.[] | select(.pull_request? | not) | select(.title == $t) | .number][0] // empty')" +if [[ -n "${existing}" ]]; then + echo "INFO: drift issue already open (#${existing}); nothing to do." + exit 0 +fi + +issue_number="" +if ! issue_number="$(gh api "repos/${repo}/issues" -X POST -f title="${title}" -f body="${body}" --jq .number)"; then + echo "ERROR: failed to create drift issue in ${repo}." >&2 + exit 4 +fi + +echo "INFO: created drift issue #${issue_number}." + +# Best-effort labels: only add labels that already exist, otherwise the API call would hard-fail. +desired_labels=("security" "area:pipeline") +labels_available="$(gh label list -R "${repo}" -L 200 --json name --jq '.[].name' 2>/dev/null || true)" +for label in "${desired_labels[@]}"; do + if echo "${labels_available}" | grep -Fxq "${label}"; then + if gh issue edit "${issue_number}" -R "${repo}" --add-label "${label}" >/dev/null 2>&1; then + echo "INFO: added label '${label}'." + else + echo "WARN: failed to add label '${label}' to issue #${issue_number}." >&2 + fi + else + echo "INFO: label '${label}' not present in repo; skipping." + fi +done + diff --git a/tools/ci/codeql-default-setup-guardrail/get_state.sh b/tools/ci/codeql-default-setup-guardrail/get_state.sh new file mode 100755 index 0000000..27b8c9d --- /dev/null +++ b/tools/ci/codeql-default-setup-guardrail/get_state.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo="${GITHUB_REPOSITORY:-${REPO:-}}" +if [[ -z "${repo}" ]]; then + echo "ERROR: missing GITHUB_REPOSITORY/REPO env (expected owner/name)." >&2 + exit 2 +fi + +retry() { + local -r max="${1}"; shift + local -r base_sleep="${1}"; shift + local attempt=1 + local sleep_s="${base_sleep}" + while true; do + if "$@"; then + return 0 + fi + if [[ "${attempt}" -ge "${max}" ]]; then + return 1 + fi + echo "WARN: command failed (attempt ${attempt}/${max}); retrying in ${sleep_s}s..." >&2 + sleep "${sleep_s}" + attempt="$((attempt + 1))" + sleep_s="$((sleep_s * 2))" + done +} + +state="" +if ! state="$(retry 4 1 gh api "repos/${repo}/code-scanning/default-setup" --jq .state)"; then + echo "ERROR: failed to query CodeQL default-setup state via GitHub API." >&2 + exit 3 +fi + +drift="false" +if [[ "${state}" != "not-configured" ]]; then + drift="true" +fi + +echo "INFO: default-setup state='${state}', drift='${drift}'." + +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "state=${state}" + echo "drift=${drift}" + } >> "${GITHUB_OUTPUT}" +fi + From 91611aa986d74edc3269fbc84f24d8a508557b5a Mon Sep 17 00:00:00 2001 From: GitHub Copilot Agent Date: Sat, 14 Feb 2026 19:43:16 +0100 Subject: [PATCH 2/6] ci(codeql): GH token permissions fuer default-setup API --- .github/workflows/ci.yml | 1 + .github/workflows/codeql-default-setup-guardrail.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 339ff99..dabf97b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ permissions: contents: read security-events: read pull-requests: read + administration: read concurrency: group: ci-${{ github.ref }} diff --git a/.github/workflows/codeql-default-setup-guardrail.yml b/.github/workflows/codeql-default-setup-guardrail.yml index 2a6d6f4..f542953 100644 --- a/.github/workflows/codeql-default-setup-guardrail.yml +++ b/.github/workflows/codeql-default-setup-guardrail.yml @@ -9,6 +9,7 @@ on: permissions: contents: read issues: write + administration: read concurrency: group: codeql-default-setup-guardrail From b18aafe59a8e25646684b512d4307d75c39dc8b2 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Agent Date: Sat, 14 Feb 2026 19:46:25 +0100 Subject: [PATCH 3/6] ci(codeql): Optional PAT fuer default-setup endpoint --- .github/workflows/ci.yml | 2 +- .github/workflows/codeql-default-setup-guardrail.yml | 3 ++- tools/ci/check-codeql-default-setup.sh | 11 +++++++++++ .../ci/codeql-default-setup-guardrail/ensure_issue.sh | 9 ++++++++- tools/ci/codeql-default-setup-guardrail/get_state.sh | 9 ++++++++- 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dabf97b..2de7121 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,6 @@ permissions: contents: read security-events: read pull-requests: read - administration: read concurrency: group: ci-${{ github.ref }} @@ -83,6 +82,7 @@ jobs: - name: Run Entry Check env: GH_TOKEN: ${{ github.token }} + CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN: ${{ secrets.CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN }} run: bash -euo pipefail tools/ci/bin/run.sh preflight - name: Upload Artifact if: always() diff --git a/.github/workflows/codeql-default-setup-guardrail.yml b/.github/workflows/codeql-default-setup-guardrail.yml index f542953..dcc995a 100644 --- a/.github/workflows/codeql-default-setup-guardrail.yml +++ b/.github/workflows/codeql-default-setup-guardrail.yml @@ -9,7 +9,6 @@ on: permissions: contents: read issues: write - administration: read concurrency: group: codeql-default-setup-guardrail @@ -30,6 +29,7 @@ jobs: id: state env: GH_TOKEN: ${{ github.token }} + CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN: ${{ secrets.CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN }} run: bash tools/ci/codeql-default-setup-guardrail/get_state.sh - name: Open Issue On Drift (idempotent) @@ -37,6 +37,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} GITHUB_REPOSITORY: ${{ github.repository }} + CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN: ${{ secrets.CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN }} run: bash tools/ci/codeql-default-setup-guardrail/ensure_issue.sh - name: Fail Run On Drift (fail-closed) diff --git a/tools/ci/check-codeql-default-setup.sh b/tools/ci/check-codeql-default-setup.sh index 3c9b457..882a5dd 100755 --- a/tools/ci/check-codeql-default-setup.sh +++ b/tools/ci/check-codeql-default-setup.sh @@ -13,6 +13,9 @@ DEFAULT_SETUP_JSON="${OUT_DIR}/default-setup.json" mkdir -p "${OUT_DIR}" : > "${RAW_LOG}" +BASE_GH_TOKEN="${GH_TOKEN:-}" +CODEQL_TOKEN="${CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN:-}" + log() { printf '%s\n' "$*" | tee -a "${RAW_LOG}" >/dev/null } @@ -54,10 +57,18 @@ attempt=1 delay=2 max_attempts=3 while true; do + if [[ -n "${CODEQL_TOKEN}" ]]; then + export GH_TOKEN="${CODEQL_TOKEN}" + fi if gh api "repos/${REPO}/code-scanning/default-setup" > "${DEFAULT_SETUP_JSON}" 2>> "${RAW_LOG}"; then + export GH_TOKEN="${BASE_GH_TOKEN}" break fi + export GH_TOKEN="${BASE_GH_TOKEN}" if (( attempt >= max_attempts )); then + if rg -n "Resource not accessible by integration \\(HTTP 403\\)" "${RAW_LOG}" >/dev/null 2>&1; then + fail "GitHub API 403 fuer CodeQL Default Setup. GITHUB_TOKEN reicht hier nicht aus; setze Secret CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN (Fine-Grained PAT, Repo: Administration Read, Security Events Read)." + fi fail "GitHub API fuer CodeQL Default Setup fehlgeschlagen" fi log "WARN: API-Fehler, retry ${attempt}/${max_attempts}" diff --git a/tools/ci/codeql-default-setup-guardrail/ensure_issue.sh b/tools/ci/codeql-default-setup-guardrail/ensure_issue.sh index 098f3a5..9d86e57 100755 --- a/tools/ci/codeql-default-setup-guardrail/ensure_issue.sh +++ b/tools/ci/codeql-default-setup-guardrail/ensure_issue.sh @@ -7,6 +7,9 @@ if [[ -z "${repo}" ]]; then exit 2 fi +BASE_GH_TOKEN="${GH_TOKEN:-}" +CODEQL_TOKEN="${CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN:-}" + retry() { local -r max="${1}"; shift local -r base_sleep="${1}"; shift @@ -27,10 +30,15 @@ retry() { } state="" +if [[ -n "${CODEQL_TOKEN}" ]]; then + export GH_TOKEN="${CODEQL_TOKEN}" +fi if ! state="$(retry 4 1 gh api "repos/${repo}/code-scanning/default-setup" --jq .state)"; then + export GH_TOKEN="${BASE_GH_TOKEN}" echo "ERROR: failed to query CodeQL default-setup state via GitHub API." >&2 exit 3 fi +export GH_TOKEN="${BASE_GH_TOKEN}" if [[ "${state}" == "not-configured" ]]; then echo "INFO: state is not-configured; no drift issue required." @@ -75,4 +83,3 @@ for label in "${desired_labels[@]}"; do echo "INFO: label '${label}' not present in repo; skipping." fi done - diff --git a/tools/ci/codeql-default-setup-guardrail/get_state.sh b/tools/ci/codeql-default-setup-guardrail/get_state.sh index 27b8c9d..394545f 100755 --- a/tools/ci/codeql-default-setup-guardrail/get_state.sh +++ b/tools/ci/codeql-default-setup-guardrail/get_state.sh @@ -7,6 +7,9 @@ if [[ -z "${repo}" ]]; then exit 2 fi +BASE_GH_TOKEN="${GH_TOKEN:-}" +CODEQL_TOKEN="${CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN:-}" + retry() { local -r max="${1}"; shift local -r base_sleep="${1}"; shift @@ -27,10 +30,15 @@ retry() { } state="" +if [[ -n "${CODEQL_TOKEN}" ]]; then + export GH_TOKEN="${CODEQL_TOKEN}" +fi if ! state="$(retry 4 1 gh api "repos/${repo}/code-scanning/default-setup" --jq .state)"; then + export GH_TOKEN="${BASE_GH_TOKEN}" echo "ERROR: failed to query CodeQL default-setup state via GitHub API." >&2 exit 3 fi +export GH_TOKEN="${BASE_GH_TOKEN}" drift="false" if [[ "${state}" != "not-configured" ]]; then @@ -45,4 +53,3 @@ if [[ -n "${GITHUB_OUTPUT:-}" ]]; then echo "drift=${drift}" } >> "${GITHUB_OUTPUT}" fi - From 4c581345212f6b8d57382fa096364193729a4953 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Agent Date: Sat, 14 Feb 2026 19:48:07 +0100 Subject: [PATCH 4/6] ci(codeql): 403 detection ohne rg --- tools/ci/check-codeql-default-setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ci/check-codeql-default-setup.sh b/tools/ci/check-codeql-default-setup.sh index 882a5dd..be56ea8 100755 --- a/tools/ci/check-codeql-default-setup.sh +++ b/tools/ci/check-codeql-default-setup.sh @@ -66,7 +66,7 @@ while true; do fi export GH_TOKEN="${BASE_GH_TOKEN}" if (( attempt >= max_attempts )); then - if rg -n "Resource not accessible by integration \\(HTTP 403\\)" "${RAW_LOG}" >/dev/null 2>&1; then + if grep -qF "Resource not accessible by integration (HTTP 403)" "${RAW_LOG}"; then fail "GitHub API 403 fuer CodeQL Default Setup. GITHUB_TOKEN reicht hier nicht aus; setze Secret CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN (Fine-Grained PAT, Repo: Administration Read, Security Events Read)." fi fail "GitHub API fuer CodeQL Default Setup fehlgeschlagen" From 6df7834391b106965e9596679fef275f3d40611d Mon Sep 17 00:00:00 2001 From: GitHub Copilot Agent Date: Sat, 14 Feb 2026 19:49:41 +0100 Subject: [PATCH 5/6] docs(security): PAT requirement fuer CodeQL default-setup guardrail --- docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD b/docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD index a31cb55..85e3c73 100644 --- a/docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD +++ b/docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD @@ -16,3 +16,15 @@ Erwartung: - Merge-Gate (fail-closed): `tools/ci/check-codeql-default-setup.sh` ist Teil von `preflight`. - Drift Detection: `.github/workflows/codeql-default-setup-guardrail.yml` laeuft taeglich und erstellt bei Drift ein Issue. +## Token / Permissions (wichtig) +Der GitHub API Endpoint `GET /repos/{owner}/{repo}/code-scanning/default-setup` ist in GitHub Actions mit dem standardmaessigen `GITHUB_TOKEN` in diesem Repo nicht erreichbar (typisch: `HTTP 403 Resource not accessible by integration`). + +Damit der Guardrail in PR-CI und im scheduled Workflow deterministisch funktioniert, ist ein Fine-Grained PAT als Repo-Secret erforderlich: +- Secret-Name: `CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN` +- Minimalrechte (Repository permissions): + - Administration: Read-only + - Security events: Read-only + +Wiring: +- PR-CI: `.github/workflows/ci.yml` setzt `CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN` als env fuer `preflight`. +- Scheduled: `.github/workflows/codeql-default-setup-guardrail.yml` nutzt `CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN`, falls vorhanden. From bdd942eca407fdb59c8a892bdedbf0102dcc3a40 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Agent Date: Sun, 15 Feb 2026 13:00:20 +0100 Subject: [PATCH 6/6] ci(codeql): Review-Fixes (Idempotenz/Robustheit/Evidence) --- .../codeql-default-setup-guardrail.yml | 2 +- .../010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD | 2 +- tools/ci/check-codeql-default-setup.sh | 31 +++++++---- .../ensure_issue.sh | 51 ++++++++++++++----- .../get_state.sh | 28 +++++++--- 5 files changed, 82 insertions(+), 32 deletions(-) diff --git a/.github/workflows/codeql-default-setup-guardrail.yml b/.github/workflows/codeql-default-setup-guardrail.yml index dcc995a..620e99f 100644 --- a/.github/workflows/codeql-default-setup-guardrail.yml +++ b/.github/workflows/codeql-default-setup-guardrail.yml @@ -12,7 +12,7 @@ permissions: concurrency: group: codeql-default-setup-guardrail - cancel-in-progress: true + cancel-in-progress: false defaults: run: diff --git a/docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD b/docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD index 85e3c73..9ac1f64 100644 --- a/docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD +++ b/docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD @@ -6,7 +6,7 @@ GitHub **CodeQL Default Setup** muss deshalb **deaktiviert** sein (`state=not-co ## Verifikation ```bash -gh api /repos/tomtastisch/FileClassifier/code-scanning/default-setup --jq '{state,updated_at,schedule}' +gh api repos/{owner}/{repo}/code-scanning/default-setup --jq '{state,updated_at,schedule}' ``` Erwartung: diff --git a/tools/ci/check-codeql-default-setup.sh b/tools/ci/check-codeql-default-setup.sh index be56ea8..50f5806 100755 --- a/tools/ci/check-codeql-default-setup.sh +++ b/tools/ci/check-codeql-default-setup.sh @@ -29,9 +29,15 @@ fail() { echo "- status: fail" echo "- reason: ${reason}" } > "${SUMMARY_MD}" - jq -n --arg reason "${reason}" \ - '{schema_version:1,check_id:"codeql-default-setup-guardrail",status:"fail",reason:$reason,evidence_paths:["artifacts/ci/preflight/codeql-default-setup-guardrail/raw.log","artifacts/ci/preflight/codeql-default-setup-guardrail/summary.md"]}' \ - > "${RESULT_JSON}" + if [[ -f "${DEFAULT_SETUP_JSON}" ]]; then + jq -n --arg reason "${reason}" \ + '{schema_version:1,check_id:"codeql-default-setup-guardrail",status:"fail",reason:$reason,evidence_paths:["artifacts/ci/preflight/codeql-default-setup-guardrail/raw.log","artifacts/ci/preflight/codeql-default-setup-guardrail/default-setup.json","artifacts/ci/preflight/codeql-default-setup-guardrail/summary.md"]}' \ + > "${RESULT_JSON}" + else + jq -n --arg reason "${reason}" \ + '{schema_version:1,check_id:"codeql-default-setup-guardrail",status:"fail",reason:$reason,evidence_paths:["artifacts/ci/preflight/codeql-default-setup-guardrail/raw.log","artifacts/ci/preflight/codeql-default-setup-guardrail/summary.md"]}' \ + > "${RESULT_JSON}" + fi exit 1 } @@ -52,19 +58,26 @@ fi if [[ -z "${REPO}" ]]; then fail "Repository-Slug konnte nicht bestimmt werden" fi +if [[ ! "${REPO}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + fail "Ungueltiger Repository-Slug: '${REPO}' (erwartet owner/name)" +fi + +gh_api() { + local -r endpoint="$1"; shift + if [[ -n "${CODEQL_TOKEN}" ]]; then + GH_TOKEN="${CODEQL_TOKEN}" GH_REPO="${REPO}" gh api "${endpoint}" "$@" + else + GH_REPO="${REPO}" gh api "${endpoint}" "$@" + fi +} attempt=1 delay=2 max_attempts=3 while true; do - if [[ -n "${CODEQL_TOKEN}" ]]; then - export GH_TOKEN="${CODEQL_TOKEN}" - fi - if gh api "repos/${REPO}/code-scanning/default-setup" > "${DEFAULT_SETUP_JSON}" 2>> "${RAW_LOG}"; then - export GH_TOKEN="${BASE_GH_TOKEN}" + if gh_api "repos/{owner}/{repo}/code-scanning/default-setup" > "${DEFAULT_SETUP_JSON}" 2>> "${RAW_LOG}"; then break fi - export GH_TOKEN="${BASE_GH_TOKEN}" if (( attempt >= max_attempts )); then if grep -qF "Resource not accessible by integration (HTTP 403)" "${RAW_LOG}"; then fail "GitHub API 403 fuer CodeQL Default Setup. GITHUB_TOKEN reicht hier nicht aus; setze Secret CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN (Fine-Grained PAT, Repo: Administration Read, Security Events Read)." diff --git a/tools/ci/codeql-default-setup-guardrail/ensure_issue.sh b/tools/ci/codeql-default-setup-guardrail/ensure_issue.sh index 9d86e57..ec8ae5f 100755 --- a/tools/ci/codeql-default-setup-guardrail/ensure_issue.sh +++ b/tools/ci/codeql-default-setup-guardrail/ensure_issue.sh @@ -1,13 +1,18 @@ #!/usr/bin/env bash set -euo pipefail +IFS=$'\n\t' +export LC_ALL=C repo="${GITHUB_REPOSITORY:-${REPO:-}}" if [[ -z "${repo}" ]]; then echo "ERROR: missing GITHUB_REPOSITORY/REPO env (expected owner/name)." >&2 exit 2 fi +if [[ ! "${repo}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo "ERROR: invalid repo slug: '${repo}' (expected owner/name)." >&2 + exit 2 +fi -BASE_GH_TOKEN="${GH_TOKEN:-}" CODEQL_TOKEN="${CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN:-}" retry() { @@ -29,16 +34,24 @@ retry() { done } +gh_api() { + local -r endpoint="$1"; shift + if [[ -n "${CODEQL_TOKEN}" ]]; then + GH_TOKEN="${CODEQL_TOKEN}" GH_REPO="${repo}" gh api "${endpoint}" "$@" + else + GH_REPO="${repo}" gh api "${endpoint}" "$@" + fi +} + state="" -if [[ -n "${CODEQL_TOKEN}" ]]; then - export GH_TOKEN="${CODEQL_TOKEN}" -fi -if ! state="$(retry 4 1 gh api "repos/${repo}/code-scanning/default-setup" --jq .state)"; then - export GH_TOKEN="${BASE_GH_TOKEN}" +if ! state="$(retry 4 1 gh_api "repos/{owner}/{repo}/code-scanning/default-setup" --jq .state)"; then echo "ERROR: failed to query CodeQL default-setup state via GitHub API." >&2 exit 3 fi -export GH_TOKEN="${BASE_GH_TOKEN}" +if [[ -z "${state}" ]]; then + echo "ERROR: invalid API response (missing state)." >&2 + exit 3 +fi if [[ "${state}" == "not-configured" ]]; then echo "INFO: state is not-configured; no drift issue required." @@ -52,28 +65,38 @@ body=$'Der CI-Guardrail hat festgestellt, dass **GitHub CodeQL Default Setup** a body+=$'Impact:\n- Advanced CodeQL Workflow (`.github/workflows/codeql.yml`) kann dadurch keine SARIF-Ergebnisse wie erwartet verarbeiten.\n\n' body+=$'Fix:\n- In GitHub UI: Settings -> Code security and analysis -> CodeQL -> Default setup deaktivieren\n- Oder per API: `PATCH /repos/{owner}/{repo}/code-scanning/default-setup` mit `state=not-configured`\n\n' body+=$'Evidence:\n- Siehe Workflow-Logs und `artifacts/ci/preflight/codeql-default-setup-guardrail/`.\n\n' -body+=$"Observed state: \`${state}\`\n\n" +if [[ ! "${state}" =~ ^[a-z-]+$ ]]; then + state="unknown" +fi +body+="Observed state: \`${state}\`"$'\n\n' body+="${marker}"$'\n' -existing="$(gh api "repos/${repo}/issues?state=open&per_page=100" --paginate | jq -r --arg t "${title}" '[.[] | select(.pull_request? | not) | select(.title == $t) | .number][0] // empty')" +existing="$(gh_api "repos/{owner}/{repo}/issues?state=open&per_page=100" --paginate --slurp | jq -r --arg t "${title}" '[.[] | .[] | select(.pull_request? | not) | select(.title == $t) | .number][0] // empty')" if [[ -n "${existing}" ]]; then echo "INFO: drift issue already open (#${existing}); nothing to do." exit 0 fi issue_number="" -if ! issue_number="$(gh api "repos/${repo}/issues" -X POST -f title="${title}" -f body="${body}" --jq .number)"; then - echo "ERROR: failed to create drift issue in ${repo}." >&2 - exit 4 +if ! issue_number="$(gh_api "repos/{owner}/{repo}/issues" -X POST -f title="${title}" -f body="${body}" --jq .number)"; then + # Benign race: another run may have created the issue after our initial check. + existing_after_create="$(gh_api "repos/{owner}/{repo}/issues?state=open&per_page=100" --paginate --slurp | jq -r --arg t "${title}" '[.[] | .[] | select(.pull_request? | not) | select(.title == $t) | .number][0] // empty')" + if [[ -n "${existing_after_create}" ]]; then + issue_number="${existing_after_create}" + echo "INFO: drift issue was created concurrently (#${issue_number}); proceeding." + else + echo "ERROR: failed to create drift issue in ${repo}." >&2 + exit 4 + fi fi echo "INFO: created drift issue #${issue_number}." # Best-effort labels: only add labels that already exist, otherwise the API call would hard-fail. desired_labels=("security" "area:pipeline") -labels_available="$(gh label list -R "${repo}" -L 200 --json name --jq '.[].name' 2>/dev/null || true)" +labels_available="$(gh label list -R "${repo}" -L 200 --json name 2>/dev/null || echo '[]')" for label in "${desired_labels[@]}"; do - if echo "${labels_available}" | grep -Fxq "${label}"; then + if jq -er --arg label "${label}" '.[] | select(.name == $label) | true' <<<"${labels_available}" >/dev/null 2>&1; then if gh issue edit "${issue_number}" -R "${repo}" --add-label "${label}" >/dev/null 2>&1; then echo "INFO: added label '${label}'." else diff --git a/tools/ci/codeql-default-setup-guardrail/get_state.sh b/tools/ci/codeql-default-setup-guardrail/get_state.sh index 394545f..0fbe265 100755 --- a/tools/ci/codeql-default-setup-guardrail/get_state.sh +++ b/tools/ci/codeql-default-setup-guardrail/get_state.sh @@ -1,13 +1,18 @@ #!/usr/bin/env bash set -euo pipefail +IFS=$'\n\t' +export LC_ALL=C repo="${GITHUB_REPOSITORY:-${REPO:-}}" if [[ -z "${repo}" ]]; then echo "ERROR: missing GITHUB_REPOSITORY/REPO env (expected owner/name)." >&2 exit 2 fi +if [[ ! "${repo}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo "ERROR: invalid repo slug: '${repo}' (expected owner/name)." >&2 + exit 2 +fi -BASE_GH_TOKEN="${GH_TOKEN:-}" CODEQL_TOKEN="${CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN:-}" retry() { @@ -29,16 +34,25 @@ retry() { done } +gh_api() { + local -r endpoint="$1"; shift + if [[ -n "${CODEQL_TOKEN}" ]]; then + GH_TOKEN="${CODEQL_TOKEN}" GH_REPO="${repo}" gh api "${endpoint}" "$@" + else + GH_REPO="${repo}" gh api "${endpoint}" "$@" + fi +} + state="" -if [[ -n "${CODEQL_TOKEN}" ]]; then - export GH_TOKEN="${CODEQL_TOKEN}" -fi -if ! state="$(retry 4 1 gh api "repos/${repo}/code-scanning/default-setup" --jq .state)"; then - export GH_TOKEN="${BASE_GH_TOKEN}" +# Backoff schedule for max=4, base=1s: 1s, 2s, 4s. +if ! state="$(retry 4 1 gh_api "repos/{owner}/{repo}/code-scanning/default-setup" --jq .state)"; then echo "ERROR: failed to query CodeQL default-setup state via GitHub API." >&2 exit 3 fi -export GH_TOKEN="${BASE_GH_TOKEN}" +if [[ -z "${state}" ]]; then + echo "ERROR: invalid API response (missing state)." >&2 + exit 3 +fi drift="false" if [[ "${state}" != "not-configured" ]]; then