pub mod apex;
pub mod apex_taint;
pub mod bash;
pub mod bash_taint;
pub mod c;
pub mod c_taint;
pub mod common;
pub mod config;
pub mod cross_file;
pub mod csharp;
pub mod csharp_taint;
pub mod generic_mode;
pub mod go;
pub mod go_taint;
pub mod java;
pub mod java_taint;
pub mod javascript;
pub mod javascript_taint;
pub mod kotlin;
pub mod kotlin_taint;
pub mod manifest;
pub mod php;
pub mod php_taint;
pub mod pq;
pub mod python;
pub mod python_aliases;
pub mod python_taint;
pub mod ruby;
pub mod ruby_taint;
pub mod rust_lang;
pub mod scala;
pub mod scala_taint;
pub mod semgrep_compat;
pub mod semgrep_taint;
pub mod solidity;
pub mod solidity_taint;
pub mod swift;
pub mod swift_taint;
pub mod taint_engine;
use crate::{Finding, Language, Severity};
use std::path::Path;
static BUNDLED_RULES: include_dir::Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/rules");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaintEngine {
Apex,
Bash,
C,
CSharp,
Go,
Java,
JavaScript,
Kotlin,
Php,
Python,
Ruby,
Scala,
Solidity,
Swift,
}
#[derive(Debug, Clone)]
pub struct RegistryTaintSpec {
pub rule_id: &'static str,
pub language: Language,
pub engine: TaintEngine,
pub spec: taint_engine::TaintSpec,
}
pub struct AnalysisPlan<'a> {
pub ast_rules: Vec<&'a dyn Rule>,
pub taint_specs: Vec<RegistryTaintSpec>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AstAnalysisRequirement {
SyntaxTree,
FileContext,
}
#[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,
cnsa2_deadline = $deadline:expr,
applies_to_filename = $filename: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 applies_to_path(&self, path: &std::path::Path) -> bool {
path.file_name().and_then(|f| f.to_str()) == Some($filename)
}
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,
applies_to_filename = $filename: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 applies_to_path(&self, path: &std::path::Path) -> bool {
path.file_name().and_then(|f| f.to_str()) == Some($filename)
}
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 ast_analysis_requirement(&self) -> $crate::rules::AstAnalysisRequirement {
$crate::rules::AstAnalysisRequirement::FileContext
}
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 ast_analysis_requirement(&self) -> $crate::rules::AstAnalysisRequirement {
$crate::rules::AstAnalysisRequirement::FileContext
}
fn check_with_context(
&self,
$src: &str,
$tree: &tree_sitter::Tree,
$ctx: &$crate::rules::FileContext<'_>,
) -> Vec<$crate::Finding> {
let $self_ = self;
$($check_body)*
}
}
};
}
macro_rules! register_rules {
($registry:expr, [$($rule:path),+ $(,)?]) => {
$(
$registry.register(Box::new($rule));
)+
};
}
#[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 java_same_package_paths: Option<Vec<std::path::PathBuf>>,
pub ruby_same_package_paths: Option<Vec<std::path::PathBuf>>,
pub php_same_package_paths: Option<Vec<std::path::PathBuf>>,
pub csharp_same_package_paths: Option<Vec<std::path::PathBuf>>,
pub kotlin_same_package_paths: Option<Vec<std::path::PathBuf>>,
pub secret_thresholds: common::SecretScanThresholds,
}
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 ast_analysis_requirement(&self) -> AstAnalysisRequirement {
AstAnalysisRequirement::SyntaxTree
}
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_ng::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>,
secret_thresholds: common::SecretScanThresholds,
}
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(),
secret_thresholds: common::SecretScanThresholds::default(),
}
}
pub fn new() -> Self {
let mut registry = Self::empty();
register_rules!(
registry,
[
javascript::NoEval,
javascript::NoHardcodedSecret,
javascript::NoSqlInjection,
javascript::NoXssInnerHtml,
javascript::NoCommandInjection,
javascript::NoDocumentWrite,
javascript::NoOpenRedirect,
javascript::NoWeakCrypto,
javascript::PqVulnerableCrypto,
javascript::NoPathTraversal,
javascript::NoSsrf,
javascript::NoPrototypePollution,
javascript::NoUnsafeRegex,
javascript::NoCorsStar,
javascript::ExpressNoHardcodedSessionSecret,
javascript::ExpressCookieNoSecure,
javascript::ExpressCookieNoHttpOnly,
javascript::ExpressCookieNoSameSite,
javascript::ExpressSessionSaveUninitializedTrue,
javascript::ExpressSessionResaveTrue,
javascript::ExpressDirectResponseWrite,
javascript::JwtHardcodedSecret,
javascript::JwtNoneAlgorithm,
javascript::JwtIgnoreExpiration,
javascript::JwtDecodeWithoutVerify,
javascript::JwtVerifyMissingAlgorithms,
javascript::NoUnsafeFormatString,
javascript::TaintXssInnerHtml,
javascript::TaintSqlInjection,
javascript::TaintEval,
javascript::TaintCommandInjection,
javascript::TaintSsrf,
javascript::TaintSsti,
javascript::TaintXpathInjection,
javascript::TaintLdapInjection,
javascript::TaintLogInjection,
javascript::TaintXxe,
javascript::NoUnsafeDeserialization,
]
);
registry.register_opt_in(Box::new(javascript::HardcodedCryptoAlgorithm));
registry.register_opt_in(Box::new(javascript::PqReadyCrypto));
register_rules!(registry, [javascript::TaintNosqlInjection]);
register_rules!(
registry,
[
python::NoEval,
python::NoHardcodedSecret,
python::NoSqlInjection,
python::NoCommandInjection,
python::NoPathTraversal,
python::NoSsrf,
python::NoWeakCrypto,
python::PqVulnerableCrypto,
python::NoPickle,
python::NoYamlLoad,
python::NoDebugTrue,
python::NoOpenRedirect,
python::NoCorsStar,
python::FlaskDebugMode,
python::DjangoSecretKeyHardcoded,
python::FlaskSecretKeyHardcoded,
python::SessionCookieSecureDisabled,
python::SessionCookieHttpOnlyDisabled,
python::SessionCookieSameSiteDisabled,
python::CsrfCookieSecureDisabled,
python::CsrfCookieHttpOnlyDisabled,
python::CsrfCookieSameSiteDisabled,
python::CsrfExempt,
python::WtfCsrfDisabled,
python::WtfCsrfCheckDefaultDisabled,
python::DjangoAllowedHostsWildcard,
python::SecureSslRedirectDisabled,
python::TaintPickleDeserialization,
python::TaintEvalFromRequest,
python::TaintCommandInjectionFromRequest,
python::TaintSsrfFromRequest,
python::TaintYamlLoadFromRequest,
python::TaintSqlInjectionFromRequest,
python::TaintSsti,
python::TaintXpathInjection,
python::TaintLdapInjection,
python::TaintLogInjection,
python::TaintXxe,
python::JwtNoVerify,
python::JwtHardcodedSecret,
]
);
registry.register_opt_in(Box::new(python::HardcodedCryptoAlgorithm));
registry.register_opt_in(Box::new(python::PqReadyCrypto));
register_rules!(registry, [python::TaintNosqlInjection]);
register_rules!(
registry,
[
go::NoSqlInjection,
go::NoCommandInjection,
go::NoHardcodedSecret,
go::NoWeakCrypto,
go::PqVulnerableCrypto,
go::NoSsrf,
go::InsecureTlsSkipVerify,
go::MissingSslMinVersion,
go::CookieMissingSecure,
go::CookieMissingHttpOnly,
go::MathRandomUsed,
go::GinNoTrustedProxies,
go::NetHttpNoTimeout,
go::TaintCommandInjection,
go::TaintSqlInjection,
go::TaintSsrf,
go::TaintSsti,
go::TaintXpathInjection,
go::TaintLdapInjection,
go::TaintLogInjection,
go::NoUnsafeDeserialization,
go::JwtNoVerify,
go::JwtHardcodedSecret,
go::TaintNosqlInjection,
go::TaintPathTraversal,
]
);
registry.register_opt_in(Box::new(go::PqReadyCrypto));
register_rules!(
registry,
[
java::NoSqlInjection,
java::NoCommandInjection,
java::NoUnsafeDeserialization,
java::NoSsrf,
java::NoPathTraversal,
java::NoWeakCrypto,
java::PqVulnerableCrypto,
java::NoHardcodedSecret,
java::NoXxe,
java::SpringCsrfDisabled,
java::SpringCorsPermissive,
java::NoXss,
java::TaintSqlInjection,
java::TaintCommandInjection,
java::TaintSsrf,
java::TaintUnsafeDeserialization,
]
);
registry.register_opt_in(Box::new(java::HardcodedCryptoAlgorithm));
registry.register_opt_in(Box::new(java::PqReadyCrypto));
register_rules!(
registry,
[
php::NoEval,
php::NoCommandInjection,
php::NoSqlInjection,
php::NoUnserialize,
php::NoFileInclusion,
php::NoWeakCrypto,
php::NoHardcodedSecret,
php::NoSsrf,
php::NoExtract,
php::NoPregEval,
]
);
register_rules!(
registry,
[
ruby::NoEval,
ruby::NoCommandInjection,
ruby::NoSqlInjection,
ruby::NoMassAssignment,
ruby::NoUnsafeDeserialization,
ruby::NoOpenRedirect,
ruby::NoCsrfSkip,
ruby::NoHtmlSafe,
ruby::NoHardcodedSecret,
ruby::NoWeakCrypto,
ruby::NoSsrf,
ruby::NoPathTraversal,
ruby::TaintCommandInjection,
ruby::TaintSqlInjection,
ruby::TaintXss,
ruby::TaintUnsafeDeserialization,
ruby::TaintOpenRedirect,
]
);
register_rules!(
registry,
[
csharp::NoSqlInjection,
csharp::NoCommandInjection,
csharp::NoUnsafeDeserialization,
csharp::NoSsrf,
csharp::NoPathTraversal,
csharp::NoWeakCrypto,
csharp::NoHardcodedSecret,
csharp::NoXxe,
csharp::NoLdapInjection,
csharp::NoCorsStar,
csharp::TaintSqlInjection,
csharp::TaintCommandInjection,
csharp::TaintXss,
csharp::TaintOpenRedirect,
csharp::TaintXxe,
csharp::TaintUnsafeLoad,
]
);
register_rules!(
registry,
[
swift::NoHardcodedSecret,
swift::NoCommandInjection,
swift::NoWeakCrypto,
swift::NoInsecureTransport,
swift::NoEvalJs,
swift::NoSqlInjection,
swift::NoInsecureKeychain,
swift::NoTlsDisabled,
swift::NoPathTraversal,
swift::NoSsrf,
swift::TaintSqlInjection,
swift::TaintCommandInjection,
swift::TaintJsInjection,
swift::TaintNsexpressionInjection,
]
);
register_rules!(
registry,
[
kotlin::NoSqlInjection,
kotlin::NoCommandInjection,
kotlin::NoUnsafeDeserialization,
kotlin::NoSsrf,
kotlin::NoPathTraversal,
kotlin::NoWeakCrypto,
kotlin::NoHardcodedSecret,
kotlin::NoXxe,
kotlin::NoCorsStar,
kotlin::NoEval,
kotlin::TaintSqlInjection,
kotlin::TaintCommandInjection,
kotlin::TaintSsrf,
]
);
register_rules!(
registry,
[
solidity::TaintArbitraryDelegatecall,
solidity::TaintUnprotectedSelfdestruct,
solidity::TaintUncheckedCall,
]
);
register_rules!(
registry,
[
c::TaintFormatString,
c::TaintCommandInjection,
c::TaintBufferOverflow,
c::TaintSqlInjection,
]
);
register_rules!(
registry,
[apex::TaintSoqlInjection, apex::TaintSoslInjection]
);
register_rules!(
registry,
[
bash::TaintCommandInjection,
bash::TaintPathTraversal,
bash::TaintSsrf,
]
);
register_rules!(
registry,
[
scala::TaintSqlInjection,
scala::TaintCommandInjection,
scala::TaintXss,
scala::TaintPathTraversal,
scala::TaintSsrf,
]
);
register_rules!(
registry,
[
php::TaintCommandInjection,
php::TaintSqlInjection,
php::TaintXss,
php::TaintFileInclusion,
php::TaintUnsafeDeserialization,
]
);
register_rules!(
registry,
[
rust_lang::UnsafeBlock,
rust_lang::TransmuteUsage,
rust_lang::NoCommandInjection,
rust_lang::NoSqlInjection,
rust_lang::NoWeakHash,
rust_lang::PqVulnerableCrypto,
rust_lang::NoHardcodedSecret,
rust_lang::TlsVerifyDisabled,
rust_lang::NoSsrf,
rust_lang::NoPathTraversal,
rust_lang::NoUnwrapInLib,
]
);
registry.register_opt_in(Box::new(rust_lang::PqReadyCrypto));
register_rules!(
registry,
[
config::NginxPqVulnerableTls,
config::ApachePqVulnerableTls,
config::HAProxyPqVulnerableTls,
config::DockerfileInsecureTlsEnv,
manifest::OsvVulnerableDependency,
manifest::CargoLockPqCrypto,
manifest::RequirementsTxtPqCrypto,
manifest::PoetryLockPqCrypto,
manifest::PipfileLockPqCrypto,
manifest::PnpmLockPqCrypto,
manifest::PackageLockPqCrypto,
]
);
registry.register_opt_in(Box::new(config::NginxPqReadyTls));
registry.register_opt_in(Box::new(config::ApachePqReadyTls));
registry.register_opt_in(Box::new(config::HAProxyPqReadyTls));
registry.register_opt_in(Box::new(manifest::CargoLockPqReadyCrypto));
registry.register_opt_in(Box::new(manifest::RequirementsTxtPqReadyCrypto));
for rule in semgrep_compat::load_semgrep_rules_from_embedded(&BUNDLED_RULES) {
registry.register(rule);
}
registry
}
pub fn register(&mut self, rule: Box<dyn Rule>) {
self.rules.push(rule);
}
pub fn set_secret_thresholds(&mut self, thresholds: common::SecretScanThresholds) {
self.secret_thresholds = thresholds;
}
pub fn secret_thresholds(&self) -> common::SecretScanThresholds {
self.secret_thresholds
}
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 taint_specs_for_language(&self, language: Language) -> Vec<RegistryTaintSpec> {
let enabled_rule_ids: std::collections::HashSet<&str> = self
.rules
.iter()
.filter(|rule| rule.language() == language)
.map(|rule| rule.id())
.collect();
builtin_taint_specs_for_language(language)
.into_iter()
.filter(|spec| enabled_rule_ids.contains(spec.rule_id))
.collect()
}
pub fn analysis_plan_for_path<'a>(
&'a self,
language: Language,
path: &Path,
) -> AnalysisPlan<'a> {
let taint_specs = self.taint_specs_for_language(language);
let taint_rule_ids: std::collections::HashSet<&str> =
taint_specs.iter().map(|spec| spec.rule_id).collect();
let ast_rules = self
.rules
.iter()
.filter(|rule| {
rule.language() == language
&& rule.applies_to_path(path)
&& !taint_rule_ids.contains(rule.id())
})
.map(|rule| rule.as_ref())
.collect();
let taint_specs = taint_specs
.into_iter()
.filter(|spec| {
self.rules.iter().any(|rule| {
rule.id() == spec.rule_id
&& rule.language() == language
&& rule.applies_to_path(path)
})
})
.collect();
AnalysisPlan {
ast_rules,
taint_specs,
}
}
pub fn configure_rules(
&mut self,
rule_options: &std::collections::HashMap<String, serde_yaml_ng::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)
}
pub fn all_rules(&self) -> &[Box<dyn Rule>] {
&self.rules
}
pub fn apply_rule_filter(&mut self, enable: &[String], disable: &[String]) -> Vec<String> {
self.apply_rule_filter_with_known(enable, disable, &std::collections::HashSet::new())
}
pub fn apply_rule_filter_with_known(
&mut self,
enable: &[String],
disable: &[String],
additional_known: &std::collections::HashSet<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())
&& !additional_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
}
}
fn builtin_taint_specs_for_language(language: Language) -> Vec<RegistryTaintSpec> {
match language {
Language::Apex => apex_taint::apex_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::Apex,
spec,
})
.collect(),
Language::Bash => bash_taint::bash_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::Bash,
spec,
})
.collect(),
Language::C => c_taint::c_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::C,
spec,
})
.collect(),
Language::CSharp => csharp_taint::csharp_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::CSharp,
spec,
})
.collect(),
Language::Go => go::go_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::Go,
spec,
})
.collect(),
Language::Java => java_taint::java_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::Java,
spec,
})
.collect(),
Language::JavaScript => javascript::js_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::JavaScript,
spec,
})
.collect(),
Language::Python => python::python_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::Python,
spec,
})
.collect(),
Language::Kotlin => kotlin_taint::kotlin_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::Kotlin,
spec,
})
.collect(),
Language::Ruby => ruby_taint::ruby_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::Ruby,
spec,
})
.collect(),
Language::Php => php_taint::php_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::Php,
spec,
})
.collect(),
Language::Scala => scala_taint::scala_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::Scala,
spec,
})
.collect(),
Language::Solidity => solidity_taint::solidity_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::Solidity,
spec,
})
.collect(),
Language::Swift => swift_taint::swift_taint_rule_specs()
.into_iter()
.map(|(rule_id, spec)| RegistryTaintSpec {
rule_id,
language,
engine: TaintEngine::Swift,
spec,
})
.collect(),
_ => Vec::new(),
}
}
#[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 registry_exposes_enabled_taint_specs() {
let mut registry = RuleRegistry::new();
let unknown = registry.apply_rule_filter(
&[
"py/taint-eval".to_string(),
"py/taint-sql-injection".to_string(),
"py/no-eval".to_string(),
],
&[],
);
assert!(unknown.is_empty());
let specs = registry.taint_specs_for_language(Language::Python);
let ids: std::collections::BTreeSet<&str> = specs.iter().map(|spec| spec.rule_id).collect();
assert_eq!(ids.len(), 2);
assert!(ids.contains("py/taint-eval"));
assert!(ids.contains("py/taint-sql-injection"));
assert!(specs.iter().all(|spec| spec.language == Language::Python
&& spec.engine == TaintEngine::Python
&& !spec.spec.sinks.is_empty()));
}
#[test]
fn analysis_plan_splits_taint_specs_from_ast_rules() {
let mut registry = RuleRegistry::new();
let unknown = registry.apply_rule_filter(
&["py/taint-eval".to_string(), "py/no-eval".to_string()],
&[],
);
assert!(unknown.is_empty());
let plan = registry.analysis_plan_for_path(Language::Python, Path::new("app.py"));
assert_eq!(plan.ast_rules.len(), 1);
assert_eq!(plan.ast_rules[0].id(), "py/no-eval");
assert_eq!(plan.taint_specs.len(), 1);
assert_eq!(plan.taint_specs[0].rule_id, "py/taint-eval");
}
#[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_ng::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_ng::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());
}
#[test]
fn bundled_kernel_dirty_frag_rules_load_by_default() {
let registry = RuleRegistry::new();
let ids: std::collections::HashSet<&str> = registry.rules.iter().map(|r| r.id()).collect();
let expected = [
"semgrep/kernel/dirty-frag/skb-inplace-skcipher-no-cow",
"semgrep/kernel/dirty-frag/skb-inplace-aead-no-cow",
"semgrep/kernel/dirty-frag/scatterwalk-store-on-shared-sgl",
"semgrep/kernel/dirty-frag/scatterwalk-store-on-shared-sgl-authencesn",
"semgrep/kernel/dirty-frag/rxrpc-verify-response-dispatch",
];
for id in expected {
assert!(
ids.contains(id),
"expected bundled rule {} to be registered by default, got: {:?}",
id,
ids.iter()
.filter(|i| i.starts_with("semgrep/kernel"))
.collect::<Vec<_>>()
);
}
assert!(
!ids.contains("semgrep/kernel/dirty-frag/esp-shared-frag-decrypt-guard-codeql"),
"CodeQL rule leaked into the Semgrep registry"
);
}
#[test]
fn empty_registry_does_not_load_bundled_rules() {
let registry = RuleRegistry::empty();
assert!(registry.rules.is_empty());
}
}