Skip to content

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 03 May 13:42
· 28 commits to main since this release

Highlights: This release matures ciguard from per-pipeline scanner to org-wide audit deliverable. Slices 14b/14c/15/16/17 add cross-pipeline topology with live-API verification, an org-level dashboard with per-repo D3 drill-downs, MCP redaction + audit-log hardening, and three pin-discipline rules. Slice 10 ships ciguard.dev — a static landing page on Cloudflare Pages with CAA pinning, DMARC/SPF email-spoofing lockdown, 404 page, robots, and sitemap. Atheris weekly fuzz finding #18 (pyyaml RecursionError on deeply-nested input) is fixed. Test count: 743 → 962 (+219).

Security

  • Fuzz #18 — pyyaml RecursionError now wrapped as ValueError on every yaml.safe_load site. Atheris weekly run (2026-05-03) found a 1915-byte deeply-nested YAML input that escaped each parser's yaml.YAMLError handler. Production sites in parser/github_actions.py, parser/gitlab_parser.py, ignore.py, and topology/loader.py now wrap RecursionError as the same ValueError / TopologyLoadError they emit for malformed YAML. The Atheris harness's EXPECTED tuple gains RecursionError so the harness keeps catching new crash classes. Severity assessment: low for the CLI (trusted file input), but real-DoS for the GitHub App webhook + MCP server paths that ship in this release. Original Atheris artifact preserved at tests/fixtures/fuzz/recursion-yaml-issue-18.bin; regression test pins the bug class with a synthetic 2000-deep nest. Closes #18.

Added — Slice 17 (org-level audit dashboard — three sessions)

Session 1 — repo enumeration + scan + rollup

  • ciguard audit-org --org <name> walks every repo in a GitHub organisation, fetches pipeline files via the Contents API, materialises them to a tempdir per repo, runs scan_repo() against the tempdir, and rolls per-repo results into one OrgAuditReport. Routing through the same scan_repo() helper the CLI + MCP scan_repo tool already use means every per-file finding the org dashboard reports is the same finding scan-repo would have reported run locally.
  • OrgProvider Protocol (list_repos(org) + fetch_pipeline_files(repo)) keeps platform-specific wiring isolated. GitHubOrgProvider is the only concrete implementation today; GitLab / Bitbucket / Azure DevOps fit the same shape. Pagination via ?page=N&per_page=100 up to 5,000 repos; user-vs-org fallback when the slug isn't an org; 404 → None for soft-miss handling on root-level pipeline file probes (.gitlab-ci.yml / Jenkinsfile most repos won't have).
  • Filtering: --include / --exclude regex on owner/name, --limit caps the audit universe (after filtering). Forks + archived repos honoured by Contents API behaviour. CIGUARD_GITHUB_TOKEN for auth; CIGUARD_GITHUB_API_URL override for GHE.
  • Output: text-format dashboard summary by default; --format html writes a self-contained dark-mode org dashboard (severity chips, grade distribution, platform mix, skipped/error tracking) reusing the existing visual vocabulary; --format json emits the full OrgAuditReport.

Session 2 — image inventory + pin-discipline rollup

  • OrgAuditReport.image_inventory — list of one record per distinct image-name (dedup key is the bare name, not the full image:tag string, because the auditor's question is "how many flavours of python are we running"). Each record carries the set of repos using it, distinct tag values seen, pin-status mix, registries seen, and total reference count.
  • OrgAuditReport.image_inconsistencies — subset where distinct_tag_count > 1, the headline auditor callout ("twelve flavours of python across fifty services"). Sorted by descending repo-count then ascending name.
  • OrgAuditReport.pin_discipline — counts + 1-decimal percentages for digest / tag / mutable across every image reference in every scanned repo. The denominator the audit-scope spec wanted.
  • Implementation in new audit_org/images.py re-parses each pipeline file in the materialised tempdir and calls the existing extract_image_references() from sca_rules, so the digest/tag/mutable classification matches what the per-pipeline scan reports — single source of truth.
  • Dashboard surfaces: image-inventory panel with the multi-variant inconsistencies upfront; text summary gains a pin-discipline percentage line + an inconsistency call-out listing the top 5 multi-variant images with their tag set.

Session 3 — per-repo drill-down maps

  • ciguard audit-org --output-dir DIR writes the dashboard to <DIR>/dashboard.html and one html-interactive pipeline map per discovered pipeline file under <DIR>/repos/<owner>__<name>/<stem>.html. Dashboard repo rows render link chips for each rendered map so an auditor can drill from the org view straight into a specific pipeline's D3 visualisation.
  • Filesystem layout chosen so relative <a href> from the dashboard resolves without a server (works in file:// mode), browsable via ls, no nested-deep paths that confuse operators copying the deliverable to a fileshare. Slash in owner/name collapsed to __ (avoids needing to mkdir a subdirectory per owner).
  • CLI surface: --output-dir DIR flag. When set, the dashboard always writes to <DIR>/dashboard.html; explicit --output is honoured only when --output-dir is unset (preserves the session-1 single-file behaviour).

Test count across Slice 17: 874 → 965 (+91) — 23 audit-org orchestrator tests, 14 image-inventory tests, 5 drill-down tests, 8 dashboard-rendering tests, 11 reporter tests, plus Contents API materialisation integration coverage.

Added — Slice 16 session 4 (topology live-API verification)

  • ciguard topology --verify cross-checks the operator-asserted topology against the live SCM platform on the gate dimension, complementing session 3's drift panel on the file dimension. Surfaces three drift kinds: environment-not-found (asserted env doesn't exist), gate-not-actual (asserted protection missing on the live env), gate-actual-not-asserted (live env has a rule the topology under-claims).
  • GitHubProvider implementation reads /repos/{owner}/{repo}/environments and /repos/{owner}/{repo}/branches/{default}/protection, normalising GitHub's protection-rule shape onto ciguard's gate vocabulary (required_reviewersmanual_approval + required_reviewer; wait_timerwait_timer; branch_policydeployment_environment_protection). Token via CIGUARD_GITHUB_TOKEN; CIGUARD_GITHUB_API_URL override for GHE.
  • Provider is a Protocol (single fetch(repo) → RepoSnapshot method) so GitLab / Bitbucket can plug in later without reshaping the verifier. Verifier is pure orchestration — tests inject a stub Provider, no network.
  • Renders into the topology HTML via a "Live verification" panel (drift table + per-repo snapshot table + unverifiable list), into the JSON output via a verification field, and into the text summary via a posture line + per-record breakdown. Panel omitted when --verify is off or every check is clean.
  • Test count: 874 → 907 (+33) — 25 verify tests covering provider mapping, branch-protection 401/404 handling, drift detection across all three kinds, branch-level vs env-level gate routing, repo dedup, unverifiable edges. 8 HTML panel tests covering omission, danger/warning styling, per-repo snapshot table, summary count, HTML escaping.

Added — Slice 16 session 3 (cross-pipeline scan overlay)

  • ciguard topology --scan-output <json> — pair a scan-repo aggregate with the topology to overlay per-environment + per-(service, env) severity chips on the swimlane HTML page. Every cell that maps to a scanned pipeline gets compact severity chips (1 C, 2 H, 1 L ...); each environment header carries per-environment totals across every pipeline that deploys to that env. Cells with zero findings get a small clean marker so the operator can distinguish "scanned and clean" from "no scan data".
  • ciguard topology --scan-repo <path> — convenience flag that runs scan-repo internally and feeds the result. Mutually exclusive with --scan-output. Errors degrade gracefully: failure to read the scan output / failure of the internal scan-repo prints a warning and renders without the overlay rather than failing the whole operation.
  • Drift panel added to the topology HTML page — surfaces asserted-vs-actual mismatches: pipelines the topology declares but scan-repo didn't find (renames / deletions) AND scanned pipelines that no DeployEdge claims (orphan workflows). Both are auditor-relevant. Panel is omitted entirely when the asserted topology and the scanned files are perfectly aligned.
  • New aggregator module at src/ciguard/topology/aggregate.py — pure function aggregate_scan_into_topology(topology, scan_result) -> dict returning {by_env, by_edge, unmatched_pipelines, unmatched_files}. Composite keys serialise as "<service>::<env>" so the result is JSON-clean. --format json now embeds this under scan_aggregate when an overlay was requested.
  • Test count: 856 → 874 (+18) — 14 aggregator tests covering per-edge counts, per-env totals, drift detection, shared-pipeline edge cases, empty inputs; plus 8 HTML overlay tests covering severity chip rendering, env-header totals, summary line, clean-cell marker, drift panel presence/absence, backwards-compat with aggregate=None.

Added — Slice 16 session 2 (topology HTML swimlane reporter)

  • ciguard topology --format html --output topology.html — self-contained dark-mode swimlane page rendering the cross-pipeline graph: services × environments grid with promotion-transition arrows showing which gates protect each step, plus secret-scope blast-radius and network-reachability panels. Same visual vocabulary as inventory_html.py and html_interactive.py so the audit deliverables are coherent. Print-friendly via @media print. Pure function in new src/ciguard/reporter/topology_html.py — no JavaScript, no dependencies, unit-tested on the rendered string.
  • Layout decisions: environments order left-to-right by tier (development → test → staging → production); production-tier columns get a red-tinted header so they're always findable; deploys to production with no gates render with a red border + danger-tinted background; gateless promotion transitions render with a red arrow and explicit no gates chip in the inter-column transition row.
  • Top-of-page warnings banner enumerates the auditor's highest-signal red flags: gateless deploys to production-tier environments AND gateless transitions whose to_env is production-tier. Operators see the worst posture issues without scrolling. Banner is omitted entirely when there are none — clean topologies stay quiet.
  • Bottom panels: secret-scope blast radius (which environments + services share each scope?) and network reachability (transitive closure of can_reach per segment, marked isolated when empty). Both omitted when their underlying entity collection is empty.
  • Why standalone vs embedded in the per-pipeline visualiser: topology is per-org, the visualiser is per-pipeline. Embedding the same topology page in every pipeline's HTML would duplicate the data N times. Slice 17's org-level audit will combine both into one page.
  • Test count: 836 → 856 (+20)tests/test_topology_html.py exercises document shape, swimlane ordering + production highlighting, deploy-cell + empty-cell + danger states, transition-row arrow direction, warnings-banner presence/absence, secret + network panels, HTML escaping for operator-supplied evil, and write_report directory creation.

Added — Slice 16 session 1 (topology data model + YAML loader)

  • ciguard topology CLI verb — validates and summarises a ciguard.topology.yml file (the cross-pipeline graph: services, environments, deploy edges, promotion gates, secret scopes, network segments). Auto-discovers from cwd upward, same convention as .ciguardignore. Text format prints an auditor-focused posture summary (production deploy targets + their gates, gateless promotions called out in yellow, secret-scope blast radius, network reachability per segment); --format json emits the full validated Topology model for downstream tooling.
  • Topology data model in src/ciguard/models/topology.py — six pydantic entities (Service, Environment, DeployEdge, EnvTransition, SecretScope, NetworkSegment) + Topology aggregate root. Cross-reference integrity validated at construction time: a typo in a service id, environment id, or network segment fails loud at load, not at query time.
  • Query helpers on Topology answering the auditor questions named in the audit-scope spec: pipelines_for_environment(env), services_sharing_secret(scope), transitions_without_gates(), reachable_segments(segment) (transitive closure of can_reach), production_environments() (matches prod / production / live tiers, case-insensitive).
  • YAML loader in src/ciguard/topology/loader.pyload(path) returns a validated Topology; discover(start) walks up from a directory looking for ciguard.topology.yml, stopping at .git or filesystem root. Single TopologyLoadError wraps every failure mode (missing file, YAML parse error, schema mismatch, cross-reference violation) with the file path prepended so error messages point at the actual .yml file.
  • Sample fixture at tests/fixtures/topology/sample.topology.yml — realistic 3-service / 4-environment shape exercising every entity type, including the deliberate worker → prod edge with no gates and the dev → prod transition with no approval gate so the auditor warnings have something to flag.
  • Test count: 805 → 836 (+31) — model construction + cross-reference validation + every query helper + every loader error mode + discovery walk semantics.

Deferred to follow-on Slice 16 sessions

  • Auto-discovery from scan-repo output — derive a partial topology from pipeline files alone, no operator YAML required. Useful first-run experience.
  • Live-API verification — cross-check the asserted topology against GitHub deployment-environments + branch-protection APIs (and GitLab equivalents) to flag drift between asserted and actual.
  • Visual rendering — a topology HTML page (probably standalone like the inventory page) showing environment swimlanes, deploy-edge graph, gate annotations, network-segmentation overlays.
  • Cross-pipeline aggregation — feed a scan-repo output into the topology to compute per-environment finding counts.

Added — Slice 14b (infrastructure inventory — first 3 probes)

  • ciguard inventory CLI verb — live admin-API audit of CI/CD tooling, distinct from the pipeline-file scanning the rest of ciguard does. Reads operator-supplied env vars, calls each tool's admin API for version + edition, cross-references with endoflife.date for EOL/EOS warnings.
  • Three priority probes shipped:
    • Jenkins (/api/json) — CIGUARD_JENKINS_URL + _USER + _TOKEN. HTTP Basic auth.
    • GitLab self-host (/api/v4/version) — CIGUARD_GITLAB_URL + _TOKEN. PRIVATE-TOKEN header. Distinguishes CE vs EE.
    • GitHub Enterprise (/api/v3/meta) — CIGUARD_GHE_URL + _TOKEN. Bearer token auth.
  • Probe protocol in src/ciguard/inventory/probes.pyProbe Protocol + InventoryRunner orchestrator + register() decorator + shared http_get_json() HTTP helper (8s timeout, 5 MB response cap, clear error messages on 401 / 404 / non-JSON / oversize).
  • Auth model — strict env-var gate. Missing vars → configured=False, no network call, silent skip. Configured probe with API failure → error field populated, runner never crashes.
  • EOL enrichmentenrich_with_eol() looks up each tool's version against endoflife.date (jenkins, gitlab, github-enterprise-server slugs). Falls back to major.minor matching when patch versions don't have their own cycle entry. Reuses the existing ~/.ciguard/cache/ directory and --offline flag.
  • Output formats — coloured text table by default (tool, version, edition, status, notes), --format json for the full InventoryReport model, --output PATH to write to file (ANSI codes stripped on file write). --fail-on {none,approaching-eol,end-of-support,end-of-life,error} exit-code gate for CI use.
  • Status taxonomy — five states: unconfigured / ok / approaching-eol (≤180 days) / end-of-support / end-of-life / error. Single source of truth in InventoryEntry.status.
  • README "Infrastructure inventory" section + Network egress table updated to include the operator-supplied admin-API destinations.
  • Test count: 743 → 777 (+34) — per-probe happy-path + error-mode tests, runner orchestration tests, EOL enrichment tests, registry drift guard, HTTP helper micro-tests covering the size cap + auth header + 401 messaging.

Added — Slice 14b session 2 (5 incremental probes + standalone HTML)

  • Five remaining probes shipped:
    • Sonatype Nexus (/service/rest/v1/system/info, falls back to /status/check on 403) — CIGUARD_NEXUS_URL + _USER + _PASSWORD. HTTP Basic. Surfaces edition (PRO/OSS) when available.
    • JFrog Artifactory (/api/system/version, auto-prefixes /artifactory/ when missing) — CIGUARD_ARTIFACTORY_URL + (_TOKEN for 7.x OR _USER + _PASSWORD for 6.x). Surfaces license as edition.
    • SonarQube (/api/server/version, plain-text response) — CIGUARD_SONAR_URL + _TOKEN. HTTP Basic with token-as-username (SonarQube convention).
    • ArgoCD (/api/version) — CIGUARD_ARGOCD_URL + _TOKEN. Bearer JWT. Strips leading v from version (v2.10.42.10.4) so endoflife matching works.
    • Harbor (/api/v2.0/systeminfo) — CIGUARD_HARBOR_URL + _USER + _PASSWORD. HTTP Basic. Strips v prefix AND build hash (v2.10.1-abc12342.10.1).
  • Standalone HTML inventory — new ciguard inventory --format html writes a self-contained dark-mode page with the same visual vocabulary as the pipeline visualiser (chip-style status badges, severity colours, print-friendly via @media print). Right shape for the "email this to a client" deliverable. Implementation in new src/ciguard/reporter/inventory_html.py — pure function, no JavaScript, no dependencies.
  • Test count: 777 → 805 (+28) — 17 new probe tests + 11 HTML reporter tests (drift-guard for all 8 probes registering rolled into the existing TestProbeRegistry class).
  • Bug fix during the slice: Artifactory probe's URL path-detection ("/artifactory" in base) wrongly matched https://artifactory.example because the // substring satisfied the leading /. Replaced with a proper urlsplit().path check.

Deferred — Project ciguard/PROJECT-STATE.md for current scope

  • Plugin inventory for Jenkins (/pluginManager/api/json?depth=1) — needs Overall/Administer permission; opt-in flag rather than default. Not a blocker for the audit deliverable.

Added — Slice 15 (MCP hardening)

  • CIGUARD_MCP_REDACT_LEVEL=full|partial|raw env var, default full. Every MCP tool response now passes through a per-level redaction layer in src/ciguard/mcp/redaction.py before reaching the LLM client. At full (default), every finding's evidence field is replaced by a stable 8-char SHA-256 fingerprint (redacted:abc12345) — LLM still gets actionable rule_id + severity + location + remediation, but the underlying string never crosses the boundary. Absolute paths (pipeline_name, file_path, baseline_path, etc.) collapse to basenames. Response capped at 256 KB. partial keeps evidence + rewrites paths relative to CIGUARD_MCP_ROOT (1 MB cap). raw is full passthrough (10 MB safety cap). Unknown / typo'd values fall back to full — fail-safe.
  • MCP audit log at ~/.ciguard/mcp-audit.jsonl. Every invocation appends one JSONL record {ts, tool, redact_level, args, response_bytes, had_error}. The args object is redaction-aware — what the audit log captures matches what the response leaked, no more. Path overridable via CIGUARD_MCP_AUDIT_PATH; opt-out via CIGUARD_MCP_AUDIT_DISABLED=1 (mirrors the CIGUARD_MCP_DISABLED truthy convention). Audit failures never poison the response — best-effort with stderr-only error logging.
  • README "What ciguard MCP can see" section — explicit per-tool exposure table at every redaction level. The disclosure table is the user-facing contract; if a future tool is added, this table is the first thing that updates.
  • Threat model Surface 10 (MCP-mediated data exfiltration) added to Project ciguard/THREAT_MODEL.md with 7 STRIDE rows + DREAD scoring + new trust-boundary edges. Three CYCLE-1.5 self-pentest PoC checks lined up: default-level resolution, level-cannot-be-downgraded-by-tool-arg, audit-log written even on mid-response crash.
  • tests/conftest.py — autouse fixture redirects MCP audit log to a per-test tempfile so test runs don't accumulate records in ~/.ciguard/.

Added — Slice 14c (pin-discipline rule emitters)

  • SCA-PIN-002 — Image Uses Mutable Tag (High). Cross-platform supply-chain rule. Fires for image references using a mutable tag name (:latest, :stable, :edge, :prod, :production, :main, :master, :dev, :development, :nightly) or no tag at all (Docker resolves bare names to :latest). Severity High by default — this is the OWASP CICD-SEC-3 / tj-actions-changed-files-style attack surface. Standards mapping: SLSA Level 2+, NIST SSDF PW.4.1, CIS Docker 4.7. Intentionally overlaps with the per-platform PIPE-001 / GHA-PIPE-001 / JKN-PIPE-001 :latest checks — same surface, two perspectives (platform-rule + cross-platform supply-chain). Operators who find the duplicate noisy can .ciguardignore either side.
  • SCA-PIN-004 — Helm Chart Pulled Without Pinned Version (Medium). Cross-platform script-content rule. Fires for helm install / helm upgrade / helm pull invocations inside any job script that lack a --version flag, or where --version is set to a mutable label (latest, stable, main, master, dev, *). helm template and helm upgrade --reuse-values are skipped — they're legitimate version-not-required patterns. Standards mapping: OWASP CICD-SEC-3, PCI DSS 4.0 Req 6.3.

Deferred

  • SCA-PIN-003 (cross-pipeline drift detection) was scoped in Slice 14c but requires aggregating image references across multiple pipeline files in a single scan — that lives in repo_scan.py, not the per-pipeline rule contract. Will land alongside cross-pipeline aggregation plumbing in a follow-up.

Added — Slice 10 (ciguard.dev landing page — two sessions)

Session 1 — landing page MVP

  • Astro 6.x static landing page for ciguard.dev. Output is plain HTML/CSS in dist/ — no client-side JS, no analytics, no runtime, no telemetry. Cloudflare Pages serves the built directory directly.
  • Page structure (single index.astro): hero with brand mark + dual CTA (docs + GitHub); "What it produces" four cards (pipeline visualiser, infra inventory, multi-env topology, org-level dashboard); capability snapshot — 6 stats + 8 CLI verbs grid; "How it ships" four install tiles (PyPI, PyPI[mcp], GHCR, pre-commit); supply-chain provenance row (Sigstore, SBOM attestations, public Cycle 1 report); final CTA strip + footer.
  • Visual coherence: same dark-mode palette + typography as the in-app HTML deliverables (html_interactive.py, topology_html.py, inventory_html.py, org_audit_html.py) so the brand reads as one family across the marketing surface and the audit artifacts. Brand mark CIGuard (uppercase G with sky→indigo gradient) for the wordmark; identifier ciguard for code paths.
  • Self-contained: no external scripts, no fonts, no analytics. CSP in public/_headers reflects that — default-src 'self', plus a documented 'unsafe-inline' exception on style-src because Astro inlines per-component styles (LANDING-003, accepted architectural).

Session 2 — Cloudflare Pages auto-deploy

  • .github/workflows/landing-deploy.yml — checkout → setup-node@22 → npm ci → npm run build → verify dist/index.html present → wrangler-action pages deploy. SHA-pinned actions: actions/checkout v6.0.2, actions/setup-node v6.4.0, cloudflare/wrangler-action v3.15.0. Concurrency group landing-deploy (last-push-wins). paths-filter scoped to landing/** so engine commits don't trigger marketing redeploys.
  • Permissions trimmed to contents: read + deployments: write (the Pages deployment record). Repo secrets: CLOUDFLARE_API_TOKEN (Pages:Edit + Zone:DNS:Edit, restricted to ciguard.dev) + CLOUDFLARE_ACCOUNT_ID. Scoped token rather than global API key matches the v0.9.1 deployment-hardening posture (least-privilege, revocable independently if leaked).
  • landing/wrangler.jsonc — Pages config as code: project name ciguard, output dir ./dist, compatibility_date 2026-05-02. Build-settings changes go through PR review rather than dashboard hunting.
  • landing/README.md rewritten "Deploy" section — explicit 6-step cutover bootstrap (mint scoped Cloudflare API token → add 2 repo secrets → click "Add custom domain"). Workflow is dormant until secrets are present, so the commit doesn't change any external state on its own.
  • Workflow does NOT gate the engine's required status checks. CI / CodeQL / Dogfood / Code Scanning gate are unaffected by changes under landing/**.

Fixed — Slice 10 follow-up

  • Brand mark applied to all six prose mentions — header, hero lede, supply-chain provenance paragraph, final CTA, footer mark, footer copyright line (© 2026 CIGuard contributors). CSS adjustment: .brand-mark no longer hardcodes 20px so inline prose mentions inherit surrounding font size; header keeps the larger 20px via .brand .brand-mark scoped rule, footer keeps its existing .footer-mark 18px override.
  • LANDING-001 — 404 page + robots.txt + sitemap.xml. Layer-1 pentest of the deployed site (close-out at Project ciguard/Pentest Reports/2026-05-02-landing-page.md) flagged that all unknown paths returned HTTP 200 with the homepage body — Cloudflare Pages was doing SPA-style fallback because dist/ had no 404.html. Symptoms: search-engine duplicate content, masked recon probes (/admin, /.git, /.env all returning 200), confused operators on mistyped URLs. Three artifacts added: src/pages/404.astro (Astro auto-generates dist/404.html, served with real HTTP 404 when no other route matches; reuses Base layout so brand + headers stay consistent), public/robots.txt (explicit User-agent: * / Allow: / plus a Sitemap: pointer), public/sitemap.xml (single-URL sitemap). Mozilla Observatory grade unchanged at A+ (10/10, score 120) — this commit closes the recon-surface gap that Observatory's rubric doesn't measure but auditors do. LANDING-002 (CAA) remediated separately below; LANDING-003 (CSP 'unsafe-inline' on style-src) accepted as Astro architectural.
  • LANDING-002 — Cloudflare DNS-hardening helper at scripts/cloudflare_setup_ciguard_dev.sh. One-shot, idempotent helper: adds CAA records pinning cert issuance to Google Trust Services + Let's Encrypt + DigiCert (matching Cloudflare Pages's auto-provisioner) plus an iodef reporting channel; optional --enable-dnssec toggle for the natural follow-on (closes the "hijack DNS, remove CAA, then issue rogue cert" path that CAA alone leaves open). Re-running detects existing matching records and skips, so it's safe in CI / cron / on-call runbooks. Looks up zone id by name (survives zone recreation). --dry-run for safe preview before committing changes. The script does NOT publish the DNSSEC DS record at the registrar — it prints the DS string + a deeplink to the Cloudflare registrar panel where you paste it (one-click since registrar = Cloudflare).
  • pyjq tokeniser fix in scripts/cloudflare_setup_ciguard_dev.sh. The pyjq helper was splitting the path expression on | only, never on . — so .result.expires_on was looked up as a single key 'result.expires_on' on the response object instead of result then expires_on. The zone-resolve path .result.[].id happened to coincide with the correct token sequence by accident; .result.expires_on (and any other purely-dotted path) silently returned {}. Fix: tokenise into a flat list where [] is its own item and every other piece is dot-split. Verified end-to-end with a real run on ciguard.dev — 4 CAA records added, public DNS confirms via 1.1.1.1 + 8.8.8.8, idempotent re-run shows ✓ already present for all four.

Maintenance

  • 378 pytest DeprecationWarning hits → 0. Three sites switched from datetime.utcnow() to datetime.now(tz=timezone.utc): models/pipeline.py (Report.scan_timestamp), models/inventory.py (InventoryReport.scan_timestamp), reporter/pdf_report.py (PDF footer). Format shifts from 2026-05-02T13:00:00 to 2026-05-02T13:00:00+00:00 — functionally identical; downstream consumers slice [:19] / [:10] for display so rendered output is unchanged.
  • Code-scanning quality alerts swept:
    • #30 (py/unused-local-variable, test_app_scheduler.py:86) — real bug: the _inner() async test body was defined but never invoked via _run(_inner). Test was passing vacuously. Added the missing call; assertions actually run now (and pass — the underlying fix from 1f65836 was correct).
    • #32 + #33 (py/unused-global-variable, html_interactive.py:90, 97) — vestigial _SEVERITY_COLOURS / _NEUTRAL_BORDER constants from an early Slice 14a draft; the palette was inlined into _VIEWER_CSS once the visual language stabilised.
    • #35 (py/repeated-import, html_interactive.py:1582) — local import re as _re shadowing the module-level alias.
    • #40 (py/ineffectual-statement, inventory/probes.py:49) dismissed in-API as documented false positive — ... is the canonical PEP 544 Protocol-stub idiom.
  • ruff F401/F841/E402 cleared, CI lint green. 8 × F401 unused imports, 1 × F841 unused local in sca_rules.py, 1 × E402 import-not-at-top in reporter/html_interactive.py. Local pytest had been passing because lint isn't in the local test loop; CI's checks/lint step had been failing on every push since Slice 14b session 1.
  • CodeQL py/bad-tag-filter regex tightening (alerts #31 + #50). test_no_external_script_references_in_html matcher made case-insensitive (defensive — we ship lowercase, spec allows uppercase). </script> close-tag matcher broadened from </script\s*> to </script[^>]*> (matches HTML5-spec close tags carrying garbage attributes — \s* doesn't). Two CodeQL alerts dismissed in-API as documented false positives (#47, #48): both flag secret_scopes as "sensitive data" but the field carries operator-supplied LABELS for groups of secrets, never credential values.

Documentation

  • Rule numbering convention ratified in the audit-scope memory: ciguard rule IDs follow the <FAMILY>-<SUBFAMILY>-<NNN> flat-counter pattern (Pattern 2 — Checkov / Bandit style). Severity, sub-classification, and standards mappings live in metadata fields on the Finding object, not encoded in the ID. Earlier audit-scope spec had SCA-PIN-001 = mutable tag (High); corrected to keep the existing v0.6.0 digest rule at SCA-PIN-001 and add the new mutable-tag rule at the next free number.