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#

.atmos/rules/raw-sql.yml
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 an ERROR finding, no matter how many files or calls sit in between, with the full trace attached.

Where rules live#

SourceWhen 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 RulesetDelivered with every scan, nothing to manage. Skip with --no-managed-rules.

When the same rule id appears twice, the first wins, in the order above, so your rules always take precedence over the Managed Ruleset. A rule that fails to load does not stop the scan. See exit codes.

Anatomy of a rule#

FieldMeaning
idUnique rule identifier, used for deduplication and reporting.
languagesWhich languages the rule applies to: javascript, typescript, java, csharp, or regex for plain text regex rules.
severityINFO, WARNING (the default) or ERROR.
messageWhat the developer reads. Say what is wrong and how to fix it.
metadataOptional context carried into reports: cwe, owasp, category, and security-severity (a 0 to 10 score carried into SARIF).
pathsinclude / exclude glob lists (* and ?) that scope the rule to parts of the tree.
patterns | mode: taintThe matching logic itself: a pattern rule or a taint rule.

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:

.atmos/rules/route-auth.yml
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|/$)).*
#x27;

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. This is the shape the agent skill generates for your own framework and middleware names.

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.

Testing and tuning#

Point Atmos at a rule file directly while you iterate, and read the results as JSON:

Terminal
atmos sast . --rules my-rule.yml --format json
atmos sast . -v        # diagnostics on stderr when a flow seems missed

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.