HackBlue

Tools · Secrets and dependency scanning · 04 / 06

Writing a Semgrep rule for your codebase, with tests

Write a Semgrep rule for a mistake specific to your own code, add ruleid and ok test annotations, and run semgrep --test so the rule stays correct as it evolves.

12 min read · practice only with authorization

When a custom rule pays off

Public rules cover generic mistakes. Teams also have local ones: an internal HTTP helper that must always receive a timeout, or a templating function that must not receive raw user input. After the second incident caused by the same pattern, a rule is cheaper than another reminder in the wiki.

The rule

This rule flags calls to the team’s fetch_json helper that omit the timeout keyword. pattern matches the call, pattern-not removes calls that pass timeout. Metavariables such as $URL match any expression.

Snippet
rules:
  - id: hb-fetch-json-needs-timeout
    languages: [python]
    severity: WARNING
    message: fetch_json without timeout= can hang a worker; pass timeout=5 or less
    patterns:
      - pattern: hbclient.fetch_json($URL, ...)
      - pattern-not: hbclient.fetch_json($URL, ..., timeout=$T, ...)

Test annotations

A test file sits next to the rule with the same base name. A ruleid comment marks the line that must match; ok marks a line that must not. semgrep --test runs every rule against its test file and reports misses in both directions.

Snippet
# rules/hb-fetch-json-needs-timeout.py
import hbclient

# ruleid: hb-fetch-json-needs-timeout
hbclient.fetch_json(url)

# ok: hb-fetch-json-needs-timeout
hbclient.fetch_json(url, timeout=3)

$ semgrep --test rules/
1/1: ✓ All tests passed

Worked change: a false positive found later

A month later a developer wraps the helper in functools.partial with a timeout already bound. The rule flags the partial’s later calls. The fix is a new ok case in the test file first, watching the test fail, and then a pattern-not-inside clause. The test file now documents that edge for whoever edits the rule next.

Snippet
      - pattern-not-inside: |
          $F = functools.partial(hbclient.fetch_json, ..., timeout=$T, ...)
          ...

# ok: hb-fetch-json-needs-timeout
get = functools.partial(hbclient.fetch_json, timeout=2)
get(url)

Diagram

Semgrep pattern operators used here

Each operator narrows or widens the match.

  • pattern

    Code that must match, with metavariables for any expression.

  • pattern-not

    A shape that removes a match when it also fits.

  • pattern-not-inside

    Removes matches located inside a surrounding shape.

  • ruleid / ok

    Test comments that say which lines must and must not match.

Check yourself

1. What does $URL mean in a Semgrep pattern?

2. Which annotation marks a line that must not be flagged?

3. A new false positive appears. What is the first edit?