v0.11.1
Highlights: The GitHub App receiver now ships a real scan executor — v0.10.0's stub is gone. On every webhook, the App mints an installation token, fetches the repo tarball at the head SHA from GET /repos/{o}/{r}/tarball/{ref}, extracts it under a tempfile.TemporaryDirectory() with per-member validation, runs repo_scan.scan_repo(include_findings=True) against the extracted tree, and posts the findings back as a Check Run + PR comment. v0.11.1 also closes the only finding from the Cycle 1.5 self-pentest (F-9.6.1, NUL bytes in _safe_md_inline), adds a 12-section production-deploy guide (DEPLOYMENT.md), and fixes a CodeQL py/tarslip advisory by switching tar.extractall() to per-member tar.extract(). Test count: 962 → 977 (+15).
Security
- F-9.6.1 —
_safe_md_inline()now strips ASCII NUL + remaining C0 control chars. Surfaced by Cycle 1.5 self-pentest (2026-05-03). The inline PR-comment sanitiser previously stripped CR/LF/tab and HTML-encoded<>but let\x00through. CWE-158 (Improper Neutralization of Null Byte). CVSS v3.1 3.7 (Low). The block-level sibling_safe_md_block()was already safe via its 4-backtick fence wrapping. New regression PoC attests/regression/cycle1.5/F-9.6.1-null-byte-inline.pylocks the fix in CI. Disclosure: same-day publishable perfeedback_coordinated_disclosure_window.mdrationale (verified ~zero installs at test time); no GHSA filed. - CodeQL
py/tarslip(CWE-22) advisory closed — v0.11.1's clone executor previously calledtar.extractall(path=dest)after a manual member-validation pass; CodeQL pattern-matchesextractall()regardless of preceding validation, ANDextractalloperates on the livememberslist which a hostiletarfilesubclass could re-mutate between validation and extraction. Now validates AND extracts one member at a time viatar.extract(member, path=dest)— TOCTOU window closed, CodeQL silenced. On Python 3.12+ also passesfilter="data"belt-and-braces. - Cycle 1.5 self-pentest closed green. 21 of 21 in-scope rows Verified across Surface 9 (GitHub App receiver, 13 STRIDE rows) + Surface 10 (MCP redaction, 7 PoC rows) + 4-row Cycle-1 regression retest. All four 2026-04-27 GHSAs remain CLOSED in v0.11.1. Full CREST-style report at
Pentest Reports/2026-05-03-cycle-1.5.mdin the planning vault. Two reportable findings: F-9.6.1 (Low, fixed) + F-9.1.1 (Info, ASCII OWS in signature header — issue #23, defence-in-depth observation only, no exploitation chain).
Added — GitHub App scan executor
clone_and_scan_executorinsrc/ciguard/app/clone_executor.py— replaces v0.10.0's_stub_scan_executoras the default infactory.py. Pipeline: mint installation token →GET /repos/{o}/{r}/tarball/{ref}withAuthorization: token <ghs_…>(token in header, never URL) → extract undertempfile.TemporaryDirectory()with per-member safety validation →repo_scan.scan_repo(include_findings=True)→ translate to PR-comment-renderer shape. Stub kept available for tests viacreate_app(scan_executor=...)injection.- Why tarball, not
git clone: nogitbinary in the deploy image (smaller container, fewer CVEs to patch); single HTTP request instead of multi-stage clone+fetch+checkout; tarball auto-derived from the ref so no "did the shallow clone include this SHA?" edge cases; smaller transfer (no.git/objects). - Threat-model controls landed: 200 MB streaming size cap (
TarballTooLarge) closes Surface 9 row "Webhook handler DoS"; token inAuthorizationheader (never URL) closes Surface 9 row "Installation token leakage"; per-memberextract()after validation closes archive-injection sub-row of "Multi-tenant baseline.json bleed"; HTTP 401 from tarball API triggerstokens.invalidate_token()to honour Surface 9 row "Cached installation token used after revocation". repo_scan.scan_repo(include_findings=True)— new opt-in keyword. Default off preserves v0.9.x dict shape that the CLI scan-repo + MCP scan_repo tool depend on. With it on, the result dict gainsfindings(flat list across all files), per-filefindings, and aggregaterisk_score/grade(worst per-file score gates the repo).
Added — DEPLOYMENT.md
- 12-section production-deploy guide at
DEPLOYMENT.md— operator-facing companion to README. Covers: what you're deploying (single-process, in-memory queue, 200 MB cap), App registration viadeploy/app/manifest.yml, secret material + file-mode hygiene, dedicated non-root UID (useradd --system ciguard), reverse proxy + TLS (full nginx/webhookblock withrequest_buffering offso raw bytes reach HMAC unmodified), systemd unit with the hardening directives that close Surface 9 row "App private key exposure" (NoNewPrivileges,ProtectSystem=strict,PrivateTmp,ProtectKernel*,MemoryDenyWriteExecute,SystemCallFilter=@system-service,ReadWritePathsbound to storage), container layout (--cap-drop=ALL --security-opt no-new-privileges, read-only root), storage layout<root>/<install_id>/<owner>/<repo>/baseline.json+ backup posture, observability (logger map + what to alert on), upgrades + rollback (Sigstore + PEP 740 verification snippets), full env-var contract for all 12CIGUARD_*vars, 11-box pre-flight checklist. - README "Running the GitHub App" section bumped from v0.10.0 to v0.11.1; stub-status callout removed; cross-reference to
DEPLOYMENT.mdadded.
Cycle 1.5 follow-up issues filed (no time pressure; ship in next minor)
- #20 — Drop
Actions: readfromTHREAT_MODEL.mdSurface 9 +deploy/app/manifest.yml(the v0.11 stub-and-real scan paths don't use it; tarball clone carries workflow files anyway). Documentation patch. - #21 — Document MCP redaction-layer truncation behaviour (
_enforce_size_capcollapses to a marker rather than packing-to-fit when the cap fires). Docstring expansion. - #22 — Lint
.pemfile mode in_load_private_key()— defence-in-depth (operator's filesystem permissions are the primary control, butos.stat() & 0o077 == 0would surface misconfigurations earlier). Hardening. - #23 — Permissive ASCII OWS in
X-Hub-Signature-256header (F-9.1.1 from the cycle 1.5 report — Info, CVSS 0.0). Library-level OWS-strip per RFC 7230 §3.2.4 means leading SPACE/TAB in the signature value is silently accepted. HMAC over the body is unaffected; defence-in-depth concern only. Address only on a concrete WAF/IDS gap report.
Engineering hygiene
- Per-member
tar.extract()inclone_executor— cleaner thanextractall()because the validation and extraction happen on the samememberobject reference. No risk of TOCTOU if a hostiletarfilesubclass remutatesmembersbetween validation pass and extraction. assert sys.version_info >= (3, 12)removed fromclone_executorimport time — was blocking 3.10/3.11 test runs (the package's declaredpython_requires=">=3.10"envelope). Replaced with manual_safe_extractthat works on all supported Pythons; 3.12+ still getsfilter="data"belt-and-braces.# nosec B310annotation on the singleurllib.request.urlopencall site, with rationale: URL constructed from the hardcodedGITHUB_API_BASEconstant + caller-controlled owner/repo/ref; scheme always https; no file:// or custom-scheme exposure.- § → "Section" sweep across DEPLOYMENT.md and the Cycle 1.5 vault docs — long-standing user preference (not legalistic). Memory
feedback_documentation_conventions.mdstrengthened with the second-time-broke-it warning + reset recipe.