use super::ast::{callee_ends_with, callee_starts_with, first_named_arg, has_descendant_kind};
use super::sinks::{Sink, SinkKind, classify_sink};
use super::sources::{SourceKind, node_has_source};
use super::{TaintLang, TaintSignal};
use crate::review::diff::ChangedFile;
use crate::review::signals::behavioral::truncate_str;
use std::collections::HashMap;
use tree_sitter::Node;
pub(super) fn detect(
root: Node<'_>,
content: &str,
lang: TaintLang,
file: &ChangedFile,
out: &mut Vec<TaintSignal>,
) {
detect_scope(root, content, lang, file, out);
}
fn detect_scope(
root: Node<'_>,
content: &str,
lang: TaintLang,
file: &ChangedFile,
out: &mut Vec<TaintSignal>,
) {
let tainted = seed_tainted(root, content, lang);
check_sinks(root, content, lang, file, &tainted, out);
detect_nested_scopes(root, content, lang, file, out);
}
fn detect_nested_scopes(
node: Node<'_>,
content: &str,
lang: TaintLang,
file: &ChangedFile,
out: &mut Vec<TaintSignal>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if lang.is_flow_scope(child) {
detect_scope(child, content, lang, file, out);
} else {
detect_nested_scopes(child, content, lang, file, out);
}
}
}
fn seed_tainted(root: Node<'_>, content: &str, lang: TaintLang) -> HashMap<String, SourceKind> {
let mut assignments: Vec<Assignment<'_>> = Vec::new();
collect_assignments(root, lang, content, &mut assignments);
assignments.sort_by_key(|assignment| assignment.rhs.start_byte());
let mut tainted: HashMap<String, SourceKind> = HashMap::new();
for assignment in &assignments {
match node_has_source(assignment.rhs, content, lang)
.or_else(|| node_mentions_tainted(assignment.rhs, content, lang, &tainted))
{
Some(kind) => {
for name in &assignment.names {
tainted.insert(name.clone(), kind);
}
}
None if !assignment.augmenting => {
for name in &assignment.names {
tainted.remove(name);
}
}
None => {}
}
}
tainted
}
struct Assignment<'a> {
names: Vec<String>,
rhs: Node<'a>,
augmenting: bool,
}
fn collect_assignments<'a>(
node: Node<'a>,
lang: TaintLang,
content: &str,
out: &mut Vec<Assignment<'a>>,
) {
if let Some((lhs, rhs)) = assignment_parts(node, lang) {
let mut names = Vec::new();
collect_lhs_names(lhs, content, &mut names);
if !names.is_empty() {
out.push(Assignment {
names,
rhs,
augmenting: is_augmenting(node, lang),
});
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if !lang.is_flow_scope(child) {
collect_assignments(child, lang, content, out);
}
}
}
fn is_augmenting(node: Node<'_>, lang: TaintLang) -> bool {
match lang {
TaintLang::Python => node.kind() == "augmented_assignment",
TaintLang::Go => {
node.kind() == "assignment_statement" && {
let mut cursor = node.walk();
node.children(&mut cursor).any(|child| {
matches!(
child.kind(),
"+=" | "-="
| "*="
| "/="
| "%="
| "&="
| "|="
| "^="
| "<<="
| ">>="
| "&^="
)
})
}
}
TaintLang::Js => false,
}
}
fn assignment_parts<'a>(node: Node<'a>, lang: TaintLang) -> Option<(Node<'a>, Node<'a>)> {
let kind = node.kind();
let is_assignment = match lang {
TaintLang::Js => matches!(kind, "variable_declarator" | "assignment_expression"),
TaintLang::Python => matches!(kind, "assignment" | "augmented_assignment"),
TaintLang::Go => matches!(kind, "short_var_declaration" | "assignment_statement"),
};
if !is_assignment {
return None;
}
let (lhs_field, rhs_field) = if kind == "variable_declarator" {
("name", "value")
} else {
("left", "right")
};
Some((
node.child_by_field_name(lhs_field)?,
node.child_by_field_name(rhs_field)?,
))
}
fn collect_lhs_names(lhs: Node<'_>, content: &str, out: &mut Vec<String>) {
match lhs.kind() {
"identifier" | "shorthand_property_identifier_pattern" => {
if let Ok(text) = lhs.utf8_text(content.as_bytes()) {
out.push(text.to_string());
}
}
"object_pattern"
| "array_pattern"
| "object_assignment_pattern"
| "pattern_list"
| "tuple_pattern"
| "list_pattern"
| "expression_list" => {
let mut cursor = lhs.walk();
for child in lhs.named_children(&mut cursor) {
collect_lhs_names(child, content, out);
}
}
"pair_pattern" => {
if let Some(value) = lhs.child_by_field_name("value") {
collect_lhs_names(value, content, out);
}
}
_ => {}
}
}
fn node_mentions_tainted(
node: Node<'_>,
content: &str,
lang: TaintLang,
tainted: &HashMap<String, SourceKind>,
) -> Option<SourceKind> {
if lang.is_flow_scope(node) {
return None;
}
if super::sanitizers::is_sanitizer_call(node, content, lang) {
return None;
}
if node.kind() == "identifier" {
let text = node.utf8_text(content.as_bytes()).ok()?;
if let Some(source) = tainted.get(text) {
return Some(*source);
}
}
let mut cursor = node.walk();
node.named_children(&mut cursor)
.find_map(|child| node_mentions_tainted(child, content, lang, tainted))
}
fn check_sinks(
node: Node<'_>,
content: &str,
lang: TaintLang,
file: &ChangedFile,
tainted: &HashMap<String, SourceKind>,
out: &mut Vec<TaintSignal>,
) {
if let Some(sink) = classify_sink(node, content, lang) {
let line = node.start_position().row + 1;
if file.contains_line(line) {
if let Some(source) = sink_taint(&sink, content, lang, tainted) {
let call_text = node.utf8_text(content.as_bytes()).unwrap_or("");
out.push(TaintSignal {
source,
sink: sink.kind,
path: file.path_string(),
line,
detail: format!(
"{} reaches {}: {}",
source.label(),
sink.kind.label(),
truncate_str(call_text, 60)
),
});
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if !lang.is_flow_scope(child) {
check_sinks(child, content, lang, file, tainted, out);
}
}
}
fn sink_taint(
sink: &Sink<'_>,
content: &str,
lang: TaintLang,
tainted: &HashMap<String, SourceKind>,
) -> Option<SourceKind> {
match sink.kind {
SinkKind::Sql => sql_taint(sink.args, content, lang, tainted),
_ => node_has_source(sink.args, content, lang)
.or_else(|| node_mentions_tainted(sink.args, content, lang, tainted)),
}
}
fn sql_taint(
args: Node<'_>,
content: &str,
lang: TaintLang,
tainted: &HashMap<String, SourceKind>,
) -> Option<SourceKind> {
let first = first_named_arg(args)?;
let first_text = first.utf8_text(content.as_bytes()).unwrap_or("");
if is_string_building(first, content, lang) {
return node_has_source(first, content, lang)
.or_else(|| node_mentions_tainted(first, content, lang, tainted));
}
if first.kind() == "identifier" {
return tainted.get(first_text).copied();
}
node_has_source(first, content, lang)
}
fn is_string_building(node: Node<'_>, content: &str, lang: TaintLang) -> bool {
match lang {
TaintLang::Js => matches!(node.kind(), "template_string" | "binary_expression"),
TaintLang::Python => {
node.kind() == "binary_operator"
|| (node.kind() == "string" && has_descendant_kind(node, "interpolation"))
|| (node.kind() == "call" && callee_ends_with(node, content, ".format"))
}
TaintLang::Go => {
node.kind() == "binary_expression"
|| (node.kind() == "call_expression"
&& callee_starts_with(node, content, "fmt.Sprint"))
}
}
}