Snyk’s Java scanning isn’t just about listing your dependencies; it’s a dynamic analysis that traces the exact transitive dependency graph your build tools actually resolve.

Let’s see it in action. Imagine a simple pom.xml for a Maven project:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/1999/xlink"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>my-java-app</artifactId>
    <version>1.0.0</version>

    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.1</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>31.0.1-jre</version>
        </dependency>
    </dependencies>
</project>

When you run snyk test, Snyk doesn’t just look at these two direct dependencies. It invokes Maven (or Gradle) with specific goals to get the resolved dependency tree. For Maven, this often involves something akin to mvn dependency:tree. Snyk then parses this output, which might look like:

[INFO] com.example:my-java-app:jar:1.0.0
[INFO] +- org.apache.logging.log4j:log4j-core:jar:2.17.1:compile
[INFO] |  +- org.apache.logging.log4j:log4j-api:jar:2.17.1:compile
[INFO] |  \- org.apache.commons:commons-lang3:jar:3.12.0:compile
[INFO] \- com.google.guava:guava:jar:31.0.1-jre:compile
[INFO]    +- com.google.guava:failureaccess:jar:1.0.1:compile
[INFO]    +- com.google.code.findbugs:jsr305:jar:3.0.2:compile
[INFO]    \- com.google.errorprone:error_prone_annotations:jar:2.5.1:compile

Snyk then takes this precise tree, including versions of transitive dependencies like log4j-api, commons-lang3, failureaccess, jsr305, and error_prone_annotations, and compares them against its vulnerability database. This is crucial because a vulnerability might exist in a dependency you didn’t explicitly list, but which is pulled in by one that you did.

The problem Snyk’s Java scanning solves is the inherent complexity and opacity of dependency management in modern Java development. Developers often rely on their build tools to resolve the final set of libraries. However, understanding which specific versions of all libraries (direct and transitive) are present, and whether they contain known vulnerabilities, is a manual and error-prone process without a tool like Snyk. Snyk automates this by leveraging the build tool’s own resolution logic, ensuring accuracy.

The core mechanism is this: Snyk acts as an orchestrator. It understands the command-line interfaces and configuration files of Maven (pom.xml, settings.xml) and Gradle (build.gradle, settings.gradle). When you run snyk test or snyk monitor, Snyk first identifies your project type and then executes the appropriate build tool commands to generate a machine-readable representation of the resolved dependency graph. For Maven, it might be mvn -B -s <settings.xml_path> -Dmaven.repo.local=<local_repo_path> dependency:list -DoutputFile=<output_file>, or for Gradle, ./gradlew --build-cache --configure-cache dependencies --configuration runtimeClasspath --scan-dependencies --output-file <output_file>. The -B flag for Maven, for example, ensures it runs in batch mode without interactive prompts, which is vital for CI/CD. Snyk then parses the output of these commands, or directly inspects the .jar files in the build artifact’s dependency directories, to build its internal representation of your project’s runtime classpath. This graph is then cross-referenced with Snyk’s vulnerability intelligence.

The exact levers you control are primarily through your build tool’s configuration. For Maven, this means how you declare dependencies in pom.xml and how your settings.xml configures repositories and proxies. For Gradle, it’s your build.gradle files, particularly dependency declarations and repository configurations. Snyk respects these configurations, including dependency exclusions and version overrides, ensuring that the scan reflects what your application will actually run with. For instance, if you exclude a transitive dependency in your pom.xml like this:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>parent-lib</artifactId>
    <version>1.0.0</version>
    <exclusions>
        <exclusion>
            <groupId>com.example</groupId>
            <artifactId>vulnerable-transitive</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Snyk will see that vulnerable-transitive is not present in the resolved graph and will not report vulnerabilities for it, even if it’s known to be vulnerable when included.

A subtle but powerful aspect of Snyk’s Java scanning is its handling of "dependency conflicts." When multiple versions of the same library are declared (either directly or transitively), the build tool (Maven or Gradle) applies a resolution strategy (e.g., Maven’s "nearest-wins" or Gradle’s latest version). Snyk performs the same resolution logic that your build tool does. If your pom.xml lists log4j-core:2.17.1 and a transitive dependency brings in log4j-core:2.15.0, Maven will typically resolve to 2.17.1. Snyk will then scan 2.17.1. If, however, your build tool explicitly forces an older version through a <dependencyManagement> block or a Gradle resolutionStrategy, Snyk will honor that forced version and scan that specific version, even if a newer one is available in a repository. This ensures Snyk reports vulnerabilities based on the actual runtime artifact, not just what’s declared.

The next hurdle you’ll likely encounter is understanding how Snyk integrates with different package managers and build systems beyond just Maven and Gradle, such as Bazel or even manual classpath scanning for custom builds.

Want structured learning?

Take the full Snyk course →