<?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: Henri Aycard</title>
    <description>The latest articles on DEV Community by Henri Aycard (@henriaycard).</description>
    <link>https://dev.to/henriaycard</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4169035%2Fd4564734-d479-45d3-9b71-391c92c6b2c7.jpg</url>
      <title>DEV Community: Henri Aycard</title>
      <link>https://dev.to/henriaycard</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/henriaycard"/>
    <language>en</language>
    <item>
      <title>New CVEs every day, migrations every sprint: I built an autopilot for both</title>
      <dc:creator>Henri Aycard</dc:creator>
      <pubDate>Wed, 07 Oct 2026 13:56:43 +0000</pubDate>
      <link>https://dev.to/henriaycard/new-cves-every-day-migrations-every-sprint-i-built-an-autopilot-for-both-3m2c</link>
      <guid>https://dev.to/henriaycard/new-cves-every-day-migrations-every-sprint-i-built-an-autopilot-for-both-3m2c</guid>
      <description>&lt;p&gt;Every week brings a new batch of CVEs. Every sprint, some framework, runtime or driver reaches end of life. And in any company older than a couple of years, the code does not live in one repository: a Java backend talks to an Angular front-end, both depend on a Python ops layer and an Oracle database, and each of them pins its own versions.&lt;/p&gt;

&lt;p&gt;Keeping that up to date by hand means someone, somewhere, tracking release notes, reading advisories, guessing which upgrade breaks which module, and in which order. It does not scale, and the gap between "a fix exists" and "the fix is in production" is exactly where attackers live.&lt;/p&gt;

&lt;p&gt;So I built an autopilot for it: &lt;strong&gt;&lt;a href="https://github.com/HenriAycard/migration-control" rel="noopener noreferrer"&gt;migration-control&lt;/a&gt;&lt;/strong&gt;, an open-source tool where Claude agents look at &lt;strong&gt;all your repositories and languages at once&lt;/strong&gt;, find what is outdated or vulnerable, plan the migration in the order your modules allow, and prepare the pull requests. You stop tracking versions and CVEs yourself — you &lt;strong&gt;pilot&lt;/strong&gt; the migrations from one dashboard and &lt;strong&gt;approve&lt;/strong&gt; what ships.&lt;/p&gt;

&lt;p&gt;And because handing your repositories to an AI should not be a leap of faith, there is one hard rule: &lt;strong&gt;no agent ever holds a write credential.&lt;/strong&gt; Every write goes through a human approval.&lt;/p&gt;

&lt;p&gt;Here is a real run, with the real numbers, what went wrong, and what it cost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopne1assbd2fpl6senwm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopne1assbd2fpl6senwm.gif" alt="Migration Control replaying a scan" width="600" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The estate
&lt;/h2&gt;

&lt;p&gt;I needed something realistic and public, so I took Spring PetClinic and pinned it to 2018-era releases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Pinned to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;petclinic-rest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Java backend, REST API&lt;/td&gt;
&lt;td&gt;Spring Boot 1.5, Java 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;petclinic-angular&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;front-end, calls the REST API&lt;/td&gt;
&lt;td&gt;Angular 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;petclinic-infra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Python ops health check, Oracle inventory, Maven Oracle profile&lt;/td&gt;
&lt;td&gt;Python 3.6, Oracle 19c base release (no patch since 2019)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The whole estate is described in one file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;petclinic&lt;/span&gt;
&lt;span class="na"&gt;estate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;petclinic-rest&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/spring-petclinic/spring-petclinic-rest.git&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1.5.2&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;petclinic-infra&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;petclinic-angular&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/spring-petclinic/spring-petclinic-angular.git&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22935fc04933e4dec3b20bcfb8720f56b09f170d&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;petclinic-rest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;petclinic-infra&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;petclinic-infra.tar.gz&lt;/span&gt;
&lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;approvers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;tech-lead&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;dba&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;platform-ops&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;sandbox&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;openjdk-17-jdk&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;maven&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;depends_on&lt;/code&gt; matters: the grader later checks that each of those constraints was actually analysed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — the scan (45 minutes, one revision)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mig scan&lt;/code&gt; starts a scanner agent in a fresh Docker container, with the three modules checked out at their production refs. Its job, in short: inventory every component from the real build files, find the latest official version and security patch for each, list every CVE &lt;strong&gt;with an official source&lt;/strong&gt; (vendor advisory, registry, NVD, OSV, GitHub advisory — otherwise it must tag the claim &lt;code&gt;UNVERIFIED&lt;/code&gt;), locate the breaking changes in &lt;em&gt;this&lt;/em&gt; code with &lt;code&gt;file:line&lt;/code&gt;, and actually build on target versions.&lt;/p&gt;

&lt;p&gt;What it produced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;184 components&lt;/strong&gt; across the three modules (45 production, the rest test and build toolchain)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;749 unique CVEs&lt;/strong&gt; affecting production components, &lt;strong&gt;67 critical&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;44 of 45&lt;/strong&gt; production components outdated, &lt;strong&gt;18&lt;/strong&gt; end of life&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8 builds attempted&lt;/strong&gt; on target versions — &lt;strong&gt;1 passed&lt;/strong&gt;. The seven failures are not a bug: their quoted compiler and runtime errors &lt;em&gt;are&lt;/em&gt; the impact evidence, each linked to an impact item.&lt;/li&gt;
&lt;li&gt;an explicit alert that the Oracle 19c database had &lt;strong&gt;no Release Update applied since its 2019 base release&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then something I like: a &lt;strong&gt;separate grader&lt;/strong&gt; read the outputs against an 8-criterion rubric and said &lt;strong&gt;no&lt;/strong&gt;. The inventory was incomplete — some components declared in the build files were missing. Its notes went back to the agent, which fixed the inventory, and the second grading pass was &lt;code&gt;satisfied&lt;/code&gt;. Nobody had to read 180 rows to catch it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgba3g1wrfs1yx3yhvbj7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgba3g1wrfs1yx3yhvbj7.png" alt="Estate view: modules as islands, components as bubbles sized by CVE count" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — the plan (18 minutes)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mig plan&lt;/code&gt; hands the validated report to a planner agent. Its output is meant to be signable by a tech lead and a security officer: ordered waves, each with measurable entry and exit gates, a rollback procedure, the approvers it needs, risk and effort — and no calendar estimates.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;10 waves&lt;/strong&gt;, ordered by cross-module constraints: database patching first, then the OS, a patched JDK 8, the JDBC driver, Spring Boot 1.5 → 2.7 (a mandatory intermediate step), JDK 17 + Boot 3.5, JDK 25 + Boot 4.1, Python 3.14, Angular 6 → 22 one major at a time, then end-to-end validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;44 / 44&lt;/strong&gt; outdated production components covered&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10 blocking decisions&lt;/strong&gt; it refused to take for humans (target versions, how a password is injected in production…)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;wave 4 proven&lt;/strong&gt; in the sandbox: the patch was applied to a throwaway copy, its exit gates were run, and the patch passes &lt;code&gt;git apply --check&lt;/code&gt;. I re-checked that myself on a fresh clone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The grader accepted it on the first pass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfg3vzv8zz21eaiew9zs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfg3vzv8zz21eaiew9zs.png" alt="Journey view: the plan as a metro line" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — the pull request (6 minutes, and one click from me)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mig pr 4&lt;/code&gt; starts a PR agent for the proven wave (Oracle JDBC driver &lt;code&gt;ojdbc6 11.2.0.4&lt;/code&gt; → &lt;code&gt;ojdbc8 23.26.3.0.0&lt;/code&gt;). It applied the wave on fresh checkouts and re-ran the wave's gates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8 gates passed&lt;/strong&gt;: build and tests with the Oracle profile, driver resolved and packaged, runs on the JDK 8 app server, REST contract unchanged, zero known CVEs for the driver…&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 gates left for rollout&lt;/strong&gt;, honestly labelled as such — they need a real database or staging, which a sandbox does not have. The agent is not allowed to claim a gate it did not run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it stopped. The agent has no token. It produced a patch, a &lt;code&gt;pr.json&lt;/code&gt;, and a description written for approvers (why, file:line changes, gate results with quoted output, rollback, one checkbox per approver, "do not merge until every approver has signed off").&lt;/p&gt;

&lt;p&gt;I reviewed the diff in the dashboard and clicked &lt;strong&gt;Approve &amp;amp; publish&lt;/strong&gt;. Only then did the &lt;code&gt;mig&lt;/code&gt; CLI — on my machine, with my token — push a branch and open the pull request. One file, three lines changed, exactly the patch I had read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safety model, in one table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No write credential for agents&lt;/td&gt;
&lt;td&gt;PRs/MRs are opened by the CLI after a human approves the prepared diff, and never merged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets stay put&lt;/td&gt;
&lt;td&gt;keys and tokens never reach prompts, logs or the dashboard page, and never appear on a command line (&lt;code&gt;GIT_ASKPASS&lt;/code&gt; for git, variable names for containers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation by default&lt;/td&gt;
&lt;td&gt;one Docker container per run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local-only dashboard&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;127.0.0.1&lt;/code&gt;, and every action needs a per-start secret embedded in the page plus a same-origin request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sourced claims&lt;/td&gt;
&lt;td&gt;official sources or &lt;code&gt;UNVERIFIED&lt;/code&gt; — findings are for humans to review, not an authority&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Running it on a Claude subscription
&lt;/h2&gt;

&lt;p&gt;The first version ran only on Claude Managed Agents (the cloud API). Then my API credit ran out mid-test, which raised a fair question: why not run on my own machine with Claude Code?&lt;/p&gt;

&lt;p&gt;So there is now a local runner: Claude Code in headless mode (&lt;code&gt;claude -p --output-format stream-json&lt;/code&gt;) inside a container, with the same prompts and contracts. The managed pieces are rebuilt locally — the grader is a second &lt;code&gt;claude -p&lt;/code&gt; call that must return a verdict matching a JSON Schema (&lt;code&gt;--json-schema&lt;/code&gt;), and failed criteria are sent back to the agent's conversation with &lt;code&gt;--resume&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;During the planner run I hit my subscription's session limit after 52 seconds. The first version lost the run. Now progress is checkpointed: a run that hits a usage limit is &lt;strong&gt;paused&lt;/strong&gt;, its workspace and conversation kept, and &lt;code&gt;mig resume &amp;lt;run&amp;gt;&lt;/code&gt; picks it up exactly where it stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost
&lt;/h2&gt;

&lt;p&gt;Equivalent at API list prices, on this deliberately heavy estate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scan (2 grading passes)&lt;/th&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;PR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;~45 min · ~$20&lt;/td&gt;
&lt;td&gt;~18 min · ~$5&lt;/td&gt;
&lt;td&gt;~6 min · ~$2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is why runs are on demand rather than on every commit. On a subscription, a heavy scan uses a large share of a usage window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;It is alpha. Prompts, schemas and commands may change. Publishing to GitLab is covered by tests but has not opened a real merge request yet. And the agents' findings are claims backed by sources, which humans still need to review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;You can look around without a key or any cost. It replays the recorded runs, and the 🎬 Demo button narrates them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;git+https://github.com/HenriAycard/migration-control
&lt;span class="nb"&gt;mkdir &lt;/span&gt;petclinic &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;petclinic
mig init &lt;span class="nt"&gt;--example&lt;/span&gt; petclinic
mig dashboard &lt;span class="nt"&gt;--offline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo: &lt;strong&gt;&lt;a href="https://github.com/HenriAycard/migration-control" rel="noopener noreferrer"&gt;https://github.com/HenriAycard/migration-control&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I would love feedback, especially from platform and security teams who run migrations in regulated environments: what would you need to trust a plan like this enough to sign it?&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
