DEV Community

Cover image for New CVEs every day, migrations every sprint: I built an autopilot for both
Henri Aycard
Henri Aycard

Posted on

New CVEs every day, migrations every sprint: I built an autopilot for both

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.

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.

So I built an autopilot for it: migration-control, an open-source tool where Claude agents look at all your repositories and languages at once, 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 pilot the migrations from one dashboard and approve what ships.

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

Here is a real run, with the real numbers, what went wrong, and what it cost.

Migration Control replaying a scan

The estate

I needed something realistic and public, so I took Spring PetClinic and pinned it to 2018-era releases:

Module What it is Pinned to
petclinic-rest Java backend, REST API Spring Boot 1.5, Java 8
petclinic-angular front-end, calls the REST API Angular 6
petclinic-infra Python ops health check, Oracle inventory, Maven Oracle profile Python 3.6, Oracle 19c base release (no patch since 2019)

The whole estate is described in one file:

version: 1
project: petclinic
estate:
  - name: petclinic-rest
    repo: https://github.com/spring-petclinic/spring-petclinic-rest.git
    ref: v1.5.2
    depends_on: [petclinic-infra]
  - name: petclinic-angular
    repo: https://github.com/spring-petclinic/spring-petclinic-angular.git
    ref: 22935fc04933e4dec3b20bcfb8720f56b09f170d
    depends_on: [petclinic-rest]
  - name: petclinic-infra
    path: petclinic-infra.tar.gz
policy:
  approvers: [tech-lead, security, dba, platform-ops]
sandbox:
  packages: { apt: [openjdk-17-jdk, maven] }
Enter fullscreen mode Exit fullscreen mode

depends_on matters: the grader later checks that each of those constraints was actually analysed.

Step 1 — the scan (45 minutes, one revision)

mig scan 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 with an official source (vendor advisory, registry, NVD, OSV, GitHub advisory — otherwise it must tag the claim UNVERIFIED), locate the breaking changes in this code with file:line, and actually build on target versions.

What it produced:

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

Then something I like: a separate grader read the outputs against an 8-criterion rubric and said no. 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 satisfied. Nobody had to read 180 rows to catch it.

Estate view: modules as islands, components as bubbles sized by CVE count

Step 2 — the plan (18 minutes)

mig plan 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.

  • 10 waves, 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
  • 44 / 44 outdated production components covered
  • 10 blocking decisions it refused to take for humans (target versions, how a password is injected in production…)
  • wave 4 proven in the sandbox: the patch was applied to a throwaway copy, its exit gates were run, and the patch passes git apply --check. I re-checked that myself on a fresh clone.

The grader accepted it on the first pass.

Journey view: the plan as a metro line

Step 3 — the pull request (6 minutes, and one click from me)

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

  • 8 gates passed: 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…
  • 3 gates left for rollout, 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.

Then it stopped. The agent has no token. It produced a patch, a pr.json, 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").

I reviewed the diff in the dashboard and clicked Approve & publish. Only then did the mig 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.

The safety model, in one table

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

Running it on a Claude subscription

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?

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

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 paused, its workspace and conversation kept, and mig resume <run> picks it up exactly where it stopped.

What it cost

Equivalent at API list prices, on this deliberately heavy estate:

Scan (2 grading passes) Plan PR
~45 min · ~$20 ~18 min · ~$5 ~6 min · ~$2

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.

Limits

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.

Try it

You can look around without a key or any cost. It replays the recorded runs, and the 🎬 Demo button narrates them:

pipx install git+https://github.com/HenriAycard/migration-control
mkdir petclinic && cd petclinic
mig init --example petclinic
mig dashboard --offline
Enter fullscreen mode Exit fullscreen mode

The repo: https://github.com/HenriAycard/migration-control

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?

Top comments (0)