Concepts
Anchor upgrades
Some transitive vulnerabilities have no clean fix at the leaf. The cleanest remediation is to raise the thing that pins it: the BOM, the parent, or the managed library. Argus can find that upgrade, prove it, and prefer it over a pile of overrides.
What it is
The strategy ladder resolves a transitive vulnerability by OVERRIDE-ing the offending version through dependencyManagement, or excluding and re-adding it. That always works, but it treats the symptom. Often the real fix is one level up: the project’s Spring Boot BOM, its in-house parent, or a first-party library already ships a newer release that pins the fixed transitive. An anchor upgrade raises that managed version source instead: one deliberate bump in place of several standalone overrides.
It is never a blind bump. For every candidate, Argus re-resolves the graph, re-queries OSV to confirm the advisory is gone and no new advisory appeared, and qualifies the result with mvn verify. If no candidate both resolves and verifies, it falls back to the deterministic ladder. The anchor upgrade is preferred only when it is clean and proven.
The three anchors
Argus looks for three kinds of managed version source that can govern a vulnerable transitive:
- An imported BOM: a
<dependencyManagement>import such asorg.springframework.boot:spring-boot-dependencies, usually pinned through a${…version}property. - A parent POM: a custom in-house parent or a public starter parent whose newer release raises the vulnerable dependency to a safe version.
- A managed dependency: a first-party library pinned in
dependencyManagementwhose newer release fixes the graph.
A property-versioned anchor is pinned for a trial with a Maven -D override on top of the POM planning sees (the cleaned copy, when stale overrides were reclaimed first), so a pin that cleanup just released cannot be mistaken for the anchor’s own effect; a literal version (a hard-coded <parent><version>) is trialled by materialising a candidate POM in an isolated workspace. The bump that ships edits the property or the literal version in place, format-preserving like every other Argus patch.
Since 3.0.1, the property does not have to live in your POM. The common enterprise shape (a custom parent defines <spring-boot.version> and the app imports the Boot BOM with it) is resolved by reading the <parent> chain from the local repository. When the newer BOM verifies, the shipped fix declares the property locally in your <properties> block: a child property outranks the parent’s, so one line moves the whole platform without touching the parent POM. An unreadable parent chain is skipped with a logged reason, never guessed.
How a candidate is judged
For an anchor governing one or more vulnerable transitives, Argus enumerates newer stable releases (ascending) and, smallest-first, asks of each candidate:
- Does it resolve? Re-resolve the SBOM with the anchor pinned to the candidate and re-query OSV: the target advisories must be gone, and no new advisory may have appeared.
- Does it build? Qualify the candidate with
mvn verify(the full test phase, not just compile).
Argus accepts the smallest version that resolves and verifies. One anchor often governs several vulnerable transitives at once; they are evaluated from a single re-resolution and qualified with a single mvn verify, so the cost is per candidate version, not per advisory. A target clears only when the advisory no longer affects that specific component at its resolved version, so a CVE that also touches a sibling artifact the bump did not move never blocks a genuine fix. Coverage is attributed only to the targets the shipped version actually fixes: Argus never claims a fix it did not prove.
Since 2.9.0 Argus first probes the newest candidate for movement: if bumping the anchor to its newest release does not change a target’s resolved version (because the target is pinned by a <dependencyManagement> entry, an imported BOM, or a repackaged internal artifact), then no candidate can, so Argus abandons after a single re-resolution and logs the pin, rather than trialling every version. This is what keeps a hopeless upgrade from burning a Maven resolve per candidate.
Default-on behaviour
Anchor upgrades are enabled by default. Argus tries the cleaner anchor fix first, records the evidence, and falls back to the ladder when no candidate both resolves and builds. Set ARGUS_ANCHOR_UPGRADE=off when you need to skip the extra trial loop and use only BUMP, OVERRIDE, and EXCLUDE+ADD.
# Anchor upgrades run by default.
argus run --pom pom.xml --no-publish
# Opt out when you need the classic ladder only.
ARGUS_ANCHOR_UPGRADE=off argus run --pom pom.xml
# Send the per-dependency reasoning to a file (the TUI hides stderr).
ARGUS_ANCHOR_LOG=anchor.log argus run --pom pom.xmlPublic auto, internal opt-in
Public anchors (a Spring Boot BOM, a public starter parent) are eligible for upgrade automatically. Internal or off-limits anchors are not, because organisations deliberately control their own libraries. An internal anchor opts in through internal_upgrades: true on its libraries entry in config.yml. An artifact can be both frozen (upgrade: never, never overridden) and internal-upgrade-eligible (may be bumped to a newer release): the two are complementary.
Module companions (3.1). Platforms modularise between releases: a class the application compiles against moves into its own module, and the new version’s starters stop supplying that module transitively, so every candidate build fails with package … does not exist and the upgrade used to be a guaranteed decline. Argus now reads the missing packages from the failed build, proves the providing module against the candidate BOM’s own dependencyManagement (including a jar receipt confirming the package really lives there), and retries the candidate once with the version-less dependency added. An accepted upgrade ships the companion in the same merge request under a “Companion modules added” section, with a PATCH comment naming the package and the source file that proved the need. Anything unprovable declines exactly as before; disable entirely with detection.module_companions: "off" or ARGUS_MODULE_COMPANIONS=off.
# config.yml — internal anchors are upgrade-eligible only when they opt in
version: 3
libraries:
- match: "com.acme.internal:platform-bom"
upgrade: never
internal_upgrades: true # frozen, but may move as an anchor
- match: "com.acme.internal:acme-parent"
upgrade: never
internal_upgrades: trueWhere versions come from
To upgrade an anchor, Argus must know which newer versions exist. By default it asks Maven, inheriting your settings.xml, mirrors, and credentials. If you configure registries in config.yml, Argus instead reads versions directly from those repositories over HTTPS. That is faster, and it discovers imported BOMs and parents that Maven’s own reporting hides.
The decision log
Every anchor considered, every candidate trialled, each re-scan and mvn verify outcome, and the final decision are recorded per dependency. The log always writes to anchor.log next to report.json by default, so it is there without any configuration. Set ARGUS_ANCHOR_LOG to redirect it to a different path, or ARGUS_ANCHOR_LOG="" to suppress the file. The merge request carries the same evidence.
msg="anchor upgrade accepted" anchor=org.springframework.boot:spring-boot-dependencies from=4.0.5 to=4.0.6 resolved=[org.springframework:spring-webmvc tools.jackson.core:jackson-core]
msg="anchor upgrade fell back to ladder" anchor=org.springframework.boot:spring-boot-dependencies target=org.apache.tomcat.embed:tomcat-embed-core trials=1