mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 04:29:27 +00:00
feat(ruleset): tag framework-specific knowledge as optional priors (decision-engine task 2)
ruleset/frameworks.toml lists 19 framework and library ids (wordpress, flask, django, express,
spring, jakarta-servlet, react, vue and 11 libraries), each with its languages, dependency
package names and a symbol lexicon used only by the lint and for stratification.
Every framework/library entry of section 2 is tagged `framework`/`library`, and mixed entries
are split so a tag covers a whole entry (split pieces keep the entry's id and flags, in place):
semantic facts 38 tagged entries (wordpress 6, django 6, flask 4, express 4, jakarta-servlet 3,
pyyaml 2, one each for spring, sqlalchemy, requests, httpx, lodash, node-http-clients,
escape-html, sanitize-html, dompurify, esapi, commons-text, react, vue); the dispatch fact
behind the hook-as-source path is wordpress; the PHP layout row is renamed php_layout and stays
untagged (a product filter). Quick rules: 9 tagged (wordpress 6, flask, pyyaml,
node-http-clients); php-unserialize is split into php-unserialize and php-maybe-unserialize;
the lint also caught WordPress escapers in exclusions and axios/yaml.load in mixed rules, so
those keep their ids as tagged rules and gain a `fallback_for` language-level rule (6) that is
active only when the prior is off.
load_facts and load_ruleset take priors ("all" by default -- today's scan --, "off", or a set
of ids). tests/test_semantic_priors.py: the lint (no untagged entry contains a lexicon symbol
of its language), "off" removes exactly the tagged entries and activates the fallbacks, split
entries keep their language-level calls. Rule and layout counts in three existing tests follow the split and the rename.
Unchanged defaults: test_benchmark_output_is_byte_identical_to_golden_baseline passes, and the
quick findings of all 15 benchmark manifests (full StaticFinding records) are byte-identical
before and after. priors=off on the same manifests: 61 -> 57 findings; the 4 lost are js-ssrf
(axios), php-wpdb-sql-composition, python-ssti and python yaml.load; 5 PHP and 1 Python finding
move to their fallback rule at the same location. Semantic calls with priors off: php 75/92,
python 32/77, javascript 42/70, java 24/41, c 27/27 (the engine-side effect needs Joern and was
not measured).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d1896cb8e7
commit
af81bccc70
@@ -26,6 +26,7 @@ status = "enabled"
|
||||
min_evidence_level = "static_corroboration"
|
||||
precision_estimate = 0.032
|
||||
version = "1"
|
||||
framework = "wordpress"
|
||||
pattern = '''\$wpdb->(get_var|get_results|get_row|get_col|query|replace)\s*\(\s*(?!\$wpdb->prepare)(?:"[^"\n]*\{?\$(?!wpdb->)[A-Za-z_]|[^\n;]*?["']\s*\.\s*\$(?!wpdb->)[A-Za-z_]|[^\n;]*?\$_(?:GET|POST|REQUEST|COOKIE)\b)'''
|
||||
|
||||
[[rule]]
|
||||
@@ -54,8 +55,23 @@ status = "enabled"
|
||||
min_evidence_level = "static_corroboration"
|
||||
precision_estimate = 0.034
|
||||
version = "1"
|
||||
framework = "wordpress"
|
||||
pattern = '''(?<![$\w>])(?=(VALUES|IN|ORDER|GROUP|LIMIT|OFFSET|WHERE|AND|OR|SET|HAVING)\b)(?:(?:VALUES|IN)\s*\(|(?:ORDER|GROUP)\s+BY\b[^"'\n]*|(?:LIMIT|OFFSET)\s+(?:\d+\s*,\s*)?|(?:WHERE|AND|OR|SET|HAVING)\s+[\w.`]+\s*(?:=|<>|!=|<=|>=|<|>|LIKE)\s*)["']\s*\.\s*\$(?!wpdb->)(?!\w*(?:format|placeholder))[A-Za-z_]'''
|
||||
|
||||
# Active only when the rule above is dropped (priors off); status and precision are the tagged rule's measurement.
|
||||
[[rule]]
|
||||
rule_id = "php-sql-unquoted-concat-language"
|
||||
title = "PHP SQL literal concatenates a variable into an unquoted position (language-level)"
|
||||
languages = ["php"]
|
||||
cwe = "CWE-89"
|
||||
tags = ["injection"]
|
||||
status = "enabled"
|
||||
min_evidence_level = "static_corroboration"
|
||||
precision_estimate = 0.034
|
||||
version = "1"
|
||||
fallback_for = "php-sql-unquoted-concat"
|
||||
pattern = '''(?<![$\w>])(?=(VALUES|IN|ORDER|GROUP|LIMIT|OFFSET|WHERE|AND|OR|SET|HAVING)\b)(?:(?:VALUES|IN)\s*\(|(?:ORDER|GROUP)\s+BY\b[^"'\n]*|(?:LIMIT|OFFSET)\s+(?:\d+\s*,\s*)?|(?:WHERE|AND|OR|SET|HAVING)\s+[\w.`]+\s*(?:=|<>|!=|<=|>=|<|>|LIKE)\s*)["']\s*\.\s*\$(?!\w*(?:format|placeholder))[A-Za-z_]'''
|
||||
|
||||
[[rule]]
|
||||
rule_id = "php-sql-unquoted-interpolation"
|
||||
title = "PHP SQL string interpolates a variable into an unquoted position"
|
||||
@@ -66,8 +82,23 @@ status = "enabled"
|
||||
min_evidence_level = "static_corroboration"
|
||||
precision_estimate = 0.129
|
||||
version = "1"
|
||||
framework = "wordpress"
|
||||
pattern = '''(?<![$\w>])(?=(VALUES|IN|ORDER|GROUP|LIMIT|OFFSET|WHERE|AND|OR|SET|HAVING)\b)(?:(?:VALUES|IN)\s*\(\s*|(?:ORDER|GROUP)\s+BY\b[^"'\n;]*?|(?:LIMIT|OFFSET)\s+(?:\d+\s*,\s*)?|(?:WHERE|AND|OR|SET|HAVING)\s+[\w.`]+\s*(?:=|<>|!=|<=|>=|<|>|LIKE)\s*)\{?\$(?!wpdb->)(?!\w*(?:format|placeholder))[A-Za-z_]'''
|
||||
|
||||
# Active only when the rule above is dropped (priors off); status and precision are the tagged rule's measurement.
|
||||
[[rule]]
|
||||
rule_id = "php-sql-unquoted-interpolation-language"
|
||||
title = "PHP SQL string interpolates a variable into an unquoted position (language-level)"
|
||||
languages = ["php"]
|
||||
cwe = "CWE-89"
|
||||
tags = ["injection"]
|
||||
status = "enabled"
|
||||
min_evidence_level = "static_corroboration"
|
||||
precision_estimate = 0.129
|
||||
version = "1"
|
||||
fallback_for = "php-sql-unquoted-interpolation"
|
||||
pattern = '''(?<![$\w>])(?=(VALUES|IN|ORDER|GROUP|LIMIT|OFFSET|WHERE|AND|OR|SET|HAVING)\b)(?:(?:VALUES|IN)\s*\(\s*|(?:ORDER|GROUP)\s+BY\b[^"'\n;]*?|(?:LIMIT|OFFSET)\s+(?:\d+\s*,\s*)?|(?:WHERE|AND|OR|SET|HAVING)\s+[\w.`]+\s*(?:=|<>|!=|<=|>=|<|>|LIKE)\s*)\{?\$(?!\w*(?:format|placeholder))[A-Za-z_]'''
|
||||
|
||||
# sprintf-built SQL with a quoted %s and no escaping or coercion anywhere in the statement (phpMyFAQ
|
||||
# CVE-2026-46364). Statement-scoped, so it spans lines up to the closing `);`.
|
||||
[[rule]]
|
||||
@@ -80,8 +111,23 @@ status = "enabled"
|
||||
min_evidence_level = "static_corroboration"
|
||||
precision_estimate = 0.109
|
||||
version = "1"
|
||||
framework = "wordpress"
|
||||
pattern = '''(sprintf)\(\s*["']\s*(?:SELECT|INSERT|UPDATE|DELETE|REPLACE)\b(?=(?:(?!\);)[\s\S]){0,2000}?'%s')(?=(?:(?![Ee]scape|quote\(|intval\(|absint\(|\(int\)|esc_sql\()[\s\S]){0,2000}?\);)'''
|
||||
|
||||
# Active only when the rule above is dropped (priors off); status and precision are the tagged rule's measurement.
|
||||
[[rule]]
|
||||
rule_id = "php-sql-sprintf-unescaped-language"
|
||||
title = "PHP sprintf-built SQL with an unescaped %s value (language-level)"
|
||||
languages = ["php"]
|
||||
cwe = "CWE-89"
|
||||
tags = ["injection"]
|
||||
status = "enabled"
|
||||
min_evidence_level = "static_corroboration"
|
||||
precision_estimate = 0.109
|
||||
version = "1"
|
||||
fallback_for = "php-sql-sprintf-unescaped"
|
||||
pattern = '''(sprintf)\(\s*["']\s*(?:SELECT|INSERT|UPDATE|DELETE|REPLACE)\b(?=(?:(?!\);)[\s\S]){0,2000}?'%s')(?=(?:(?![Ee]scape|quote\(|intval\(|\(int\))[\s\S]){0,2000}?\);)'''
|
||||
|
||||
# --- command / code (command, code_eval) -------------------------------------------------------------------
|
||||
|
||||
[[rule]]
|
||||
@@ -120,8 +166,22 @@ status = "shadow"
|
||||
min_evidence_level = "static_corroboration"
|
||||
precision_estimate = 0.25
|
||||
version = "1"
|
||||
pattern = '''(?<![\w>:$\\])(?<!function )(?=(unserialize)\s*\()unserialize\s*\((?![^\n]*allowed_classes)[^\n;]*(?:\$[A-Za-z_]|php://input)'''
|
||||
|
||||
[[rule]]
|
||||
rule_id = "php-maybe-unserialize"
|
||||
title = "WordPress maybe_unserialize of request input"
|
||||
languages = ["php"]
|
||||
cwe = "CWE-502"
|
||||
tags = ["deserialization"]
|
||||
status = "shadow"
|
||||
min_evidence_level = "static_corroboration"
|
||||
precision_estimate = 0.25
|
||||
version = "1"
|
||||
framework = "wordpress"
|
||||
# `maybe_unserialize` (WordPress) runs on every option and meta read, so it counts only with request input.
|
||||
pattern = '''(?<![\w>:$\\])(?<!function )(?=(unserialize|maybe_unserialize)\s*\()(?:unserialize\s*\((?![^\n]*allowed_classes)[^\n;]*(?:\$[A-Za-z_]|php://input)|maybe_unserialize\s*\([^\n;]*(?:\$_(?:GET|POST|REQUEST|COOKIE)\b|php://input))'''
|
||||
# Split from php-unserialize (2026-09-30), whose measured status and precision it keeps.
|
||||
pattern = '''(?<![\w>:$\\])(?<!function )(?=(maybe_unserialize)\s*\()maybe_unserialize\s*\([^\n;]*(?:\$_(?:GET|POST|REQUEST|COOKIE)\b|php://input)'''
|
||||
|
||||
# --- output encoding (echo) --------------------------------------------------------------------------------
|
||||
|
||||
@@ -135,8 +195,23 @@ status = "shadow"
|
||||
min_evidence_level = "static_corroboration"
|
||||
precision_estimate = 0.333
|
||||
version = "1"
|
||||
framework = "wordpress"
|
||||
pattern = '''(?m)^(?![^\n]*(?:htmlspecialchars|htmlentities|esc_html|esc_attr|esc_url|esc_js|intval|absint|json_encode|urlencode)\s*\()[^\n]*?(\becho\b|\bprint\b|<\?=)(?:[^\n;]*?[.,])?\s*\$_(?:GET|POST|REQUEST|COOKIE|SERVER)\[[^\]\n]*\]\s*(?:[.,;]|\?>)'''
|
||||
|
||||
# Active only when the rule above is dropped (priors off); status and precision are the tagged rule's measurement.
|
||||
[[rule]]
|
||||
rule_id = "php-xss-echo-input-language"
|
||||
title = "PHP echo of request input without HTML escaping (language-level)"
|
||||
languages = ["php"]
|
||||
cwe = "CWE-79"
|
||||
tags = ["injection"]
|
||||
status = "shadow"
|
||||
min_evidence_level = "static_corroboration"
|
||||
precision_estimate = 0.333
|
||||
version = "1"
|
||||
fallback_for = "php-xss-echo-input"
|
||||
pattern = '''(?m)^(?![^\n]*(?:htmlspecialchars|htmlentities|intval|json_encode|urlencode)\s*\()[^\n]*?(\becho\b|\bprint\b|<\?=)(?:[^\n;]*?[.,])?\s*\$_(?:GET|POST|REQUEST|COOKIE|SERVER)\[[^\]\n]*\]\s*(?:[.,;]|\?>)'''
|
||||
|
||||
# --- path / inclusion (filesystem, file_inclusion) ---------------------------------------------------------
|
||||
|
||||
[[rule]]
|
||||
|
||||
Reference in New Issue
Block a user