Tools · Secrets and dependency scanning · 03 / 06
Semgrep in CI: scan only new code and triage the first findings
Run Semgrep with a curated ruleset in a pull-request job, use a baseline commit so only new findings fail the build, and triage the first results with the team that owns the code.
Where Semgrep fits
Semgrep matches code by structure rather than text. A rule written as a code pattern finds calls with any variable names and formatting. Rulesets such as p/owasp-top-ten or a language pack give a starting point; the team trims them to rules it is willing to act on.
A pull-request job that fails only on new findings
--baseline-commit compares the scan against the merge base and reports only findings introduced by the branch. That lets a team adopt a scanner on an old codebase without fixing every historic result first. --error makes Semgrep exit non-zero when findings remain.
semgrep scan --config p/python --config rules/ \ --baseline-commit "$(git merge-base origin/main HEAD)" \ --error --sarif -o semgrep.sarif # local, readable output semgrep scan --config p/python app/
A finding as a developer sees it
The rule id, message, and exact lines make the result reviewable without opening a separate tool.
app/reports.py
python.lang.security.audit.formatted-sql-query.formatted-sql-query
Detected possible formatted SQL query. Use parameterized queries instead.
41┆ cur.execute(f"SELECT * FROM invoices WHERE team = '{team}'")
Ran 312 rules on 84 files: 1 finding.Worked triage week
The first full run reports 63 historic findings. The team sorts them by rule: 40 come from one rule that flags every subprocess call, including ones with fixed arguments. That rule is removed from the CI config and kept for a quarterly audit instead. The SQL finding above is real and fixed with a parameter.
When a finding is accepted, the developer adds a nosemgrep comment naming the rule and a reason, so the exception is visible in code review.
cur.execute("SELECT * FROM invoices WHERE team = %s", (team,))
subprocess.run(["git", "rev-parse", "HEAD"]) # nosemgrep: dangerous-subprocess-use -- fixed argvDiagram
Adopting Semgrep without a backlog wall
- Pick rules
Start with a pack you will act on.
--config p/python --config rules/
- Baseline
Report only what the branch introduced.
--baseline-commit "$(git merge-base origin/main HEAD)"
- Gate
Fail the job on new findings.
--error --sarif -o semgrep.sarif
- Tune
Drop noisy rules; document accepted lines.
# nosemgrep: rule-id -- reason
Check yourself
1. Which option makes a pull-request scan ignore findings that already existed on main?
2. One rule produces 40 findings that are all fixed-argument subprocess calls. What is a reasonable change?
3. How is an accepted finding best recorded?