“Add security scanning to the pipeline” is not one task. It is at least six, they find different classes of problem, and running them all as blocking gates on every pull request is the fastest way to get every one of them disabled.
Here is the map: what each layer covers, what to use, and where it belongs.
The six layers
| Layer | Finds | Typical open-source tool |
|---|---|---|
| SAST | bugs in your own code | Semgrep, CodeQL |
| SCA | vulnerable dependencies | Trivy, Grype, OWASP Dependency-Check |
| Secrets | credentials in the repo | Gitleaks, TruffleHog |
| IaC | misconfigured infrastructure | Checkov, Trivy, KICS |
| Container | vulnerable base images | Trivy, Grype |
| Licence | incompatible licences | Trivy, Syft + Grant |
Most teams do SCA and container scanning, because those are easiest, and stop. That leaves your own code and your secrets unscanned, which is where the interesting incidents come from.
SAST: your code
Semgrep is where to start. Rules are patterns that look like the code they match, so you can read a rule and know what it does:
rules:
- id: subprocess-shell-true
pattern: subprocess.$FUNC(..., shell=True, ...)
message: shell=True with interpolated input allows command injection
languages: [python]
severity: ERROR
semgrep --config=auto --error .
--config=auto pulls curated rulesets for the languages it detects. Start there, then add rules for your own footguns. The internal helper that must never be called with user input, the deprecated crypto wrapper, the direct DB access that should go through the repository layer.
That last category is where SAST earns its cost. Generic rules find generic bugs, and your generic bugs are mostly already caught by your linter and type checker. A rule encoding your architecture’s rules catches things no off-the-shelf ruleset can.
CodeQL is more powerful and considerably slower. It builds a queryable database of your code and does real dataflow analysis, so it finds multi-step taint paths Semgrep misses. It is free for public repositories on GitHub. Run it nightly on main rather than per-PR; it is too slow for the inner loop.
Language-specific tools are still worth adding since they encode ecosystem knowledge: gosec for Go, bandit for Python, eslint-plugin-security for JavaScript.
SCA: your dependencies
Statistically, most of your attack surface is code you did not write.
trivy fs --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed .
Trivy reads lockfiles directly, so it understands the resolved dependency tree rather than the declared ranges. That distinction matters. The vulnerability is in what you install, not what you asked for.
The critical question SCA tools mostly cannot answer is reachability: is the vulnerable function ever called? A CVE in a code path your application never executes is not the same risk as one in your request handler, but almost every tool reports them identically. Until reachability analysis is mainstream, this is human triage, and it is why “zero CVEs” is the wrong target. Triaged and justified is the right one.
Turn on automated dependency updates (Dependabot or Renovate) and treat that as the primary remediation path. Scanning tells you about the problem; automated updates fix it while nobody is looking.
Secrets: the one to do first
If you run exactly one scanner, run this one. A leaked credential is not a theoretical vulnerability. It is an active incident with a known exploitation path.
gitleaks detect --source . --redact
Two separate jobs here, and both are needed:
Pre-commit, so secrets never enter history:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
Full history scan, once, because something is already in there:
gitleaks detect --source . --log-opts="--all --full-history"
Expect hits. When you find one, rotating the credential is the entire remediation. Rewriting history is optional cleanup and does not un-leak anything. Anyone who cloned the repository already has it, and if the repository was ever public, assume it was scraped within minutes.
IaC and containers
Covered in detail in the Checkov and Trivy posts. The short version: scan IaC on pull request, scan the Terraform plan in the apply pipeline, scan images at build before push, and run the Trivy Operator in-cluster so you also know about what is already deployed.
Where each one runs
This is what decides whether the programme survives. Match the gate to the feedback loop:
Pre-commit (seconds). Secrets only. Fast, and a secret caught here never needs rotating.
Pull request (under five minutes). Semgrep on changed files, SCA on lockfile changes, IaC on changed files. Blocking, but only on new findings. Use baselines. semgrep --baseline-commit is the equivalent of Checkov’s baseline file.
Main branch (minutes). Full-repo Semgrep, full SCA, container scan of the built image, SBOM generation. Blocking on the release path.
Nightly (as long as it takes). CodeQL, full-history secret scan, unfixed-CVE reporting, licence audit. Non-blocking, reported to the platform team.
The organising principle: fail the build only on findings that are new and that the person in front of you can fix. Everything else is a report. Violate that and people learn to click through your gates, and a gate people click through is worse than no gate, because it produces a false sense of coverage.
The noise problem
Every one of these tools finds hundreds of things on first run. How you handle that determines whether anyone trusts the results in six months.
Baseline everything on day one. Accept current state, block regressions. Burn down deliberately.
Suppress with a reason and an expiry. Every ignore entry needs a written justification and a date it comes back for review. Suppressions without expiry are permanent by accident.
Fail on HIGH and CRITICAL only. MEDIUM and below go to a report. If everything is a build failure, nothing is.
Route findings to SARIF. GitHub renders SARIF inline on the diff. A finding on line 42 of the file being reviewed gets fixed; the same finding in a CI log does not.
Own the platform’s findings. If the base image you publish carries forty CVEs, that is your ticket, not forty teams’ tickets.
A reasonable starting point
If you are beginning from nothing, in this order:
- Gitleaks pre-commit and full history scan. Highest severity, lowest effort, and you will find something.
- Trivy image scan in the build pipeline with
--ignore-unfixed --severity HIGH,CRITICAL. - Checkov or Trivy config on IaC pull requests, with a baseline.
- Semgrep with
--config=auto, non-blocking for a month, then blocking on new findings. - Renovate or Dependabot, so remediation is mostly automatic.
- Trivy Operator in-cluster, to see what is actually running.
- CodeQL nightly, once the rest is quiet enough that people still read the output.
Each step is a week or two of work, and every one of them delivers value before the next begins. The temptation is to buy a platform that claims to do all six, which is fine, but it does not remove the triage work, and the triage work is the whole job.