Writing Custom Semgrep Rules for Your Codebase
Generic SAST rulesets are good at generic bugs. What they cannot know is that your organisation has a blessed way to build queries, a deprecated auth helper nobody should call, and an internal template function that must always escape its input. Those are the flaws that actually bite mature teams, and catching them means writing rules that encode your own conventions. Semgrep has become the common tool for this because its rules read almost like the code they match.
Semgrep matches patterns against the abstract syntax tree rather than raw text, so it understands code structure — whitespace, variable names, and formatting don't matter, but the shape of an expression does. A rule is YAML: a pattern to find, a message to show, and a severity. The pattern language mirrors the target language, which means you can often write a rule by pasting the bad code and generalising the parts that vary.
The building blocks
- Metavariables (
$X) match any expression and bind it, so the same value can be referred to across the pattern — the equivalent of a wildcard that remembers what it matched. pattern-eithermatches any of several patterns;pattern-notexcludes the safe cases so you don't flag correct usage.pattern-insidescopes a match to a surrounding context — only inside a request handler, only within a certain class.- Taint mode declares
pattern-sources,pattern-sinks, andpattern-sanitizers, giving you full source-to-sink data-flow rules tuned to your own wrappers.
- id: no-raw-query-builder
pattern: db.rawQuery("..." + $INPUT)
message: Use the parameterised queryBuilder; rawQuery with concatenation is SQLi-prone.
languages: [java]
severity: ERROR
Rules worth writing first
Start where you already know the pain. If a past incident traced to a specific misuse, write the rule that would have caught it — that is the highest-confidence, most-defensible rule you will ever ship. Then encode your paved road: flag anyone bypassing the sanctioned auth check, the approved crypto wrapper, or the mandatory output encoder. These rules don't just find bugs; they enforce the architecture, keeping the safe path the default path as the codebase and the team both grow.
Test your rules like code
A rule that never fires is worse than useless — it manufactures false confidence. Semgrep supports test files that annotate exactly which lines should and should not match, and those tests belong in version control beside the rule. Write a fixture with both a vulnerable case and a safe case, and confirm the rule catches the first and stays silent on the second. When you tune the rule later, the tests tell you whether you fixed the noise or broke the detection.
From one repo to the whole org
Custom rules gain their power at scale. A rule proven in one service can roll out across every repository, turning a single team's hard-won lesson into an organisation-wide guardrail. Keep them in a shared, reviewed rule repository, version them, and treat a new rule with the same care as a code change — because a sloppy rule that floods the pipeline with noise does the same damage as any other adoption-killer.
The generic scanner tells you what every codebase gets wrong. A custom rule tells you what yours gets wrong — and turns each discovered weakness into a permanent tripwire, so the same flaw can never quietly reappear in code no one thought to check.
- Take a past incident that traced to a specific code pattern and check whether a rule now exists that would catch it — that is the highest-confidence rule you will ever ship.
- Confirm your custom rules ship with test fixtures (a vulnerable case and a safe case), or a rule that quietly stopped matching is manufacturing false confidence.
Writing one rule is approachable; building and maintaining a tested, org-wide ruleset around your architecture is what our assessment runs for you.