#!/usr/bin/env bash # ============================================================================= # L0 static check (offline, no cluster) — vuln-triage-bot-workflow-guards # Serves: R-VULN-05 (runs on a schedule, fails loudly), R-VULN-07 (bot conventions, docs/17). # # GIVEN .gitea/workflows/cve-triage.yaml, # THEN (1) it runs on `15 7 * * *` and has a workflow_dispatch `dry_run` input; # (2) concurrency is set (the workflow never runs against itself); # (3) a step fails the job HARD when RENOVATE_TOKEN is empty (exit non-zero, not skip); # AND (4) the evaluator is run on mutated copies and must flag each (non-vacuity). # A missing workflow file is a FAIL: this is whole-repo scoped and has no skip path. # ============================================================================= set -euo pipefail DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "${DIR}/../../../.." && pwd)" exec python3 - "$ROOT" <<'PY' import copy, re, sys from pathlib import Path import yaml ROOT = Path(sys.argv[1]) NAME = "vuln-triage-bot-workflow-guards" FAILURES, CHECKS = [], 0 WF = ROOT / ".gitea" / "workflows" / "cve-triage.yaml" def check(cond, desc): global CHECKS CHECKS += 1 if not cond: FAILURES.append(desc) if not WF.is_file(): print(f"CHECK FAIL ({NAME}): .gitea/workflows/cve-triage.yaml is absent (the BOT's deliverable); " f"whether it fails on a missing credential cannot be asserted without it", file=sys.stderr) raise SystemExit(1) doc = yaml.safe_load(WF.read_text()) def evaluate(doc): bad = [] triggers = doc.get(True) if True in doc else doc.get("on") # YAML reads `on:` as boolean True if not isinstance(triggers, dict): return ["no triggers parsed"] if "15 7 * * *" not in [s.get("cron") for s in triggers.get("schedule") or [] if isinstance(s, dict)]: bad.append("no schedule with cron 15 7 * * *") inputs = ((triggers.get("workflow_dispatch") or {}).get("inputs") or {}) if "dry_run" not in inputs: bad.append("workflow_dispatch has no dry_run input") conc = doc.get("concurrency") jobs = doc.get("jobs") or {} if not conc and not any(isinstance(j, dict) and j.get("concurrency") for j in jobs.values()): bad.append("no concurrency set (the bot must not run against itself)") hard_fail = False for j in jobs.values(): for step in (j.get("steps") or []) if isinstance(j, dict) else []: run = str(step.get("run", "")) if ("RENOVATE_TOKEN" in run and re.search(r"(-z|!\s*-n)[^\n]*RENOVATE_TOKEN|RENOVATE_TOKEN[^\n]*(-z)", run) and re.search(r"exit\s+[1-9]", run)): hard_fail = True if not hard_fail: bad.append("no step fails hard (exit non-zero) when RENOVATE_TOKEN is empty") return bad for v in evaluate(doc): check(False, f"(1-3) {v}") check(isinstance(doc.get("jobs"), dict) and doc["jobs"], "(1) the workflow declares jobs") # (4) non-vacuity def mutate(fn): m = copy.deepcopy(doc) fn(m) return m def trig(m): return m.get(True) if True in m else m.get("on") def no_cron(m): trig(m).pop("schedule", None) def no_dry(m): trig(m)["workflow_dispatch"] = {} def no_conc(m): m.pop("concurrency", None) for j in m.get("jobs", {}).values(): if isinstance(j, dict): j.pop("concurrency", None) def no_guard(m): for j in m.get("jobs", {}).values(): for step in j.get("steps") or []: if "RENOVATE_TOKEN" in str(step.get("run", "")): step["run"] = 'echo "::warning::RENOVATE_TOKEN unset, skipping"' for name, fn in (("cron removed", no_cron), ("dry_run input removed", no_dry), ("concurrency removed", no_conc), ("credential check softened", no_guard)): check(bool(evaluate(mutate(fn))), f"(4) evaluator missed mutant: {name}") if FAILURES: print(f"CHECK FAIL ({NAME}): {len(FAILURES)} of {CHECKS} assertions failed", file=sys.stderr) for f in FAILURES: print(f" - {f}", file=sys.stderr) raise SystemExit(1) print(f"PASS ({NAME}): {CHECKS} assertions") PY