Inspiration

On 11 September 2026 the reporting duties of the EU Cyber Resilience Act took effect. Every manufacturer of a product with digital elements sold in the EU must now warn ENISA within 24 hours of becoming aware of an actively exploited vulnerability, notify within 72 hours, and file a final report within 14 days of a fix. Fines go up to €15M or 2.5% of global turnover.

Code generation is solved. What happens next is now a legal deadline, and it falls hardest on old software. Most exploited vulnerabilities aren't new: CISA keeps listing CVEs that are years old, and the products still running them are often on release lines that no longer get fixes. "Just upgrade" isn't a 24-hour answer for a 2019 PHP portal.

We wanted the whole response, from "a CSIRT says this is exploited" to "safe in production, report drafted", to run with no human in the engineering path, including when there is no patch to apply.

Who it's for

The PSIRT or security lead at any manufacturer selling software or connected devices into the EU, and the counsel who signs the ENISA filing, especially at companies still shipping older stacks. On real runs, the loop went from exploitation notice to patched, verified production in 14 minutes. Where no patch existed, it went to a tested mitigation in production in 10 minutes, both with no human involved and the legal drafts already written.

And for everyone else there's CRA Check: drop a lockfile (Python, PHP or npm) and see in seconds what is actively exploited, which fixes stay on your release line, and which lines have no fix at all. It runs in the browser, nothing is uploaded, and it uses the same rules as the agents.

It matters most for legacy software. Migration is real but slow: about 29% of PHP sites still run PHP 7 and 8% PHP 5, both out of support (W3Techs, Aug 2026), and CISA keeps adding decade-old CVEs to its exploited list. The CRA still starts the clock. So CRA Autopilot covers a legacy stack too: Hearthline's 2019 PHP installer portal runs Twig 1.x (end of life, 13 OSV advisories). The watcher reads its composer.lock, picks the fix on the installed release line (1.44.8, not a rewrite onto Twig 3), and resolves the fixed tag's commit from Packagist so the agent never guesses a hash. In a live drill the agents patched it, and production reported twig/twig 1.44.8 8 minutes after merge. When no in-line fix exists (12 of Twig 1.x's 13 advisories, including a CRITICAL one), the loop never jumps a major version under the clock. It routes to mitigate: an agent closes the vulnerable path in our own code, proves it with a PHP test that CI really runs, and records OpenVEX inline_mitigations_already_exist. The gate checks all of it, and a follow-up issue tracks the real upgrade. In the live drill for the CRITICAL advisory, the mitigation was in production at T0+10m, fully hands-off.

We also replayed five real KEV-listed incidents (PHPUnit, Drupalgeddon2, Laravel, Laravel Ignition, Airflow) with real KEV, OSV and endoflife.date data. A clock starts every time, with the correct in-line fix. In each case that fix had existed 1.5 to 5.4 years before CISA listed the exploitation: the gap is noticing and shipping, which is exactly what the loop automates.

What it does

CRA Autopilot runs the CRA response loop for Hearthline, a demo connected-thermostat cloud API:

  1. Monitor: an hourly cra-watch job compares what we ship (the lockfile plus what production reports it is running) against OSV, CISA KEV and the PSIRT intel register.
  2. Plan: a new actively exploited advisory opens a clock issue holding T0 and the 24h, 72h and 14-day deadlines.
  3. Assess (Duo): the assessor agent reads our code to decide whether the vulnerable behaviour is reachable, and posts the call sites and its reasoning.
  4. Create (Duo):
    • Affected: the fixer agent writes the patch, a regression test and the MR, and the warner agent drafts the ENISA early warning.
    • Not affected: the VEX agent records an OpenVEX not_affected statement instead.
  5. Govern: the CRA gate refuses to merge unless:
    • the MR is linked to its clock;
    • changes stay inside allowed paths (agents can't touch CI, infra or the gate);
    • a regression test was added;
    • OSV confirms the advisory is gone;
    • no new exploited advisory was introduced;
    • the version was bumped.

Then it sets auto-merge. Duo Code Review reviews the agent's patch, and Duo Fix Pipeline repairs a broken MR pipeline.

  1. Verify · Secure · Package · Release: tests, SAST, secret detection, dependency and container scanning, keyless cosign signing, the CycloneDX SBOM and OpenVEX published to the package registry, and a GitLab Release.
  2. Configure · Deploy: Terraform (GitLab-managed state) manages Artifact Registry and Cloud Run. Production gets a zero-traffic candidate that must prove it runs exactly the release SBOM before traffic moves.
  3. Monitor: a post-deploy health watch with automatic rollback, then a re-scan of what production now runs.
  4. Report: the real timeline is written on the clock issue, with the Art. 14(2)(b) notification, the final report and the Art. 14(8) customer notice. A dossier agent turns them into a filing pack for legal.

A public dashboard (GitLab Pages) shows every clock ticking live, and what production is running right now.

How we built it

  • Two custom flows (flow registry v1):
    • article14-response: 4 agents, conditional routing on the assessor's one-word verdict, and toolsets locked to what each agent needs. The agents hand off through the clock issue itself.
    • article14-dossier.

Both are published to the AI Catalog and validated with GitLab's own DryRunFlowValidator.

  • A custom Duo Chat agent, CRA Advisor: read-only (12 read tools, no write tools). It answers questions about running clocks, deadlines and gate verdicts from the project's records. Installed from versioned YAML through the AI Catalog API.
  • Flows API and triggers: triggers ignore bot activity, so CI jobs hand work to agents through POST /ai/duo_workflows/workflows. CI also starts the foundational code_review/v1 and fix_pipeline/v1 flows. Both custom flows also answer @-mentions.
  • A deterministic engine (article14/, stdlib-only Python): SBOM, feeds, clock, VEX, gate, ENISA drafts. Agents make judgement calls; pipelines prove facts.
  • Google Cloud: Cloud Run in europe-west1, Artifact Registry, and keyless auth through Workload Identity Federation with GitLab OIDC. The org policy forbids service account keys, and none exist.

Challenges we ran into

  • Flows can't trigger flows: no flow can start another and bots can't fire triggers, so the chain is orchestrated by CI through the Flows API.
  • Cloud Run reserves /healthz: Google's front end answers it with its own 404. Our zero-traffic canary caught the resulting startup-probe failure before any user saw it.
  • No keys anywhere: to stay keyless end to end, the signed image is promoted to Artifact Registry by digest with crane, so the cosign signature stays valid.
  • Agents must not judge themselves: the first live run showed the gate executing from the MR's own branch, so an agent could in principle have edited the code judging it. The gate now always runs from main.
  • Scoped labels are exclusive: GitLab allows one x:: label per scope, so agents' phase labels silently replaced the clock label. Phases now use one scope on purpose (cra-phase::…), and clocks are identified by a state block only the watcher writes.
  • Custom flows forbid response schemas: routing moved to a one-word verdict, with the details handed over through the clock issue.

Trust and safety

Hands-off only works if the agents can't hurt you. Enforced in code, not just prompted:

  • The gate always runs from main, so agents can't edit what judges them.
  • Agents can only change requirements.txt, app/, legacy/ and tests/.
  • A bumped composer.lock must point at the exact commit Packagist publishes for that tag.
  • No major-version jumps under the clock; with no fix on the line, only a proven mitigation can settle it.
  • A VEX statement can only cover its own clock's advisory and version, and earlier tests and VEX records are append-only.
  • A clock is only closed when GitLab's record shows the gate job passed on the merged commit.
  • A "not affected" claim must address every file a static scan finds using the package.
  • An optional human review, by change kind or severity, for teams that want Supervised.
  • An exploited OS or runtime CVE leads to a digest-pinned base-image rebuild MR, merged only if the scan shows the CVE gone.
  • A heartbeat alarm when the hourly watcher goes quiet.
  • A "not affected" VEX claim must cite real files that import the package.
  • Uncertain verdicts route to patch, never to "not affected".
  • Clocks are identified by a state block only the watcher writes.
  • Every agent has a prompt-injection guard.
  • Nothing is ever submitted to ENISA automatically.

The two GitLab settings we couldn't change with a Developer role (pinning the CI config path and restricting pushes to main) are documented as known limits.

Accomplishments we're proud of

  • The agents audit their own records. Asked about the open clock, the read-only CRA Advisor found four issues in the system's own data. One was a real timestamp bug: reports recorded "live in production" when the report ran, not when traffic moved. The others were a missing drill label that showed passed deadlines as overdue, an unattributed Member States list, and ambiguous severity wording. All four were fixed in code and corrected transparently on the affected clocks.
  • CRA Check, a free tool for every manufacturer. The engine's rules in a browser page: Python, PHP and npm lockfiles, real KEV, OSV and endoflife.date data, nothing uploaded.
  • Reliable in real use, not just a demo.
    • For Go, Rust, Ruby and npm, a CI job applies the dependency bump with the real package manager, so every supported language gets patch MRs.
    • Where OSV lists vulnerable functions, the clock shows whether our code calls them.
    • A Google Cloud uptime check watches the watcher from outside GitLab.
    • Any GitLab project can adopt it with one CI include.
  • Exploit chains, not just single CVEs. About 1 in 30 KEV entries is exploited as a chain. The watcher flags a chain when every link ships (verified on real data: litellm + starlette) and tells the clock to break either link.
  • Beyond one stack. The engine reads Python, PHP, npm, Go, Rust, Ruby and Java lockfiles, or any CycloneDX SBOM. Exploitation evidence comes from CISA KEV and ENISA's own EU Vulnerability Database. Replaying Log4Shell starts a clock from real data.
  • No fix? Mitigate, don't migrate under the clock. For a CRITICAL advisory with no fix on Twig 1.x, the agents shipped a proven in-line mitigation to production at T0+10m. A gate check refuses major-version jumps.
  • Red-teamed our own gate. Ten flaws found and fixed, including VEX statements that could silence a different clock and a report that trusted comments; the report now verifies the gate job itself. The fix-selection logic was stress-tested on 1,196 real advisories with no wrong choices.
  • Real incidents replayed. Five KEV-listed library vulnerabilities, replayed with real data: a clock and the correct in-line fix every time. The replay also caught two of our own bugs (a release-candidate fix, EOL gaps).
  • Legacy PHP, end to end. A live drill patched an end-of-life Twig 1.x portal in-line, checked against Packagist, and production reported the fixed version. It also caught one of our own brittle tests, and GitLab's Fix Pipeline flow flagged that within minutes.
  • All 9 DevSecOps stages, each doing real work.
  • The gate makes agents safe to run hands-off: they can propose anything, but only evidence ships.
  • "Not affected" is a first-class answer. It produces a signed-off OpenVEX record rather than an unnecessary emergency patch.

What we learned

The right split is agents for judgement, pipelines for proof. Reachability, patch authoring and narrative need an LLM. Deadlines, SBOM diffs and traffic shifts must be deterministic and auditable.

What's next

  • System-level coverage: SBOMs of the built container image (OS packages, the PHP or Python runtime, system libraries), so the same loop patches a base image or runtime, not just a library pin.
  • Mitigations beyond code (WAF rules, configuration), with the same proof-of-place gate.
  • Article 14(3) severe-incident reports, and multi-product, group-level cases.
  • Vulnerability intelligence for the agents over MCP, including ENISA and CSIRT exploitation feeds.
  • Multi-product SBOM inventory across released versions.
  • Submission through the ENISA Single Reporting Platform once it offers an API, still behind a human approval step.

Honesty notes

  • The exploitation signal in the demo is simulated: neither demo CVE is in CISA KEV. Everything after that signal is real: agents, MRs, gate, pipelines, Cloud Run, and the drafts on the clock issue.
  • Security tests are defensive (version floors and behaviour). There are no exploit payloads in the repo.
  • Agent session pages are members-only on GitLab. Publicly, the agents' work shows as notes and MRs authored by the ai-article-14-* service accounts, and as duo_workflow pipelines.

Built With

  • artifact-registry
  • fastapi
  • gitlab-ci
  • gitlab-duo-agent-platform
  • gitlab-pages
  • google-cloud-run
  • osv
  • python
  • terraform
Share this project:

Updates

Submission history