Overview
Most Java vulnerabilities ship in dependencies, not first-party code — Log4Shell made that permanent institutional memory — so security testing starts with the dependency tree and works inward: SCA for known CVEs, SAST for insecure code patterns, DAST against the running app, fuzzing for the parsers, and secrets scanning over the repo. None of it matters unless it runs in the build; the tooling below all has Maven/Gradle or CI-native integration.
Key points
- SCA (software composition analysis): OWASP dependency-check matches the full transitive tree against the NVD and other CVE sources (free; needs an NVD API key for sane update speed); Snyk adds a commercial curated DB, fix PRs and reachability hints. Transitive dependencies are the point — your directs are the minority of the tree.
- SAST: SpotBugs + the FindSecBugs plugin covers the classic JVM sinks (injection, XXE, weak crypto, deserialisation); Semgrep runs fast, writable pattern rules well-suited to PR diffs; SonarQube folds security rules and hotspot review into the quality gate.
- DAST: OWASP ZAP probes the running application (baseline passive scan vs full active scan) — catches misconfiguration and behaviour static tools cannot see; automate via the ZAP GitHub Actions / container against an ephemeral deployment.
- Fuzzing on the JVM: Jazzer (libFuzzer-based) does coverage-guided fuzzing of Java targets and powers Java in OSS-Fuzz; point it at anything parsing untrusted bytes.
- Secrets scanning: gitleaks/trufflehog in pre-commit and CI, GitHub secret scanning with push protection on the platform side; scan history, not just HEAD.
- SBOM: generate CycloneDX from Maven/Gradle so consumers and later audits can answer "are we affected?" without re-resolving the tree.
- Framework note: exercise DAST against real framework config — Spring Security filter
chains, Quarkus and Micronaut security annotations are exactly the layer ZAP validates
and SAST mostly cannot; unit-test authorisation rules too (e.g. Spring Security's
@WithMockUser/MockMvc support).
Details
Layer by layer
| Layer | Question answered | Tools | When it runs |
|---|---|---|---|
| SCA | Known-vulnerable dependencies? | dependency-check, Snyk, gradle dependencies-based audit |
Every build + scheduled (new CVEs hit old code) |
| SAST | Insecure first-party patterns? | SpotBugs+FindSecBugs, Semgrep, SonarQube | PR / merge |
| DAST | Is the running app exploitable? | OWASP ZAP (baseline in PR, full scan nightly) | Against ephemeral/staging deploys |
| Fuzzing | Do parsers crash on hostile input? | Jazzer, OSS-Fuzz for OSS projects | Continuous or nightly jobs |
| Secrets | Credentials in the repo? | gitleaks, trufflehog, GitHub push protection | Pre-commit + CI + history audits |
Build & CI wiring
- Maven:
org.owasp:dependency-check-mavenbound toverifywithfailBuildOnCVSS(e.g. 7.0);com.github.spotbugs:spotbugs-maven-pluginwith thefindsecbugs-plugindependency;cyclonedx-maven-pluginfor the SBOM. - Gradle:
org.owasp.dependencycheck,com.github.spotbugs,org.cyclonedx.bomplugins — same knobs. - Use a suppression file (
dependency-check-suppressions.xml, reviewed in PRs) for false positives; an unsuppressed red build that everyone ignores trains the team to ignore red builds. - Split fast checks (SCA cache-hit, Semgrep, gitleaks) into the PR gate; slow ones (full ZAP active scan, long fuzz runs, full NVD refresh) into scheduled pipelines.
- Rescan on a schedule, not only on change: yesterday's clean build is vulnerable the day a new CVE lands on an unchanged dependency.
Examples
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>${depcheck.version}</version>
<configuration>
<failBuildOnCVSS>7.0</failBuildOnCVSS>
<suppressionFiles>
<suppressionFile>dependency-check-suppressions.xml</suppressionFile>
</suppressionFiles>
<nvdApiKeyEnvironmentVariable>NVD_API_KEY</nvdApiKeyEnvironmentVariable>
</configuration>
<executions>
<execution><goals><goal>check</goal></goals></execution>
</executions>
</plugin>
// Jazzer fuzz target — feed hostile bytes to a parser
public class InvoiceParserFuzzTest {
@FuzzTest
void parseNeverThrowsUnexpected(FuzzedDataProvider data) {
try {
InvoiceParser.parse(data.consumeRemainingAsBytes());
} catch (InvalidInvoiceException expected) {
// declared failure mode is fine; anything else is a finding
}
}
}
Related
- Testing in Java — strategies & tooling map — parent map of the strategy portfolio.
- AppSec fundamentals — the vulnerability classes these tools hunt.
- Java security — the platform's security model and libraries under test.
- Build ecosystem — the Maven/Gradle machinery this all bolts onto.