use crate::rules::cross_file::{FunctionTaintSummary, ParamSinkFlow};
use std::collections::{BTreeSet, HashMap, HashSet};
use tree_sitter::Node;
pub(crate) fn walk_scope_nodes<'tree>(
scope: Node<'tree>,
source: &str,
is_scope_node: impl Fn(&str) -> bool + Copy,
visitor: &mut impl FnMut(Node<'tree>, &str),
) {
fn walk_node<'tree>(
node: Node<'tree>,
source: &str,
is_scope_node: impl Fn(&str) -> bool + Copy,
visitor: &mut impl FnMut(Node<'tree>, &str),
) {
if is_scope_node(node.kind()) {
return;
}
visitor(node, source);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_node(child, source, is_scope_node, visitor);
}
}
let mut cursor = scope.walk();
for child in scope.children(&mut cursor) {
walk_node(child, source, is_scope_node, visitor);
}
}
#[derive(Debug, Clone)]
pub enum NodeMatcher {
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,
},
}
impl NodeMatcher {
pub fn description(&self) -> &str {
match self {
NodeMatcher::Attribute { description, .. } => description,
NodeMatcher::Call { description, .. } => description,
NodeMatcher::ParamName { description, .. } => description,
NodeMatcher::MethodName { description, .. } => description,
NodeMatcher::CallRegex { description, .. } => description,
NodeMatcher::MethodNameRegex { description, .. } => description,
NodeMatcher::ReceiverCall { description, .. } => description,
NodeMatcher::FieldName { description, .. } => description,
NodeMatcher::Subscript { description, .. } => description,
NodeMatcher::MemberAssign { description, .. } => description,
NodeMatcher::BinopFormat { description, .. } => description,
NodeMatcher::ObjectLiteralValue { description, .. } => description,
NodeMatcher::ReturnValue { description, .. } => description,
NodeMatcher::TypedName { description, .. } => description,
NodeMatcher::TypedAssignTarget { description, .. } => description,
NodeMatcher::LiteralString { description, .. } => description,
NodeMatcher::LooseEquality { description } => description,
NodeMatcher::TaintedCallee { description } => description,
NodeMatcher::TaintedSubscriptKey { description, .. } => description,
NodeMatcher::CallArgSource { description, .. } => description,
NodeMatcher::FirstParamSource { description } => description,
NodeMatcher::DecoratedParamSource { description, .. } => description,
NodeMatcher::CallArgConcat { description, .. } => description,
NodeMatcher::ConstructorArgSink { description, .. } => description,
NodeMatcher::PropertyAssignSink { description, .. } => description,
NodeMatcher::MethodArgSink { description, .. } => description,
NodeMatcher::ReceiverProvenanceCall { description, .. } => description,
NodeMatcher::LiteralArgCall { description, .. } => description,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TaintSpec {
pub sources: Vec<NodeMatcher>,
pub sinks: Vec<NodeMatcher>,
pub sanitizers: Vec<NodeMatcher>,
}
#[derive(Debug, Clone)]
pub struct Propagator {
pub method: Option<String>,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequiresExpr {
Label(String),
Not(Box<RequiresExpr>),
And(Box<RequiresExpr>, Box<RequiresExpr>),
Or(Box<RequiresExpr>, Box<RequiresExpr>),
}
impl RequiresExpr {
pub fn eval(&self, labels: &BTreeSet<String>) -> bool {
match self {
RequiresExpr::Label(l) => labels.contains(l),
RequiresExpr::Not(inner) => !inner.eval(labels),
RequiresExpr::And(a, b) => a.eval(labels) && b.eval(labels),
RequiresExpr::Or(a, b) => a.eval(labels) || b.eval(labels),
}
}
pub fn referenced_labels(&self, out: &mut BTreeSet<String>) {
match self {
RequiresExpr::Label(l) => {
out.insert(l.clone());
}
RequiresExpr::Not(inner) => inner.referenced_labels(out),
RequiresExpr::And(a, b) | RequiresExpr::Or(a, b) => {
a.referenced_labels(out);
b.referenced_labels(out);
}
}
}
}
#[derive(Debug, Clone)]
pub struct Relabel {
pub from: String,
pub to: String,
}
#[derive(Debug, Clone)]
pub struct LabelPolicy {
pub source_label: String,
pub relabels: Vec<Relabel>,
pub sink_requires: RequiresExpr,
}
#[derive(Debug, Clone)]
pub struct TaintFinding {
pub sink_start_byte: usize,
pub sink_end_byte: usize,
pub sink_line: usize,
pub sink_column: usize,
pub sink_end_line: usize,
pub sink_end_column: usize,
pub source_description: String,
pub sink_description: String,
pub source_line: usize,
pub source_range: Option<(usize, usize)>,
pub rule_id_hint: Option<String>,
pub hops: u8,
}
#[derive(Clone)]
pub enum RuleFilter<'a> {
Single(&'a str),
Any(&'a HashSet<String>),
}
impl<'a> RuleFilter<'a> {
pub fn allows(&self, rule_id: &str) -> bool {
match self {
RuleFilter::Single(id) => *id == rule_id,
RuleFilter::Any(set) => set.contains(rule_id),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ReturnTaintSummary {
pub direct_source: Option<String>,
pub params_to_return: Vec<usize>,
}
pub type ReturnSummary = HashMap<String, ReturnTaintSummary>;
pub struct BatchedRule<'a> {
pub rule_id: &'a str,
pub spec: &'a TaintSpec,
}
pub(super) struct BatchedTaintGroup {
pub spec: TaintSpec,
pub sink_to_rules: HashMap<String, Vec<String>>,
pub allowed_rule_ids: HashSet<String>,
}
pub(super) struct MatchedSink {
pub description: String,
pub attribution_key: Option<String>,
pub rule_ids: Vec<String>,
}
#[derive(Clone, Debug)]
pub(super) struct TaintInfo {
pub description: String,
pub line: usize,
pub labels: Option<BTreeSet<String>>,
pub source_range: Option<(usize, usize)>,
}
#[derive(Default)]
pub(super) struct TaintState {
pub tainted: HashMap<String, TaintInfo>,
}
impl TaintState {
pub fn taint(&mut self, name: String, description: String, line: usize) {
self.tainted.insert(
name,
TaintInfo {
description,
line,
labels: None,
source_range: None,
},
);
}
pub fn taint_ranged(
&mut self,
name: String,
description: String,
line: usize,
source_range: Option<(usize, usize)>,
) {
self.tainted.insert(
name,
TaintInfo {
description,
line,
labels: None,
source_range,
},
);
}
pub fn taint_labeled(
&mut self,
name: String,
description: String,
line: usize,
labels: Option<BTreeSet<String>>,
) {
self.tainted.insert(
name,
TaintInfo {
description,
line,
labels,
source_range: None,
},
);
}
pub fn clear(&mut self, name: &str) {
self.tainted.remove(name);
}
pub fn info(&self, name: &str) -> Option<&TaintInfo> {
self.tainted.get(name)
}
}
pub(super) struct AnalysisContext<'a, CF> {
pub source: &'a str,
pub spec: &'a TaintSpec,
pub aliases: Option<&'a super::common::AliasTable>,
pub summaries: &'a ReturnSummary,
pub cross_file: Option<&'a CF>,
pub sink_to_rules: Option<&'a HashMap<String, Vec<String>>>,
pub label_policy: Option<&'a LabelPolicy>,
}
pub(super) trait TaintLanguageAdapter<CF> {
fn is_nested_scope(kind: &str) -> bool;
fn dispatch_walk_node(
node: Node<'_>,
ctx: &AnalysisContext<'_, CF>,
state: &mut TaintState,
findings: &mut Vec<TaintFinding>,
);
fn dispatch_summary_node(
node: Node<'_>,
ctx: &AnalysisContext<'_, CF>,
state: &mut TaintState,
findings: &mut Vec<TaintFinding>,
return_taint: &mut Option<String>,
);
#[allow(dead_code)]
fn expression_taint(
expr: Node<'_>,
ctx: &AnalysisContext<'_, CF>,
state: &TaintState,
) -> Option<(String, usize)>;
fn seed_params(func_node: Node<'_>, ctx: &AnalysisContext<'_, CF>, state: &mut TaintState);
fn get_body(func_node: Node<'_>) -> Option<Node<'_>> {
func_node.child_by_field_name("body")
}
}
pub(super) fn walk_body_generic<T: TaintLanguageAdapter<CF>, CF>(
node: Node<'_>,
ctx: &AnalysisContext<'_, CF>,
state: &mut TaintState,
findings: &mut Vec<TaintFinding>,
) {
if T::is_nested_scope(node.kind()) {
return;
}
T::dispatch_walk_node(node, ctx, state, findings);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_body_generic::<T, CF>(child, ctx, state, findings);
}
}
pub(super) fn walk_body_for_summary_generic<T: TaintLanguageAdapter<CF>, CF>(
node: Node<'_>,
ctx: &AnalysisContext<'_, CF>,
state: &mut TaintState,
findings: &mut Vec<TaintFinding>,
return_taint: &mut Option<String>,
) {
if T::is_nested_scope(node.kind()) {
return;
}
T::dispatch_summary_node(node, ctx, state, findings, return_taint);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_body_for_summary_generic::<T, CF>(child, ctx, state, findings, return_taint);
}
}
pub(super) fn analyze_function_generic<T: TaintLanguageAdapter<CF>, CF>(
func_node: Node<'_>,
ctx: &AnalysisContext<'_, CF>,
findings: &mut Vec<TaintFinding>,
) {
let mut state = TaintState::default();
T::seed_params(func_node, ctx, &mut state);
let Some(body) = T::get_body(func_node) else {
return;
};
walk_body_generic::<T, CF>(body, ctx, &mut state, findings);
}
pub(super) fn summarize_function_generic<T, CF>(
func_node: Node<'_>,
ctx: &AnalysisContext<'_, CF>,
) -> Option<String>
where
T: TaintLanguageAdapter<CF>,
{
let mut state = TaintState::default();
T::seed_params(func_node, ctx, &mut state);
let body = T::get_body(func_node)?;
let mut scratch: Vec<TaintFinding> = Vec::new();
let mut return_taint: Option<String> = None;
walk_body_for_summary_generic::<T, CF>(body, ctx, &mut state, &mut scratch, &mut return_taint);
return_taint
}
pub(super) fn summarize_function_return_generic<T, CF>(
func_node: Node<'_>,
ctx: &AnalysisContext<'_, CF>,
collect_param_names: impl Fn(Node<'_>, &str) -> Vec<String>,
) -> ReturnTaintSummary
where
T: TaintLanguageAdapter<CF>,
{
let direct_source = summarize_function_generic::<T, CF>(func_node, ctx);
let mut summary = ReturnTaintSummary {
direct_source,
params_to_return: Vec::new(),
};
let empty_summary = ReturnSummary::new();
for (param_idx, param_name) in collect_param_names(func_node, ctx.source)
.into_iter()
.enumerate()
{
let synthetic_spec = TaintSpec {
sources: vec![NodeMatcher::ParamName {
names: vec![param_name.clone()],
description: format!("parameter '{}'", param_name),
}],
sinks: vec![],
sanitizers: ctx.spec.sanitizers.clone(),
};
let param_ctx = AnalysisContext {
source: ctx.source,
spec: &synthetic_spec,
aliases: ctx.aliases,
summaries: &empty_summary,
cross_file: None,
sink_to_rules: None,
label_policy: None,
};
if summarize_function_generic::<T, CF>(func_node, ¶m_ctx).is_some() {
summary.params_to_return.push(param_idx);
}
}
summary
}
pub(super) fn extract_cross_file_summary_for_function<T, CF>(
func_node: Node<'_>,
func_name: &str,
param_names: &[String],
source: &str,
aliases: Option<&super::common::AliasTable>,
rule_specs: &[(&str, TaintSpec)],
) -> Option<FunctionTaintSummary>
where
T: TaintLanguageAdapter<CF>,
{
extract_cross_file_summary_for_function_cf::<T, CF>(
func_node,
func_name,
param_names,
source,
aliases,
rule_specs,
None,
)
}
pub(super) fn extract_cross_file_summary_for_function_cf<T, CF>(
func_node: Node<'_>,
func_name: &str,
param_names: &[String],
source: &str,
aliases: Option<&super::common::AliasTable>,
rule_specs: &[(&str, TaintSpec)],
cross_file: Option<&CF>,
) -> Option<FunctionTaintSummary>
where
T: TaintLanguageAdapter<CF>,
{
if param_names.is_empty() {
return None;
}
let mut params_to_sink: Vec<ParamSinkFlow> = Vec::new();
let mut params_to_return: Vec<usize> = Vec::new();
let mut batched_sinks: Vec<NodeMatcher> = Vec::new();
let mut sink_desc_to_rule: HashMap<&str, &str> = HashMap::new();
let mut sanitizer_rules: Vec<(&str, &TaintSpec)> = Vec::new();
for (rule_id, rule_spec) in rule_specs {
if rule_spec.sanitizers.is_empty() {
for sink in &rule_spec.sinks {
sink_desc_to_rule.insert(sink.description(), rule_id);
batched_sinks.push(sink.clone());
}
} else {
sanitizer_rules.push((rule_id, rule_spec));
}
}
let empty_summary = ReturnSummary::new();
let composition_sanitizers: Vec<NodeMatcher> = if cross_file.is_some() {
let mut merged: Vec<NodeMatcher> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for (_, rule_spec) in rule_specs {
for s in &rule_spec.sanitizers {
if seen.insert(matcher_fingerprint(s)) {
merged.push(s.clone());
}
}
}
merged
} else {
Vec::new()
};
let placeholder_source = NodeMatcher::ParamName {
names: vec![],
description: String::new(),
};
let mut return_spec = TaintSpec {
sources: vec![placeholder_source.clone()],
sinks: vec![],
sanitizers: composition_sanitizers.clone(),
};
let mut batched_spec = TaintSpec {
sources: vec![placeholder_source.clone()],
sinks: batched_sinks,
sanitizers: composition_sanitizers.clone(),
};
let mut sanitizer_specs: Vec<TaintSpec> = sanitizer_rules
.iter()
.map(|(_, rule_spec)| TaintSpec {
sources: vec![placeholder_source.clone()],
sinks: rule_spec.sinks.clone(),
sanitizers: if cross_file.is_some() {
composition_sanitizers.clone()
} else {
rule_spec.sanitizers.clone()
},
})
.collect();
for (param_idx, param_name) in param_names.iter().enumerate() {
let synthetic_source = NodeMatcher::ParamName {
names: vec![param_name.clone()],
description: format!("parameter '{}'", param_name),
};
return_spec.sources[0] = synthetic_source.clone();
let return_ctx = AnalysisContext {
source,
spec: &return_spec,
aliases,
summaries: &empty_summary,
cross_file,
sink_to_rules: None,
label_policy: None,
};
let mut return_findings = Vec::new();
let mut return_state = TaintState::default();
T::seed_params(func_node, &return_ctx, &mut return_state);
if let Some(body) = T::get_body(func_node) {
let mut return_taint: Option<String> = None;
walk_body_for_summary_generic::<T, CF>(
body,
&return_ctx,
&mut return_state,
&mut return_findings,
&mut return_taint,
);
if return_taint.is_some() && !params_to_return.contains(¶m_idx) {
params_to_return.push(param_idx);
}
}
let mut seen: HashSet<(usize, String)> = HashSet::new();
if !batched_spec.sinks.is_empty() || cross_file.is_some() {
batched_spec.sources[0] = synthetic_source.clone();
let batched_ctx = AnalysisContext {
source,
spec: &batched_spec,
aliases,
summaries: &empty_summary,
cross_file,
sink_to_rules: None,
label_policy: None,
};
let mut findings = Vec::new();
analyze_function_generic::<T, CF>(func_node, &batched_ctx, &mut findings);
for f in &findings {
let rule_id = match sink_desc_to_rule.get(f.sink_description.as_str()) {
Some(&r) => Some(r.to_string()),
None => f.rule_id_hint.clone(),
};
if let Some(rule_id) = rule_id {
if seen.insert((param_idx, rule_id.clone())) {
params_to_sink.push(ParamSinkFlow {
param_index: param_idx,
sink_rule_id: rule_id,
sink_description: f.sink_description.clone(),
});
}
}
}
}
for (idx, (rule_id, _)) in sanitizer_rules.iter().enumerate() {
sanitizer_specs[idx].sources[0] = synthetic_source.clone();
let sink_ctx = AnalysisContext {
source,
spec: &sanitizer_specs[idx],
aliases,
summaries: &empty_summary,
cross_file,
sink_to_rules: None,
label_policy: None,
};
let mut findings = Vec::new();
analyze_function_generic::<T, CF>(func_node, &sink_ctx, &mut findings);
for f in &findings {
let attributed = f
.rule_id_hint
.clone()
.unwrap_or_else(|| rule_id.to_string());
if seen.insert((param_idx, attributed.clone())) {
params_to_sink.push(ParamSinkFlow {
param_index: param_idx,
sink_rule_id: attributed,
sink_description: f.sink_description.clone(),
});
}
}
}
}
if params_to_sink.is_empty() && params_to_return.is_empty() {
return None;
}
Some(FunctionTaintSummary {
name: func_name.to_string(),
params_to_return,
params_to_sink,
})
}
pub(super) fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
&source[node.byte_range()]
}
pub const ANY_PARAM_WILDCARD: &str = "$<any-param>";
pub(super) fn param_names_are_wildcard(names: &[String]) -> bool {
names.iter().any(|n| n == ANY_PARAM_WILDCARD)
}
pub(super) fn build_batched_taint_groups(rules: &[BatchedRule<'_>]) -> Vec<BatchedTaintGroup> {
let mut groups: Vec<Vec<usize>> = Vec::new();
for (i, r) in rules.iter().enumerate() {
let mut placed = false;
for g in groups.iter_mut() {
let rep = rules[g[0]].spec;
if sanitizer_fingerprints_eq(&rep.sanitizers, &r.spec.sanitizers) {
g.push(i);
placed = true;
break;
}
}
if !placed {
groups.push(vec![i]);
}
}
let mut out = Vec::new();
for group in groups {
let mut merged_sources: Vec<NodeMatcher> = Vec::new();
let mut merged_sinks: Vec<NodeMatcher> = Vec::new();
let mut seen_source_keys: HashSet<String> = HashSet::new();
let mut seen_sink_keys: HashSet<String> = HashSet::new();
let mut sink_to_rules: HashMap<String, Vec<String>> = HashMap::new();
let mut allowed_rule_ids: HashSet<String> = HashSet::new();
for idx in &group {
let rule = &rules[*idx];
allowed_rule_ids.insert(rule.rule_id.to_string());
for src in &rule.spec.sources {
let source_key = matcher_fingerprint(src);
if seen_source_keys.insert(source_key) {
merged_sources.push(src.clone());
}
}
for sink in &rule.spec.sinks {
let sink_key = matcher_fingerprint(sink);
let rule_ids = sink_to_rules.entry(sink_key.clone()).or_default();
if !rule_ids.iter().any(|id| id == rule.rule_id) {
rule_ids.push(rule.rule_id.to_string());
}
if seen_sink_keys.insert(sink_key) {
merged_sinks.push(sink.clone());
}
}
}
out.push(BatchedTaintGroup {
spec: TaintSpec {
sources: merged_sources,
sinks: merged_sinks,
sanitizers: rules[group[0]].spec.sanitizers.clone(),
},
sink_to_rules,
allowed_rule_ids,
});
}
out
}
pub(super) fn match_call_sink(
spec: &TaintSpec,
resolved_callee: &str,
sink_to_rules: Option<&HashMap<String, Vec<String>>>,
) -> Option<MatchedSink> {
let final_segment = resolved_callee
.rsplit('.')
.next()
.unwrap_or(resolved_callee);
let root_segment = resolved_callee.split('.').next().unwrap_or(resolved_callee);
spec.sinks.iter().find_map(|matcher| match matcher {
NodeMatcher::Call { canonical, .. } if canonical.as_str() == resolved_callee => {
Some(matched_sink_for_matcher(matcher, sink_to_rules))
}
NodeMatcher::MethodName { method, .. } if method == final_segment => {
Some(matched_sink_for_matcher(matcher, sink_to_rules))
}
NodeMatcher::CallRegex { regex, .. } if regex.is_match(resolved_callee) => {
Some(matched_sink_for_matcher(matcher, sink_to_rules))
}
NodeMatcher::MethodNameRegex { regex, .. } if regex.is_match(final_segment) => {
Some(matched_sink_for_matcher(matcher, sink_to_rules))
}
NodeMatcher::ReceiverCall { receiver, .. }
if root_segment == receiver.as_str() && resolved_callee.contains('.') =>
{
Some(matched_sink_for_matcher(matcher, sink_to_rules))
}
_ => None,
})
}
pub(super) fn match_member_assign_sink(
spec: &TaintSpec,
field_name: &str,
sink_to_rules: Option<&HashMap<String, Vec<String>>>,
) -> Option<MatchedSink> {
spec.sinks.iter().find_map(|matcher| match matcher {
NodeMatcher::MemberAssign { field, .. } if field == field_name => {
Some(matched_sink_for_matcher(matcher, sink_to_rules))
}
_ => None,
})
}
pub(super) fn match_binop_format_sink(
spec: &TaintSpec,
sink_to_rules: Option<&HashMap<String, Vec<String>>>,
) -> Option<MatchedSink> {
spec.sinks.iter().find_map(|matcher| match matcher {
NodeMatcher::BinopFormat { .. } => Some(matched_sink_for_matcher(matcher, sink_to_rules)),
_ => None,
})
}
pub(super) fn match_object_literal_sink(
spec: &TaintSpec,
sink_to_rules: Option<&HashMap<String, Vec<String>>>,
) -> Option<MatchedSink> {
spec.sinks.iter().find_map(|matcher| match matcher {
NodeMatcher::ObjectLiteralValue { .. } => {
Some(matched_sink_for_matcher(matcher, sink_to_rules))
}
_ => None,
})
}
pub(super) fn match_return_value_sink(
spec: &TaintSpec,
sink_to_rules: Option<&HashMap<String, Vec<String>>>,
) -> Option<MatchedSink> {
spec.sinks.iter().find_map(|matcher| match matcher {
NodeMatcher::ReturnValue { .. } => Some(matched_sink_for_matcher(matcher, sink_to_rules)),
_ => None,
})
}
fn matched_sink_for_matcher(
matcher: &NodeMatcher,
sink_to_rules: Option<&HashMap<String, Vec<String>>>,
) -> MatchedSink {
let key = matcher_fingerprint(matcher);
let rule_ids = sink_to_rules
.and_then(|map| map.get(&key).cloned())
.unwrap_or_default();
MatchedSink {
attribution_key: if rule_ids.is_empty() { None } else { Some(key) },
description: matcher.description().to_string(),
rule_ids,
}
}
pub(super) fn attribution_hint_for_sink(sink: &MatchedSink) -> Option<String> {
match &sink.attribution_key {
Some(key) => Some(key.clone()),
None => match sink.rule_ids.as_slice() {
[rule_id] => Some(rule_id.clone()),
_ => None,
},
}
}
pub(super) fn push_attributed_findings(
out: &mut Vec<(String, TaintFinding)>,
findings: Vec<TaintFinding>,
sink_to_rules: &HashMap<String, Vec<String>>,
) {
for finding in findings {
let Some(hint) = finding.rule_id_hint.clone() else {
continue;
};
if let Some(rule_ids) = sink_to_rules.get(&hint) {
for rule_id in rule_ids {
let mut attributed = finding.clone();
attributed.rule_id_hint = Some(rule_id.clone());
out.push((rule_id.clone(), attributed));
}
} else {
let mut attributed = finding;
attributed.rule_id_hint = Some(hint.clone());
out.push((hint, attributed));
}
}
}
pub(super) fn taint_finding_for_node(
node: Node<'_>,
source_description: String,
sink_description: String,
source_line: usize,
rule_id_hint: Option<String>,
hops: u8,
) -> TaintFinding {
taint_finding_for_node_ranged(
node,
source_description,
sink_description,
source_line,
rule_id_hint,
hops,
None,
)
}
pub(super) fn taint_finding_for_node_ranged(
node: Node<'_>,
source_description: String,
sink_description: String,
source_line: usize,
rule_id_hint: Option<String>,
hops: u8,
source_range: Option<(usize, usize)>,
) -> TaintFinding {
let start = node.start_position();
let end = node.end_position();
TaintFinding {
sink_start_byte: node.start_byte(),
sink_end_byte: node.end_byte(),
sink_line: start.row + 1,
sink_column: start.column + 1,
sink_end_line: end.row + 1,
sink_end_column: end.column + 1,
source_description,
sink_description,
source_line,
source_range,
rule_id_hint,
hops,
}
}
pub(super) fn cross_file_taint_finding(
node: Node<'_>,
source_description: String,
source_line: usize,
sink_description: &str,
callee_name: &str,
sink_rule_id: &str,
) -> TaintFinding {
taint_finding_for_node(
node,
source_description,
format!("{sink_description} (via cross-file call to {callee_name})"),
source_line,
Some(sink_rule_id.to_string()),
2,
)
}
pub(super) fn sanitizer_fingerprints_eq(a: &[NodeMatcher], b: &[NodeMatcher]) -> bool {
if a.len() != b.len() {
return false;
}
let fingerprint = |matchers: &[NodeMatcher]| -> Vec<String> {
let mut v: Vec<String> = matchers.iter().map(matcher_fingerprint).collect();
v.sort();
v
};
fingerprint(a) == fingerprint(b)
}
pub(super) fn matcher_fingerprint(m: &NodeMatcher) -> String {
match m {
NodeMatcher::Attribute {
root,
field,
description,
} => format!("A|{root}|{field}|{description}"),
NodeMatcher::Call {
canonical,
description,
} => format!("C|{canonical}|{description}"),
NodeMatcher::ParamName { names, description } => {
format!("P|{}|{description}", names.join(","))
}
NodeMatcher::MethodName {
method,
description,
} => {
format!("M|{method}|{description}")
}
NodeMatcher::CallRegex { regex, description } => {
format!("CR|{}|{description}", regex.as_str())
}
NodeMatcher::MethodNameRegex { regex, description } => {
format!("MR|{}|{description}", regex.as_str())
}
NodeMatcher::ReceiverCall {
receiver,
description,
} => {
format!("R|{receiver}|{description}")
}
NodeMatcher::FieldName { field, description } => {
format!("F|{field}|{description}")
}
NodeMatcher::Subscript { base, description } => {
format!("S|{}|{description}", base.as_deref().unwrap_or("*"))
}
NodeMatcher::MemberAssign { field, description } => {
format!("MA|{field}|{description}")
}
NodeMatcher::BinopFormat { description } => {
format!("BF|{description}")
}
NodeMatcher::ObjectLiteralValue { description } => {
format!("OL|{description}")
}
NodeMatcher::ReturnValue { description } => {
format!("RV|{description}")
}
NodeMatcher::TypedName {
type_name,
description,
} => {
format!("TN|{type_name}|{description}")
}
NodeMatcher::TypedAssignTarget {
type_name,
description,
} => {
format!("TAT|{type_name}|{description}")
}
NodeMatcher::LiteralString { description, regex } => {
format!("LS|{}|{description}", regex.as_deref().unwrap_or(""))
}
NodeMatcher::LooseEquality { description } => {
format!("LE|{description}")
}
NodeMatcher::TaintedCallee { description } => {
format!("TC|{description}")
}
NodeMatcher::TaintedSubscriptKey { base, description } => {
format!("TSK|{}|{description}", base.as_deref().unwrap_or("*"))
}
NodeMatcher::CallArgSource {
method,
arg_index,
description,
} => {
format!("CAS|{method}|{arg_index}|{description}")
}
NodeMatcher::FirstParamSource { description } => {
format!("FPS|{description}")
}
NodeMatcher::DecoratedParamSource {
decorator,
description,
} => {
format!("DPS|{decorator}|{description}")
}
NodeMatcher::CallArgConcat {
method,
description,
} => {
format!("CAC|{method}|{description}")
}
NodeMatcher::ConstructorArgSink {
class_names,
arg_index,
description,
} => {
format!("CTOR|{}|{arg_index}|{description}", class_names.join(","))
}
NodeMatcher::PropertyAssignSink {
property_names,
description,
} => {
format!("PAS|{}|{description}", property_names.join(","))
}
NodeMatcher::MethodArgSink {
methods,
arg_index,
description,
} => {
format!("MAS|{}|{arg_index}|{description}", methods.join(","))
}
NodeMatcher::ReceiverProvenanceCall {
init_receiver,
init_method,
init_arg,
method,
description,
} => {
format!("RPC|{init_receiver}.{init_method}({init_arg})|{method}|{description}")
}
NodeMatcher::LiteralArgCall {
method,
arg,
description,
} => {
format!("LAC|{method}({arg})|{description}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rule_spec(source: NodeMatcher, sink: NodeMatcher) -> TaintSpec {
TaintSpec {
sources: vec![source],
sinks: vec![sink],
sanitizers: vec![],
}
}
fn param_source(name: &str, description: &str) -> NodeMatcher {
NodeMatcher::ParamName {
names: vec![name.to_string()],
description: description.to_string(),
}
}
fn call_sink(canonical: &str, description: &str) -> NodeMatcher {
NodeMatcher::Call {
canonical: canonical.to_string(),
description: description.to_string(),
}
}
#[test]
fn batched_group_keeps_distinct_matchers_with_same_description() {
let spec_a = rule_spec(
param_source("request", "input"),
call_sink("a.exec", "exec"),
);
let spec_b = rule_spec(param_source("ctx", "input"), call_sink("b.exec", "exec"));
let rules = [
BatchedRule {
rule_id: "rule-a",
spec: &spec_a,
},
BatchedRule {
rule_id: "rule-b",
spec: &spec_b,
},
];
let groups = build_batched_taint_groups(&rules);
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].spec.sources.len(), 2);
assert_eq!(groups[0].spec.sinks.len(), 2);
let matched = match_call_sink(&groups[0].spec, "a.exec", Some(&groups[0].sink_to_rules))
.expect("a.exec should match");
assert_eq!(matched.rule_ids, vec!["rule-a".to_string()]);
}
#[test]
fn batched_group_fans_out_identical_sink_matchers_to_all_owner_rules() {
let spec_a = rule_spec(param_source("request", "input"), call_sink("exec", "exec"));
let spec_b = rule_spec(param_source("request", "input"), call_sink("exec", "exec"));
let rules = [
BatchedRule {
rule_id: "rule-a",
spec: &spec_a,
},
BatchedRule {
rule_id: "rule-b",
spec: &spec_b,
},
];
let groups = build_batched_taint_groups(&rules);
let group = &groups[0];
assert_eq!(group.spec.sources.len(), 1);
assert_eq!(group.spec.sinks.len(), 1);
let matched = match_call_sink(&group.spec, "exec", Some(&group.sink_to_rules))
.expect("exec should match");
assert_eq!(
matched.rule_ids,
vec!["rule-a".to_string(), "rule-b".to_string()]
);
let finding = TaintFinding {
sink_start_byte: 0,
sink_end_byte: 4,
sink_line: 1,
sink_column: 1,
sink_end_line: 1,
sink_end_column: 5,
source_description: "input".to_string(),
sink_description: "exec".to_string(),
source_line: 1,
source_range: None,
rule_id_hint: attribution_hint_for_sink(&matched),
hops: 1,
};
let mut out = Vec::new();
push_attributed_findings(&mut out, vec![finding], &group.sink_to_rules);
let rule_ids: Vec<String> = out.into_iter().map(|(rule_id, _)| rule_id).collect();
assert_eq!(rule_ids, vec!["rule-a".to_string(), "rule-b".to_string()]);
}
}