Skip to content
S
ai

How I Built an AI-Assisted Dependency Vulnerability Fixer

ai13 min read

We had roughly 50 open dependency vulnerability findings across 20 repositories, many waiting longer to be picked up than they would take to fix. So I built an internal remediation system for my team to reduce the engineering hours spent each week triaging and fixing them. It brought the findings into one place, classified what could be proposed automatically, chose between scripted and AI-assisted remediation, validated each change, and opened a reviewable pull request. The difficult part was not changing one package version. It was designing a workflow that could operate across very different repositories without bypassing the tests, ownership, and human judgment that made those changes safe.

The five-minute fix that takes two weeks

Vulnerability scanners are excellent at finding problems. They are less good at creating the conditions for those problems to be fixed.

The usual workflow looks like this:

  1. A scanner discovers a vulnerable dependency.
  2. It creates a finding in a security queue.
  3. The repository owner is notified.
  4. The finding competes with product work, incidents, support, and everything else.
  5. Eventually, an engineer bumps a version and opens a pull request.

There is little technical uncertainty in that sequence when a fixed version is already known. The delay comes from coordination and the effort required to stop other work and change context.

Nobody starts the morning excited to regenerate a lockfile. The task is repetitive, interruptive, and individually small enough to postpone. Each postponement seems reasonable. Across dozens of repositories, those reasonable local decisions create an unreasonable global backlog.

Another reminder or a dashboard full of red numbers does not remove that delay. A better approach is to automate the steps that do not need judgment, then make the remaining decisions easy to see and act on.

Fixable does not mean safe

When I first grouped the findings, roughly 85% had a known fix version. The advisory effectively said: upgrade package X from version A to version B.

The remaining findings had no direct fix. Those needed actual security and engineering judgment: assess exposure, investigate a workaround, replace the dependency, accept the risk, or wait for an upstream release.

That split gave the project its first useful boundary:

  • Mechanically fixable: a target version exists and the change can be proposed automatically.
  • Judgment required: no supported target exists, the remediation changes architecture, or the risk cannot be understood from package metadata alone.

I initially described the first group as requiring “no judgment.” That was too strong.

A known fix version does not prove that an upgrade is safe. A patch can alter transitive dependencies, regenerate a surprisingly large lockfile, fail a build, run installation scripts, or expose an incompatibility in code that has not changed for years.

The better description is bounded judgment. The change is predictable enough to automate, but the result must still pass through the repository’s tests, ownership rules, and review process.

The tool proposes a change. It does not declare the change safe.

How the system worked

The first release centered on an internal web dashboard for the team. It collected findings, grouped them by repository, and put a Create fix pull request button beside the ones with known fixes. The goal was practical: reduce the time engineers lost each week to finding owners, checking advisories, updating dependencies, and preparing pull requests.

A scheduled task fetched findings from the scanner and converted them into one consistent format. It stored the result in a local data store, so opening the dashboard did not depend on the scanner responding quickly. Starting a fix launched a background job that cloned the repository, chose a remediation path, checked the patch, and opened a pull request.

High-level architecture showing vulnerability findings flowing through a scheduled import into a data store and web dashboard, with fix requests running through an in-process remediation job before opening a pull request

Most page requests simply read from the data store. They were already fast enough that further optimisation would not have helped the user.

I kept the storage layer focused on the needs of the first release: one application instance and one durable data set. That gave us reliable queries with almost no infrastructure.

The server returned complete HTML pages rather than loading the data later in the browser. We had dozens of repositories and hundreds of findings, not millions of rapidly changing records. A heavier frontend would have added loading states and client-side data management without improving the experience.

These were decisions about reversibility, not claims that one storage or rendering approach is always best. Both let us ship quickly, learn from real usage, and replace the component later without changing the product model.

The simplest architecture that can answer the current question is often the best discovery tool.

Start deterministic, then earn the agent

The remediation pipeline has four stages:

  1. Turn each scanner finding into the same set of fields: package, current version, fix version, package ecosystem, and repository.
  2. Classify whether the finding is mechanically fixable.
  3. Produce and validate a candidate patch.
  4. Open a pull request for the repository owner to review.

The first two stages should use ordinary, repeatable code. An LLM should not decide whether a known fix version exists when the advisory already provides that fact.

The third stage is where the implementation can branch.

For a direct dependency in a conventional manifest, a scripted edit is usually enough. Read the file with a parser built for that package format, keep its existing version-range style, update the version, and let the package manager regenerate the lockfile.

This path is cheap, fast, explainable, and easy to test. Given the same finding and repository state, it should produce the same patch every time.

The agent earns its keep in the long tail:

  • The vulnerable package is transitive and must be changed through an override or parent dependency.
  • The repository is a monorepo with several manifests and lockfiles.
  • Workspace protocols or shared version catalogues coordinate dependency versions.
  • The correct package-manager command depends on repository-specific tooling.
  • A clean remediation touches multiple files.
  • Lockfile regeneration fails and requires investigation.

In these cases, the job is no longer “replace this string.” It is “understand how this repository manages dependencies, make the smallest coherent change, and demonstrate that the package graph still resolves.”

That is a good use of an agent. The answer depends on clues inside the repository, and encoding every possible layout and tool combination would create an endless collection of special cases.

It is still not a good reason to send every easy edit through an LLM.

I built the exciting path first. In hindsight, I should have shipped a deterministic fixer for direct dependencies, measured what it could not handle, and introduced the agent only for that remainder. The deterministic path would probably have covered around 70% of the initial backlog before the agent was needed.

The agent worked as a bounded repair loop

The agent did not receive a vague instruction to “fix the repository.” Each job began with one finding: the affected package, current version, target version, package ecosystem, and repository. The task was narrow before the model saw any code.

The loop then looked like this:

  1. Inspect the repository’s manifests, lockfiles, workspace configuration, and local instructions.
  2. Form a small remediation plan based on how that repository manages dependencies.
  3. Edit as few files as possible and regenerate the dependency graph using the repository’s own tools.
  4. Run bounded validation such as dependency resolution, focused tests, and a diff check.
  5. If validation failed, feed the error and current diff back into the next iteration.
  6. Return the candidate patch on success; otherwise stop rather than retry indefinitely.

The agentic remediation loop starts from a constrained vulnerability finding, inspects the repository, proposes the smallest patch, regenerates the dependency graph, and either uses validation evidence for another bounded attempt or opens a pull request for human review

The validation output was more useful than another general prompt. A package-manager error, a failing test, or an unexpectedly large diff gave the next attempt something concrete to investigate. The model could correct one assumption instead of starting again.

The first version did not enforce this boundary as strongly as it should have. Jobs ran inside the web application process. Over the next few months, I moved them behind an explicit execution boundary and added the limits shown in the diagram.

Success still meant “candidate patch ready for review,” not “vulnerability fixed.” The loop could show that the dependency graph resolved and the repository’s checks passed. It could not know every production assumption or accept risk on behalf of the repository owner.

AI should pay rent by reducing exceptional code, not by replacing reliable code we already know how to write.

The fixer is also a security boundary

An automated vulnerability fixer is an unusually easy tool to trust too much. It exists to improve security, so its own attack surface can receive less scrutiny than it deserves.

The agent clones a repository, reads repository-controlled content, changes dependency declarations, and runs a package manager. Every one of those steps crosses a trust boundary.

A repository can contain instructions that the agent must treat as untrusted data. Installing a package can run scripts. A compromised dependency can use any network access or credentials available to the job. Even a well-intentioned agent can produce a large patch that passes its checks but is still wrong for the system.

The most important safety decision was therefore that the output remained a pull request. The fixer did not merge its own work. Existing CI, code ownership, and human review remained the authority for accepting the change.

That boundary makes the operation reversible. A poor patch costs review time; it does not silently change production.

As the tool gained wider use, I hardened that execution boundary:

  • Each job ran in an isolated, disposable environment.
  • Short-lived, least-privileged credentials could create a branch and pull request but could not merge.
  • Application secrets were kept away from repository-controlled processes.
  • Time, CPU, memory, and network access were bounded.
  • Installation scripts and arbitrary build commands were treated as untrusted execution.
  • Each job recorded the advisory, requested version change, commands run, validation result, and final diff.
  • Duplicate requests were rejected before they could create competing pull requests.

These controls are not AI-specific. A deterministic bot running npm install has most of the same risks. The agent simply makes the boundary easier to notice because its behaviour is less constrained by code we wrote in advance.

Asynchronous without building a platform

Simple fixes complete quickly. Agentic fixes can take several minutes while the tool clones a repository, inspects it, edits files, regenerates a lockfile, and validates the result.

The UI starts the job and checks its status every five seconds. For work measured in minutes, that feels responsive enough. WebSockets would have added connection and reconnection handling without making the result arrive any sooner.

The first version kept job state in memory. If the application restarted, an unfinished job was lost and the user had to start it again.

That is not the design I would use for hundreds of jobs. At our initial volume, adding a durable queue and separate workers would have created more operational work than the occasional lost job.

The important part was making that trade-off consciously:

  • Jobs were short-lived.
  • Repeating a failed job was cheap.
  • The pull-request boundary prevented duplicate execution from changing production.
  • We could see when usage justified moving state into the database and execution into workers.

“No queue” was not an assertion that jobs never need queues. It was a decision to buy operational simplicity until the evidence changed.

Severity was the wrong sort order

The most useful product decision had nothing to do with AI.

Our first dashboard sorted findings by severity. That seemed obvious and turned out to be wrong.

A critical finding with plenty of remediation time remaining may be less actionable today than a moderate finding that is already overdue. Severity describes potential impact. It does not, by itself, answer the operational question: what should I work on next?

We changed the primary ordering to the remediation deadline:

  1. Overdue.
  2. Approaching the deadline.
  3. In compliance.

Severity remained visible, but urgency controlled the queue. The page started reflecting the decision an engineer actually needed to make.

We also made fixability a first-class metric.

“There are 50 open findings” feels like an unbounded security backlog. “Most of these already have known fixes and can be turned into reviewable pull requests” feels tractable.

That did not change the underlying risk. It changed the team’s ability to see a path through it.

Good operational tooling should not only display state. It should reduce the distance between understanding the state and taking the next safe action.

The dashboard was more valuable than I expected

I assumed the fix button would be the product and the dashboard would be the wrapper around it. Usage taught me the opposite.

The team used the dashboard even when they were not generating fixes. It replaced a manual weekly audit, made ownership gaps visible, and gave planning conversations a shared view of urgency and fixability.

The fixer reduced the cost of action. The dashboard reduced the cost of coordination.

In a multi-repository environment, coordination was often the larger cost.

I wish I had instrumented the remediation funnel from the first release: findings classified, fixes attempted, patches generated, validation passed, pull requests opened, pull requests merged, and failures grouped by reason.

Without that funnel, it is easy to celebrate activity instead of effectiveness. A generated pull request is not a remediated vulnerability. The merge - and eventually the scanner confirming the finding is gone - is the outcome that matters.

How the system evolved

These lessons did not remain a list of things I would do differently. Over the next few months, I kept iterating on the tool and built them into the system.

I added the deterministic fixer. It handled the common direct-dependency changes quickly and left the agent with a concrete set of repository-specific failures to solve.

I isolated execution. The early in-process runner helped validate the product, but it did not remain the permanent trust boundary as more repositories and users were added.

I instrumented the full funnel. The system tracked validation, review, merge, and scanner-confirmed closure instead of treating a generated pull request as the final outcome.

I moved coordination into shared state. The local data store worked for one application instance. Shared storage made findings and job state available as the system grew beyond that first shape.

I expanded the dashboard as a product of its own. Sorting by urgency, showing ownership, and exposing fixability removed weekly coordination work even when nobody used the agent.

The real engineering challenge

Most dependency vulnerability remediation is not a novel security investigation. It is a known input, a known target version, and a packaging change that still needs ordinary engineering validation.

AI helps with the messy remainder: unfamiliar repository layouts, transitive dependencies, monorepos, and tooling we did not anticipate. That remainder matters. It just should not define the entire architecture.

The more important design work was deciding what could be automated safely, keeping the final authority with repository owners, and presenting the backlog in a way that made the next action obvious.

The hard part was never getting an LLM to edit a manifest. It was reducing the activation energy without removing the judgment that protects the system.

Thanks for reading ✌️

Stay in the loop

Practical engineering notes, without the inbox noise.

Notes on distributed systems, resilient software, and engineering in the real world - usually once or twice a month.

Unsubscribe anytime. Prefer a feed? Subscribe via RSS.