Guides
Custom rules
Atmos runs your own YAML rules on every plan. A pattern rule matches a shape of code. A taint rule names where untrusted data enters (a source), the dangerous call it must not reach (a sink) and the checks that clear it on the way (sanitizers), and Atmos connects them across files.
Your first rule#
rules: - id: atmos.ts.raw-sql-from-request languages: [typescript] severity: ERROR mode: taint pattern-sources: - pattern: req.query.$X pattern-sinks: - pattern: db.query(`...`) message: >- Untrusted request input reaches a raw SQL query. Use a parameterized query.
Drop this in .atmos/rules/ and run atmos scan . Any value read from req.query that reaches a raw db.query template becomes a high severity finding, no matter how many files or calls sit in between, with the full trace attached.
Where rules live#
| Source | When it loads |
|---|---|
| --rules <RULES> | A YAML rule file, or a directory of them, passed explicitly. |
| .atmos/rules/ | On every scan, unless --no-local-rules. The committed home for project rules. See the .atmos folder. |
| Managed Ruleset | Delivered with every scan, nothing to manage. Skip with --no-managed-rules on atmos scan and atmos sast. |
When the same rule id appears twice, the first wins, in the order above, so your rules always take precedence over the Managed Ruleset. Atmos names each managed rule that one of your rules replaces. A rule that fails to load does not stop the scan. See exit codes.
Anatomy of a rule#
| Field | Meaning |
|---|---|
| id | Unique rule identifier, used for deduplication and reporting. |
| languages | Which languages the rule applies to: javascript, typescript, java, csharp, or regex for plain text regex rules. |
| severity | INFO, WARNING (the default) or ERROR. See severity. |
| message | What the developer reads. Say what is wrong and how to fix it. |
| metadata | Optional context carried into reports, such as cwe, owasp and category. security-severity, a score from 0 to 10, sets the severity. |
| paths | include / exclude glob lists (* and ?) that scope the rule to parts of the tree. |
| patterns | mode: taint | The matching logic itself: a pattern rule or a taint rule. |
Severity#
The report, --fail-on and the platform show each finding with the severity its rule sets. A security-severity score in the rule's metadata decides it, and without a score the rule's severity does:
| In the rule | Severity |
|---|---|
| security-severity 9 or more | critical |
| security-severity 7 or more, or severity: ERROR | high |
| security-severity 4 or more, or severity: WARNING | medium |
| security-severity above 0, or severity: INFO | low |
| security-severity 0 | info |
Pattern operators#
Patterns match the structure of your code, so formatting and comments never affect a match. Metavariables like $X capture any expression, and ellipses act as wildcards, as in f(...), { ... }, and chained forms like foo(...).bar(...).
Rules compose conditions with pattern, pattern-either, patterns (all must hold), pattern-not, pattern-inside and pattern-not-inside, plus metavariable-pattern, metavariable-regex, metavariable-comparison and focus-metavariable. Typed metavariables, object, import and decoration patterns are supported, and metavariable-analysis with the redos analyzer flags regexes prone to catastrophic backtracking.
A pattern rule that encodes a project convention:
rules: - id: express-route-missing-authentication languages: [javascript, typescript] severity: ERROR message: >- This route is registered with only a handler and no authentication middleware. Add the auth middleware (e.g. requireAuth) or mark the route public. metadata: category: security cwe: "CWE-862: Missing Authorization" owasp: "A01:2021 - Broken Access Control" patterns: - pattern: $APP.$METHOD($PATH, $HANDLER) # exactly (path, handler) - metavariable-regex: metavariable: $PATH regex: '^(?!.*(login|register|signup|health|metrics|public|webhook|/$)).*$'
The first pattern matches any route registered with exactly two arguments, so routes that already pass a middleware chain do not match, and the metavariable-regex exempts the routes that are public on purpose.
Taint mode#
mode: taint rules declare pattern-sources, pattern-sinks and pattern-sanitizers, and Atmos connects them by following your code's real call paths across files. Tracking is precise about fields, so a value read from req.body stays tainted through any number of calls while unrelated fields on the same object stay clean.
Sources and sinks can carry taint labels via label:, and sinks can require combinations via requires:, so a rule can express "reaches the query and was never validated" precisely. focus-metavariable narrows the reported span to the argument that matters.
Checks that clear a value#
Code often checks a value before it uses it, as an archive extraction checks that each entry's resolved path stays inside the destination folder. A sanitizer with by-side-effect: guard matches that check, and its focus-metavariable names the checked variable. Atmos then treats the variable as clean only where the check held:
pattern-sanitizers: - by-side-effect: guard patterns: - pattern: $P.startsWith(...) - focus-metavariable: $P
const target = path.resolve(dest, entry.name);
if (!target.startsWith(dest)) {
return;
}
// clean here: the check held to get this far
fs.writeFileSync(target, data);- In the branch of the
ifwhere the check is true, the variable is clean. - When the branch where the check fails always ends in
returnorthrow(orcontinueorbreakin a loop), the code after theifis clean too. - A check whose failing branch carries on, for example one that only logs, clears nothing after the
if. Neither does a check after the dangerous call. - A new untrusted value assigned to the variable after the check is reported as usual.
Some checks are written in their failing form: target.indexOf(dest) !== 0 is true exactly where the path does not start with the folder. Set by-side-effect: guard-false on such a check, and the variable is clean only where the check is false, so if (target.indexOf(dest) !== 0) throw ... clears the code after it:
pattern-sanitizers: - by-side-effect: guard-false patterns: - pattern: $P.indexOf(...) !== 0 - focus-metavariable: $P
Pick guard or guard-false by what the check means. A check that does not split safe values from unsafe ones guards nothing in either form: indexOf(dest) > 0 is false both for a path inside the folder (0) and for a path that does not contain it (-1). A guard clears the variable for its own rule only. With by-side-effect: true, a sanitizer clears the variable everywhere after the match, whichever branch the code takes.
Testing and tuning#
Point Atmos at a rule file directly while you iterate, and read the results as JSON:
atmos sast . --rules my-rule.yml --format json
Tune the rule toward zero false positives before committing it. Exempt deliberate cases inside the rule, with pattern-not or a metavariable-regex allowlist, or with a marker that is visible in the code, like an allowAnonymous middleware, @Public() in Java or [AllowAnonymous] in C#, so reviewers can see which routes are public on purpose. Once the rule is quiet on the current tree, commit it to .atmos/rules/ and let CI enforce it from then on.