v0.11.0
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.YAMLErrorhandler. Production sites inparser/github_actions.py,parser/gitlab_parser.py,ignore.py, andtopology/loader.pynow wrapRecursionErroras the sameValueError/TopologyLoadErrorthey emit for malformed YAML. The Atheris harness'sEXPECTEDtuple gainsRecursionErrorso 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 attests/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, runsscan_repo()against the tempdir, and rolls per-repo results into oneOrgAuditReport. Routing through the samescan_repo()helper the CLI + MCP scan_repo tool already use means every per-file finding the org dashboard reports is the same findingscan-repowould have reported run locally.OrgProviderProtocol (list_repos(org)+fetch_pipeline_files(repo)) keeps platform-specific wiring isolated.GitHubOrgProvideris the only concrete implementation today; GitLab / Bitbucket / Azure DevOps fit the same shape. Pagination via?page=N&per_page=100up 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/--excluderegex onowner/name,--limitcaps the audit universe (after filtering). Forks + archived repos honoured by Contents API behaviour.CIGUARD_GITHUB_TOKENfor auth;CIGUARD_GITHUB_API_URLoverride for GHE. - Output: text-format dashboard summary by default;
--format htmlwrites a self-contained dark-mode org dashboard (severity chips, grade distribution, platform mix, skipped/error tracking) reusing the existing visual vocabulary;--format jsonemits the fullOrgAuditReport.
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 fullimage:tagstring, 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 wheredistinct_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.pyre-parses each pipeline file in the materialised tempdir and calls the existingextract_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 DIRwrites the dashboard to<DIR>/dashboard.htmland onehtml-interactivepipeline 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 infile://mode), browsable vials, no nested-deep paths that confuse operators copying the deliverable to a fileshare. Slash inowner/namecollapsed to__(avoids needing to mkdir a subdirectory per owner). - CLI surface:
--output-dir DIRflag. When set, the dashboard always writes to<DIR>/dashboard.html; explicit--outputis honoured only when--output-diris 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 --verifycross-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).GitHubProviderimplementation reads/repos/{owner}/{repo}/environmentsand/repos/{owner}/{repo}/branches/{default}/protection, normalising GitHub's protection-rule shape onto ciguard's gate vocabulary (required_reviewers→manual_approval+required_reviewer;wait_timer→wait_timer;branch_policy→deployment_environment_protection). Token viaCIGUARD_GITHUB_TOKEN;CIGUARD_GITHUB_API_URLoverride for GHE.- Provider is a Protocol (single
fetch(repo) → RepoSnapshotmethod) 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
verificationfield, and into the text summary via a posture line + per-record breakdown. Panel omitted when--verifyis 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 ascan-repoaggregate 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 smallcleanmarker so the operator can distinguish "scanned and clean" from "no scan data".ciguard topology --scan-repo <path>— convenience flag that runsscan-repointernally 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-repodidn't find (renames / deletions) AND scanned pipelines that noDeployEdgeclaims (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 functionaggregate_scan_into_topology(topology, scan_result) -> dictreturning{by_env, by_edge, unmatched_pipelines, unmatched_files}. Composite keys serialise as"<service>::<env>"so the result is JSON-clean.--format jsonnow embeds this underscan_aggregatewhen 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 asinventory_html.pyandhtml_interactive.pyso the audit deliverables are coherent. Print-friendly via@media print. Pure function in newsrc/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 explicitno gateschip 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_envis 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_reachper segment, markedisolatedwhen 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.pyexercises 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, andwrite_reportdirectory creation.
Added — Slice 16 session 1 (topology data model + YAML loader)
ciguard topologyCLI verb — validates and summarises aciguard.topology.ymlfile (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 jsonemits the full validatedTopologymodel for downstream tooling.- Topology data model in
src/ciguard/models/topology.py— six pydantic entities (Service,Environment,DeployEdge,EnvTransition,SecretScope,NetworkSegment) +Topologyaggregate 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
Topologyanswering 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 ofcan_reach),production_environments()(matchesprod/production/livetiers, case-insensitive). - YAML loader in
src/ciguard/topology/loader.py—load(path)returns a validatedTopology;discover(start)walks up from a directory looking forciguard.topology.yml, stopping at.gitor filesystem root. SingleTopologyLoadErrorwraps 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.ymlfile. - Sample fixture at
tests/fixtures/topology/sample.topology.yml— realistic 3-service / 4-environment shape exercising every entity type, including the deliberateworker → prodedge with no gates and thedev → prodtransition 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-repooutput — 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-repooutput into the topology to compute per-environment finding counts.
Added — Slice 14b (infrastructure inventory — first 3 probes)
ciguard inventoryCLI 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-TOKENheader. Distinguishes CE vs EE. - GitHub Enterprise (
/api/v3/meta) —CIGUARD_GHE_URL+_TOKEN. Bearertokenauth.
- Jenkins (
- Probe protocol in
src/ciguard/inventory/probes.py—ProbeProtocol +InventoryRunnerorchestrator +register()decorator + sharedhttp_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 →errorfield populated, runner never crashes. - EOL enrichment —
enrich_with_eol()looks up each tool's version against endoflife.date (jenkins,gitlab,github-enterprise-serverslugs). Falls back to major.minor matching when patch versions don't have their own cycle entry. Reuses the existing~/.ciguard/cache/directory and--offlineflag. - Output formats — coloured text table by default (
tool,version,edition,status,notes),--format jsonfor the fullInventoryReportmodel,--output PATHto 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 inInventoryEntry.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/checkon 403) —CIGUARD_NEXUS_URL+_USER+_PASSWORD. HTTP Basic. Surfacesedition(PRO/OSS) when available. - JFrog Artifactory (
/api/system/version, auto-prefixes/artifactory/when missing) —CIGUARD_ARTIFACTORY_URL+ (_TOKENfor 7.x OR_USER+_PASSWORDfor 6.x). Surfaceslicenseasedition. - 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 leadingvfrom version (v2.10.4→2.10.4) so endoflife matching works. - Harbor (
/api/v2.0/systeminfo) —CIGUARD_HARBOR_URL+_USER+_PASSWORD. HTTP Basic. Stripsvprefix AND build hash (v2.10.1-abc1234→2.10.1).
- Sonatype Nexus (
- Standalone HTML inventory — new
ciguard inventory --format htmlwrites 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 newsrc/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 matchedhttps://artifactory.examplebecause the//substring satisfied the leading/. Replaced with a properurlsplit().pathcheck.
Deferred — Project ciguard/PROJECT-STATE.md for current scope
- Plugin inventory for Jenkins (
/pluginManager/api/json?depth=1) — needsOverall/Administerpermission; opt-in flag rather than default. Not a blocker for the audit deliverable.
Added — Slice 15 (MCP hardening)
CIGUARD_MCP_REDACT_LEVEL=full|partial|rawenv var, defaultfull. Every MCP tool response now passes through a per-level redaction layer insrc/ciguard/mcp/redaction.pybefore reaching the LLM client. Atfull(default), every finding'sevidencefield 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.partialkeeps evidence + rewrites paths relative toCIGUARD_MCP_ROOT(1 MB cap).rawis full passthrough (10 MB safety cap). Unknown / typo'd values fall back tofull— 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}. Theargsobject is redaction-aware — what the audit log captures matches what the response leaked, no more. Path overridable viaCIGUARD_MCP_AUDIT_PATH; opt-out viaCIGUARD_MCP_AUDIT_DISABLED=1(mirrors theCIGUARD_MCP_DISABLEDtruthy 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.mdwith 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-platformPIPE-001/GHA-PIPE-001/JKN-PIPE-001:latestchecks — same surface, two perspectives (platform-rule + cross-platform supply-chain). Operators who find the duplicate noisy can.ciguardignoreeither side.SCA-PIN-004— Helm Chart Pulled Without Pinned Version (Medium). Cross-platform script-content rule. Fires forhelm install/helm upgrade/helm pullinvocations inside any job script that lack a--versionflag, or where--versionis set to a mutable label (latest,stable,main,master,dev,*).helm templateandhelm upgrade --reuse-valuesare 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 inrepo_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 markCIGuard(uppercase G with sky→indigo gradient) for the wordmark; identifierciguardfor code paths. - Self-contained: no external scripts, no fonts, no analytics. CSP in
public/_headersreflects that —default-src 'self', plus a documented'unsafe-inline'exception onstyle-srcbecause 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 grouplanding-deploy(last-push-wins).paths-filterscoped tolanding/**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 nameciguard, output dir./dist,compatibility_date 2026-05-02. Build-settings changes go through PR review rather than dashboard hunting.landing/README.mdrewritten "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-markno longer hardcodes 20px so inline prose mentions inherit surrounding font size; header keeps the larger 20px via.brand .brand-markscoped rule, footer keeps its existing.footer-mark18px 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 becausedist/had no404.html. Symptoms: search-engine duplicate content, masked recon probes (/admin,/.git,/.envall returning 200), confused operators on mistyped URLs. Three artifacts added:src/pages/404.astro(Astro auto-generatesdist/404.html, served with real HTTP 404 when no other route matches; reuses Base layout so brand + headers stay consistent),public/robots.txt(explicitUser-agent: * / Allow: /plus aSitemap: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 aniodefreporting channel; optional--enable-dnssectoggle 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-runfor 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_onwas looked up as a single key'result.expires_on'on the response object instead ofresultthenexpires_on. The zone-resolve path.result.[].idhappened 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()todatetime.now(tz=timezone.utc):models/pipeline.py(Report.scan_timestamp),models/inventory.py(InventoryReport.scan_timestamp),reporter/pdf_report.py(PDF footer). Format shifts from2026-05-02T13:00:00to2026-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 from1f65836was correct). - #32 + #33 (py/unused-global-variable,
html_interactive.py:90, 97) — vestigial_SEVERITY_COLOURS/_NEUTRAL_BORDERconstants from an early Slice 14a draft; the palette was inlined into_VIEWER_CSSonce the visual language stabilised. - #35 (py/repeated-import,
html_interactive.py:1582) — localimport re as _reshadowing 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.
- #30 (py/unused-local-variable,
- 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 inreporter/html_interactive.py. Local pytest had been passing because lint isn't in the local test loop; CI'schecks/lintstep 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_htmlmatcher 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 flagsecret_scopesas "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 theFindingobject, not encoded in the ID. Earlier audit-scope spec hadSCA-PIN-001= mutable tag (High); corrected to keep the existing v0.6.0 digest rule atSCA-PIN-001and add the new mutable-tag rule at the next free number.