Where rules live
Open Code Security → Rules Catalog (the SAST Rules page), then the Custom Rules tab → New rule. The editor has:| Field | Set in | Notes |
|---|---|---|
| Name | UI text field | Display label in the Custom Rules list. |
| Description | UI text field | Optional free-form context, shown in the catalog. |
| Primary language | UI dropdown | One of python, javascript, typescript, java, go, csharp, ruby, php, kotlin, swift, rust, scala, bash, terraform, dockerfile, generic. A catalog display label — the YAML body’s languages: array is what the scanner actually uses (see Anatomy). |
| Display severity | UI dropdown | One of critical, high, medium, low, info. A display label for the Custom Rules list — it is not sent to the scanner. The severity that drives findings (and the quality gate) is the rule-file severity: inside the YAML body (see Anatomy). |
| Configure for | UI dropdown (page header) | Organization (default) = fires on every repo’s CI scan. <specific repo> = fires only when scanning that repo. See Scoping: org vs repo below. |
| Enabled (fires on scans) | UI checkbox | Unchecked = rule stays saved but doesn’t ship to scans. |
| Rule body (YAML) | Code editor | The actual rule body — id, message, pattern…, etc. Validated server-side; see Anatomy. |
GET /api/ci/sast-ruleset.yaml?repo=<repo-name> (the repo name, which the backend resolves to a repository ID), which returns every enabled rule the org has, including:
- The catalog rule packs (OWASP, CWE, AI/LLM, TigerGate’s internal catalog).
- All custom rules where
repository_id IS NULL(org-scoped). - All custom rules where
repository_id = <this repo>(repo-scoped).
--config — there’s nothing to commit to your repo for these rules.
Anatomy of a rule
The YAML body is what you type in the editor. It uses TigerGate’s pattern rule syntax (AST-aware). Example:| Field | Required | What it does |
|---|---|---|
id | Yes | Unique within scope. 1–149 chars: letters, digits, _, -, .. |
languages | Yes | Non-empty array of language identifiers. The validator accepts the full supported set — broader than the common list in the UI dropdown above, so identifiers like c, cpp, hcl, solidity, html, and json are also valid — but rejects any identifier it doesn’t recognize. |
severity | Yes | Rule-file severity: ERROR / WARNING / INFO / INVENTORY / EXPERIMENT. This is what the scanner reads and what drives findings. The Display severity dropdown (critical / high / …) is a separate catalog-display label only — it is not sent to the scanner. |
message | Yes | Shown on every finding. First sentence = title; rest = body. Make it actionable. |
| One pattern form | Yes | pattern, patterns, pattern-either, pattern-regex, or a taint definition (sources/sinks). |
paths.include / paths.exclude | optional | Glob patterns relative to the repo root. |
metadata | optional | Free-form. cwe, owasp, category, references show in the dashboard finding card. |
fix | optional | Auto-fix template. Triggers the “Apply suggestion” button on PRs. |
Pattern syntax
The pattern is written in the syntax of the target language — a Python pattern looks like Python, a Go pattern looks like Go.pattern — match one expression
eval(req.body.code), eval(form_data), eval(x) — anything passed as an argument to eval.
patterns — AND together
All sub-patterns must match.
requests.get that isn’t passing a hardcoded https:// URL, inside a function definition.”
pattern-either — OR together
Any one sub-pattern matches.
pattern-not — exclude a sub-match
Used inside patterns:.
pattern-inside / pattern-not-inside — scope
Match only when the code is (or isn’t) nested inside another pattern.
pattern-regex — fallback to regex
For things the parser can’t see (config files, raw strings). Less accurate — prefer structural patterns.
Metavariables
Capital-prefixed identifiers starting with$ capture anything of the matching shape:
| Form | Captures |
|---|---|
$X | A single expression (name, call, literal, …) |
$...ARGS | Zero or more arguments / statements |
... | ”anything goes here” inside the AST shape |
$"...regex..." | A string literal matching the regex |
if x == x:.
Constrain further with metavariable-pattern / metavariable-regex:
Taint mode (source → sink)
For data-flow vulnerabilities — SQLi, command injection, SSRF — match a flow, not a single call site.pattern: rule but slower — only use it for true data-flow bugs.
Path scoping
.gitignore. include is union; exclude runs after include. Tests are the most common false-positive source — exclude them by default unless the rule is specifically about test code.
Auto-fix template
If the fix is mechanical, add afix: block. The PR review surface renders it as a one-click commit suggestion:
fix-regex:
fix: is provided, TigerGate’s LLM still tries to suggest one at PR review for known remediation patterns — see AI Code Review.
Worked examples — per language
Python — pickle deserialization (taint)
UI: language =python, severity = critical. YAML body:
TypeScript / JavaScript — forbidden child_process.exec
UI: language = typescript, severity = high. YAML body:
Java — Spring @Value reading a secret from properties
UI: language = java, severity = medium. YAML body:
Go — SQL injection via string concat
UI: language =go, severity = critical. YAML body:
Terraform — public S3 bucket
UI: language =terraform, severity = high. YAML body:
Dockerfile — running as root
UI: language =dockerfile, severity = medium. YAML body:
Generic (regex) — hardcoded Stripe key
UI: language =generic, severity = critical. YAML body:
Scoping: org vs repo
Custom rules can be scoped at two levels — set via the Configure for dropdown at the top of the SAST Rules page:| Scope | Behavior | When to use |
|---|---|---|
| Organization (default) | Rule fires on every repo’s CI scan in the org. | Codebase-wide invariants, forbidden APIs, architectural rules. |
| A specific repository | Rule fires only when scanning that repo. | Rolling out a noisy rule to one repo first, repo-specific invariants. |
How rules are picked at scan time
When a scan runs against repository X, the backend serves every enabled custom rule where:id, both go to the scanner and the result is undefined.
Rule of thumb: keep ids unique across scopes. If you need a repo-specific override, give it a different ID (e.g. no-eval org-wide + no-eval-billing for the billing service) or delete the org-level rule before adding the repo-level one.
Database-level uniqueness
A singleid is unique within its scope:
- One
idper org for org-scoped rules. - One
idper(org, repo)for repo-scoped rules.
no-eval and a repo-A-scoped no-eval and a repo-B-scoped no-eval can all coexist — the uniqueness check is per-row-shape, not global.
Performance & accuracy tips
- Prefer
patternoverpattern-regex— structural patterns are AST-aware and much more accurate. - Scope to paths.
paths.includecuts scan time dramatically. - Use taint mode only for true data-flow bugs. ~5–10× slower than a plain pattern.
- Avoid
...at the top of a pattern. It matches anywhere, usually not what you want. - Exclude tests unless the rule is specifically about test code.
- Match the YAML’s
severity:to the UI severity dropdown — keep them aligned so the quality gate behaves predictably.
How to iterate safely
A dashboard rule only runs once saved — the scan fetches the ruleset from the backend, so there’s no way to try an unsaved rule against a fixture locally. The safe-iteration flow is:Author in a repo-scoped slot
Switch the Configure for dropdown at the top of the SAST Rules page to a single repository (the noisiest one is usually the best test bed).
Validate in the editor
Click Validate. The server checks the YAML, required fields, severity enum,
id character set, supported languages, and that a pattern form is present. Errors render inline. Fix them. (It does not execute your pattern: against a parser — a pattern that’s valid YAML but invalid code in the target language passes Validate and only errors at scan time.)Save and trigger one scan
Click Validate & Save, then trigger a CI run on a PR (or push to a branch). The CLI auto-fetches the rule on the next scan — no client-side config to touch.
Triage the findings
Inspect findings in Code Security → Findings. If the rule is too noisy, edit the YAML (tighten
pattern-not, add paths.exclude) and re-scan.Common validation errors
| Error | Cause | Fix |
|---|---|---|
Define one of pattern, patterns, pattern-either, pattern-regex, or pattern-sources | No pattern form provided | Add one of the pattern fields |
severity must be one of ERROR, WARNING, INFO, INVENTORY, EXPERIMENT | Lowercased severity inside the YAML, e.g. severity: high | Use the uppercase rule-file enum. (The lowercase critical/high/… picker is the UI dropdown, separate field.) |
id fails the character check | id has spaces, /, unicode, or is too long | 1–149 chars of letters, digits, underscore, hyphen, or dot; first char must be a letter or digit |
languages must be a non-empty array | languages: python (string, not array) | Wrap in []; each entry must be a supported language |
Unknown top-level key 'severitye' | Typo in a top-level key (warning, not error) | Fix the key name |
pattern parse error (surfaces in the CI scan log, not the Validate button) | Pattern isn’t valid syntax in the target language | Rewrite the pattern in the language’s real syntax and re-run the scan |
id character set, supported languages, and that a pattern form is present. It does not run your pattern: through the language parser, so a pattern that is valid YAML but invalid code in the target language passes Validate and only errors at scan time (last row).