Security Scanners Worth Running in CI

Last updated 2026-03-31 | SPUNK13 LLC | spunk.bet

Dependency scanners, and the false-positive problem

npm audit is free and already installed, and its output is famously noisy. It flags advisories against the whole dependency tree regardless of whether the vulnerable code path is reachable, which is why a fresh scaffold of a frontend project can report dozens of "high severity" issues that all sit inside a build-time transitive dependency and cannot be triggered by anything a user does. Run it as npm audit --omit=dev --audit-level=high so it reports on what ships, and treat the dev-dependency results as a separate, lower-priority queue.

OSV-Scanner is the better default. It reads your lockfile and queries the OSV database, which aggregates advisories across ecosystems including npm, PyPI, Go, Maven, crates.io and Linux distributions, so one tool covers a polyglot repo and the version ranges are precise. It runs in seconds and produces far fewer duplicate entries than per-ecosystem tools.

Dependabot and Renovate are not scanners, they are the fix half. Renovate is the more configurable of the two: you can group all non-major updates into one weekly pull request, auto-merge patch bumps that pass CI, and set a minimum release age so you are not the first to install a freshly compromised package. Running a scanner without an update mechanism just produces a growing list.

Containers, images and infrastructure code

Trivy is the widest-coverage tool in this space and the one to install first. It scans container images for OS package and language dependency vulnerabilities, scans filesystems and Git repositories, checks Terraform, Kubernetes manifests, Dockerfiles and CloudFormation against misconfiguration rules, and detects secrets. A typical application image scans in well under a minute after the first vulnerability database download.

Syft and Grype are the pairing when you want an SBOM as an artifact: Syft generates the software bill of materials in SPDX or CycloneDX format, Grype scans that SBOM for known vulnerabilities. Producing the SBOM at build time and scanning it later means you can re-check an image you shipped six months ago against advisories published since, without rebuilding it.

Practical gate: fail the build on fixable critical and high findings in the runtime layer, and report everything else. A rule that fails on any CVE at any severity gets bypassed within a fortnight, usually with a blanket ignore file that then hides a real one.

Static analysis of your own code

Everything above scans other people's code. Semgrep scans yours, matching patterns against the syntax tree rather than text, so a rule can say "a SQL string built by concatenation reaching a query call" without caring about formatting or variable names. The community rule packs cover injection, unsafe deserialisation, hardcoded credentials and framework-specific mistakes, and writing a custom rule takes about as long as writing the regex you would otherwise have written badly. It is fast enough to run on every pull request, and running it in diff mode against the changed lines only is what keeps it fast on a large repo.

CodeQL is heavier and deeper. It compiles the codebase into a queryable database and runs taint-tracking queries that follow data from a source such as a request parameter to a sink such as a shell call, across files and function boundaries. That finds genuine multi-hop vulnerabilities Semgrep's local pattern matching misses, at the cost of a build step and scan times that run into many minutes on a large project. Nightly or on merge to main, not on every push. Bandit fills the same niche for Python specifically and runs in seconds, catching subprocess calls with shell=True, yaml.load without a safe loader, and weak hashing.

Secrets, which are the ones that get exploited

gitleaks and trufflehog scan commits for credentials. The distinction matters: gitleaks is regex-and-entropy based and very fast, so it works well as a pre-commit hook that blocks the key before it ever reaches a remote. trufflehog goes further by verifying candidate secrets against the relevant provider API, so it can tell you a found AWS key is live rather than a revoked example, which cuts the false-positive rate dramatically.

Scan full history once, not just the tip. A key committed and removed two years ago is still in the object database, still in every clone, and still valid unless someone rotated it. And if you find one, rotate first and rewrite history second; the rewrite alone fixes nothing because the credential has already been distributed.

Running applications and exposed services

OWASP ZAP is the free dynamic scanner. Its baseline scan spiders a running application and passively checks responses for missing security headers, cookie flags, information disclosure and mixed content, typically in a few minutes, which makes it a reasonable job against a staging deployment on every release. The full active scan sends attack payloads, takes far longer, and must only be pointed at systems you own.

nuclei takes a different approach: a large library of YAML templates describing specific known issues, from exposed .git directories and default admin panels to particular CVEs in named products. It is fast because each template is a targeted check rather than a general crawl, and it is the right tool for asking "is anything on our estate running a known-vulnerable version of X". For the transport layer, testssl.sh enumerates cipher suites, protocol versions, certificate chain problems and known TLS attacks against a host, and ssh-audit does the equivalent for an SSH daemon's key exchange, host key and MAC algorithms.

Keeping the output usable

The failure mode of every programme like this is a wall of findings nobody reads. Four rules keep it alive. Split blocking from reporting: fast scanners with low false-positive rates (secrets, OSV-Scanner, Semgrep diff mode) block the pull request; slow or noisy ones (CodeQL, full ZAP, dev-dependency advisories) write to a dashboard reviewed on a schedule. Scan the diff on pull requests and the whole repository nightly, so the pull request check stays under a minute or two. Require every suppression to carry a reason and an expiry date rather than allowing a bare ignore. And track the age of open findings rather than the count, because the count only tells you how much scanning you turned on, while the age tells you whether anyone is acting on it.

Explore the SPUNK13 network

Visit spunk.bet400+ Free Tools
Dev ToolsCasinoMemesAstrologyScam DBBacklinksEbooksAdvertise