Search this question and you get listicles ranking tools nobody ran. This is the other thing: seventeen scanners pointed at one deliberately broken application, every number copied from a terminal, every command reproducible from the Range.
The single most useful result came out of the DAST chapter, and it decides how you should build a pipeline:
No free tool found both the SQL injection and the reflected XSS. Semgrep found the XSS and missed the SQLi. ZAP found the SQLi and missed the XSS. Two mature, well-configured scanners, one small application, and neither one alone would have told you it was unsafe to ship.
Exactly one tool found both — and it was the paid one. Snyk Code caught the SQLi, both XSS instances, and a hardcoded password three dedicated secrets scanners missed. That is the clearest answer this track produced to "what does commercial SAST buy me", and it is worth reading against the SCA and IaC results below, where the same vendor added almost nothing.
Verified against the Range: gitleaks 8.30.1, TruffleHog, ggshield 1.53.0, Semgrep 1.166.0, Trivy 0.72.0, Grype 0.117.0, Syft 1.51.0, Docker Scout 1.20.3, Checkov 3.2, tfsec 1.28.14, hadolint, Dockle 0.4.15, ZAP stable, Nuclei 3.9.0, and Snyk 1.1306.3 (Code, Open Source and IaC — three separate products). Seventeen in total.
What was under test
One small Express app with four planted flaws — a SQL injection, a reflected XSS, an unauthenticated /debug/config returning AWS credentials, and hardcoded keys — plus the Dockerfile that builds it and the Terraform that would deploy it. Small enough that a careful human finds everything in twenty minutes. That is the point: any gap below is a gap on an easy target.
Secrets — three tools, one credential
The same AWS key pair appears in app/server.js and docker/Dockerfile.
| Result | Checks if key is live | Cross-repo identity | |
|---|---|---|---|
| gitleaks 8.30.1 | 2 findings | no | no |
| TruffleHog | 2 findings (incl. one deleted from HEAD) |
yes (verified: 0) |
no |
| ggshield 1.53.0 | 1 secret, 2 occurrences | yes (Invalid) |
yes (Secret SHA) |
All three missed DB_PASSWORD = 'hunter2' and JWT_SIGNING_KEY = 'dev-secret-do-not-use'. Not one flagged either. High-confidence detectors are what make validity checking work; unstructured secrets are the price, in every tool.
Verdict: gitleaks. Free, offline, fast, and it finds what the others find. Add TruffleHog on a schedule for history and validity. GitGuardian earns its keep only when the same finding arriving twice across many repos is your actual problem — see the org-scale lesson.
SAST — the gap that defines the whole exercise
Semgrep p/javascript + p/secrets 4 findings SQLi at server.js:56 — MISSED
Semgrep --config=auto (206 rules) 7 findings SQLi at server.js:56 — MISSED
Semgrep + 12-line custom taint rule SQLi — FOUND
Snyk Code 9 findings SQLi — FOUND, no custom ruleThe SQLi is db.exec() on a concatenated query string. Semgrep's registry rules do not model sql.js as a SQL sink, so no path is traced — under any ruleset tested. Twelve lines of taint rule naming the sink, and it fires immediately.
Snyk Code found it with nothing configured, and found two more things Semgrep did not:
server.js |
Flaw | Semgrep | Snyk Code |
|---|---|---|---|
| 27 | Hardcoded password hunter2 |
no | yes |
| 56 | SQL injection | no | yes |
| 63 | XSS via the catch block |
no | yes |
| 73 | Reflected XSS | yes | yes |
Line 63 is the sharpest of the three: user input reaches the browser through the error handler, having passed through the sql variable first. Second-order reflection, invisible to pattern matching. And line 27 is a hardcoded password that gitleaks, TruffleHog and GitGuardian all missed — hunter2 has no shape to match, so only a tool reading the assignment target catches it.
The finding here is not "Semgrep is weak". It is that default rules cover common frameworks, and your risk lives in the parts of your stack that are not common — plus the parts nobody on your team has thought to declare a sink. A clean SAST run against a library nobody wrote rules for is not evidence of anything.
Verdict: Semgrep, plus rules you write — then Snyk Code for the sinks you did not think of. The free tier is genuinely strong and the custom rules are where coverage of your code comes from. But this is the one category in the whole track where the commercial tool found real vulnerabilities the free one did not, in the same direction, three times. Full comparison.
SCA — 54 versus 63, and a warning about counting
Same app/package-lock.json, two scanners:
| Findings | Critical | High | |
|---|---|---|---|
| Trivy 0.72.0 | 54 | 3 | 21 |
| Snyk 1.1306.3 | 63 | 2 | 22 |
Trivy's include ejs CVE-2022-29078 (CRITICAL), minimist (CRITICAL), lodash (1 CRITICAL + 4 HIGH), axios (11 HIGH). One command, and it also covered the Dockerfile (7) and Terraform (11) in the same pass.
At CVE level: 51 shared, 3 Trivy-only, 5 Snyk-only. Roughly 90% overlap — so the paid tool's exclusive set here is five CVEs. Two of Trivy's three exclusives are GHSA advisories with no CVE at all, which is a reminder that scanners do not share an identifier space and cross-tool matching is harder than it looks.
The number that reframes the whole category came out of Snyk's own output: upgradable: 63, patchable: 1 — every single finding is fixed by a version bump. The useful question was never 54 versus 63.
Verdict: Trivy to gate, Dependabot to remediate. Trivy is the highest coverage-per-unit-of-setup available free; Dependabot opens the pull requests that actually close these, also free. Buy Snyk for reachability and the audit record, not for five CVEs. Full comparison.
Container images — the numbers that should end raw CVE counts
Identical image, same afternoon:
| Critical | High | Total | |
|---|---|---|---|
| Trivy 0.72.0 | 229 | 1,622 | 7,696 |
| Grype 0.117.0 | 267 | 1,167 | 3,995 |
| Docker Scout 1.20.3 | 28 | 192 | 836 |
836 to 7,696 — a factor of nine — on the same bytes. Different severity vocabularies (Grype has a Negligible band worth 915 findings), different databases, different matching strictness.
A raw CVE count is a fact about your scanner and the date you ran it, not about your image. Never put one in an SLA, a report, or a board slide without naming the tool and the date beside it.
Docker Scout was the most conservative and the most useful: it attributed 136 of its 192 highs to the node:18 base image and quantified the fix — moving to node:26-slim removes 131 highs and 5 criticals. That is a task with an estimate. "7,696 vulnerabilities" is a number people learn to ignore.
Three more container checks, none redundant:
- hadolint (Dockerfile): 3 findings
- trivy config (same Dockerfile): 7 — including
DS-0002root user andDS-0031secrets in ENV ×3 - Dockle (built image):
CIS-DI-0010FATAL for credentials baked into layers, plus nine setuid binaries inherited from the base
Dockle sees what shipped, not what was written — the strongest available argument for a distroless base.
Verdict: Trivy to gate, Scout locally for remediation, Dockle for CIS evidence. Grype as a scheduled second opinion; compare the set difference, never the totals.
IaC — where the migration advice is wrong
| Findings | Flags Action="*" on Resource="*" |
Project status | |
|---|---|---|---|
| Checkov 3.2 | 26 failed / 13 passed | yes | active |
| tfsec 1.28.14 | 14 | yes | frozen since March 2026 |
| trivy config 0.72.0 | 11 | no — no such check | active |
| Snyk IaC 1.1306.3 | 11 | yes (medium) | active |
| Terrascan | — | — | archived |
Aqua acquired tfsec and points users at Trivy. Follow that advice exactly and you lose the wildcard-IAM finding: AWS-0057 is not in Trivy's ruleset at all — confirmed with --include-non-failures, it is never evaluated. Full administrative access goes unreported, and every build still passes.
Snyk IaC and trivy config both returned 11 — and only 9 were the same findings. Snyk alone flags the IAM policy and S3 MFA delete; Trivy alone flags CMK encryption and the missing security-group description. Equal totals are not agreement, which is the strongest argument in this article for comparing sets rather than counts.
Then there is severity. The identical unrestricted-egress rule:
tfsec CRITICAL trivy config CRITICAL Snyk IaC LOWSnyk reported zero criticals on this Terraform. So a single policy — "block on CRITICAL" — blocks twice, once, or never, depending only on which tool you installed. Severity is a vendor's calibration, not a property of your infrastructure.
Verdict: Checkov owns the IaC gate. Broadest ruleset, actively developed, and it catches what the others do not. Run trivy config alongside for its container and secret coverage — just do not let it be the only thing looking at your Terraform. Migration detail · Snyk comparison.
DAST — the result that changes the architecture
| ZAP baseline | ZAP full (active) | Nuclei (community) | Nuclei (custom) | |
|---|---|---|---|---|
| URLs reached | 4 | 7 | 1 | as supplied |
| SQL injection | — | found | — | not tested |
| Reflected XSS | — | PASSED (never crawled /greet) |
— | found |
/debug/config leak |
— | missed | — | found |
| Severity ceiling | 0 FAIL / 9 WARN | — | all [info] |
critical + high |
ZAP's passive baseline reported zero failures on an app with a working SQL injection. Its active scan found the SQLi from a single ' returning a 500 — then passed every XSS rule, because nothing links to /greet and the spider only ever reached four URLs.
Nuclei's thousands of community templates produced nothing but [info] — they encode known issues, and your application is not one. Two templates written in ten minutes found the XSS and the credential leak at critical and high.
Verdict: ZAP baseline on every pull request, ZAP active on a schedule, Nuclei with your own templates in the gate. And feed ZAP an OpenAPI spec or URL list — an unfed scan covers a fraction of your app and never tells you how small that fraction was.
The stack this actually argues for
Free tools, one of each job, chosen on measured coverage:
# secrets gitleaks pre-commit + CI (TruffleHog scheduled)
# SAST semgrep + custom taint rules for your own sinks
# SCA trivy fs + Dependabot for upgrade PRs
# IaC checkov gate (trivy config alongside)
# Dockerfile hadolint + trivy config
# image trivy image gate (grype scheduled, scout local)
# CIS dockle --exit-level fatal
# SBOM syft retain as evidence, attach to image
# DAST zap-baseline PR zap-full + nuclei (own templates) scheduledTwo rules make it hold together:
Gate on one tool per job. Two blocking scanners for the same artefact means two databases, two ignore files, and an argument about which is right at 5pm on a Friday. Everything else runs scheduled and reports.
Threshold on --ignore-unfixed / --only-fixed. A vulnerability with no available patch is not a task. Blocking on it teaches people to bypass the gate, and a bypassed gate is worth less than no gate.
What the free tools genuinely do not do
Being straight about this matters more than the rankings:
- Reachability. No free scanner knows whether your code calls the vulnerable function. A CVE in a dead path and one in your request handler look identical. This is the real commercial differentiator.
- Incident state across repositories. Owner, status, history, "we already rotated this". Console output is not an audit record — and for APRA CPS 234 the record is the artefact, not the scan.
- Triage at volume. 7,696 findings with no dedup and no ownership is not a queue anyone works.
Below roughly twenty repositories, the free stack above is not a compromise — it is the correct answer, and the money is better spent on someone to act on the output. Above that, the platforms are selling triage and evidence, not detection. Buy on that basis, and make the vendor run against your Range before you sign.
Scanners not in this comparison
Named, so the omission is not mistaken for a verdict: CodeQL, SonarQube, Burp Suite, Black Duck and Dependabot were not run here. CodeQL and Dependabot need a public repository and a real pull-request cycle; the rest are commercial products without a usable free tier for this. Every table above reflects only what was actually executed. Those tools get their own lessons, with their own runs.
Snyk was measured too — Code, Open Source and IaC, all against the same three artefacts, with the results folded into the sections above rather than kept separate. Note that Snyk Code is disabled by default per organisation and returns a 403 (SNYK-CODE-0005) until switched on in the web UI, which is easy to mistake for an authentication failure.
What you learned
- No free tool found both the SQLi and the XSS. Semgrep found one, ZAP the other. One scanner is not a strategy.
- Snyk Code found both, plus a third flaw nobody else saw — an XSS reaching the browser through a
catchblock. The one category where paying was clearly justified. - 836 vs 3,995 vs 7,696 CVEs on identical bytes. Never publish a raw count without the tool name and the date.
- Semgrep's defaults missed the SQL injection because they do not model
sql.js. Custom rules are where coverage of your stack comes from. - Trivy has no IAM wildcard check. Migrating from tfsec on the vendor's advice silently drops a full-admin finding.
- Snyk IaC and Trivy both returned 11, sharing only 9. Equal totals are not agreement.
- The same egress rule is CRITICAL to two tools and LOW to a third. A "block on CRITICAL" policy blocks twice, once or never depending on the vendor.
- tfsec is frozen, Terrascan is archived. Check
last_pushon every scanner you gate on, quarterly. - All three secrets tools missed
hunter2— but Snyk Code caught it, by reading the variable name rather than the value. Structure is what detectors need; semantics is what beats them. - ZAP passively reported zero failures on an app with a working SQLi, and Nuclei's community templates were all
[info]. Feed your scanners your endpoints. - Gate on one tool per job, threshold on fixable findings only, run the rest scheduled.
- The free stack is the right answer at small scale. Platforms sell triage, reachability and evidence — buy on that, not on detection.