Concepts

The strategy ladder

For every advisory, Argus walks a fixed ladder of remediation strategies and takes the first rung that applies. The same inputs always produce the same fix.

The idea

There is rarely one way to fix a vulnerable dependency, and the right one depends on where the dependency sits in your graph. A library you declare yourself is fixed differently from one pulled in three levels down, and a version frozen by a parent POM needs different handling again. Rather than guess, Argus encodes these cases as a ranked ladder and always takes the highest rung that fits. There is no heuristic and no scoring: the decision is mechanical and reproducible.

The three rungs

The ladder has three strategies. For each advisory, Argus walks this decision and takes the first rung that fits:

01Is the vulnerable dependency declared directly in this POM?

Yes
BUMP

Raise the version in place, or bump the property it points to.

No · transitive

02Does a parent POM hard-pin its version via dependencyManagement?

No
OVERRIDE

Pin the fixed version through dependencyManagement.

Yes
EXCLUDE + ADD

Cut the vulnerable path with an exclusion, then add the fixed version as a direct dependency.

BUMP

When the vulnerable dependency is declared directly in your POM, Argus raises its version, either inline or by updating the property the version refers to. This is the cleanest possible fix: one version string changes, and Argus annotates it with a comment recording the advisory and the reason.

pom.xml (before)
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.9</version>
</dependency>
pom.xml (after)
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <!-- PATCH|BUMP|CVE-2022-XXXX|Bumped commons-text from 1.9 to 1.10.0 to fix vulnerability. -->
    <version>1.10.0</version>
</dependency>

A pin you already manage

BUMP also covers a case that looks transitive but is really yours to fix. If a vulnerable coordinate is reached transitively but you pin its version in your own <dependencyManagement> (a deliberate choice you made before Argus arrived), that managed entry is the authority governing the version for every module. Argus raises it in place rather than reaching for EXCLUDE + ADD. One edit, and the pin that was the problem becomes the fix; no stale vulnerable version is left in the file to govern a sibling module or reappear if a workaround is removed.

pom.xml (before)
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>1.33</version>
        </dependency>
    </dependencies>
</dependencyManagement>
pom.xml (after)
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <!-- PATCH|BUMP|CVE-2022-1471|Bumped snakeyaml from 1.33 to 2.0 to fix vulnerability. -->
            <version>2.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

OVERRIDE

When the vulnerable dependency is transitive (something you never declared) and no parent freezes its version, Argus pins the fixed version in <dependencyManagement>. Maven then resolves the whole graph to that version. If the POM has no <dependencyManagement> block, Argus synthesises one.

pom.xml (before)
<dependencyManagement>
    <dependencies>
    </dependencies>
</dependencyManagement>
pom.xml (after)
<dependencyManagement>
    <dependencies>
        <!-- PATCH|OVERRIDE|CVE-2022-XXXX|Forced commons-text to 1.10.0 via dependencyManagement (transitive) to fix vulnerability. -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-text</artifactId>
            <version>1.10.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

EXCLUDE + ADD

Sometimes a transitive dependency’s version is hard-pinned by a parent POM’s <dependencyManagement>. A plain override in your project would be outranked, so it cannot win. In that case Argus does two things: it adds an <exclusion> to the direct ancestor that drags the vulnerable library in, and it declares the fixed version as a direct dependency of your project. The vulnerable path is cut, and the safe version takes its place.

pom.xml (before)
<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-core</artifactId>
    <version>2.5.30</version>
</dependency>
pom.xml (after)
<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-core</artifactId>
    <version>2.5.30</version>
    <exclusions>
        <exclusion>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- PATCH|EXCLUDE|CVE-2023-XXXX|Excluded commons-fileupload from struts2-core and added it as a direct dependency to fix vulnerability. -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.5</version>
</dependency>

Choosing the version

Whichever rung applies, Argus still has to pick a target version. It uses Maven’s own ComparableVersion ordering (the same semantics Maven uses to resolve conflicts), so 1.9 and 1.10 compare correctly and qualifiers like alpha, beta, rc, and snapshot rank where Maven would put them. From the safe versions, Argus prefers the smallest move: a patch bump over a minor, a minor over a major. Pre-release versions are never selected automatically.

Breaking changes

When the only safe version is a major bump, Argus marks the remediation as breaking. That signal flows through to the merge request, which is labelled accordingly so a human knows to look more closely before merging. See Merge requests for how breaking changes are surfaced.

Anchor upgrades

The ladder treats the symptom: it pins the offending transitive directly. Before shipping one of those transitive fixes, Argus now checks whether the vulnerable version is governed by an anchor: an imported BOM, a parent POM, or a managed dependency. If a newer anchor release raises the vulnerable transitive to a safe version, Argus can ship one anchor bump instead of a pile of overrides.

Anchor trials are enabled by default. For each candidate, Argus re-resolves the graph, confirms the target advisories disappear without introducing a new advisory, then qualifies the candidate with mvn verify. Public anchors are eligible automatically; internal or frozen anchors opt in with internal_upgrades: true on their libraries entry in config.yml. If no anchor candidate both resolves and builds, Argus falls back to the three-rung ladder above.

Direct parent upgrades

When the ladder would publish a transitive OVERRIDE or EXCLUDE+ADD, Argus also checks whether the direct dependency on the path already has a same-major release that fixes the vulnerable transitive naturally. If that parent candidate clears the advisory, introduces no new advisory, and passes mvn verify, Argus ships a direct parent upgrade instead of pinning the transitive leaf.

If no parent candidate verifies, the ladder plan remains unchanged. Set ARGUS_PARENT_UPGRADE=off to skip this trial loop.

The npm rungs

npm support is earlier-stage than Maven’s, but npm projects walk the same ladder in spirit, with one cheaper rung at the top. LOCK_REFRESH applies when the declared range already admits a fixed version (^6.5.2 admitting 6.5.3, say), so only package-lock.json is stale. Argus regenerates the lockfile entry and touches nothing else; it is the npm equivalent of a fix that was already yours for the taking.

When the range itself excludes the fix, Argus edits the range in dependencies or devDependencies and regenerates the lockfile: the npm BUMP. For transitive findings it adds a root overrides entry with an argus.overrides marker, the npm OVERRIDE, which the stale-override check can later prove redundant and remove. See npm projects for the full treatment.