* chore: Exclude CLAUDE.md from Cargo.toml

* feat: add callgraph module and integrate into main analysis flow

* feat: enhance CLI with new severity filtering and analysis modes

* feat: update CHANGELOG with recent enhancements and fixes to severity filtering and output handling

* feat: implement state-model dataflow analysis for resource lifecycle and auth state

* feat: enhance diagnostic output formatting and add evidence structure

* feat: implement attack surface ranking for diagnostics with scoring and sorting

* feat: add comprehensive documentation for installation, usage, and rules reference

* feat: add multiple language support for command execution and evaluation endpoints

* feat: implement inline suppression for findings using `nyx:ignore` comments

* feat: add confidence levels to AST patterns and update output structure

* feat: implement low-noise prioritization system with category filtering, rollup grouping, and configurable budgets

* feat: bump version to 0.4.0 and update changelog with new features and improvements

* feat: add dead code allowances to various functions in mod.rs and real_world_tests.rs
This commit is contained in:
Eli Peter 2026-02-25 21:16:36 -05:00 committed by GitHub
parent 19b578c5c4
commit 1bbe4b1cfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
456 changed files with 25628 additions and 1228 deletions

View file

@ -1,40 +1,95 @@
use crate::patterns::{Pattern, Severity};
use crate::evidence::Confidence;
use crate::patterns::{Pattern, PatternCategory, PatternTier, Severity};
/// C AST patterns.
///
/// Taint rules cover `system`/`popen`/`exec*` (command injection),
/// `sprintf`/`strcpy`/`strcat` (buffer overflow sinks), and `printf`/`fprintf`
/// (format-string sinks). AST patterns here focus on **banned-by-default
/// functions** (`gets`, `scanf %s`) and **format-string** variants not covered
/// by taint, since these are dangerous regardless of data origin.
pub const PATTERNS: &[Pattern] = &[
// ── Tier A: Banned functions (always dangerous) ────────────────────
Pattern {
id: "strcpy_call",
description: "strcpy() usage",
query: "(call_expression function: (identifier) @id (#eq? @id \"strcpy\")) @vuln",
id: "c.memory.gets",
description: "gets() — no bounds checking, always exploitable",
query: r#"(call_expression function: (identifier) @id (#eq? @id "gets")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "strcat_call",
description: "strcat() usage",
query: "(call_expression function: (identifier) @id (#eq? @id \"strcat\")) @vuln",
id: "c.memory.strcpy",
description: "strcpy() — no bounds checking on destination buffer",
query: r#"(call_expression function: (identifier) @id (#eq? @id "strcpy")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "sprintf_call",
description: "sprintf() (no length limit)",
query: "(call_expression function: (identifier) @id (#eq? @id \"sprintf\")) @vuln",
id: "c.memory.strcat",
description: "strcat() — no bounds checking on destination buffer",
query: r#"(call_expression function: (identifier) @id (#eq? @id "strcat")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "gets_call",
description: "gets() usage",
query: "(call_expression function: (identifier) @id (#eq? @id \"gets\")) @vuln",
id: "c.memory.sprintf",
description: "sprintf() — no length limit on output buffer",
query: r#"(call_expression function: (identifier) @id (#eq? @id "sprintf")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "scanf_with_percent_s",
description: "scanf(\"%s\") without length specifier",
query: "(call_expression function: (identifier) @id (#eq? @id \"scanf\") arguments: (argument_list (string_literal) @fmt (#match? @fmt \".*%s.*\"))) @vuln",
id: "c.memory.scanf_percent_s",
description: "scanf(\"%s\") — unbounded string read",
query: r#"(call_expression
function: (identifier) @id (#eq? @id "scanf")
arguments: (argument_list
(string_literal) @fmt (#match? @fmt "%s")))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
// ── Tier A: Command execution ──────────────────────────────────────
Pattern {
id: "c.cmdi.system",
description: "system() — shell command execution",
query: r#"(call_expression function: (identifier) @id (#eq? @id "system")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CommandExec,
confidence: Confidence::High,
},
Pattern {
id: "system_call",
description: "system() shell execution",
query: "(call_expression function: (identifier) @id (#eq? @id \"system\")) @vuln",
id: "c.cmdi.popen",
description: "popen() — shell command execution with pipe",
query: r#"(call_expression function: (identifier) @id (#eq? @id "popen")) @vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::CommandExec,
confidence: Confidence::High,
},
// ── Tier A: Format-string ──────────────────────────────────────────
Pattern {
id: "c.memory.printf_no_fmt",
description: "printf(var) — format-string vulnerability when first arg is not literal",
query: r#"(call_expression
function: (identifier) @id (#eq? @id "printf")
arguments: (argument_list
. (identifier) @arg))
@vuln"#,
severity: Severity::High,
tier: PatternTier::B,
category: PatternCategory::MemorySafety,
confidence: Confidence::Medium,
},
];

View file

@ -1,40 +1,106 @@
use crate::patterns::{Pattern, Severity};
use crate::evidence::Confidence;
use crate::patterns::{Pattern, PatternCategory, PatternTier, Severity};
/// C++ AST patterns.
///
/// Inherits C banned-function concerns plus C++-specific patterns like
/// `reinterpret_cast` and `const_cast`. Taint rules overlap with C rules
/// for `system`/`sprintf`/`strcpy`/`strcat`.
pub const PATTERNS: &[Pattern] = &[
// ── Tier A: Banned C functions (inherited) ─────────────────────────
Pattern {
id: "strcpy_call",
description: "strcpy() usage",
query: "(call_expression function: (identifier) @id (#eq? @id \"strcpy\")) @vuln",
id: "cpp.memory.gets",
description: "gets() — no bounds checking, always exploitable",
query: r#"(call_expression function: (identifier) @id (#eq? @id "gets")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "strcat_call",
description: "strcat() usage",
query: "(call_expression function: (identifier) @id (#eq? @id \"strcat\")) @vuln",
id: "cpp.memory.strcpy",
description: "strcpy() — no bounds checking on destination buffer",
query: r#"(call_expression function: (identifier) @id (#eq? @id "strcpy")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "sprintf_call",
description: "sprintf() (no length limit)",
query: "(call_expression function: (identifier) @id (#eq? @id \"sprintf\")) @vuln",
id: "cpp.memory.strcat",
description: "strcat() — no bounds checking on destination buffer",
query: r#"(call_expression function: (identifier) @id (#eq? @id "strcat")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "gets_call",
description: "gets() usage",
query: "(call_expression function: (identifier) @id (#eq? @id \"gets\")) @vuln",
id: "cpp.memory.sprintf",
description: "sprintf() — no length limit on output buffer",
query: r#"(call_expression function: (identifier) @id (#eq? @id "sprintf")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
// ── Tier A: Command execution ──────────────────────────────────────
Pattern {
id: "cpp.cmdi.system",
description: "system() — shell command execution",
query: r#"(call_expression function: (identifier) @id (#eq? @id "system")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CommandExec,
confidence: Confidence::High,
},
Pattern {
id: "system_call",
description: "system() shell execution",
query: "(call_expression function: (identifier) @id (#eq? @id \"system\")) @vuln",
id: "cpp.cmdi.popen",
description: "popen() — shell command execution",
query: r#"(call_expression function: (identifier) @id (#eq? @id "popen")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CommandExec,
confidence: Confidence::High,
},
// ── Tier A: Dangerous casts ────────────────────────────────────────
// C++ casts are parsed as call_expression with template_function
Pattern {
id: "cpp.memory.reinterpret_cast",
description: "reinterpret_cast — type-punning cast",
query: r#"(call_expression
function: (template_function
name: (identifier) @n (#eq? @n "reinterpret_cast")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "reinterpret_cast",
description: "reinterpret_cast usage",
query: "(reinterpret_cast_expression) @vuln",
id: "cpp.memory.const_cast",
description: "const_cast — removes const/volatile qualifier",
query: r#"(call_expression
function: (template_function
name: (identifier) @n (#eq? @n "const_cast")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
// ── Tier B: Format-string (variable first arg) ─────────────────────
Pattern {
id: "cpp.memory.printf_no_fmt",
description: "printf(var) — format-string vulnerability when first arg is not literal",
query: r#"(call_expression
function: (identifier) @id (#eq? @id "printf")
arguments: (argument_list
. (identifier) @arg))
@vuln"#,
severity: Severity::High,
tier: PatternTier::B,
category: PatternCategory::MemorySafety,
confidence: Confidence::Medium,
},
];

View file

@ -1,34 +1,120 @@
use crate::patterns::{Pattern, Severity};
use crate::evidence::Confidence;
use crate::patterns::{Pattern, PatternCategory, PatternTier, Severity};
/// Go AST patterns.
///
/// Taint rules cover `exec.Command` (command injection), `db.Query`/`db.Exec`
/// (SQL sinks). AST patterns here focus on **TLS misconfiguration**,
/// **weak crypto**, **unsafe.Pointer**, and **hardcoded secrets**.
pub const PATTERNS: &[Pattern] = &[
// ── Tier A: Command execution ──────────────────────────────────────
Pattern {
id: "exec_command",
description: "os/exec Command construction",
query: "(call_expression function: (selector_expression field: (field_identifier) @f (#eq? @f \"Command\"))) @vuln",
severity: Severity::Medium,
},
Pattern {
id: "http_insecure_tls",
description: "&http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}",
query: "(composite_literal type: (selector_expression field: (field_identifier) @t (#eq? @t \"Transport\")) body: (literal_value (keyed_element key: (identifier) @k (#eq? @k \"TLSClientConfig\") value: (composite_literal body: (literal_value (keyed_element key: (identifier) @ik (#eq? @ik \"InsecureSkipVerify\") value: (true)))))) @vuln",
id: "go.cmdi.exec_command",
description: "exec.Command() — arbitrary process execution",
query: r#"(call_expression
function: (selector_expression
field: (field_identifier) @f (#eq? @f "Command")))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CommandExec,
confidence: Confidence::High,
},
// ── Tier A: Unsafe pointer ─────────────────────────────────────────
Pattern {
id: "unsafe_pointer",
description: "Use of unsafe.Pointer",
query: "(qualified_type type: (selector_expression field: (field_identifier) @f (#eq? @f \"Pointer\"))) @vuln",
severity: Severity::High,
},
Pattern {
id: "md5_sha1",
description: "crypto/md5 or crypto/sha1 usage",
query: "(call_expression function: (selector_expression object: (identifier) @pkg (#match? @pkg \"md5|sha1\"))) @vuln",
id: "go.memory.unsafe_pointer",
description: "unsafe.Pointer — bypasses Go type system",
query: r#"(call_expression
function: (selector_expression
operand: (identifier) @pkg (#eq? @pkg "unsafe")
field: (field_identifier) @f (#eq? @f "Pointer")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
// ── Tier A: TLS misconfiguration ───────────────────────────────────
Pattern {
id: "hardcoded_secret",
description: "Hard-coded string that looks like an API key/token",
query: "(interpreted_string_literal) @s (#match? @s \"(?i)(api|secret|token|password)[=:]?[ \\t]*[A-Za-z0-9_\\-]{8,}\")",
id: "go.transport.insecure_skip_verify",
description: "InsecureSkipVerify: true — disables TLS certificate validation",
query: r#"(keyed_element
(literal_element
(identifier) @k (#eq? @k "InsecureSkipVerify"))
(literal_element (true)))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::InsecureTransport,
confidence: Confidence::High,
},
// ── Tier A: Weak crypto ────────────────────────────────────────────
Pattern {
id: "go.crypto.md5",
description: "md5.New() / md5.Sum() — weak hash algorithm",
query: r#"(call_expression
function: (selector_expression
operand: (identifier) @pkg (#eq? @pkg "md5")))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
Pattern {
id: "go.crypto.sha1",
description: "sha1.New() / sha1.Sum() — weak hash algorithm",
query: r#"(call_expression
function: (selector_expression
operand: (identifier) @pkg (#eq? @pkg "sha1")))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
// ── Tier B: SQL injection (concatenation heuristic) ────────────────
Pattern {
id: "go.sqli.query_concat",
description: "db.Query/Exec with concatenated string argument",
query: r#"(call_expression
function: (selector_expression
field: (field_identifier) @f (#match? @f "^(Query|Exec|QueryRow)$"))
arguments: (argument_list
(binary_expression) @concat))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::B,
category: PatternCategory::SqlInjection,
confidence: Confidence::Medium,
},
// ── Tier A: Hardcoded secrets ──────────────────────────────────────
Pattern {
id: "go.secrets.hardcoded_key",
description: "Variable with secret-like name assigned a string literal",
query: r#"(short_var_declaration
left: (expression_list
(identifier) @name (#match? @name "(?i)(password|secret|api_?key|token|private_?key)"))
right: (expression_list
(interpreted_string_literal) @val))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Secrets,
confidence: Confidence::High,
},
// ── Tier A: Deserialization ────────────────────────────────────────
Pattern {
id: "go.deser.gob_decode",
description: "gob.NewDecoder — Go binary deserialization",
query: r#"(call_expression
function: (selector_expression
operand: (identifier) @pkg (#eq? @pkg "gob")
field: (field_identifier) @f (#eq? @f "NewDecoder")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Deserialization,
confidence: Confidence::High,
},
];

View file

@ -1,40 +1,116 @@
use crate::patterns::{Pattern, Severity};
use crate::evidence::Confidence;
use crate::patterns::{Pattern, PatternCategory, PatternTier, Severity};
/// Java AST patterns.
///
/// Taint rules cover `Runtime.exec` (command injection) and
/// `executeQuery`/`executeUpdate`/`prepareStatement` (SQL sinks).
/// AST patterns here focus on **deserialization**, **reflection**,
/// **SQL with concatenation** (Tier B heuristic), and **weak crypto**.
pub const PATTERNS: &[Pattern] = &[
// ── Tier A: Deserialization ────────────────────────────────────────
Pattern {
id: "runtime_exec",
description: "Runtime.getRuntime().exec(...) arbitrary-command execution",
query: "(method_invocation object: (method_invocation name: (identifier) @n (#eq? @n \"getRuntime\")) name: (identifier) @id (#eq? @id \"exec\")) @vuln",
id: "java.deser.readobject",
description: "ObjectInputStream.readObject() — unsafe deserialization",
// Match any .readObject() call — the method name is specific enough.
query: r#"(method_invocation
name: (identifier) @id (#eq? @id "readObject"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::Deserialization,
confidence: Confidence::High,
},
// ── Tier A: Command execution ──────────────────────────────────────
Pattern {
id: "class_for_name",
description: "Dynamic reflection via Class.forName(...)",
query: "(method_invocation object: (identifier) @c (#eq? @c \"Class\") name: (identifier) @id (#eq? @id \"forName\")) @vuln",
severity: Severity::Medium,
},
Pattern {
id: "object_deserialization",
description: "java.io.ObjectInputStream#readObject() deserialization",
query: "(method_invocation object: (identifier) @o (#eq? @o \"ObjectInputStream\") name: (identifier) @id (#eq? @id \"readObject\")) @vuln",
id: "java.cmdi.runtime_exec",
description: "Runtime.getRuntime().exec() — shell command execution",
query: r#"(method_invocation
object: (method_invocation
name: (identifier) @n (#eq? @n "getRuntime"))
name: (identifier) @id (#eq? @id "exec"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CommandExec,
confidence: Confidence::High,
},
// ── Tier A: Reflection ─────────────────────────────────────────────
Pattern {
id: "insecure_random",
description: "java.util.Random used where SecureRandom is expected",
query: "(object_creation_expression type: (identifier) @t (#eq? @t \"Random\")) @vuln",
id: "java.reflection.class_forname",
description: "Class.forName() — dynamic class loading",
query: r#"(method_invocation
object: (identifier) @c (#eq? @c "Class")
name: (identifier) @id (#eq? @id "forName"))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Reflection,
confidence: Confidence::High,
},
Pattern {
id: "thread_stop",
description: "Deprecated Thread.stop() invocation",
query: "(method_invocation name: (identifier) @id (#eq? @id \"stop\") object: (identifier) @obj (#eq? @obj \"Thread\")) @vuln",
id: "java.reflection.method_invoke",
description: "Method.invoke() — reflective method invocation",
query: r#"(method_invocation
name: (identifier) @id (#eq? @id "invoke"))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Reflection,
confidence: Confidence::High,
},
// ── Tier B: SQL injection (concatenation heuristic) ────────────────
Pattern {
id: "java.sqli.execute_concat",
description: "SQL execute with concatenated string argument",
query: r#"(method_invocation
name: (identifier) @id (#match? @id "^execute(Query|Update)?$")
arguments: (argument_list
(binary_expression) @concat))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::B,
category: PatternCategory::SqlInjection,
confidence: Confidence::Medium,
},
// ── Tier A: Weak crypto ────────────────────────────────────────────
Pattern {
id: "java.crypto.insecure_random",
description: "new Random() — java.util.Random is not cryptographically secure",
query: r#"(object_creation_expression
type: (type_identifier) @t (#eq? @t "Random"))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
Pattern {
id: "sql_concat",
description: "SQL built with string concatenation",
query: "(method_invocation name: (identifier) @id (#match? @id \"execute(Query|Update)?\") arguments: (argument_list (binary_expression) @concat)) @vuln",
id: "java.crypto.weak_digest",
description: "MessageDigest.getInstance(\"MD5\"/\"SHA1\") — weak hash algorithm",
query: r#"(method_invocation
object: (identifier) @c (#eq? @c "MessageDigest")
name: (identifier) @id (#eq? @id "getInstance")
arguments: (argument_list
(string_literal) @alg (#match? @alg "(?i)(md5|sha-?1)")))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
// ── Tier A: XSS (servlet) ──────────────────────────────────────────
Pattern {
id: "java.xss.getwriter_print",
description: "response.getWriter().print/println — direct output without encoding",
query: r#"(method_invocation
object: (method_invocation
name: (identifier) @gw (#eq? @gw "getWriter"))
name: (identifier) @id (#match? @id "^(print|println|write)$"))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Xss,
confidence: Confidence::High,
},
];

View file

@ -1,117 +1,182 @@
use crate::patterns::{Pattern, Severity};
use crate::evidence::Confidence;
use crate::patterns::{Pattern, PatternCategory, PatternTier, Severity};
/// JavaScript AST patterns.
///
/// Taint rules cover `eval` (code injection), `innerHTML` (XSS),
/// `location.href` (open redirect), and `child_process.exec/spawn` (command
/// injection). AST patterns here add **new Function()**, **document.write**,
/// **setTimeout with string**, **deserialization**, **prototype pollution**,
/// **XSS sinks** not covered by taint, and **weak crypto**.
pub const PATTERNS: &[Pattern] = &[
// ── Tier A: Code execution ─────────────────────────────────────────
Pattern {
id: "eval_call",
description: "Use of eval()",
query: "(call_expression function: (identifier) @id (#eq? @id \"eval\")) @vuln",
id: "js.code_exec.eval",
description: "eval() — dynamic code execution",
query: r#"(call_expression
function: (identifier) @id (#eq? @id "eval"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
Pattern {
id: "new_function",
description: "new Function() constructor",
query: "(new_expression constructor: (identifier) @id (#eq? @id \"Function\")) @vuln",
id: "js.code_exec.new_function",
description: "new Function() constructor — eval equivalent",
query: r#"(new_expression
constructor: (identifier) @id (#eq? @id "Function"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
Pattern {
id: "document_write",
description: "document.write() call",
query: "(call_expression function: (member_expression object: (identifier) @obj (#eq? @obj \"document\") property: (property_identifier) @prop (#eq? @prop \"write\"))) @vuln",
id: "js.code_exec.settimeout_string",
description: "setTimeout/setInterval with string argument — implicit eval",
query: r#"(call_expression
function: (identifier) @id (#match? @id "^(setTimeout|setInterval)$")
arguments: (arguments (string) @code))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
// ── Tier A: XSS sinks ──────────────────────────────────────────────
Pattern {
id: "settimeout_string",
description: "setTimeout / setInterval with a string argument",
query: "(call_expression function: (identifier) @id (#match? @id \"setTimeout|setInterval\") arguments: (arguments (string) @code . _)) @vuln",
id: "js.xss.document_write",
description: "document.write() — XSS sink",
query: r#"(call_expression
function: (member_expression
object: (identifier) @obj (#eq? @obj "document")
property: (property_identifier) @prop (#match? @prop "^(write|writeln)$")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Xss,
confidence: Confidence::High,
},
Pattern {
id: "json_parse",
description: "JSON.parse on dynamic string",
query: "(call_expression function: (member_expression object: (identifier) @obj (#eq? @obj \"JSON\") property: (property_identifier) @prop (#eq? @prop \"parse\"))) @vuln",
id: "js.xss.outer_html",
description: "Assignment to .outerHTML — XSS sink",
query: r#"(assignment_expression
left: (member_expression
property: (property_identifier) @prop (#eq? @prop "outerHTML")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Xss,
confidence: Confidence::High,
},
Pattern {
id: "js.xss.insert_adjacent_html",
description: "insertAdjacentHTML() — XSS sink",
query: r#"(call_expression
function: (member_expression
property: (property_identifier) @prop (#eq? @prop "insertAdjacentHTML")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Xss,
confidence: Confidence::High,
},
// ── Tier A: Prototype pollution ────────────────────────────────────
Pattern {
id: "js.prototype.proto_assignment",
description: "Assignment to __proto__ — prototype pollution",
query: r#"(assignment_expression
left: (member_expression
property: (property_identifier) @prop (#eq? @prop "__proto__")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Prototype,
confidence: Confidence::High,
},
Pattern {
id: "js.prototype.extend_object",
description: "Assignment to Object.prototype — prototype mutation",
query: r#"(assignment_expression
left: (member_expression
object: (member_expression
object: (identifier) @obj (#eq? @obj "Object")
property: (property_identifier) @mid (#eq? @mid "prototype"))))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Prototype,
confidence: Confidence::High,
},
// ── Tier A: Weak crypto ────────────────────────────────────────────
Pattern {
id: "js.crypto.weak_hash",
description: "crypto.createHash with weak algorithm (md5/sha1)",
query: r#"(call_expression
function: (member_expression
property: (property_identifier) @prop (#eq? @prop "createHash"))
arguments: (arguments
(string) @alg (#match? @alg "\"(md5|sha1)\"")))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
Pattern {
id: "outer_html_assignment",
description: "Assignment to element.outerHTML",
query: "(assignment_expression
left: (member_expression
property: (property_identifier) @prop
(#eq? @prop \"outerHTML\"))) @vuln",
id: "js.crypto.math_random",
description: "Math.random() — not cryptographically secure",
query: r#"(call_expression
function: (member_expression
object: (identifier) @obj (#eq? @obj "Math")
property: (property_identifier) @prop (#eq? @prop "random")))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
// ── Tier A: Open redirect ──────────────────────────────────────────
Pattern {
id: "js.xss.location_assign",
description: "Assignment to location/location.href — open redirect",
query: r#"(assignment_expression
left: (member_expression
object: (identifier) @obj (#match? @obj "^(window|location|document)$")
property: (property_identifier) @prop (#match? @prop "^(location|href)$")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Xss,
confidence: Confidence::High,
},
// ── Tier A: Insecure transport ─────────────────────────────────────
Pattern {
id: "insert_adjacent_html",
description: "insertAdjacentHTML() call",
query: "(call_expression
function: (member_expression
property: (property_identifier) @prop
(#eq? @prop \"insertAdjacentHTML\"))) @vuln",
severity: Severity::Medium,
id: "js.transport.fetch_http",
description: "fetch() over plain HTTP",
query: r#"(call_expression
function: (identifier) @id (#eq? @id "fetch")
arguments: (arguments
(string) @url (#match? @url "^\"http://")))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::InsecureTransport,
confidence: Confidence::Medium,
},
// ── Tier A: Cookie manipulation ────────────────────────────────────
Pattern {
id: "location_href_assignment",
description: "Assignment to window.location / location.href",
query: "(assignment_expression
left: (member_expression
object: (identifier) @obj
(#match? @obj \"^(window|location|document|self|top|parent|frames)$\")
property: (property_identifier) @prop
(#match? @prop \"^(location|href)$\"))) @vuln",
severity: Severity::High,
},
Pattern {
id: "cookie_assignment",
id: "js.xss.cookie_write",
description: "Write to document.cookie",
query: "(assignment_expression
left: (member_expression
object: (identifier) @obj
(#eq? @obj \"document\")
property: (property_identifier) @prop
(#eq? @prop \"cookie\"))) @vuln",
severity: Severity::Medium,
},
Pattern {
id: "proto_pollution",
description: "Assignment to __proto__ (prototype pollution)",
query: "(assignment_expression
left: (member_expression
property: (property_identifier) @prop
(#eq? @prop \"__proto__\"))) @vuln",
severity: Severity::Low,
},
Pattern {
id: "weak_hash_md5",
description: "crypto.createHash(\"md5\")",
query: "(call_expression
function: (member_expression
object: (identifier) @obj
(#eq? @obj \"crypto\")
property: (property_identifier) @prop
(#eq? @prop \"createHash\"))
arguments: (arguments
(string) @alg
(#eq? @alg \"md5\"))) @vuln",
severity: Severity::Low,
},
Pattern {
id: "regexp_constructor_string",
description: "new RegExp() with a dynamic string",
query: "(new_expression
constructor: (identifier) @id
(#eq? @id \"RegExp\")
arguments: (arguments (string) @pattern)) @vuln",
severity: Severity::Low,
},
Pattern {
id: "dangerous_extend_builtin",
description: "Extending Object.prototype (may lead to collisions/pollution)",
query: "(assignment_expression
left: (member_expression
object: (identifier) @obj
(#eq? @obj \"Object\")
property: (property_identifier) @prop
(#eq? @prop \"prototype\"))) @vuln",
query: r#"(assignment_expression
left: (member_expression
object: (identifier) @obj (#eq? @obj "document")
property: (property_identifier) @prop (#eq? @prop "cookie")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Xss,
confidence: Confidence::High,
},
];

View file

@ -1,3 +1,43 @@
//! # AST Pattern Conventions
//!
//! Each language file exports a `PATTERNS` slice of [`Pattern`] structs.
//!
//! ## ID format
//!
//! `<lang>.<category>.<specific>` — e.g. `java.deser.readobject`, `py.cmdi.os_system`.
//!
//! Language prefixes: `rs`, `java`, `py`, `js`, `ts`, `c`, `cpp`, `go`, `php`, `rb`.
//!
//! ## Tiers
//!
//! * **Tier A** — structural presence is high-signal (e.g. `gets()`, `eval()`).
//! * **Tier B** — requires a heuristic guard in the query (e.g. SQL with concatenated
//! arg, format-string with variable first arg).
//!
//! ## Severity
//!
//! * **High** — command exec, deserialization, banned C functions.
//! * **Medium** — SQL concat, reflection, XSS sinks, casts.
//! * **Low** — weak crypto, insecure randomness, code-quality (`unwrap`/`expect`/`panic`).
//!
//! Note: the default `min_severity` filter skips Low patterns; they only appear when
//! the user explicitly lowers the threshold.
//!
//! ## No-duplicate rule
//!
//! If a vulnerability class is already detected by taint analysis (e.g. `eval` as a
//! sink, `system` as a sink), the AST pattern is still kept for `--ast-only` mode but
//! uses a distinct ID namespace (`js.code_exec.eval` vs `taint-unsanitised-flow`).
//! The dedup pass in `ast.rs` prevents exact-duplicate findings at the same location.
//!
//! ## Adding a new pattern
//!
//! 1. Pick the language file under `src/patterns/<lang>.rs`.
//! 2. Choose tier, category, severity per the rules above.
//! 3. Write the tree-sitter query — test with `cargo test --test pattern_tests`.
//! 4. Add a snippet to `tests/fixtures/patterns/<lang>/positive.<ext>`.
//! 5. Add the ID to the positive test assertion in `tests/pattern_tests.rs`.
pub mod c;
pub mod cpp;
mod go;
@ -9,6 +49,7 @@ mod ruby;
pub mod rust;
pub mod typescript;
use crate::evidence::Confidence;
use console::style;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
@ -16,7 +57,7 @@ use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum Severity {
High,
Medium,
@ -28,13 +69,14 @@ impl Severity {
///
/// Returns e.g. `"[HIGH] "` or `"[MEDIUM]"` — always 8 visible characters
/// so the column after the tag lines up regardless of severity.
#[allow(dead_code)] // public API for lib consumers
pub fn colored_tag(self) -> String {
// Visible widths: "[HIGH]" = 6, "[MEDIUM]" = 8, "[LOW]" = 5.
// Pad the *whole* tag to 8 visible chars (the longest, "[MEDIUM]").
let (label, styled_fn): (&str, fn(&str) -> String) = match self {
Severity::High => ("HIGH", |s| style(s).red().bold().to_string()),
Severity::Medium => ("MEDIUM", |s| style(s).yellow().bold().to_string()),
Severity::Low => ("LOW", |s| style(s).cyan().bold().to_string()),
Severity::Medium => ("MEDIUM", |s| style(s).color256(208).bold().to_string()),
Severity::Low => ("LOW", |s| style(s).color256(67).to_string()),
};
let bracket_len = label.len() + 2; // "[" + label + "]"
let pad = 8usize.saturating_sub(bracket_len);
@ -46,8 +88,8 @@ impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let styled = match *self {
Severity::High => style("HIGH").red().bold().to_string(),
Severity::Medium => style("MEDIUM").yellow().bold().to_string(),
Severity::Low => style("LOW").cyan().bold().to_string(),
Severity::Medium => style("MEDIUM").color256(208).bold().to_string(),
Severity::Low => style("LOW").color256(67).to_string(),
};
f.write_str(&styled)
}
@ -65,14 +107,132 @@ impl Severity {
}
impl FromStr for Severity {
// TODO: FIX
type Err = ();
type Err = String;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input.to_lowercase().as_str() {
"medium" => Ok(Severity::Medium),
"high" => Ok(Severity::High),
_ => Ok(Severity::Low),
match input.trim().to_ascii_uppercase().as_str() {
"HIGH" => Ok(Severity::High),
"MEDIUM" | "MED" => Ok(Severity::Medium),
"LOW" => Ok(Severity::Low),
other => Err(format!("unknown severity: '{other}'")),
}
}
}
/// A parsed severity filter expression.
///
/// Supports three forms:
/// - Single level: `"HIGH"` — matches only that level
/// - Comma list: `"HIGH,MEDIUM"` — matches any listed level
/// - Threshold: `">=MEDIUM"` — matches that level and above
///
/// Parsing is case-insensitive and tolerates whitespace around tokens.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SeverityFilter {
/// Match findings at or above this level (High >= Medium >= Low).
AtLeast(Severity),
/// Match findings whose severity is in this exact set.
AnyOf(Vec<Severity>),
}
impl SeverityFilter {
/// Parse a severity filter expression.
///
/// Examples: `"HIGH"`, `"high,medium"`, `">=MEDIUM"`, `">= low"`.
pub fn parse(expr: &str) -> Result<Self, String> {
let trimmed = expr.trim();
if trimmed.is_empty() {
return Err("empty severity expression".into());
}
// Threshold form: >=LEVEL
if let Some(rest) = trimmed.strip_prefix(">=") {
let level: Severity = rest.parse()?;
return Ok(SeverityFilter::AtLeast(level));
}
// Comma-separated list (also handles single value)
let levels: Result<Vec<Severity>, String> = trimmed
.split(',')
.map(|tok| tok.trim().parse::<Severity>())
.collect();
let levels = levels?;
if levels.is_empty() {
return Err("empty severity expression".into());
}
// Optimise single-value list
if levels.len() == 1 {
return Ok(SeverityFilter::AnyOf(levels));
}
Ok(SeverityFilter::AnyOf(levels))
}
/// Returns `true` if the given severity passes this filter.
pub fn matches(&self, sev: Severity) -> bool {
match self {
SeverityFilter::AtLeast(threshold) => {
// Severity ordering: High < Medium < Low (derived Ord).
// "at least Medium" means sev <= Medium in Ord terms.
sev <= *threshold
}
SeverityFilter::AnyOf(set) => set.contains(&sev),
}
}
}
/// Pattern confidence tier.
///
/// * **A** Structural presence alone is high-signal (e.g. `gets()`, `eval()`).
/// * **B** Requires a simple heuristic guard in the query (e.g. SQL with
/// concatenated arg, file-open with non-literal path).
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum PatternTier {
A,
B,
}
/// High-level finding category for noise reduction and prioritization.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum FindingCategory {
Security,
Reliability,
Quality,
}
impl std::fmt::Display for FindingCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FindingCategory::Security => write!(f, "Security"),
FindingCategory::Reliability => write!(f, "Reliability"),
FindingCategory::Quality => write!(f, "Quality"),
}
}
}
/// Vulnerability class that a pattern detects.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum PatternCategory {
CommandExec,
CodeExec,
Deserialization,
SqlInjection,
PathTraversal,
Xss,
Crypto,
Secrets,
InsecureTransport,
Reflection,
MemorySafety,
Prototype,
CodeQuality,
}
impl PatternCategory {
/// Map this vulnerability class to a high-level finding category.
pub fn finding_category(self) -> FindingCategory {
match self {
PatternCategory::CodeQuality => FindingCategory::Quality,
_ => FindingCategory::Security,
}
}
}
@ -80,7 +240,7 @@ impl FromStr for Severity {
/// One AST pattern with a tree-sitter query and meta-data.
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct Pattern {
/// Unique identifier (snake-case preferred).
/// Unique identifier — `<lang>.<category>.<specific>` preferred.
pub id: &'static str,
/// Human-readable explanation.
pub description: &'static str,
@ -88,6 +248,12 @@ pub struct Pattern {
pub query: &'static str,
/// Rough severity bucket.
pub severity: Severity,
/// Confidence tier (A = structural, B = heuristic-guarded).
pub tier: PatternTier,
/// Vulnerability class.
pub category: PatternCategory,
/// Confidence level for findings produced by this pattern.
pub confidence: Confidence,
}
/// Global, lazily-initialised registry: lang-name → pattern slice
@ -164,3 +330,66 @@ fn load_returns_correct_pattern_slices() {
assert!(load("brainfuck").is_empty());
}
#[test]
fn severity_from_str_rejects_unknown() {
assert!("garbage".parse::<Severity>().is_err());
}
#[test]
fn severity_filter_single() {
let f = SeverityFilter::parse("HIGH").unwrap();
assert!(f.matches(Severity::High));
assert!(!f.matches(Severity::Medium));
assert!(!f.matches(Severity::Low));
}
#[test]
fn severity_filter_comma_list() {
let f = SeverityFilter::parse("HIGH,MEDIUM").unwrap();
assert!(f.matches(Severity::High));
assert!(f.matches(Severity::Medium));
assert!(!f.matches(Severity::Low));
}
#[test]
fn severity_filter_threshold() {
let f = SeverityFilter::parse(">=MEDIUM").unwrap();
assert!(f.matches(Severity::High));
assert!(f.matches(Severity::Medium));
assert!(!f.matches(Severity::Low));
let f2 = SeverityFilter::parse(">=LOW").unwrap();
assert!(f2.matches(Severity::High));
assert!(f2.matches(Severity::Medium));
assert!(f2.matches(Severity::Low));
let f3 = SeverityFilter::parse(">=HIGH").unwrap();
assert!(f3.matches(Severity::High));
assert!(!f3.matches(Severity::Medium));
}
#[test]
fn severity_filter_case_insensitive_and_whitespace() {
let f = SeverityFilter::parse(" high , medium ").unwrap();
assert!(f.matches(Severity::High));
assert!(f.matches(Severity::Medium));
assert!(!f.matches(Severity::Low));
let f2 = SeverityFilter::parse(">= medium").unwrap();
assert!(f2.matches(Severity::High));
assert!(f2.matches(Severity::Medium));
}
#[test]
fn severity_filter_rejects_empty() {
assert!(SeverityFilter::parse("").is_err());
assert!(SeverityFilter::parse(" ").is_err());
}
#[test]
fn severity_filter_rejects_invalid_level() {
assert!(SeverityFilter::parse("CRITICAL").is_err());
assert!(SeverityFilter::parse("HIGH,CRITICAL").is_err());
assert!(SeverityFilter::parse(">=BOGUS").is_err());
}

View file

@ -1,40 +1,144 @@
use crate::patterns::{Pattern, Severity};
use crate::evidence::Confidence;
use crate::patterns::{Pattern, PatternCategory, PatternTier, Severity};
/// PHP AST patterns.
///
/// Taint rules cover `system`/`exec`/`passthru`/`shell_exec` (command
/// injection), `echo`/`print` (XSS sinks), and `mysqli_query`/`pg_query`
/// (SQL sinks). AST patterns here focus on **eval**, **deserialization**,
/// **deprecated dangerous functions**, **include with variable**, and
/// **SQL concatenation** (Tier B).
pub const PATTERNS: &[Pattern] = &[
// ── Tier A: Code execution ─────────────────────────────────────────
Pattern {
id: "eval_call",
description: "eval($code) execution",
query: "(function_call_expression function: (name) @n (#eq? @n \"eval\")) @vuln",
id: "php.code_exec.eval",
description: "eval() — dynamic code execution",
query: r#"(function_call_expression
function: (name) @n (#eq? @n "eval"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
Pattern {
id: "preg_replace_e",
description: "preg_replace with deprecated /e modifier",
query: "(function_call_expression function: (name) @n (#eq? @n \"preg_replace\") arguments: (arguments (string) @pat (#match? @pat \"/.*e.*$/\"))) @vuln",
id: "php.code_exec.create_function",
description: "create_function() — deprecated eval-like constructor",
query: r#"(function_call_expression
function: (name) @n (#eq? @n "create_function"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
Pattern {
id: "create_function",
description: "create_function(...) anonymous eval-like",
query: "(function_call_expression function: (name) @n (#eq? @n \"create_function\")) @vuln",
severity: Severity::Medium,
},
Pattern {
id: "unserialize_call",
description: "unserialize(...) on user input",
query: "(function_call_expression function: (name) @n (#eq? @n \"unserialize\")) @vuln",
id: "php.code_exec.preg_replace_e",
description: "preg_replace with /e modifier — code execution via regex",
query: r#"(function_call_expression
function: (name) @n (#eq? @n "preg_replace")
arguments: (arguments
(argument
(string) @pat (#match? @pat "/[^/]*/[a-zA-Z]*e"))))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
Pattern {
id: "mysql_query_concat",
description: "mysql_query with concatenated SQL",
query: "(function_call_expression function: (name) @n (#eq? @n \"mysql_query\") arguments: (arguments (binary_expression) @concat)) @vuln",
id: "php.code_exec.assert_string",
description: "assert() with string argument — evaluates PHP code",
query: r#"(function_call_expression
function: (name) @n (#eq? @n "assert")
arguments: (arguments
(argument (string) @code)))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
// ── Tier A: Command execution ──────────────────────────────────────
Pattern {
id: "php.cmdi.system",
description: "system/shell_exec/exec/passthru — shell command execution",
query: r#"(function_call_expression
function: (name) @n (#match? @n "^(system|shell_exec|exec|passthru|proc_open|popen)$"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CommandExec,
confidence: Confidence::High,
},
// ── Tier A: Deserialization ────────────────────────────────────────
Pattern {
id: "php.deser.unserialize",
description: "unserialize() — PHP object injection",
query: r#"(function_call_expression
function: (name) @n (#eq? @n "unserialize"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::Deserialization,
confidence: Confidence::High,
},
// ── Tier B: SQL injection (concatenation heuristic) ────────────────
Pattern {
id: "php.sqli.query_concat",
description: "mysql_query/mysqli_query with concatenated SQL string",
query: r#"(function_call_expression
function: (name) @n (#match? @n "^(mysql_query|mysqli_query)$")
arguments: (arguments
(argument (binary_expression) @concat)))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::B,
category: PatternCategory::SqlInjection,
confidence: Confidence::Medium,
},
// ── Tier B: Path traversal (include with variable) ─────────────────
Pattern {
id: "php.path.include_variable",
description: "include/require with variable path — file inclusion vulnerability",
query: r#"(include_expression (variable_name)) @vuln"#,
severity: Severity::High,
tier: PatternTier::B,
category: PatternCategory::PathTraversal,
confidence: Confidence::Medium,
},
// ── Tier A: Crypto ─────────────────────────────────────────────────
Pattern {
id: "php.crypto.md5",
description: "md5() — weak hash function",
query: r#"(function_call_expression
function: (name) @n (#eq? @n "md5"))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
Pattern {
id: "system_call",
description: "system()/shell_exec()/exec() command execution",
query: "(function_call_expression function: (name) @n (#match? @n \"system|shell_exec|exec|passthru\")) @vuln",
severity: Severity::Medium,
id: "php.crypto.sha1",
description: "sha1() — weak hash function",
query: r#"(function_call_expression
function: (name) @n (#eq? @n "sha1"))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
Pattern {
id: "php.crypto.rand",
description: "rand()/mt_rand() — not cryptographically secure",
query: r#"(function_call_expression
function: (name) @n (#match? @n "^(rand|mt_rand)$"))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
];

View file

@ -1,22 +1,178 @@
use crate::patterns::{Pattern, Severity};
use crate::evidence::Confidence;
use crate::patterns::{Pattern, PatternCategory, PatternTier, Severity};
/// Python AST patterns.
///
/// Taint rules cover `eval`/`exec`, `os.system`/`os.popen`/`subprocess.*`,
/// and `cursor.execute`. AST patterns here add coverage for **deserialization**,
/// **subprocess shell=True** (Tier B — taint doesn't check keyword args), and
/// **code execution** sinks that taint cannot structurally verify.
pub const PATTERNS: &[Pattern] = &[
// ── Tier A: Code execution ─────────────────────────────────────────
Pattern {
id: "eval_call",
description: "eval() on dynamic input",
query: "(call function: (identifier) @id (#eq? @id \"eval\")) @vuln",
id: "py.code_exec.eval",
description: "eval() — dynamic code execution",
query: r#"(call function: (identifier) @id (#eq? @id "eval")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
Pattern {
id: "exec_call",
description: "exec(...) execution of dynamic code",
query: "(call function: (identifier) @id (#eq? @id \"exec\")) @vuln",
id: "py.code_exec.exec",
description: "exec() — dynamic code execution",
query: r#"(call function: (identifier) @id (#eq? @id "exec")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
Pattern {
id: "subprocess_shell_true",
description: "subprocess.* with shell=True",
query: "(call function: (attribute object: (identifier) @pkg (#eq? @pkg \"subprocess\")) arguments: (argument_list . (keyword_argument name: (identifier) @k (#eq? @k \"shell\")) (true) @val)) @vuln",
id: "py.code_exec.compile",
description: "compile() with exec/eval mode — code compilation from string",
query: r#"(call function: (identifier) @id (#eq? @id "compile")) @vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
// ── Tier A: Command execution ──────────────────────────────────────
Pattern {
id: "py.cmdi.os_system",
description: "os.system() — shell command execution",
query: r#"(call
function: (attribute
object: (identifier) @pkg (#eq? @pkg "os")
attribute: (identifier) @fn (#eq? @fn "system")))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CommandExec,
confidence: Confidence::High,
},
Pattern {
id: "py.cmdi.os_popen",
description: "os.popen() — shell command execution",
query: r#"(call
function: (attribute
object: (identifier) @pkg (#eq? @pkg "os")
attribute: (identifier) @fn (#eq? @fn "popen")))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CommandExec,
confidence: Confidence::High,
},
// ── Tier B: subprocess with shell=True ─────────────────────────────
Pattern {
id: "py.cmdi.subprocess_shell",
description: "subprocess call with shell=True",
query: r#"(call
function: (attribute
object: (identifier) @pkg (#eq? @pkg "subprocess"))
arguments: (argument_list
(keyword_argument
name: (identifier) @k (#eq? @k "shell")
value: (true))))
@vuln"#,
severity: Severity::High,
tier: PatternTier::B,
category: PatternCategory::CommandExec,
confidence: Confidence::Medium,
},
// ── Tier A: Deserialization ────────────────────────────────────────
Pattern {
id: "py.deser.pickle_loads",
description: "pickle.loads/load — arbitrary object deserialization",
query: r#"(call
function: (attribute
object: (identifier) @pkg (#eq? @pkg "pickle")
attribute: (identifier) @fn (#match? @fn "^loads?$")))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::Deserialization,
confidence: Confidence::High,
},
Pattern {
id: "py.deser.yaml_load",
description: "yaml.load() without SafeLoader — arbitrary object instantiation",
query: r#"(call
function: (attribute
object: (identifier) @pkg (#eq? @pkg "yaml")
attribute: (identifier) @fn (#eq? @fn "load")))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::Deserialization,
confidence: Confidence::High,
},
Pattern {
id: "py.deser.shelve_open",
description: "shelve.open() — pickle-backed deserialization",
query: r#"(call
function: (attribute
object: (identifier) @pkg (#eq? @pkg "shelve")
attribute: (identifier) @fn (#eq? @fn "open")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Deserialization,
confidence: Confidence::High,
},
// ── Tier B: SQL injection (format/concat heuristic) ────────────────
Pattern {
id: "py.sqli.execute_format",
description: "cursor.execute with string concatenation — SQL injection risk",
query: r#"(call
function: (attribute
attribute: (identifier) @fn (#eq? @fn "execute"))
arguments: (argument_list
(binary_operator) @arg))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::B,
category: PatternCategory::SqlInjection,
confidence: Confidence::Medium,
},
// ── Tier A: Weak crypto ────────────────────────────────────────────
Pattern {
id: "py.crypto.md5",
description: "hashlib.md5() — weak hash algorithm",
query: r#"(call
function: (attribute
object: (identifier) @pkg (#eq? @pkg "hashlib")
attribute: (identifier) @fn (#eq? @fn "md5")))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
Pattern {
id: "py.crypto.sha1",
description: "hashlib.sha1() — weak hash algorithm",
query: r#"(call
function: (attribute
object: (identifier) @pkg (#eq? @pkg "hashlib")
attribute: (identifier) @fn (#eq? @fn "sha1")))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
// ── Tier A: Template injection ─────────────────────────────────────
Pattern {
id: "py.xss.jinja_from_string",
description: "jinja2.Template from string — potential template injection",
query: r#"(call
function: (attribute
attribute: (identifier) @fn (#eq? @fn "from_string")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Xss,
confidence: Confidence::High,
},
];

View file

@ -1,133 +1,141 @@
use crate::patterns::{Pattern, Severity};
use crate::evidence::Confidence;
use crate::patterns::{Pattern, PatternCategory, PatternTier, Severity};
/// Ruby AST patterns.
///
/// Taint rules cover `system`/`exec` (command injection), `eval` (code
/// execution), and `puts`/`print` (output sinks). AST patterns here focus on
/// **deserialization** (YAML.load, Marshal.load), **instance_eval/class_eval**,
/// **backtick shell**, **send with dynamic arg**, and **constantize**.
pub const PATTERNS: &[Pattern] = &[
// ---------- Runtime code-execution primitives ----------
// ── Tier A: Code execution ─────────────────────────────────────────
Pattern {
id: "eval_call",
description: "Kernel#eval usage",
query: r#"
(call
(identifier) @id
(#eq? @id "eval")
) @vuln
"#,
id: "rb.code_exec.eval",
description: "Kernel#eval — dynamic code execution",
query: r#"(call (identifier) @id (#eq? @id "eval")) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
Pattern {
id: "instance_eval_call",
description: "Object#instance_eval usage",
query: r#"
(call
(identifier) @id
(#eq? @id "instance_eval")
) @vuln
"#,
id: "rb.code_exec.instance_eval",
description: "instance_eval — evaluates string in object context",
query: r#"(call
method: (identifier) @id (#eq? @id "instance_eval"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
Pattern {
id: "class_eval_call",
description: "Module#class_eval / module_eval usage",
query: r#"
(call
(identifier) @id
(#match? @id "^(class_eval|module_eval)$")
) @vuln
"#,
id: "rb.code_exec.class_eval",
description: "class_eval / module_eval — evaluates string in class context",
query: r#"(call
method: (identifier) @id (#match? @id "^(class_eval|module_eval)$"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
// ---------- Shell execution ----------
// ── Tier A: Command execution ──────────────────────────────────────
Pattern {
id: "system_exec_interp",
description: "system/exec with string interpolation",
query: r#"
(call
method: (identifier) @m
(#match? @m "^(system|exec)$")
arguments: (argument_list
(string
(interpolation)+ @vuln
)
)
)
"#,
id: "rb.cmdi.backtick",
description: "Backtick shell execution",
query: r#"(subshell) @vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CommandExec,
confidence: Confidence::High,
},
// ── Tier A: Shell execution ─────────────────────────────────────────
Pattern {
id: "rb.cmdi.system_interp",
description: "system/exec call — command execution risk",
query: r#"(call
method: (identifier) @m (#match? @m "^(system|exec)$"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CommandExec,
confidence: Confidence::High,
},
// ── Tier A: Deserialization ────────────────────────────────────────
Pattern {
id: "rb.deser.yaml_load",
description: "YAML.load — arbitrary object deserialization (use safe_load instead)",
query: r#"(call
receiver: (constant) @recv (#match? @recv "^(YAML|Psych)$")
method: (identifier) @m (#eq? @m "load"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::Deserialization,
confidence: Confidence::High,
},
Pattern {
id: "backtick_command",
description: "Back-tick shell execution",
// `uname -a`
query: r#"(shell_command) @vuln"#,
id: "rb.deser.marshal_load",
description: "Marshal.load — arbitrary Ruby object deserialization",
query: r#"(call
receiver: (constant) @recv (#eq? @recv "Marshal")
method: (identifier) @m (#eq? @m "load"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::Deserialization,
confidence: Confidence::High,
},
// ---------- Dangerous deserialisation ----------
// ── Tier A: Reflection ─────────────────────────────────────────────
Pattern {
id: "yaml_load",
description: "YAML.load / Psych.load (arbitrary object deserialisation)",
query: r#"
(call
receiver: (constant) @recv
(#match? @recv "^(YAML|Psych)$")
method: (identifier) @m
(#eq? @m "load")
) @vuln
"#,
severity: Severity::High,
},
Pattern {
id: "marshal_load",
description: "Marshal.load usage",
query: r#"
(call
receiver: (constant) @recv
(#eq? @recv "Marshal")
method: (identifier) @m
(#eq? @m "load")
) @vuln
"#,
severity: Severity::High,
},
// ---------- Reflection / meta-programming ----------
Pattern {
id: "send_dynamic",
description: "send() with dynamic first argument (not a literal symbol)",
query: r#"
(call
method: (identifier) @m
(#eq? @m "send")
arguments: (argument_list
[
(identifier) ; send(method_name_var, )
(string (interpolation)+) ; send("user_#{role}", )
] @vuln
)
)
id: "rb.reflection.send_dynamic",
description: "send() with non-symbol argument — arbitrary method dispatch",
query: r#"(call
method: (identifier) @m (#eq? @m "send")
arguments: (argument_list
[(identifier) (string (interpolation)+)] @vuln))
"#,
severity: Severity::Medium,
tier: PatternTier::B,
category: PatternCategory::Reflection,
confidence: Confidence::Medium,
},
Pattern {
id: "constantize_call",
description: "ActiveSupport constantize / safe_constantize on tainted data",
query: r#"
(call
method: (identifier) @m
(#match? @m "^(constantize|safe_constantize)$")
) @vuln
"#,
id: "rb.reflection.constantize",
description: "constantize / safe_constantize — dynamic class resolution",
query: r#"(call
method: (identifier) @m (#match? @m "^(constantize|safe_constantize)$"))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Reflection,
confidence: Confidence::High,
},
// ---------- Insecure resource access ----------
// ── Tier A: SSRF ───────────────────────────────────────────────────
Pattern {
id: "open_uri_http",
description: "Kernel#open with HTTP(S) URL (open-uri auto-follow)",
query: r#"
(call
method: (identifier) @m
(#eq? @m "open")
arguments: (argument_list
(string) @url
(#match? @url "^\"https?://")
)
) @vuln
"#,
id: "rb.ssrf.open_uri",
description: "Kernel#open with HTTP URL — SSRF via open-uri",
query: r#"(call
method: (identifier) @m (#eq? @m "open")
arguments: (argument_list
(string) @url (#match? @url "^\"https?://")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::InsecureTransport,
confidence: Confidence::High,
},
// ── Tier A: Crypto ─────────────────────────────────────────────────
Pattern {
id: "rb.crypto.md5",
description: "Digest::MD5 — weak hash algorithm",
query: r#"(scope_resolution
name: (constant) @c (#eq? @c "MD5"))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
];

View file

@ -1,118 +1,170 @@
use crate::patterns::{Pattern, Severity};
use crate::evidence::Confidence;
use crate::patterns::{Pattern, PatternCategory, PatternTier, Severity};
/// Rust AST patterns.
///
/// Rust taint rules already cover `Command::new`/`arg`/`status`/`output` sinks
/// and `env::var` / `fs::read_to_string` sources, so we do NOT duplicate those.
/// Patterns here focus on **unsafe memory**, **panicking APIs**, and structural
/// code-quality signals specific to Rust.
pub const PATTERNS: &[Pattern] = &[
// ── Tier A: Memory Safety (unsafe) ─────────────────────────────────
Pattern {
id: "unsafe_block",
description: "Use of an `unsafe` block",
id: "rs.memory.transmute",
description: "std::mem::transmute — unchecked type reinterpretation",
query: r#"(call_expression
function: (scoped_identifier
path: (identifier) @p (#eq? @p "mem")
name: (identifier) @f (#eq? @f "transmute")))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "rs.memory.copy_nonoverlapping",
description: "ptr::copy_nonoverlapping — raw pointer memcpy",
query: r#"(call_expression
function: (scoped_identifier
path: (identifier) @p (#eq? @p "ptr")
name: (identifier) @f (#eq? @f "copy_nonoverlapping")))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "rs.memory.get_unchecked",
description: "get_unchecked / get_unchecked_mut — unchecked indexing",
query: r#"(call_expression
function: (field_expression
field: (field_identifier) @m
(#match? @m "^get_unchecked(_mut)?$")))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "rs.memory.mem_zeroed",
description: "std::mem::zeroed — zero-initialised memory may be UB for non-POD types",
query: r#"(call_expression
function: (scoped_identifier
path: (identifier) @p (#eq? @p "mem")
name: (identifier) @n (#eq? @n "zeroed")))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "rs.memory.ptr_read",
description: "ptr::read / ptr::read_volatile — raw pointer dereference",
query: r#"(call_expression
function: (scoped_identifier
path: (identifier) @p (#eq? @p "ptr")
name: (identifier) @n (#match? @n "^read(_volatile)?$")))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
// ── Tier A: Code quality / robustness ──────────────────────────────
Pattern {
id: "rs.quality.unsafe_block",
description: "unsafe block — manual memory safety obligation",
query: "(unsafe_block) @vuln",
severity: Severity::High,
},
Pattern {
id: "unsafe_fn",
description: "`unsafe fn` declaration",
query: "(function_item
(function_modifiers) @mods
(#match? @mods \"^unsafe\\b\")) @vuln",
severity: Severity::High,
},
Pattern {
id: "transmute_call",
description: "`std::mem::transmute` call",
query: "(call_expression
function: (scoped_identifier
path: (identifier) @p (#eq? @p \"mem\")
name: (identifier) @f (#eq? @f \"transmute\")))
@vuln",
severity: Severity::High,
},
Pattern {
id: "copy_nonoverlapping",
description: "Raw pointer `copy_nonoverlapping`",
query: "(call_expression
function: (scoped_identifier
path: (identifier) @p (#eq? @p \"ptr\")
name: (identifier) @f (#eq? @f \"copy_nonoverlapping\")))
@vuln",
severity: Severity::High,
},
Pattern {
id: "get_unchecked",
description: "`get_unchecked` / `get_unchecked_mut` slice access",
query: "(call_expression
function: (field_expression
field: (field_identifier) @m
(#match? @m \"get_unchecked(_mut)?\"))) @vuln",
severity: Severity::High,
},
Pattern {
id: "unwrap_call",
description: "`.unwrap()` call (may panic)",
query: "(call_expression
function: (field_expression
field: (field_identifier) @name
(#eq? @name \"unwrap\"))) ; exact match
@vuln",
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "expect_call",
description: "`.expect()` call (may panic)",
query: "(call_expression
function: (field_expression
field: (field_identifier) @name
(#eq? @name \"expect\"))) @vuln",
id: "rs.quality.unsafe_fn",
description: "unsafe fn declaration",
query: r#"(function_item
(function_modifiers) @mods
(#match? @mods "^unsafe"))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
Pattern {
id: "panic_macro",
description: "`panic!` macro invocation",
query: "(macro_invocation (identifier) @id (#eq? @id \"panic\")) @vuln",
severity: Severity::Medium,
},
Pattern {
id: "todo_or_unimplemented",
description: "`todo!()` / `unimplemented!()` placeholder",
query: "(macro_invocation
(identifier) @id
(#match? @id \"todo|unimplemented\")) @vuln",
id: "rs.quality.unwrap",
description: ".unwrap() — panics on None/Err",
query: r#"(call_expression
function: (field_expression
field: (field_identifier) @name (#eq? @name "unwrap")))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::CodeQuality,
confidence: Confidence::High,
},
Pattern {
id: "narrow_cast_with_as",
description: "`as` cast to an 8-/16-bit integer (possible truncation)",
query: "(type_cast_expression
type: (primitive_type) @to
(#match? @to \"^u?i(8|16)$\")) @vuln",
id: "rs.quality.expect",
description: ".expect() — panics on None/Err",
query: r#"(call_expression
function: (field_expression
field: (field_identifier) @name (#eq? @name "expect")))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::CodeQuality,
confidence: Confidence::High,
},
Pattern {
id: "mem_zeroed",
description: "`std::mem::zeroed()`",
query: "(call_expression function:(scoped_identifier path:(identifier)@p (#eq? @p \"mem\") name:(identifier)@n (#eq? @n \"zeroed\")))@vuln",
severity: Severity::High,
},
Pattern {
id: "mem_forget",
description: "`std::mem::forget()`",
query: "(call_expression function:(scoped_identifier path:(identifier)@p (#eq? @p \"mem\") name:(identifier)@n (#eq? @n \"forget\")))@vuln",
severity: Severity::Medium,
},
Pattern {
id: "ptr_read",
description: "`ptr::read_*` raw-ptr read",
query: "(call_expression function:(scoped_identifier path:(identifier)@p (#eq? @p \"ptr\") name:(identifier)@n (#match? @n \"read(_volatile)?\")))@vuln",
severity: Severity::High,
},
Pattern {
id: "arc_unwrap",
description: "`Arc::unwrap_or_else_unchecked`",
query: "(call_expression function:(scoped_identifier name:(identifier)@n (#eq? @n \"unwrap_or_else_unchecked\")))@vuln",
severity: Severity::High,
},
Pattern {
id: "dbg_macro",
description: "`dbg!()` left in code",
query: "(macro_invocation (identifier)@id (#eq? @id \"dbg\"))@vuln",
id: "rs.quality.panic_macro",
description: "panic! macro invocation",
query: r#"(macro_invocation (identifier) @id (#eq? @id "panic")) @vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::CodeQuality,
confidence: Confidence::High,
},
Pattern {
id: "rs.quality.todo",
description: "todo!() / unimplemented!() placeholder left in code",
query: r#"(macro_invocation
(identifier) @id
(#match? @id "^(todo|unimplemented)$"))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::CodeQuality,
confidence: Confidence::High,
},
// ── Tier A: Narrowing cast ─────────────────────────────────────────
Pattern {
id: "rs.memory.narrow_cast",
description: "`as` cast to 8/16-bit integer — possible truncation",
query: r#"(type_cast_expression
type: (primitive_type) @to
(#match? @to "^(u8|i8|u16|i16)$"))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::Medium,
},
Pattern {
id: "rs.memory.mem_forget",
description: "std::mem::forget — may leak resources",
query: r#"(call_expression
function: (scoped_identifier
path: (identifier) @p (#eq? @p "mem")
name: (identifier) @n (#eq? @n "forget")))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::MemorySafety,
confidence: Confidence::High,
},
];

View file

@ -1,100 +1,157 @@
use crate::patterns::{Pattern, Severity};
use crate::evidence::Confidence;
use crate::patterns::{Pattern, PatternCategory, PatternTier, Severity};
/// TypeScript AST patterns.
///
/// TypeScript shares most patterns with JavaScript. Taint rules cover `eval`,
/// `innerHTML`, and `child_process.*` sinks. AST patterns here mirror JS
/// patterns plus TS-specific `any` type-safety escapes.
pub const PATTERNS: &[Pattern] = &[
// ── Tier A: Code execution ─────────────────────────────────────────
Pattern {
id: "eval_call",
description: "Use of eval()",
query: "(call_expression function: (identifier) @id (#eq? @id \"eval\")) @vuln",
id: "ts.code_exec.eval",
description: "eval() — dynamic code execution",
query: r#"(call_expression
function: (identifier) @id (#eq? @id "eval"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
Pattern {
id: "new_function",
description: "new Function() constructor",
query: "(new_expression constructor: (identifier) @id (#eq? @id \"Function\")) @vuln",
id: "ts.code_exec.new_function",
description: "new Function() constructor — eval equivalent",
query: r#"(new_expression
constructor: (identifier) @id (#eq? @id "Function"))
@vuln"#,
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
Pattern {
id: "document_write",
description: "document.write() call",
query: "(call_expression function: (member_expression object: (identifier) @obj (#eq? @obj \"document\") property: (property_identifier) @prop (#eq? @prop \"write\"))) @vuln",
id: "ts.code_exec.settimeout_string",
description: "setTimeout/setInterval with string argument — implicit eval",
query: r#"(call_expression
function: (identifier) @id (#match? @id "^(setTimeout|setInterval)$")
arguments: (arguments (string) @code))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::CodeExec,
confidence: Confidence::High,
},
// ── Tier A: XSS sinks ──────────────────────────────────────────────
Pattern {
id: "settimeout_string",
description: "setTimeout / setInterval with a string argument",
query: "(call_expression function: (identifier) @id (#match? @id \"setTimeout|setInterval\") arguments: (arguments (string) @code . _)) @vuln",
id: "ts.xss.document_write",
description: "document.write() — XSS sink",
query: r#"(call_expression
function: (member_expression
object: (identifier) @obj (#eq? @obj "document")
property: (property_identifier) @prop (#match? @prop "^(write|writeln)$")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Xss,
confidence: Confidence::High,
},
Pattern {
id: "any_type",
description: "Type annotation of `any`",
query: "(type_annotation (predefined_type) @t (#eq? @t \"any\")) @vuln",
severity: Severity::Low,
},
Pattern {
id: "json_parse",
description: "JSON.parse on dynamic string",
query: "(call_expression function: (member_expression object: (identifier) @obj (#eq? @obj \"JSON\") property: (property_identifier) @prop (#eq? @prop \"parse\"))) @vuln",
severity: Severity::Low,
},
Pattern {
id: "as_any_assertion",
description: "Type assertion to `any` using `as any`",
query: "(as_expression type: (predefined_type) @t (#eq? @t \"any\")) @vuln",
severity: Severity::Low,
},
Pattern {
id: "type_assertion_any",
description: "Type assertion to `any` using `<any>` syntax",
query: "(type_assertion type: (predefined_type) @t (#eq? @t \"any\")) @vuln",
severity: Severity::Low,
},
Pattern {
id: "outer_html_assignment",
description: "Assignment to element.outerHTML",
query: "(assignment_expression left: (member_expression property: (property_identifier) @prop (#eq? @prop \"outerHTML\"))) @vuln",
id: "ts.xss.outer_html",
description: "Assignment to .outerHTML — XSS sink",
query: r#"(assignment_expression
left: (member_expression
property: (property_identifier) @prop (#eq? @prop "outerHTML")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Xss,
confidence: Confidence::High,
},
Pattern {
id: "insert_adjacent_html",
description: "insertAdjacentHTML() call",
query: "(call_expression function: (member_expression property: (property_identifier) @prop (#eq? @prop \"insertAdjacentHTML\"))) @vuln",
id: "ts.xss.insert_adjacent_html",
description: "insertAdjacentHTML() — XSS sink",
query: r#"(call_expression
function: (member_expression
property: (property_identifier) @prop (#eq? @prop "insertAdjacentHTML")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Xss,
confidence: Confidence::High,
},
// ── Tier A: Weak crypto ────────────────────────────────────────────
Pattern {
id: "ts.crypto.math_random",
description: "Math.random() — not cryptographically secure",
query: r#"(call_expression
function: (member_expression
object: (identifier) @obj (#eq? @obj "Math")
property: (property_identifier) @prop (#eq? @prop "random")))
@vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::Crypto,
confidence: Confidence::Medium,
},
// ── Tier A: TypeScript-specific type-safety escapes ────────────────
Pattern {
id: "ts.quality.any_annotation",
description: "Type annotation of `any` — disables type checking",
query: r#"(type_annotation (predefined_type) @t (#eq? @t "any")) @vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::CodeQuality,
confidence: Confidence::Medium,
},
Pattern {
id: "document_cookie_write",
id: "ts.quality.as_any",
description: "Type assertion `as any` — type-safety escape hatch",
query: r#"(as_expression (predefined_type) @t (#eq? @t "any")) @vuln"#,
severity: Severity::Low,
tier: PatternTier::A,
category: PatternCategory::CodeQuality,
confidence: Confidence::Medium,
},
// ── Tier A: Prototype pollution ────────────────────────────────────
Pattern {
id: "ts.prototype.proto_assignment",
description: "Assignment to __proto__ — prototype pollution",
query: r#"(assignment_expression
left: (member_expression
property: (property_identifier) @prop (#eq? @prop "__proto__")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Prototype,
confidence: Confidence::High,
},
// ── Tier A: Open redirect ──────────────────────────────────────────
Pattern {
id: "ts.xss.location_assign",
description: "Assignment to location/location.href — open redirect",
query: r#"(assignment_expression
left: (member_expression
object: (identifier) @obj (#match? @obj "^(window|location|document)$")
property: (property_identifier) @prop (#match? @prop "^(location|href)$")))
@vuln"#,
severity: Severity::Medium,
tier: PatternTier::A,
category: PatternCategory::Xss,
confidence: Confidence::High,
},
// ── Tier A: Cookie manipulation ────────────────────────────────────
Pattern {
id: "ts.xss.cookie_write",
description: "Write to document.cookie",
query: "(assignment_expression left: (member_expression object: (identifier) @obj (#eq? @obj \"document\") property: (property_identifier) @prop (#eq? @prop \"cookie\"))) @vuln",
query: r#"(assignment_expression
left: (member_expression
object: (identifier) @obj (#eq? @obj "document")
property: (property_identifier) @prop (#eq? @prop "cookie")))
@vuln"#,
severity: Severity::Low,
},
Pattern {
id: "onclick_setattribute",
description: "Element.setAttribute('onclick', …)",
query: "(call_expression function: (member_expression property: (property_identifier) @prop (#eq? @prop \"setAttribute\")) arguments: (arguments (string) @name (#eq? @name \"\\\"onclick\\\"\") . (string) @handler)) @vuln",
severity: Severity::Medium,
},
Pattern {
id: "math_random_call",
description: "Use of Math.random() for security-sensitive randomness",
query: "(call_expression function: (member_expression object: (identifier) @obj (#eq? @obj \"Math\") property: (property_identifier) @prop (#eq? @prop \"random\"))) @vuln",
severity: Severity::Low,
},
Pattern {
id: "crypto_createhash_md5",
description: "Insecure hash algorithm: crypto.createHash('md5')",
query: "(call_expression function: (member_expression object: (identifier) @obj (#eq? @obj \"crypto\") property: (property_identifier) @prop (#eq? @prop \"createHash\")) arguments: (arguments (string) @alg (#match? @alg \"(?i)\\\"md5\\\"\"))) @vuln",
severity: Severity::Medium,
},
Pattern {
id: "fetch_http_url",
description: "fetch() over plain HTTP",
query: "(call_expression function: (identifier) @id (#eq? @id \"fetch\") arguments: (arguments (string) @url (#match? @url \"^\\\"http://\"))) @vuln",
severity: Severity::Low,
},
Pattern {
id: "xhr_eval_response",
description: "eval() of XMLHttpRequest.responseText",
query: "(call_expression function: (identifier) @id (#eq? @id \"eval\") arguments: (arguments (member_expression property: (property_identifier) @prop (#eq? @prop \"responseText\")))) @vuln",
severity: Severity::High,
tier: PatternTier::A,
category: PatternCategory::Xss,
confidence: Confidence::Medium,
},
];