<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jo Moore</title>
    <description>The latest articles on DEV Community by Jo Moore (@jo_moore_9c12e09339).</description>
    <link>https://dev.to/jo_moore_9c12e09339</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3901220%2Fea1f3510-a2d4-4e4f-90f9-eff77c58ece4.png</url>
      <title>DEV Community: Jo Moore</title>
      <link>https://dev.to/jo_moore_9c12e09339</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jo_moore_9c12e09339"/>
    <language>en</language>
    <item>
      <title>How we self-pentested ciguard — Cycle 1: four findings, four advisories, two days</title>
      <dc:creator>Jo Moore</dc:creator>
      <pubDate>Mon, 27 Apr 2026 22:49:16 +0000</pubDate>
      <link>https://dev.to/jo_moore_9c12e09339/how-we-self-pentested-ciguard-cycle-1-four-findings-four-advisories-two-days-10cj</link>
      <guid>https://dev.to/jo_moore_9c12e09339/how-we-self-pentested-ciguard-cycle-1-four-findings-four-advisories-two-days-10cj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;4 findings. 4 GHSAs. 4 CVEs requested. Same-day disclosure. v0.8.2 ships with the fixes. v0.8.3 wires the four PoCs in as permanent CI regression gates so the bugs cannot silently return. Total elapsed: ~48 hours. Total cost: $0.30 in cloud spend.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ciguard, briefly
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Jo-Jo98/ciguard" rel="noopener noreferrer"&gt;&lt;strong&gt;ciguard&lt;/strong&gt;&lt;/a&gt; is a static security auditor for CI/CD pipelines — GitLab CI, GitHub Actions, and Jenkins, plus cross-platform SCA. It ships as &lt;code&gt;pip install ciguard&lt;/code&gt;, a multi-arch Docker image, and an MCP server you can plug into Claude Desktop, Claude Code, or Cursor. 44 deterministic rules across three platforms, 17 built-in policies, four output formats including SARIF 2.1.0 with native baseline diffing.&lt;/p&gt;

&lt;p&gt;It went public on PyPI on 2026-04-25. The next day, it pentested itself.&lt;/p&gt;

&lt;p&gt;This is the writeup of that engagement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why self-pentest a security tool?
&lt;/h2&gt;

&lt;p&gt;Two reasons, neither of them tactical:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One — the credibility cost of finding a bug in a security tool is multiplicative.&lt;/strong&gt; Users assume a security tool is itself secure. The moment a CVE lands against ciguard, the question every potential adopter asks is "if they couldn't keep their own code clean, why would I trust their findings about mine?" The way you avoid that is by surfacing the bugs yourself, in public, with a methodology that holds up to scrutiny.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two — pre-adoption-traction is exactly when self-pentest is cheapest.&lt;/strong&gt; ciguard had ~zero installs at v0.8.1 ship. No users are exposed by a Critical finding I'd have surfaced too late. Cost = my time + ~$0.30 in cloud spend (ephemeral droplet, destroyed at cycle close). Compare that to the cost once we have real users in real CI pipelines.&lt;/p&gt;

&lt;p&gt;So: a recurring 6-monthly cadence, starting now, scoped to the surfaces ciguard actually exposes. CREST-aligned methodology because if I ever bring in an external reviewer, the report shouldn't get dismissed as "ad-hoc."&lt;/p&gt;

&lt;h2&gt;
  
  
  The methodology
&lt;/h2&gt;

&lt;p&gt;PTES structure (Phases 0–7) for the engagement timeline. OWASP Testing Guide v4.2 for the execution checklist. CREST framing for the report so the format reads as a professional engagement, not a self-cert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lab:&lt;/strong&gt; ephemeral DigitalOcean droplet provisioned by Terraform, Ubuntu 24.04, full toolchain (atheris, ZAP, semgrep, Trivy, ffuf, gobuster, nmap) installed via cloud-init in 3-5 minutes. &lt;code&gt;make up CYCLE=1&lt;/code&gt; to bring it up; &lt;code&gt;make destroy &amp;amp;&amp;amp; make nuke&lt;/code&gt; at close-out. Total cycle cost: $0.30 of a $50 cap. The droplet is destroyed at cycle close; Cycle 2 reprovisions from scratch with whatever the latest tool versions are then.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why cloud-ephemeral instead of Kali-in-UTM&lt;/strong&gt; (the original plan)? Three reasons: my Mac doesn't have 60 GB free for a VM; "attacker tools never run on the target machine" is honoured by cloud separation; and reprovisioning Cycle 2 in October from the same Terraform doesn't depend on a snapshot that's drifted six months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; six of ciguard's eight surfaces — CLI, parsers, FastAPI Web UI, reporters, baseline+delta, MCP server. Out of scope: the GitHub App and hosted SaaS, neither of which has shipped yet — they get assessed when they do. Supply chain is out of scope because it's already covered by Trivy + OIDC publishing; social engineering and physical access are out of scope by definition for a one-person open-source project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found
&lt;/h2&gt;

&lt;p&gt;Four findings — one Medium, three Low. No Critical or High.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;CVSS v4.0&lt;/th&gt;
&lt;th&gt;GHSA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;CYCLE-1-001&lt;/strong&gt; — &lt;code&gt;discover_pipeline_files&lt;/code&gt; follows symlinks out of scan root&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Medium&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5.7&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Jo-Jo98/ciguard/security/advisories/GHSA-8cxw-cc62-q28v" rel="noopener noreferrer"&gt;GHSA-8cxw-cc62-q28v&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;CYCLE-1-002&lt;/strong&gt; — Container image runs as root&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;3.4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Jo-Jo98/ciguard/security/advisories/GHSA-jrm4-4pcf-4763" rel="noopener noreferrer"&gt;GHSA-jrm4-4pcf-4763&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;CYCLE-1-003&lt;/strong&gt; — SCA HTTP client reads response body unbounded&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;3.1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Jo-Jo98/ciguard/security/advisories/GHSA-xw8c-rrvx-f7xq" rel="noopener noreferrer"&gt;GHSA-xw8c-rrvx-f7xq&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;CYCLE-1-004&lt;/strong&gt; — Web UI missing HTTP defence-in-depth headers&lt;/td&gt;
&lt;td&gt;Low (Medium hosted)&lt;/td&gt;
&lt;td&gt;4.3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Jo-Jo98/ciguard/security/advisories/GHSA-7ww3-xvf5-cxwm" rel="noopener noreferrer"&gt;GHSA-7ww3-xvf5-cxwm&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The interesting one is CYCLE-1-001.&lt;/strong&gt; ciguard ships an MCP server. One of the tools it exposes is &lt;code&gt;scan_repo&lt;/code&gt;, which walks a directory and audits any pipeline files it finds. The threat scenario writes itself: an AI agent gets fed an adversarial prompt — &lt;em&gt;"Scan &lt;code&gt;/tmp/cloned-suspicious-repo&lt;/code&gt; for pipeline issues"&lt;/em&gt; — and the cloned-untrusted-source repo contains symlinks pointing at &lt;code&gt;~/.aws/&lt;/code&gt;, &lt;code&gt;~/.ssh/&lt;/code&gt;, or &lt;code&gt;/etc/some-secret-pipeline/&lt;/code&gt;. Discovery walks the symlinks, returns the symlink-target paths and their contents to the AI, which faithfully reports the "findings" back. Pipeline files often contain hardcoded secrets, internal hostnames, deploy keys.&lt;/p&gt;

&lt;p&gt;Confused-deputy via MCP. Realistic in 2026 in a way it wasn't a year ago. Mitigated in v0.8.2 with &lt;code&gt;follow_symlinks=False&lt;/code&gt; as the new default for the discovery walker, plus a belt-and-braces filter that drops any result whose &lt;code&gt;.resolve()&lt;/code&gt; lies outside the scan root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The other three&lt;/strong&gt; — container as root, unbounded HTTP read, and missing defence-in-depth headers — are bread-and-butter findings that any CREST-style engagement would surface. Useful catches that strengthen the posture; nothing exotic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What didn't find anything (also worth saying)
&lt;/h2&gt;

&lt;p&gt;ciguard had passed Bandit + pip-audit + Trivy gates on every commit since v0.1.4 (2026-04-25). That accumulated discipline showed up in what the cycle &lt;em&gt;didn't&lt;/em&gt; find:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Atheris coverage-guided fuzzing&lt;/strong&gt; — 220k iterations across all three parsers (&lt;code&gt;GitLabCIParser&lt;/code&gt;, &lt;code&gt;GitHubActionsParser&lt;/code&gt;, hand-rolled &lt;code&gt;JenkinsfileParser&lt;/code&gt;). Zero crashes, zero hangs, memory ceiling 65 MB. The Jenkinsfile parser was the highest-risk surface per threat model (no upstream parser to inherit hardening from); it survived clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stored XSS via Jinja template injection&lt;/strong&gt; — the HTML reporter renders findings into a template. Carefully crafted pipeline content with &lt;code&gt;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt; payloads. Auto-escape is on; the payload renders as &lt;code&gt;&amp;amp;lt;script&amp;amp;gt;alert(1)&amp;amp;lt;/script&amp;amp;gt;&lt;/code&gt; inside a &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt; block. Defence-in-depth working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YAML deserialisation&lt;/strong&gt; — every loader is &lt;code&gt;yaml.SafeLoader&lt;/code&gt;, no exceptions. &lt;code&gt;pickle&lt;/code&gt; and &lt;code&gt;eval&lt;/code&gt; and &lt;code&gt;exec&lt;/code&gt; of user input — zero occurrences in &lt;code&gt;src/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP gate (&lt;code&gt;CIGUARD_MCP_DISABLED&lt;/code&gt;) truthy parsing&lt;/strong&gt; — 25-case battery covering canonical truthy, mixed case, whitespace, falsy, empty, substring confusion (&lt;code&gt;yesno&lt;/code&gt;, &lt;code&gt;true_thing&lt;/code&gt;), numeric coincidence (&lt;code&gt;01&lt;/code&gt;, &lt;code&gt;00&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;), literal escapes. All correctly classified.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Documenting what passed matters because it forms the regression watchlist for future cycles. &lt;em&gt;These&lt;/em&gt; are the things to re-check first when a refactor lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  The disclosure decision
&lt;/h2&gt;

&lt;p&gt;Standard responsible-disclosure convention: 14-day window between fix-ship and public advisory. v0.8.2 shipped 2026-04-27. Standard would publish the GHSAs on 2026-05-11.&lt;/p&gt;

&lt;p&gt;I published all four same-day instead.&lt;/p&gt;

&lt;p&gt;The 14-day window protects &lt;strong&gt;affected users&lt;/strong&gt; — it gives them time to upgrade between the patch landing and the vulnerability becoming public. The standard policy assumes there &lt;em&gt;is&lt;/em&gt; a user base whose safety would be compromised by faster disclosure.&lt;/p&gt;

&lt;p&gt;ciguard had ~zero downloads at v0.8.1 ship. No affected users → no safety upside to waiting → standard policy adapts to &lt;em&gt;publish promptly&lt;/em&gt;. v0.8.2 has been the default &lt;code&gt;pip install ciguard&lt;/code&gt; since 2026-04-27; anyone landing on the project from this point forward gets the fixed version.&lt;/p&gt;

&lt;p&gt;The credibility upside of immediate disclosure was the deciding factor: a public set of GHSAs (with CVEs requested, propagating to NVD over the following days) is the strongest possible artefact of "we self-pentested and shipped the fixes the same day." Withholding them for two weeks would have delayed exactly the thing that makes a self-pentest valuable as a credibility signal.&lt;/p&gt;

&lt;p&gt;The decision is documented in the Cycle 1 final report's Definition of Done section. By Cycle 2 (October 2026), ciguard may have actual users, and the standard 14-day window applies again. This is a judgement call per cycle, not a permanent policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the loop — making sure these don't come back
&lt;/h2&gt;

&lt;p&gt;Cycle 1's final-report recommendations included two CI hooks that just shipped in v0.8.3:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommendation #2 — wire the four PoC scripts in as CI regression gates.&lt;/strong&gt; Each of the four PoCs has a binary outcome encoded in its exit code: &lt;code&gt;0 = EXPLOIT_CONFIRMED&lt;/code&gt;, &lt;code&gt;1 = EXPLOIT_FAILED&lt;/code&gt;. v0.8.3 adds &lt;a href="https://github.com/Jo-Jo98/ciguard/tree/main/tests/regression/cycle1" rel="noopener noreferrer"&gt;&lt;code&gt;tests/regression/cycle1/&lt;/code&gt;&lt;/a&gt; holding the four scripts as live regression copies, plus a new &lt;code&gt;regression-cycle1&lt;/code&gt; job in the reusable &lt;code&gt;_checks.yml&lt;/code&gt; workflow that runs all four on every push, every PR, and every release tag. Inverts each script's exit code so the build fails only when a regression appears. The container PoC builds the image locally first so the gate fires before publish, not after.&lt;/p&gt;

&lt;p&gt;The unit tests added in v0.8.2 cover the &lt;em&gt;fixes as code paths&lt;/em&gt;. The PoC regression gates cover the &lt;em&gt;exploit chains end-to-end&lt;/em&gt;. A future refactor that breaks the security guarantee in a different layer can pass the unit tests and fail the PoC — that's the property we want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommendation #3 — schedule a weekly atheris fuzz cron.&lt;/strong&gt; v0.8.3 adds &lt;a href="https://github.com/Jo-Jo98/ciguard/blob/main/.github/workflows/atheris-fuzz.yml" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/atheris-fuzz.yml&lt;/code&gt;&lt;/a&gt; running 1M iterations of coverage-guided fuzz across all three parsers every Sunday 06:00 UTC. Per-input timeout 10s, total budget 30 minutes. Crash uploads the input as a 30-day artifact and opens a &lt;code&gt;security&lt;/code&gt;/&lt;code&gt;fuzz-finding&lt;/code&gt; issue. Manual &lt;code&gt;workflow_dispatch&lt;/code&gt; accepts a custom iteration count for spot runs.&lt;/p&gt;

&lt;p&gt;Cycle 1 ran 220k iterations in ~2 minutes and surfaced no crashes; weekly 1M is cheap insurance against regressions when new rules or parser refactors land.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cycle 2&lt;/strong&gt; is scheduled for 2026-10-28 → 2026-11-11 (six-monthly cadence). The Terraform lab gets reprovisioned from scratch with then-current tool versions; the report template clones from Cycle 1's. Same Definition of Done, applied to whatever surfaces have shipped by then (likely the v0.9.0 GitHub App + &lt;code&gt;ciguard scan-repo&lt;/code&gt; CLI).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The four GHSAs are public&lt;/strong&gt; as of today. CVE numbers from MITRE are working their way through GitHub's CNA pipeline and should attach to the advisories over the next few days; once they propagate, the GHSAs auto-decorate with &lt;code&gt;CVE-2026-NNNN&lt;/code&gt; IDs.&lt;/p&gt;

&lt;p&gt;If you're maintaining a security tool — or any open-source tool with a non-trivial attack surface — and you've been waiting for the "right moment" to do a self-pentest: do it before you have users. The cost is your time and a few dollars. The upside is a defensible posture that doesn't depend on nobody bothering to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Jo-Jo98/ciguard" rel="noopener noreferrer"&gt;ciguard on GitHub&lt;/a&gt; — code, releases, advisories&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Jo-Jo98/ciguard/releases/tag/v0.8.3" rel="noopener noreferrer"&gt;Cycle 1 final report (PDF, ~28 pages)&lt;/a&gt; — methodology, findings, Phase 7 retest evidence, CREST-style writeup. Attached to the v0.8.3 release notes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Jo-Jo98/ciguard/security/advisories" rel="noopener noreferrer"&gt;GitHub Security Advisories&lt;/a&gt; — all four findings, CVSS vectors, fix descriptions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Jo-Jo98/ciguard/releases/tag/v0.8.2" rel="noopener noreferrer"&gt;v0.8.2 release notes&lt;/a&gt; — security hotfix details&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Jo-Jo98/ciguard/releases/tag/v0.8.3" rel="noopener noreferrer"&gt;v0.8.3 release notes&lt;/a&gt; — CI regression gates + weekly fuzz cron&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Jo-Jo98/ciguard/discussions/15" rel="noopener noreferrer"&gt;Canonical post on GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you spot something in ciguard that should have been on this list, &lt;a href="https://github.com/Jo-Jo98/ciguard/security/advisories/new" rel="noopener noreferrer"&gt;open a security advisory&lt;/a&gt;. Cycle 2 will gladly cite you.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devsecops</category>
      <category>opensource</category>
      <category>python</category>
    </item>
  </channel>
</rss>
