mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
Phase 1 (#33)
* 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:
parent
19b578c5c4
commit
1bbe4b1cfb
456 changed files with 25628 additions and 1228 deletions
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue