Code ScanningRules

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-inside
  • pattern-regex, pattern-not-regex
  • Metavariables ($VAR, $...ARGS)
  • metavariable-pattern, metavariable-regex, metavariable-comparison
  • focus-metavariable
  • fix (autofix support)
  • Taint analysis (mode: taint with pattern-sources, pattern-sinks, pattern-sanitizers, pattern-propagators) — see Taint analysis
  • Native analyzers (mode: analysis with analyzer:) for checks too context-dependent for patterns — see Analysis mode
  • Supply chain rules (r2c-internal-project-depends-on)
  • options including interfile: true for cross-file analysis and implicit_ellipsis: false for exact record matching (below)
  • min-version / max-version for 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.html

Conventions:

  • 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:2021 and/or A0X: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 into A01:2025, renamed Vulnerable/Outdated Components to Software Supply Chain Failures (A03:2025), and added A10:2025 Mishandling 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 the cwe/owasp entries by the output layer (SARIF, GitLab), never hand-written. Most rules therefore need no references at all.

Once you've written a rule, test it.

On this page