From 229+ Vulnerabilities to Zero: How AI Helped Us Modernise a Legacy Enterprise Application

A story about confronting technical debt, outsmarting false alarms, and building bridges across incompatible worlds — without rewriting everything from scratch.
security
legacy modernisation
AI
Spring
Java
Struts
Author

Atamán Vega Vega at The Agile Monkeys

Published

March 2, 2026

A story about confronting technical debt, outsmarting false alarms, and building bridges across incompatible worlds — without rewriting everything from scratch.


The Starting Point: A Ticking Clock

Every organisation that has been building software for more than a decade carries the same quiet burden: somewhere in production, there is an application that works perfectly well for the business, but is held together by components that the rest of the world has long since moved on from.

Our application — a critical enterprise system serving hundreds of thousands of users — was exactly that. It had been running reliably for years, quietly doing its job. But under the hood, it was running on frameworks and libraries that were not just old: they were exposing the organisation to real security risk.

When we ran our first automated security scan using the OWASP Dependency-Check tool, the report came back with more than 229 reported vulnerabilities. Seeing that number on the screen is a gut-punch. It feels like a building inspection that has just condemned your house.

The question was: what do we do about it?


Why You Can’t Just “Update Everything”

The instinctive response to a vulnerability report is to update everything to the latest version. Simple, right?

In practice, this is rarely simple — and for a mature enterprise application, it can be nearly impossible without effectively rewriting the system.

Our application had already been partially modernised over the years. It was running Spring 5 and Spring Security 5, with a commercial support contract covering the harder-to-maintain legacy dependencies. That commercial arrangement had kept the system compliant, but it was not a permanent solution.

The application’s entire request-handling architecture, page navigation logic, and form processing were built around Struts. Spring was the backbone of dependency injection, database access, and transaction management — essentially the skeleton of the entire system.

The modern, secure versions of both ecosystems had moved on dramatically:

  • Spring 6 requires the Jakarta EE namespace — an industry-wide rename of the javax.* package family to jakarta.* — and had dropped support for Java versions below 17
  • Spring 6 had completely removed its built-in Struts integration module (spring-struts), which existed precisely to wire Spring beans into Struts actions
  • Struts 1.2.9 was written against the old javax.* namespace and was never updated for the jakarta.* world

In plain terms: the two frameworks we depended on had evolved in ways that made them fundamentally incompatible with each other. You could upgrade one or the other, but not both — and you needed both.

This is the kind of problem that, in the past, would have meant a multi-year, multi-million dollar “big bang” rewrite. Write everything from scratch in modern technology. Hope nothing gets missed. Pray the business logic gets reproduced faithfully.

We took a different approach.


Bringing AI Into the Room

We engaged AI as an active participant in the modernisation process — not just as an autocomplete tool, but as a technical collaborator that could hold the entire codebase in context, reason about complex dependency chains, and propose solutions to problems that had no obvious off-the-shelf answer.

The first task we gave it was deceptively simple: understand the problem and tell us what is real.


The Art of the False Positive

Here is something that security reports almost never tell you clearly: not every vulnerability listed is actually a vulnerability in your application.

Automated tools work by scanning the names and version numbers of the libraries you use, then matching them against a global database of known vulnerabilities (the NVD — National Vulnerability Database). This matching process is imprecise. The tools are designed to over-report, because missing a real vulnerability is far worse than flagging a false one.

But for a team trying to act on those findings, false positives are a serious problem. They dilute attention, waste engineering time, and — most dangerously — can lead to a kind of learned helplessness where the team starts dismissing reports entirely.

AI was extraordinarily effective at distinguishing real threats from noise. Here are the categories of false positives it identified:

1. Name Collisions: When Your Code Looks Like Someone Else’s

Several of our internal project modules carried names that happened to contain common English words — words that also appeared in the names of completely unrelated third-party products flagged in the vulnerability database. The scanner matched on the word overlap and reported CVEs that had nothing to do with our code.

These were private, internal components that shared nothing — not code, not architecture, not even a category — with the affected products. AI identified this pattern immediately, articulated precisely why each match was incorrect, and generated the suppression rules needed to eliminate the noise.

2. Struts 2.x CVEs Applied to Struts 1.x

This was one of the largest categories of false positives — and one of the most technically subtle.

Apache Struts has two completely separate product lines: Struts 1.x (our framework) and Struts 2.x (a completely different framework, built on different architecture, with different security characteristics). The two share a name and a brand, but they are not the same software.

Struts 2.x has been the source of some of the most serious enterprise security vulnerabilities of the past decade — including the infamous CVE-2017-5638, which was used in the Equifax breach. The attack vectors in Struts 2.x exploits rely on OGNL (Object-Graph Navigation Language), a powerful expression evaluation engine that is a core part of Struts 2’s architecture.

Struts 1.x does not use OGNL. It never did.

Yet the vulnerability scanner was flagging our Struts 1.2.9 installation with CVEs like CVE-2024-53677, CVE-2023-50164, CVE-2023-34149, and CVE-2025-25015 — all of which are Struts 2.x OGNL injection or file upload vulnerabilities that have zero technical relevance to Struts 1.x.

AI was able to understand this architectural distinction, explain it clearly, and document exactly why each of these CVEs was inapplicable. This wasn’t just a cleanup exercise — it was a genuine security analysis that required understanding how both frameworks work internally.

3. Vulnerabilities Already Fixed in Our Version

The vulnerability scanner was also flagging CVEs against several libraries we use — specifically, vulnerabilities that had been patched and fixed many versions before the version we were actually running.

In more than one case, a CVE that was fixed in an older release was still being reported against our significantly newer version, because the scanner’s CPE (Common Platform Enumeration) matching was broad enough to catch our library by name regardless of version. Our code was never affected.

AI identified the pattern, cross-referenced each CVE advisory against the library changelogs to confirm the fix version, and produced the appropriate suppressions with a clear, auditable justification.

4. Mitigated Vulnerabilities: Context Is Everything

Some vulnerabilities are real — but the way your application uses the affected library means the attack vector simply cannot be reached.

Our application uses a third-party library for PDF generation. There are known vulnerabilities in the version we use — related to infinite loops and null pointer exceptions when processing malicious, attacker-crafted PDF files.

The key word is processing. Our application generates PDFs from trusted server-side data. It does not accept PDFs from users and parse them through iText. The attack vector requires an attacker to supply a malicious PDF for the application to read — which never happens in our workflow.

AI understood this distinction, documented the mitigation context, and correctly classified these as acceptable residual risks rather than actionable vulnerabilities.


A Structured Attack: Triage Before Tools

With the false positives cleared away, the team had a much cleaner picture of the real work ahead. But even after stripping out the noise, a meaningful backlog of genuine issues remained — and attempting to fix everything at once is a reliable path to paralysis.

The next step was to classify the remaining vulnerabilities into three tiers, based on the complexity of the fix required:

Easy: Simple version bumps. A newer release of the same library already contained the fix, the update was backwards-compatible, and changing a version number was all that was needed. These could be resolved quickly and in bulk.

Medium: Updates that required care. The fix existed, but the newer version involved API changes, configuration adjustments, or testing to confirm compatibility with the rest of the stack. These needed individual attention but had clear, documented upgrade paths.

Difficult: Cases where no straightforward fix existed — where upgrading one component would break another, or where the required version was fundamentally incompatible with a core framework already in the application.

This tiered approach transformed the work from a single overwhelming task into a structured campaign. The team started with the easy wins, clearing a large portion of the backlog quickly and building momentum. Medium-complexity fixes followed, each one narrowing the gap further. By the time the team arrived at the genuinely difficult cases, the list was short, the context was clear, and the focus was sharp.

The hardest case — the one that required the most creative engineering — was the incompatibility between Spring 6 and Struts 1.x.


The Incompatibility Problem: Building a Bridge

After eliminating the false positives, a core set of genuine modernisation challenges remained. The biggest: how do you upgrade to Spring 6 when your entire UI layer depends on a framework (Struts 1.x) that Spring 6 refuses to support?

This is where AI moved from analysis into invention.

The Problem in Plain Language

Think of it like a building with two systems that need to talk to each other: the plumbing (Spring) and the electrical wiring (Struts). You’re upgrading the plumbing to a completely new standard. But the new plumbing standard’s connectors are a different shape — and the electrical system was designed for the old connectors, and its manufacturer stopped making it decades ago.

You have three options:

  1. Replace both systems simultaneously (the “big bang” rewrite — high risk, very expensive)
  2. Don’t upgrade (accept the security debt — not acceptable)
  3. Build an adapter that speaks both languages

AI designed and implemented Option 3.

The Compatibility Layer

The solution was a purpose-built compatibility package — a set of classes that sit between the old Struts world and the new Spring/Jakarta world, translating between them at runtime.

The technical challenge has two dimensions:

Dimension 1: The namespace divide. Spring 6 (and modern Tomcat) uses the jakarta.* package namespace. Struts 1.2.9 was written against the javax.* namespace. These represent the same concepts under different names — but Java treats them as entirely different types. An object of type javax.* is simply not the same as its jakarta.* counterpart at runtime, even though they describe identical things.

Dimension 2: The missing bridge. Spring used to ship a dedicated integration module (spring-struts) that handled the wiring between Spring-managed beans and Struts actions. Spring 6 dropped it entirely. There was no replacement. The bridge simply didn’t exist anymore.

The compatibility layer AI built solves both problems:

  • A new family of classes mirroring the Struts action interfaces was created — written against the Jakarta namespace, matching the signatures of the Struts originals as closely as possible
  • A custom proxy component was written to replace the removed Spring integration. This proxy acts as the connective tissue: Struts invokes it using the old javax.* types, it looks up the correct Spring-managed action bean, translates the request and response objects across the namespace boundary, executes the action, and translates the result back

The beauty of this approach is that the existing application code — hundreds of Action classes, thousands of lines of business logic — required only import statement changes. The logic itself was untouched. The compat layer absorbed all the complexity so the application code didn’t have to.

This is what a well-designed compatibility layer should do: make the hard thing invisible.


The Result: 229+ Vulnerabilities → 0

After completing the modernisation:

Before After
Reported vulnerabilities 229+ 0
Spring Framework version 5.x 6.x
Spring Security version 5.x 6.x
Java version Java 17 Java 21
Servlet namespace javax.* jakarta.*
Struts integration Removed in Spring 6 Custom compat layer
False positives documented 0 30+ (with full audit trail)
Business logic rewritten Zero lines

The last row is the one that matters most to the business. The application behaves identically to users. No features were removed. No business logic was altered. The security posture was transformed without any of the risk that a rewrite would have introduced.


What This Tells Us About AI-Assisted Modernisation

This project illustrates something important about where AI adds genuine value in software engineering — and it isn’t just about writing code faster.

The hardest problems in legacy modernisation are not coding problems. They are reasoning problems:

  • Is this vulnerability real, or is it a false match?
  • Does this CVE apply to the way we actually use this library?
  • Is there a way to satisfy two incompatible requirements simultaneously?
  • What is the minimum change that achieves the security goal without breaking the application?

These questions require holding a large amount of context simultaneously, understanding the semantics of security advisories, and designing solutions that work within tight constraints. They are exactly the kind of problems where AI currently provides the most leverage.

The compat layer is a particularly good example. A human engineer could have designed it — but it would have required deep expertise in three different technology stacks (Struts internals, Spring internals, and the javaxjakarta migration) and the creative insight to see that a thin translation layer was the right architecture. AI could draw on all of that knowledge simultaneously and propose a coherent design quickly.

False positive analysis is another. Working through 229+ reported vulnerabilities one by one, understanding the semantics of each CVE, matching it against the actual usage patterns in the codebase, and writing precise suppression rules with documented justifications — this is tedious, high-skill work. AI can do it faster and more consistently than a human team under time pressure.


The Takeaway

Legacy modernisation projects have a reputation for being long, expensive, and risky. That reputation is largely deserved — when they are approached as “big bang” rewrites.

A better approach is surgical: understand precisely what needs to change, distinguish real risk from noise, build bridges where direct replacement is impossible, and preserve everything that works.

AI doesn’t replace the engineers who understand the system. But it amplifies what they can do — turning a project that might have taken quarters into one that takes weeks, and turning a vulnerability report that looked like a condemned building into a system that is genuinely, verifiably secure.

Zero vulnerabilities. Zero lines of business logic rewritten. One very large problem, solved.


Written February 2026.