AST patterns
AST patterns are tree-sitter queries that match dangerous structural shapes in source. No dataflow, no CFG. A match means the construct is present; it’s not proof the construct is exploitable.
Patterns run in every analysis mode. In --mode ast they’re the only active detector.
Rule IDs
<lang>.<category>.<name>
Examples: js.code_exec.eval, py.deser.pickle_loads, c.memory.gets, java.sqli.execute_concat.
Full list: rules.md.
Tiers
| Tier | Meaning |
|---|---|
| A | Structural presence alone is high-signal. gets, eval, pickle.loads, mem::transmute |
| B | Pattern includes a tree-sitter heuristic guard. Example: java.sqli.execute_concat only fires when executeQuery receives a binary_expression (string concatenation), not a literal or a parameterized statement |
Categories
| Category | Examples |
|---|---|
| CommandExec | system, os.system, Runtime.exec, backticks |
| CodeExec | eval, Function, PHP assert("string"), class_eval, instance_eval |
| Deserialization | pickle.loads, yaml.load, Marshal.load, readObject, unserialize |
| SqlInjection | executeQuery/Query/execute with concatenated argument (Tier B) |
| PathTraversal | PHP include $var |
| Xss | document.write, outerHTML, insertAdjacentHTML, getWriter().print |
| Crypto | md5, sha1, Math.random, java.util.Random for security use |
| Secrets | hardcoded API keys (Go, JS, TS) |
| InsecureTransport | InsecureSkipVerify, fetch("http://...") |
| Reflection | Class.forName, Method.invoke, send, constantize |
| MemorySafety | transmute, unsafe, gets, strcpy, sprintf |
| Prototype | __proto__ assignment, Object.prototype.* |
| Config | CORS dynamic origin, rejectUnauthorized: false, insecure session settings |
| CodeQuality | unwrap, panic!, as any |
What patterns can’t tell you
- Dataflow.
eval("1+1")(safe) andeval(userInput)(dangerous) both matchjs.code_exec.eval. The taint detector is the one that distinguishes them. - Reachability. A pattern in dead code matches identically.
- Semantics.
strcpy(dst, src)always matches, regardless of buffer sizes. - Indirect calls.
let e = eval; e(input)doesn’t matcheval. - Aliased imports.
from os import system as s; s(cmd)won’t matchsystem. - Macro expansions. Tree-sitter parses the macro call site, not the expansion.
Common false positives
| Scenario | Why | Mitigation |
|---|---|---|
eval("hardcoded literal") | Pattern matches structure | Run --mode cfg to drop AST patterns and rely on taint |
unsafe block with sound justification | Every unsafe matches rs.quality.unsafe_block | Filter >=MEDIUM (it’s Medium) or accept the noise |
.unwrap() in tests | Acceptable in test code | Default non-prod severity downgrade reduces it |
md5 for non-cryptographic checksums | Pattern can’t see intent | Suppress with --severity ">=MEDIUM" or per-line nyx:ignore |
| SQL concat with trusted data (Tier B) | Heuristic can’t verify the source | Taint is more precise; or convert to a parameterized query |
Confidence levels
Every AST pattern carries an explicit confidence:
| Confidence | Use |
|---|---|
| High | Inherently dangerous construct with no safe usage. gets, pickle.loads, eval with no guard |
| Medium | Likely issue, context may change the call. SQL concatenation (Tier B), unsafe blocks, exec |
| Low | Heuristic. Often appears in safe code. Weak crypto for checksums, unwrap outside tests, Math.random |
--min-confidence medium (or output.min_confidence = "medium") drops Low-confidence matches.
Tuning
nyx scan . --severity ">=MEDIUM" # drop Low-tier patterns
nyx scan . --severity HIGH # banned APIs and code-exec only
nyx scan . --mode cfg # drop AST patterns; keep taint + state + cfg
[scanner]
excluded_directories = ["node_modules", "vendor", "generated"]
Examples
Tier A, structural presence:
char buf[64];
gets(buf); // c.memory.gets
import pickle
data = pickle.loads(user_input) // py.deser.pickle_loads
Tier B, heuristic guard:
// Fires: concatenated argument
stmt.executeQuery("SELECT * FROM users WHERE id=" + userId); // java.sqli.execute_concat
// Does not fire: parameterized
stmt.executeQuery(preparedSql);
printf(user_input); // c.memory.printf_no_fmt: fires (variable as fmt)
printf("%s", user_input); // does not fire (literal fmt)