Skip to content

[Feature]: Central DependencyManagementInjector — kill transitive CVEs across standalone-published modules #422

Description

@julienmerconsulting

Type Surface Impact Runtime

Problem / motivation

OculiX ships several standalone Maven artifacts to Central — oculixapi, oculixide, oculix-mcp, oculix-reporter, plus the internal build-extensions. Each child pom is deliberately standalone (no <parent> reference) so a downstream project can depend on just oculixapi without dragging the whole tree.

That autonomy comes with a real cost when Scorecard or OWASP Dependency-Check flags a transitive CVE. Two options exist, and both are bad:

  • Add <parent> inheritance to every child pom — restores central control, but breaks the independent publishing lifecycle. A patched oculixapi release would suddenly require a parent-pom release too, and downstream consumers who only want the API artifact inherit a heavy parent chain.
  • Duplicate the same <dependencyManagement> block in every child pom — 4 to 5 copies to keep in sync. Humans forget. The humans who promised me they'd keep them in sync were themselves.

Concrete blocker right now: jython-slim 2.7.4 is a hard runtime dependency of OculiX (it is the scripting engine, and there is no newer release available upstream). It transitively pulls Bouncy Castle 1.75, which carries GHSA-p93r-85wp-75v3 (high, covert timing channel), GHSA-c3fc-8qff-9hwx (LDAP injection), GHSA-wg6q-6289-32hp (broken crypto algorithm), and GHSA-4cx2-fc23-5wg6 (excessive allocation). Scorecard rates this red and every downstream security review inherits the same red flag.

Proposed solution

Introduce a small Maven core extension living in the existing build-extensions/ module: DependencyManagementInjector extends AbstractMavenLifecycleParticipant.

At afterProjectsRead time, the extension walks every project in the reactor and injects a shared <dependencyManagement> block into its effective model. The pins are declared as a static array of Pin(groupId, artifactId, version) records inside the extension class, each with the GHSA identifier(s) they address as an inline comment.

Registered via SPI (META-INF/plexus/components.xml) so Maven picks it up automatically on every reactor build. No child pom needs to change. No downstream consumer needs to import a BOM.

The full extension in production today (staged, pending this issue) already carries pins for Bouncy Castle 1.84 (kills the four GHSA above) and Netty (staged for the next Scorecard tick). Refresh policy is a one-line change: any future CVE flag lands as a new Pin(...) entry, everywhere at once.

Alternatives considered

  • <parent> inheritance on every child pom — restores central control at the cost of the standalone publishing story. A downstream consumer of oculixapi shouldn't have to inherit a parent chain just because we needed a place to put a <dependencyManagement> block.
  • Duplicate the block in every pom — 4+ copies to keep in sync. Every prior attempt to keep parallel poms aligned in this codebase has drifted within a release cycle. Not a discipline problem; a structural one.
  • Publish a BOM to Central and import it in each pom — adds an indirection layer, requires every downstream consumer to know about the BOM, and does not help internal reactor builds (the BOM would still need to be imported in each child pom).
  • Post-package sanitization (Maven Shade relocate) — far too aggressive. Downstream consumers who legitimately need Bouncy Castle at runtime would find it under a mangled package name.

Where would this live?

  • Build / Maven / packaging

API impact

No — build-time only, zero runtime footprint. The extension acts on the effective model at reactor load; the produced artifacts are identical to what a hand-pinned child pom would produce. Consumers of oculixapi and its siblings see a CVE-free transitive graph via standard Maven resolution.

Use case / impact

  • Every consumer of any OculiX Maven artifact inherits a CVE-free transitive graph automatically, without knowing this extension exists.
  • Immediate scope: 4 GHSA neutralized on Bouncy Castle via a single Pin. Netty pins staged for the next Scorecard alert.
  • Zero maintenance burden for downstream users. They do not need to run mvn dependency:tree, add <exclusions>, and hand-pick replacement versions.
  • Scorecard grade on oculix-org/Oculix moves from "yellow with 4 red transitives" to "green on transitive dependencies", improving the badge in the README and the trust signal for enterprise consumers.

Additional context

The extension lives in a dedicated build-extensions/ module so it is versioned, tested, and built like any other OculiX module. It is picked up automatically by Maven on every reactor invocation via SPI — no -DbuildExtension=... flag, no user configuration.

Rationale trace: each Pin records the exact GHSA identifier(s) it addresses in an inline comment. A future maintainer revisiting whether a pin is still required after an upstream release has the reference immediately at hand, and can drop the pin without archaeology.

Cross-reference: this pattern is the standard answer in JVM projects that publish multiple standalone artifacts (Netty, Micronaut, Quarkus all use variants of it). Reinventing here rather than reusing an existing framework because the injector is 200 lines of code and pulling in a full dependency to save 200 lines would itself add transitive CVEs.

🦎

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Fields

    No fields configured for Feature.

    Projects

    Status
    ✅ Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions