Writing Rules
The xgrep rule format and the rule features it supports.
Writing Rules
xgrep uses the Semgrep YAML rule format:
rules:
- id: my-rule
pattern: eval(...)
message: Avoid using eval()
severity: WARNING
languages: [python]Supported rule features include:
pattern,patterns,pattern-either,pattern-not,pattern-inside,pattern-not-insidepattern-regex,pattern-not-regex- Metavariables (
$VAR,$...ARGS) metavariable-pattern,metavariable-regex,metavariable-comparisonfocus-metavariablefix(autofix support)- Taint analysis (
mode: taintwithpattern-sources,pattern-sinks,pattern-sanitizers,pattern-propagators) — see Taint analysis - Native analyzers (
mode: analysiswithanalyzer:) for checks too context-dependent for patterns — see Analysis mode - Supply chain rules (
r2c-internal-project-depends-on) optionsincludinginterfile: truefor cross-file analysis andimplicit_ellipsis: falsefor exact record matching (below)min-version/max-versionfor engine version constraints
For the precise semantics of each operator and metavariable form, see the Syntax reference. For the upstream specification, see the Semgrep rule syntax documentation.
Matching a record exactly
A pattern that names some of an object literal's properties matches a literal that carries others as well, at every position the literal can be written in:
pattern: http.get($URL, { host: $H })matches http.get(u, { host: h, port: 1, timeout: 5 }) — the properties the
pattern does not name are not a mismatch. This is what makes a pattern usable
against a real options object, whose shape the rule author does not control.
When a rule needs the opposite reading — the literal carries the named
properties and nothing else — set implicit_ellipsis: false:
rules:
- id: exact-options
languages: [javascript]
severity: MEDIUM
message: exactly these options
options:
implicit_ellipsis: false
pattern: f({ host: $H })That rule matches f({ host: h }) and not f({ host: h, port: 1 }).
The option applies to record patterns wherever they appear, including inside
pattern-not. Note the direction there: a strict pattern-not matches fewer
literals, so it suppresses fewer findings and the rule reports more, not less.
It does not affect the other lists a pattern can leave partly unspecified — an
implements clause or a set of annotations still match as a subset.
Version-gating a rule on a dependency
r2c-internal-project-depends-on reads the project's manifests (package.json,
go.mod, requirements.txt, pom.xml, Cargo.toml, composer.json,
*.csproj) and matches when the named package is declared, optionally within a
version constraint:
patterns:
- pattern: yaml.load($X)
- r2c-internal-project-depends-on:
namespace: npm
package: js-yaml
version: '< 4'Nesting the same clause under pattern-not inverts it — the rule matches unless
the manifest establishes the claim:
patterns:
- pattern: yaml.load($X)
- pattern-not:
r2c-internal-project-depends-on:
namespace: npm
package: js-yaml
version: '>= 4'The two are not mirror images, and the difference matters when you are gating a
security rule. The positive form fires only on proof that the project is
affected, so it stays silent when no manifest is in scan scope, when the
package is reached transitively, or when the declared version is a tag like
latest that names no version. The negated form fires in all of those cases and
goes quiet only on proof that the project is safe. For a vulnerability rule,
prefer the negated form: absence of evidence should not read as evidence of
safety.
A version is compared against the lower bound of the declared range, so ^5.2.0
and ~5.2.0 both compare as 5.2.0. A declaration that names no version at all
matches no constraint.
Security metadata
Security rules (metadata.category: security or secrets) carry standardized
classification metadata that flows into SARIF and GitLab reports:
metadata:
category: security
cwe:
- 'CWE-918: Server-Side Request Forgery (SSRF)'
owasp:
- A10:2021
- A01:2025
references:
- https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.htmlConventions:
cwe—"CWE-N: Name"using the exact name from the MITRE CWE catalog. Use concrete weakness IDs, never category or view IDs (e.g. CWE-16, CWE-275, CWE-730 are categories, not weaknesses).owasp— OWASP Top 10 categories as a bare code + edition with no descriptive text:A0X:2021and/orA0X:2025(the human-readable name is added by the output layer, so storing it would only risk drift). Tag both editions: derive the 2021 category from the CWE via the official Top 10:2021 CWE mapping, then add its 2025 equivalent — note 2025 renumbered the list, folded SSRF intoA01:2025, renamed Vulnerable/Outdated Components to Software Supply Chain Failures (A03:2025), and addedA10:2025Mishandling of Exceptional Conditions. If the CWE is unmapped, use its nearest mapped ancestor in the CWE hierarchy. CWEs with no applicable category (denial-of-service, memory-safety, integer overflow, race conditions) omit the key — do not force-fit a category.references— curated reading only, from official sources:owasp.org(including cheat sheets), NIST, IETF/RFC, W3C. No vendor or blog links. Do not add links that are derivable from the tags above — CWE definition and OWASP Top 10 category links are generated from thecwe/owaspentries by the output layer (SARIF, GitLab), never hand-written. Most rules therefore need noreferencesat all.
Once you've written a rule, test it.