pub mod common;
pub mod config;
pub mod cross_file;
pub mod csharp;
pub mod go;
pub mod go_taint;
pub mod java;
pub mod javascript;
pub mod javascript_taint;
pub mod kotlin;
pub mod manifest;
pub mod php;
pub mod python;
pub mod python_aliases;
pub mod python_taint;
pub mod ruby;
pub mod rust_lang;
pub mod semgrep_compat;
pub mod semgrep_taint;
pub mod swift;
pub mod taint_engine;
use crate::{Finding, Language, Severity};
use std::path::Path;
#[macro_export]
macro_rules! impl_rule {
(
$struct:ty,
id = $id:expr,
severity = $sev:expr,
cwe = $cwe:expr,
description = $desc:expr,
language = $lang:expr,
fn check($self_:ident, $src:ident, $tree:ident) { $($check_body:tt)* }
) => {
impl $crate::rules::Rule for $struct {
fn id(&self) -> &str { $id }
fn severity(&self) -> $crate::Severity { $sev }
fn cwe(&self) -> Option<&str> { $cwe }
fn description(&self) -> &str { $desc }
fn language(&self) -> $crate::Language { $lang }
fn check(&self, $src: &str, $tree: &tree_sitter::Tree) -> Vec<$crate::Finding> {
let $self_ = self;
$($check_body)*
}
}
};
(
$struct:ty,
id = $id:expr,
severity = $sev:expr,
cwe = $cwe:expr,
description = $desc:expr,
language = $lang:expr,
cnsa2_deadline = $deadline:expr,
fn check($self_:ident, $src:ident, $tree:ident) { $($check_body:tt)* }
) => {
impl $crate::rules::Rule for $struct {
fn id(&self) -> &str { $id }
fn severity(&self) -> $crate::Severity { $sev }
fn cwe(&self) -> Option<&str> { $cwe }
fn description(&self) -> &str { $desc }
fn language(&self) -> $crate::Language { $lang }
fn cnsa2_deadline(&self) -> Option<&'static str> { Some($deadline) }
fn check(&self, $src: &str, $tree: &tree_sitter::Tree) -> Vec<$crate::Finding> {
let $self_ = self;
$($check_body)*
}
}
};
(
$struct:ty,
id = $id:expr,
severity = $sev:expr,
cwe = $cwe:expr,
description = $desc:expr,
language = $lang:expr,
fn check_with_context($self_:ident, $src:ident, $tree:ident, $ctx:ident) { $($check_body:tt)* }
) => {
impl $crate::rules::Rule for $struct {
fn id(&self) -> &str { $id }
fn severity(&self) -> $crate::Severity { $sev }
fn cwe(&self) -> Option<&str> { $cwe }
fn description(&self) -> &str { $desc }
fn language(&self) -> $crate::Language { $lang }
fn check(&self, source: &str, tree: &tree_sitter::Tree) -> Vec<$crate::Finding> {
self.check_with_context(source, tree, &$crate::rules::FileContext::default())
}
fn check_with_context(
&self,
$src: &str,
$tree: &tree_sitter::Tree,
$ctx: &$crate::rules::FileContext<'_>,
) -> Vec<$crate::Finding> {
let $self_ = self;
$($check_body)*
}
}
};
(
$struct:ty,
id = $id:expr,
severity = $sev:expr,
cwe = $cwe:expr,
description = $desc:expr,
language = $lang:expr,
cnsa2_deadline = $deadline:expr,
fn check_with_context($self_:ident, $src:ident, $tree:ident, $ctx:ident) { $($check_body:tt)* }
) => {
impl $crate::rules::Rule for $struct {
fn id(&self) -> &str { $id }
fn severity(&self) -> $crate::Severity { $sev }
fn cwe(&self) -> Option<&str> { $cwe }
fn description(&self) -> &str { $desc }
fn language(&self) -> $crate::Language { $lang }
fn cnsa2_deadline(&self) -> Option<&'static str> { Some($deadline) }
fn check(&self, source: &str, tree: &tree_sitter::Tree) -> Vec<$crate::Finding> {
self.check_with_context(source, tree, &$crate::rules::FileContext::default())
}
fn check_with_context(
&self,
$src: &str,
$tree: &tree_sitter::Tree,
$ctx: &$crate::rules::FileContext<'_>,
) -> Vec<$crate::Finding> {
let $self_ = self;
$($check_body)*
}
}
};
}
#[derive(Default)]
pub struct FileContext<'a> {
pub python_aliases: Option<&'a common::AliasTable>,
pub javascript_aliases: Option<&'a common::AliasTable>,
pub go_aliases: Option<&'a common::AliasTable>,
pub cross_file_summaries: Option<&'a cross_file::CrossFileSummaryMap>,
pub python_import_paths: Option<&'a std::collections::HashMap<String, std::path::PathBuf>>,
pub javascript_import_paths: Option<&'a std::collections::HashMap<String, std::path::PathBuf>>,
pub go_same_package_paths: Option<Vec<std::path::PathBuf>>,
}
pub trait Rule: Send + Sync {
fn id(&self) -> &str;
fn severity(&self) -> Severity;
fn cwe(&self) -> Option<&str>;
fn description(&self) -> &str;
fn language(&self) -> Language;
fn applies_to_path(&self, _path: &Path) -> bool {
true
}
fn check(&self, source: &str, tree: &tree_sitter::Tree) -> Vec<Finding>;
fn check_with_context(
&self,
source: &str,
tree: &tree_sitter::Tree,
_ctx: &FileContext<'_>,
) -> Vec<Finding> {
self.check(source, tree)
}
fn configure(&mut self, _opts: &serde_yaml::Value) -> Result<(), String> {
Ok(())
}
fn cnsa2_deadline(&self) -> Option<&'static str> {
None
}
}
pub struct RuleRegistry {
rules: Vec<Box<dyn Rule>>,
opt_in_ids: std::collections::HashSet<String>,
}
impl Default for RuleRegistry {
fn default() -> Self {
Self::new()
}
}
impl RuleRegistry {
pub fn empty() -> Self {
Self {
rules: Vec::new(),
opt_in_ids: std::collections::HashSet::new(),
}
}
pub fn new() -> Self {
let mut registry = Self::empty();
registry.register(Box::new(javascript::NoEval));
registry.register(Box::new(javascript::NoHardcodedSecret));
registry.register(Box::new(javascript::NoSqlInjection));
registry.register(Box::new(javascript::NoXssInnerHtml));
registry.register(Box::new(javascript::NoCommandInjection));
registry.register(Box::new(javascript::NoDocumentWrite));
registry.register(Box::new(javascript::NoOpenRedirect));
registry.register(Box::new(javascript::NoWeakCrypto));
registry.register(Box::new(javascript::PqVulnerableCrypto));
registry.register(Box::new(javascript::NoPathTraversal));
registry.register(Box::new(javascript::NoSsrf));
registry.register(Box::new(javascript::NoPrototypePollution));
registry.register(Box::new(javascript::NoUnsafeRegex));
registry.register(Box::new(javascript::NoCorsStar));
registry.register(Box::new(javascript::ExpressNoHardcodedSessionSecret));
registry.register(Box::new(javascript::ExpressCookieNoSecure));
registry.register(Box::new(javascript::ExpressCookieNoHttpOnly));
registry.register(Box::new(javascript::ExpressCookieNoSameSite));
registry.register(Box::new(javascript::ExpressSessionSaveUninitializedTrue));
registry.register(Box::new(javascript::ExpressSessionResaveTrue));
registry.register(Box::new(javascript::ExpressDirectResponseWrite));
registry.register(Box::new(javascript::JwtHardcodedSecret));
registry.register(Box::new(javascript::JwtNoneAlgorithm));
registry.register(Box::new(javascript::JwtIgnoreExpiration));
registry.register(Box::new(javascript::JwtDecodeWithoutVerify));
registry.register(Box::new(javascript::JwtVerifyMissingAlgorithms));
registry.register(Box::new(javascript::NoUnsafeFormatString));
registry.register(Box::new(javascript::TaintXssInnerHtml));
registry.register(Box::new(javascript::TaintSqlInjection));
registry.register(Box::new(javascript::TaintEval));
registry.register(Box::new(javascript::TaintCommandInjection));
registry.register(Box::new(javascript::TaintSsrf));
registry.register(Box::new(javascript::TaintSsti));
registry.register(Box::new(javascript::TaintXpathInjection));
registry.register(Box::new(javascript::TaintLdapInjection));
registry.register(Box::new(javascript::TaintLogInjection));
registry.register(Box::new(javascript::TaintXxe));
registry.register(Box::new(javascript::NoUnsafeDeserialization));
registry.register_opt_in(Box::new(javascript::HardcodedCryptoAlgorithm));
registry.register(Box::new(javascript::TaintNosqlInjection));
registry.register(Box::new(python::NoEval));
registry.register(Box::new(python::NoHardcodedSecret));
registry.register(Box::new(python::NoSqlInjection));
registry.register(Box::new(python::NoCommandInjection));
registry.register(Box::new(python::NoPathTraversal));
registry.register(Box::new(python::NoSsrf));
registry.register(Box::new(python::NoWeakCrypto));
registry.register(Box::new(python::PqVulnerableCrypto));
registry.register(Box::new(python::NoPickle));
registry.register(Box::new(python::NoYamlLoad));
registry.register(Box::new(python::NoDebugTrue));
registry.register(Box::new(python::NoOpenRedirect));
registry.register(Box::new(python::NoCorsStar));
registry.register(Box::new(python::FlaskDebugMode));
registry.register(Box::new(python::DjangoSecretKeyHardcoded));
registry.register(Box::new(python::FlaskSecretKeyHardcoded));
registry.register(Box::new(python::SessionCookieSecureDisabled));
registry.register(Box::new(python::SessionCookieHttpOnlyDisabled));
registry.register(Box::new(python::SessionCookieSameSiteDisabled));
registry.register(Box::new(python::CsrfCookieSecureDisabled));
registry.register(Box::new(python::CsrfCookieHttpOnlyDisabled));
registry.register(Box::new(python::CsrfCookieSameSiteDisabled));
registry.register(Box::new(python::CsrfExempt));
registry.register(Box::new(python::WtfCsrfDisabled));
registry.register(Box::new(python::WtfCsrfCheckDefaultDisabled));
registry.register(Box::new(python::DjangoAllowedHostsWildcard));
registry.register(Box::new(python::SecureSslRedirectDisabled));
registry.register(Box::new(python::TaintPickleDeserialization));
registry.register(Box::new(python::TaintEvalFromRequest));
registry.register(Box::new(python::TaintCommandInjectionFromRequest));
registry.register(Box::new(python::TaintSsrfFromRequest));
registry.register(Box::new(python::TaintYamlLoadFromRequest));
registry.register(Box::new(python::TaintSqlInjectionFromRequest));
registry.register(Box::new(python::TaintSsti));
registry.register(Box::new(python::TaintXpathInjection));
registry.register(Box::new(python::TaintLdapInjection));
registry.register(Box::new(python::TaintLogInjection));
registry.register(Box::new(python::TaintXxe));
registry.register(Box::new(python::JwtNoVerify));
registry.register(Box::new(python::JwtHardcodedSecret));
registry.register_opt_in(Box::new(python::HardcodedCryptoAlgorithm));
registry.register(Box::new(python::TaintNosqlInjection));
registry.register(Box::new(go::NoSqlInjection));
registry.register(Box::new(go::NoCommandInjection));
registry.register(Box::new(go::NoHardcodedSecret));
registry.register(Box::new(go::NoWeakCrypto));
registry.register(Box::new(go::PqVulnerableCrypto));
registry.register(Box::new(go::NoSsrf));
registry.register(Box::new(go::InsecureTlsSkipVerify));
registry.register(Box::new(go::GinNoTrustedProxies));
registry.register(Box::new(go::NetHttpNoTimeout));
registry.register(Box::new(go::TaintCommandInjection));
registry.register(Box::new(go::TaintSqlInjection));
registry.register(Box::new(go::TaintSsrf));
registry.register(Box::new(go::TaintSsti));
registry.register(Box::new(go::TaintXpathInjection));
registry.register(Box::new(go::TaintLdapInjection));
registry.register(Box::new(go::TaintLogInjection));
registry.register(Box::new(go::NoUnsafeDeserialization));
registry.register(Box::new(go::JwtNoVerify));
registry.register(Box::new(go::JwtHardcodedSecret));
registry.register(Box::new(go::TaintNosqlInjection));
registry.register(Box::new(go::TaintPathTraversal));
registry.register(Box::new(java::NoSqlInjection));
registry.register(Box::new(java::NoCommandInjection));
registry.register(Box::new(java::NoUnsafeDeserialization));
registry.register(Box::new(java::NoSsrf));
registry.register(Box::new(java::NoPathTraversal));
registry.register(Box::new(java::NoWeakCrypto));
registry.register(Box::new(java::PqVulnerableCrypto));
registry.register(Box::new(java::NoHardcodedSecret));
registry.register(Box::new(java::NoXxe));
registry.register(Box::new(java::SpringCsrfDisabled));
registry.register(Box::new(java::SpringCorsPermissive));
registry.register(Box::new(java::NoXss));
registry.register_opt_in(Box::new(java::HardcodedCryptoAlgorithm));
registry.register(Box::new(php::NoEval));
registry.register(Box::new(php::NoCommandInjection));
registry.register(Box::new(php::NoSqlInjection));
registry.register(Box::new(php::NoUnserialize));
registry.register(Box::new(php::NoFileInclusion));
registry.register(Box::new(php::NoWeakCrypto));
registry.register(Box::new(php::NoHardcodedSecret));
registry.register(Box::new(php::NoSsrf));
registry.register(Box::new(php::NoExtract));
registry.register(Box::new(php::NoPregEval));
registry.register(Box::new(ruby::NoEval));
registry.register(Box::new(ruby::NoCommandInjection));
registry.register(Box::new(ruby::NoSqlInjection));
registry.register(Box::new(ruby::NoMassAssignment));
registry.register(Box::new(ruby::NoUnsafeDeserialization));
registry.register(Box::new(ruby::NoOpenRedirect));
registry.register(Box::new(ruby::NoCsrfSkip));
registry.register(Box::new(ruby::NoHtmlSafe));
registry.register(Box::new(ruby::NoHardcodedSecret));
registry.register(Box::new(ruby::NoWeakCrypto));
registry.register(Box::new(ruby::NoSsrf));
registry.register(Box::new(ruby::NoPathTraversal));
registry.register(Box::new(csharp::NoSqlInjection));
registry.register(Box::new(csharp::NoCommandInjection));
registry.register(Box::new(csharp::NoUnsafeDeserialization));
registry.register(Box::new(csharp::NoSsrf));
registry.register(Box::new(csharp::NoPathTraversal));
registry.register(Box::new(csharp::NoWeakCrypto));
registry.register(Box::new(csharp::NoHardcodedSecret));
registry.register(Box::new(csharp::NoXxe));
registry.register(Box::new(csharp::NoLdapInjection));
registry.register(Box::new(csharp::NoCorsStar));
registry.register(Box::new(swift::NoHardcodedSecret));
registry.register(Box::new(swift::NoCommandInjection));
registry.register(Box::new(swift::NoWeakCrypto));
registry.register(Box::new(swift::NoInsecureTransport));
registry.register(Box::new(swift::NoEvalJs));
registry.register(Box::new(swift::NoSqlInjection));
registry.register(Box::new(swift::NoInsecureKeychain));
registry.register(Box::new(swift::NoTlsDisabled));
registry.register(Box::new(swift::NoPathTraversal));
registry.register(Box::new(swift::NoSsrf));
registry.register(Box::new(kotlin::NoSqlInjection));
registry.register(Box::new(kotlin::NoCommandInjection));
registry.register(Box::new(kotlin::NoUnsafeDeserialization));
registry.register(Box::new(kotlin::NoSsrf));
registry.register(Box::new(kotlin::NoPathTraversal));
registry.register(Box::new(kotlin::NoWeakCrypto));
registry.register(Box::new(kotlin::NoHardcodedSecret));
registry.register(Box::new(kotlin::NoXxe));
registry.register(Box::new(kotlin::NoCorsStar));
registry.register(Box::new(kotlin::NoEval));
registry.register(Box::new(kotlin::TaintSqlInjection));
registry.register(Box::new(kotlin::TaintCommandInjection));
registry.register(Box::new(kotlin::TaintSsrf));
registry.register(Box::new(rust_lang::UnsafeBlock));
registry.register(Box::new(rust_lang::TransmuteUsage));
registry.register(Box::new(rust_lang::NoCommandInjection));
registry.register(Box::new(rust_lang::NoSqlInjection));
registry.register(Box::new(rust_lang::NoWeakHash));
registry.register(Box::new(rust_lang::PqVulnerableCrypto));
registry.register(Box::new(rust_lang::NoHardcodedSecret));
registry.register(Box::new(rust_lang::TlsVerifyDisabled));
registry.register(Box::new(rust_lang::NoSsrf));
registry.register(Box::new(rust_lang::NoPathTraversal));
registry.register(Box::new(rust_lang::NoUnwrapInLib));
registry.register(Box::new(config::NginxPqVulnerableTls));
registry.register(Box::new(config::ApachePqVulnerableTls));
registry.register(Box::new(config::HAProxyPqVulnerableTls));
registry.register(Box::new(config::DockerfileInsecureTlsEnv));
registry.register(Box::new(manifest::CargoLockPqCrypto));
registry.register(Box::new(manifest::RequirementsTxtPqCrypto));
registry
}
pub fn register(&mut self, rule: Box<dyn Rule>) {
self.rules.push(rule);
}
pub fn register_opt_in(&mut self, rule: Box<dyn Rule>) {
self.opt_in_ids.insert(rule.id().to_string());
self.rules.push(rule);
}
pub fn rules_for_language(&self, language: Language) -> Vec<&dyn Rule> {
self.rules
.iter()
.filter(|r| r.language() == language)
.map(|r| r.as_ref())
.collect()
}
pub fn configure_rules(
&mut self,
rule_options: &std::collections::HashMap<String, serde_yaml::Value>,
) -> Result<Vec<String>, String> {
let mut warnings = Vec::new();
for (rule_id, opts) in rule_options {
let Some(rule) = self.rules.iter_mut().find(|r| r.id() == rule_id) else {
warnings.push(format!("rule_options: unknown rule '{}'", rule_id));
continue;
};
rule.configure(opts)
.map_err(|e| format!("rule_options: invalid config for '{}': {}", rule_id, e))?;
}
Ok(warnings)
}
#[allow(dead_code)]
pub fn all_rules(&self) -> &[Box<dyn Rule>] {
&self.rules
}
pub fn apply_rule_filter(&mut self, enable: &[String], disable: &[String]) -> Vec<String> {
let known: std::collections::HashSet<&str> = self.rules.iter().map(|r| r.id()).collect();
let mut unknown: Vec<String> = Vec::new();
let mut seen_unknown: std::collections::HashSet<&str> = std::collections::HashSet::new();
for id in enable.iter().chain(disable.iter()) {
if !known.contains(id.as_str()) && seen_unknown.insert(id.as_str()) {
unknown.push(id.clone());
}
}
if !enable.is_empty() {
let enable_set: std::collections::HashSet<&str> =
enable.iter().map(|s| s.as_str()).collect();
self.rules.retain(|r| enable_set.contains(r.id()));
} else {
self.rules.retain(|r| !self.opt_in_ids.contains(r.id()));
}
if !disable.is_empty() {
let disable_set: std::collections::HashSet<&str> =
disable.iter().map(|s| s.as_str()).collect();
self.rules.retain(|r| !disable_set.contains(r.id()));
}
unknown
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(dead_code)]
fn rule_ids(registry: &RuleRegistry) -> Vec<String> {
registry.rules.iter().map(|r| r.id().to_string()).collect()
}
fn has_rule(registry: &RuleRegistry, id: &str) -> bool {
registry.rules.iter().any(|r| r.id() == id)
}
#[test]
fn apply_rule_filter_strips_opt_in_when_both_lists_empty() {
let mut registry = RuleRegistry::new();
let before_count = registry.rules.len();
let unknown = registry.apply_rule_filter(&[], &[]);
assert!(unknown.is_empty());
assert!(registry.rules.len() < before_count);
assert!(!has_rule(®istry, "js/hardcoded-crypto-algorithm"));
assert!(!has_rule(®istry, "py/hardcoded-crypto-algorithm"));
assert!(!has_rule(®istry, "java/hardcoded-crypto-algorithm"));
}
#[test]
fn apply_rule_filter_allowlist_keeps_only_listed_ids() {
let mut registry = RuleRegistry::new();
let unknown =
registry.apply_rule_filter(&["py/no-eval".to_string(), "js/no-eval".to_string()], &[]);
assert!(unknown.is_empty());
assert_eq!(registry.rules.len(), 2);
assert!(has_rule(®istry, "py/no-eval"));
assert!(has_rule(®istry, "js/no-eval"));
}
#[test]
fn apply_rule_filter_denylist_removes_listed_ids() {
let mut registry = RuleRegistry::new();
let opt_in_count = registry.opt_in_ids.len();
let before_count = registry.rules.len();
let unknown = registry.apply_rule_filter(&[], &["py/no-eval".to_string()]);
assert!(unknown.is_empty());
assert_eq!(registry.rules.len(), before_count - 1 - opt_in_count);
assert!(!has_rule(®istry, "py/no-eval"));
}
#[test]
fn apply_rule_filter_both_intersects_then_subtracts() {
let mut registry = RuleRegistry::new();
let unknown = registry.apply_rule_filter(
&["py/no-eval".to_string(), "py/no-sql-injection".to_string()],
&["py/no-eval".to_string()],
);
assert!(unknown.is_empty());
assert_eq!(registry.rules.len(), 1);
assert!(has_rule(®istry, "py/no-sql-injection"));
assert!(!has_rule(®istry, "py/no-eval"));
}
#[test]
fn apply_rule_filter_reports_unknown_rule_ids() {
let mut registry = RuleRegistry::new();
let unknown = registry.apply_rule_filter(
&["py/no-eval".to_string(), "py/does-not-exist".to_string()],
&["another/typo".to_string()],
);
assert_eq!(unknown.len(), 2);
assert!(unknown.contains(&"py/does-not-exist".to_string()));
assert!(unknown.contains(&"another/typo".to_string()));
assert!(has_rule(®istry, "py/no-eval"));
}
#[test]
fn apply_rule_filter_deduplicates_unknown_ids() {
let mut registry = RuleRegistry::new();
let unknown = registry.apply_rule_filter(
&["py/typo".to_string(), "py/typo".to_string()],
&["py/typo".to_string()],
);
assert_eq!(unknown, vec!["py/typo".to_string()]);
}
#[test]
fn configure_rules_warns_on_unknown_rule_id() {
let mut registry = RuleRegistry::new();
let mut opts = std::collections::HashMap::new();
opts.insert("py/does-not-exist".to_string(), serde_yaml::Value::Null);
let warnings = registry.configure_rules(&opts).unwrap();
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("py/does-not-exist"));
}
#[test]
fn configure_rules_no_warning_for_known_rule() {
let mut registry = RuleRegistry::new();
let mut opts = std::collections::HashMap::new();
opts.insert("py/no-eval".to_string(), serde_yaml::Value::Null);
let warnings = registry.configure_rules(&opts).unwrap();
assert!(warnings.is_empty());
}
#[test]
fn configure_rules_empty_options_is_no_op() {
let mut registry = RuleRegistry::new();
let opts = std::collections::HashMap::new();
let warnings = registry.configure_rules(&opts).unwrap();
assert!(warnings.is_empty());
}
}