Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,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()
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/codeql-default-setup-guardrail.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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: false

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 }}
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)
if: steps.state.outputs.drift == 'true'
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)
if: steps.state.outputs.drift == 'true'
run: exit 1
30 changes: 30 additions & 0 deletions docs/security/010_CODEQL_DEFAULT_SETUP_GUARDRAIL.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 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/{owner}/{repo}/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.

## 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.
1 change: 1 addition & 0 deletions tools/ci/bin/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
114 changes: 114 additions & 0 deletions tools/ci/check-codeql-default-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/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}"

BASE_GH_TOKEN="${GH_TOKEN:-}"
CODEQL_TOKEN="${CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN:-}"

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}"
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
}

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
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 gh_api "repos/{owner}/{repo}/code-scanning/default-setup" > "${DEFAULT_SETUP_JSON}" 2>> "${RAW_LOG}"; then
break
fi
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)."
fi
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."
108 changes: 108 additions & 0 deletions tools/ci/codeql-default-setup-guardrail/ensure_issue.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/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

CODEQL_TOKEN="${CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN:-}"

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
}

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 ! 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
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."
exit 0
fi

title="SECURITY: CodeQL Default Setup ist aktiviert (Guardrail)"
marker="<!-- codeql-default-setup-guardrail -->"

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'
if [[ ! "${state}" =~ ^[a-z-]+$ ]]; then
state="unknown"
fi
body+="Observed state: \`${state}\`"$'\n\n'
body+="${marker}"$'\n'

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/{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 2>/dev/null || echo '[]')"
for label in "${desired_labels[@]}"; do
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
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
69 changes: 69 additions & 0 deletions tools/ci/codeql-default-setup-guardrail/get_state.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/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

CODEQL_TOKEN="${CODEQL_DEFAULT_SETUP_GUARDRAIL_TOKEN:-}"

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
}

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=""
# 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
if [[ -z "${state}" ]]; then
echo "ERROR: invalid API response (missing state)." >&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
Loading