mod ast;
mod flow;
mod sanitizers;
mod sinks;
mod sources;
#[cfg(test)]
mod tests;
use crate::review::diff::ChangedFile;
use crate::review::signals::content::ReviewSource;
use crate::scan::language::detect_language;
use serde::Serialize;
use tree_sitter::Node;
pub use sinks::SinkKind;
pub use sources::SourceKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TaintLang {
Js,
Python,
Go,
}
impl TaintLang {
fn from_label(label: &str) -> Option<Self> {
match label {
"JavaScript" | "JavaScript React" | "TypeScript" | "TypeScript React" => Some(Self::Js),
"Python" => Some(Self::Python),
"Go" => Some(Self::Go),
_ => None,
}
}
fn is_flow_scope(self, node: Node<'_>) -> bool {
match self {
Self::Js => matches!(
node.kind(),
"function_declaration"
| "generator_function_declaration"
| "method_definition"
| "function_expression"
| "generator_function"
| "arrow_function"
),
Self::Python => matches!(node.kind(), "function_definition" | "lambda"),
Self::Go => matches!(
node.kind(),
"function_declaration" | "method_declaration" | "func_literal"
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct TaintSignal {
pub source: SourceKind,
pub sink: SinkKind,
pub path: String,
pub line: usize,
pub detail: String,
}
pub fn detect_taint(file: &ChangedFile, post_source: Option<&ReviewSource>) -> Vec<TaintSignal> {
if crate::audits::context::classify::helpers::is_test_file(&file.path) {
return Vec::new();
}
let Some(post) = post_source else {
return Vec::new();
};
let Some(lang) = detect_language(&file.path).and_then(TaintLang::from_label) else {
return Vec::new();
};
let parsed = post.parsed();
let Some(tree) = parsed.tree() else {
return Vec::new();
};
let mut signals = Vec::new();
flow::detect(tree.root_node(), post.content(), lang, file, &mut signals);
let mut unique: Vec<TaintSignal> = Vec::new();
for signal in signals {
if !unique
.iter()
.any(|existing| existing.line == signal.line && existing.sink == signal.sink)
{
unique.push(signal);
}
}
unique
}
fn is_ident_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
pub(super) fn contains_token(haystack: &str, needle: &str) -> bool {
if needle.is_empty() {
return false;
}
let mut start = 0;
while let Some(rel) = haystack[start..].find(needle) {
let s = start + rel;
let e = s + needle.len();
let before_ok = s == 0 || !haystack[..s].chars().next_back().is_some_and(is_ident_char);
let after_ok =
e >= haystack.len() || !haystack[e..].chars().next().is_some_and(is_ident_char);
if before_ok && after_ok {
return true;
}
start = e;
}
false
}