DEV Community

Cover image for Does Dependency Security Really Need the Cloud?
Gap Hunter Labs
Gap Hunter Labs

Posted on

Does Dependency Security Really Need the Cloud?

The question
I kept asking myself a simple question:
Why does checking a dependency for a known vulnerability need to send anything to the cloud?
This isn't an argument against cloud-based security tooling.
There are good reasons to use centralized services, especially when you need continuously updated data, organization-wide policies, reporting, or large-scale analysis.

I was interested in a narrower engineering problem:
What would a dependency vulnerability analyzer look like if the core analysis had to work entirely locally?

That constraint led me to build Dependency Vulnerability Companion, an IntelliJ-family plugin that analyzes dependency declarations against a vulnerability database bundled with the plugin.

The constraints were deliberately strict:

  • No account
  • No token
  • No API key
  • No network call during analysis
  • No runtime vulnerability database download The interesting part wasn't making an IDE inspection. The interesting part was seeing what those constraints did to the architecture.

The smallest useful pipeline
A dependency manifest already contains much of the information needed for a basic vulnerability check.

For example:

com.fasterxml.jackson.core
jackson-databind
...

If I can extract:

  • ecosystem
  • package
  • version
  • source location then I can compare that information against a local vulnerability dataset.

The initial pipeline therefore became:
Project file -> Dependency parser -> Normalized dependency -> Version matching -> Local vulnerability database -> IntelliJ inspection
There is no remote lookup in that path.

That sounds simple, but every box introduces its own set of problems.

Keeping the vulnerability database local
The most important architectural decision was to treat vulnerability data as part of the plugin rather than as a runtime service dependency.
The plugin contains a versioned vulnerability database inside its JAR.

The current dataset is derived from OSV bulk data and contains a selected snapshot of high- and critical-severity advisories from the GitHub Advisory Database.

At runtime, the plugin reads that data locally.

This changes the architecture from:
IDE -> Plugin -> Remote vulnerability service -> Response
to:
IDE -> Plugin -> Local vulnerability database & Dependency analysis
That gives the analyzer a useful property: the core vulnerability check doesn't depend on network availability.

It also makes the trade-off explicit. A bundled database is not a real-time feed.

If a new advisory is published after a plugin release, that advisory isn't magically available to an existing installation.

The dataset therefore becomes part of the release lifecycle.
I consider that a much better engineering trade-off than pretending that offline and real-time can be achieved simultaneously without additional infrastructure.

Dependency extraction
The plugin currently works with dependency declarations in:

  • pom.xml
  • build.gradle
  • build.gradle.kts
  • package.json
  • requirements.txt

One design decision I made was to keep the initial parsers relatively lightweight. I didn't need to build a complete compiler for each ecosystem.

The analyzer primarily needs to answer:

  • What dependency is declared?
  • What version is associated with it?
  • Where is that declaration in the source file? The output can then be normalized into a common representation: Dependency
  • ecosystem
  • package
  • version
  • source file
  • source range

Once that representation exists, the vulnerability matching layer doesn't need to care whether the original declaration came from Maven XML, Gradle Kotlin DSL, npm, or pip.
pom.xml, build.gradle, build.gradle.kts, package.json, requirements.txt -> Dependency model -> Vulnerability matcher
The parsers are ecosystem-specific. The matching layer doesn't have to be.

Why source offsets matter
Finding a vulnerable package is only half of the problem. The result needs to become useful inside the IDE.

For example, if the parser discovers com.example:library:1.2.3, I also need to know where that dependency occurs in the original document.
That means the extracted representation needs source information, not just semantic information:
dependency declaration -> extract dependency & retain source range -> lookup vulnerability -> create IntelliJ inspection & highlight original declaration
The final result isn't just "This package is vulnerable."
It is: "This declaration in the file you're currently editing matches a known vulnerable version."
That distinction makes the analysis much more useful.
Version comparison is where the problem gets interesting
The vulnerability lookup itself isn't particularly complicated. Version matching is.

A vulnerability record can describe affected versions using ranges, boundaries, or ecosystem-specific semantics.
So the analyzer has to answer questions like:

  • Is 1.2.3 affected?
  • Is 1.2.4 affected?
  • Does this version satisfy the affected range? And those questions don't necessarily have identical semantics across ecosystems.

A dependency declared as == 1.2.3 is fundamentally different from ^1.2.3 or >= 1.2.

This is where I think security tooling needs to be conservative. If the analyzer cannot reliably interpret a dependency expression, it shouldn't silently turn an approximation into a security finding.

A false sense of precision is worse than an explicitly unsupported case.

What the analyzer doesn't currently resolve
This is also why the limitations are part of the technical design.

For example, a Maven declaration such as:

> ${jackson.version}
isn't an ordinary literal version. The analyzer would need to resolve the property before it could reliably perform version matching. Gradle introduces similar problems through mechanisms such as version catalogs.
The current implementation intentionally doesn't try to become a complete replacement for Maven, Gradle, npm, or pip dependency resolution.
Instead, it focuses on declarations that can be analyzed reliably within the information available to the plugin.
npm ranges expose the same problem
Consider:
{
"dependencies": {
"some-package": "^1.4.0"
}
}
The declared range doesn't necessarily tell me the exact version installed on the machine. The lockfile may.
That creates an important distinction between declared dependency and resolved dependency.
A more complete npm analysis should be able to follow:
package.json -> declared range -> package-lock.json -> resolved version -> vulnerability matching
The dependency graph is a separate consumer of the same model
The plugin also includes a Dependency Graph view. Architecturally, it made sense to treat it as another consumer of the dependency information already extracted:
Project -> dependency model -> vulnerability analysis / dependency graph
The graph UI uses a bundled copy of Cytoscape.js and runs locally through IntelliJ's embedded browser support.
For Maven, it can inspect .m2/repository. For pip, it can inspect the local installed environment.

The important part was keeping the dependency information and the consumers of that information separate.
Offline doesn't mean complete
An offline architecture provides useful properties:
** * No network dependency during analysis
** * No account or API token required

  • Predictable local execution But it doesn't provide:
  • Real-time advisory updates
  • Complete vulnerability coverage
  • Automatic knowledge of non-visible dependencies A tool that says "no vulnerability found" is only stating: "No matching vulnerability was found in the data and dependency information available to this analyzer." Why IntelliJ? The reason for putting the analysis inside IntelliJ is about context. If I'm looking at build.gradle.kts and a dependency has a known vulnerability, the source declaration is the most useful place to surface that information. Some dependency questions can be answered at the exact place where the dependency is being edited. The resulting architecture Vulnerability data -> Local database -> Vulnerability matching -> IntelliJ inspection Project files -> Dependency extraction -> Dependency model -> Vulnerability matching / Dependency graph What I would work on next
  • Lockfile-aware dependency analysis: Resolving actual installed versions from package-lock.json.
  • Better version resolution: Handling Maven properties, Gradle version catalogs, and indirection.
  • More complete dependency graphs: Representing deeper resolved dependency trees.
  • Better ecosystem-specific semantics: Refining version comparison rules per package manager. What this experiment actually showed me The interesting result wasn't simply that vulnerability detection can work offline. The interesting part was discovering how many assumptions appear once the network is removed:
  • The vulnerability dataset becomes a build artifact.
  • Version resolution becomes an explicit problem.
  • Source locations become part of the analysis model.
  • Unsupported cases must remain explicit instead of guessed. Vulnerability inspection does not inherently require a network request. Once that became a hard constraint, the rest of the implementation became a series of engineering trade-offs. The project is open source: View Dependency Vulnerability Companion on GitHub

Top comments (0)