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