HackBlue

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.

11 min read · practice only with authorization

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.

Snippet
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.

Snippet
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.

Snippet
cur.execute("SELECT * FROM invoices WHERE team = %s", (team,))

subprocess.run(["git", "rev-parse", "HEAD"])  # nosemgrep: dangerous-subprocess-use -- fixed argv

Diagram

Adopting Semgrep without a backlog wall

  1. Pick rules

    Start with a pack you will act on.

    Flags
    --config p/python --config rules/
  2. Baseline

    Report only what the branch introduced.

    Flag
    --baseline-commit "$(git merge-base origin/main HEAD)"
  3. Gate

    Fail the job on new findings.

    Flags
    --error --sarif -o semgrep.sarif
  4. Tune

    Drop noisy rules; document accepted lines.

    Comment
    # 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?