#![allow(clippy::collapsible_if)]
use super::domain::{AuthLevel, ChainProxyState, ProductState, ResourceLifecycle};
use super::engine::Transfer;
use super::symbol::{SymbolId, SymbolInterner};
use crate::cfg::{EdgeKind, NodeInfo, StmtKind};
use crate::cfg_analysis::rules::{self, ResourcePair};
use crate::symbol::Lang;
use petgraph::graph::NodeIndex;
fn try_chain_decompose(callee: &str) -> Option<(&str, &str)> {
for ch in callee.chars() {
match ch {
'(' | ')' | '[' | ']' | '<' | '>' | '?' | '*' | '&' | ':' | ' ' | '\t' | '\n' | '-'
| '!' | ',' | ';' | '"' | '\'' | '\\' => return None,
_ => {}
}
}
let last_dot = callee.rfind('.')?;
let receiver_text = &callee[..last_dot];
let method_suffix = &callee[last_dot + 1..];
if receiver_text.is_empty() || method_suffix.is_empty() {
return None;
}
if receiver_text.split('.').any(str::is_empty) {
return None;
}
Some((receiver_text, method_suffix))
}
#[derive(Debug, Clone)]
pub struct TransferEvent {
pub kind: TransferEventKind,
pub node: NodeIndex,
pub var: SymbolId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransferEventKind {
UseAfterClose,
DoubleClose,
}
static RESOURCE_USE_PATTERNS: &[&str] = &[
"read",
"write",
"send",
"recv",
"fread",
"fwrite",
"fgets",
"fputs",
"fprintf",
"fscanf",
"fflush",
"fseek",
"ftell",
"rewind",
"feof",
"ferror",
"fgetc",
"fputc",
"getc",
"putc",
"ungetc",
"query",
"execute",
"fetch",
"sendto",
"recvfrom",
"ioctl",
"fcntl",
"strcpy",
"strncpy",
"strcat",
"strncat",
"memcpy",
"memmove",
"memset",
"memcmp",
"strcmp",
"strncmp",
"strlen",
"sprintf",
"snprintf",
".read",
".write",
".send",
".recv",
".query",
".execute",
".fetch",
"readSync",
"writeSync",
"readFileSync",
"writeFileSync",
"appendFileSync",
"ftruncateSync",
"fsyncSync",
"fstatSync",
"pipe",
"unpipe",
"resume",
"pause",
"destroy",
];
static ADMIN_PATTERNS: &[&str] = &[
"is_admin",
"hasrole",
"has_role",
"check_admin",
"require_admin",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceEffect {
Acquire,
Release,
}
#[derive(Debug, Clone)]
pub struct ResourceMethodSummary {
pub method_name: String,
pub effect: ResourceEffect,
pub class_group: crate::cfg::BodyId,
pub original_span: (usize, usize),
}
pub struct DefaultTransfer<'a> {
pub lang: Lang,
pub resource_pairs: &'a [ResourcePair],
pub interner: &'a SymbolInterner,
pub resource_method_summaries: &'a [ResourceMethodSummary],
pub ptr_proxy_hints:
Option<&'a std::collections::HashMap<String, crate::pointer::PtrProxyHint>>,
}
impl Transfer<ProductState> for DefaultTransfer<'_> {
type Event = TransferEvent;
fn apply(
&self,
node_idx: NodeIndex,
info: &NodeInfo,
edge: Option<EdgeKind>,
mut state: ProductState,
) -> (ProductState, Vec<TransferEvent>) {
let mut events = Vec::new();
match info.kind {
StmtKind::Call => {
self.apply_call(node_idx, info, &mut state, &mut events);
}
StmtKind::If => {
self.apply_if(info, edge, &mut state);
}
StmtKind::Seq => {
self.apply_assignment(node_idx, info, &mut state);
}
_ => {}
}
(state, events)
}
}
impl DefaultTransfer<'_> {
fn get_sym(&self, info: &NodeInfo, name: &str) -> Option<SymbolId> {
self.interner
.get_scoped(info.ast.enclosing_func.as_deref(), name)
}
fn try_apply_field_alias_proxy(
&self,
info: &NodeInfo,
callee: &str,
state: &mut ProductState,
) -> bool {
let Some(hints) = self.ptr_proxy_hints else {
return false;
};
let Some((receiver_text, method_suffix)) = try_chain_decompose(callee) else {
return false;
};
if receiver_text.contains('.') {
return false;
}
let recv_name: &str = match info.call.receiver.as_deref() {
Some(r) if !r.contains('.') && !r.contains('(') => r,
_ => receiver_text,
};
if hints.get(recv_name).copied() != Some(crate::pointer::PtrProxyHint::FieldOnly) {
return false;
}
let mut handled = false;
for summary in self.resource_method_summaries {
if !summary.method_name.eq_ignore_ascii_case(method_suffix) {
continue;
}
handled = true;
match summary.effect {
ResourceEffect::Acquire => {
state.chain_proxies.insert(
recv_name.to_string(),
ChainProxyState {
lifecycle: ResourceLifecycle::OPEN,
class_group: summary.class_group,
acquire_span: summary.original_span,
},
);
}
ResourceEffect::Release => {
if let Some(entry) = state.chain_proxies.get_mut(recv_name) {
if entry.class_group == summary.class_group
&& entry.lifecycle.contains(ResourceLifecycle::OPEN)
{
entry.lifecycle = ResourceLifecycle::CLOSED;
}
}
}
}
}
handled
}
fn apply_call(
&self,
node_idx: NodeIndex,
info: &NodeInfo,
state: &mut ProductState,
events: &mut Vec<TransferEvent>,
) {
let callee = match &info.call.callee {
Some(c) => c.to_ascii_lowercase(),
None => return,
};
if self.try_apply_field_alias_proxy(info, &callee, state) {
return;
}
let mut direct_acquire = false;
for pair in self.resource_pairs {
let is_acquire = pair.acquire.iter().any(|a| callee_matches(&callee, a));
let is_excluded = pair
.exclude_acquire
.iter()
.any(|e| callee_matches(&callee, e));
if is_acquire
&& !is_excluded
&& let Some(ref def) = info.taint.defines
&& let Some(sym) = self.get_sym(info, def)
{
state.resource.set(sym, ResourceLifecycle::OPEN);
direct_acquire = true;
}
}
let mut direct_release = false;
let mut released: smallvec::SmallVec<[SymbolId; 4]> = smallvec::SmallVec::new();
for pair in self.resource_pairs {
let is_release = pair.release.iter().any(|r| callee_matches(&callee, r));
if is_release {
direct_release = true;
if info.in_defer {
continue;
}
for used in &info.taint.uses {
if let Some(sym) = self.get_sym(info, used) {
if released.contains(&sym) {
continue;
}
let current = state.resource.get(sym);
if current == ResourceLifecycle::CLOSED {
events.push(TransferEvent {
kind: TransferEventKind::DoubleClose,
node: node_idx,
var: sym,
});
} else if current.contains(ResourceLifecycle::OPEN) {
state.resource.set(sym, ResourceLifecycle::CLOSED);
}
released.push(sym);
}
}
}
}
if let Some((receiver_text, method_suffix)) = try_chain_decompose(&callee) {
let receiver_is_chain = receiver_text.contains('.');
if receiver_is_chain {
for summary in self.resource_method_summaries {
if !summary.method_name.eq_ignore_ascii_case(method_suffix) {
continue;
}
match summary.effect {
ResourceEffect::Acquire => {
state.chain_proxies.insert(
receiver_text.to_string(),
ChainProxyState {
lifecycle: ResourceLifecycle::OPEN,
class_group: summary.class_group,
acquire_span: summary.original_span,
},
);
}
ResourceEffect::Release => {
if let Some(entry) = state.chain_proxies.get_mut(receiver_text) {
if entry.class_group == summary.class_group
&& entry.lifecycle.contains(ResourceLifecycle::OPEN)
{
entry.lifecycle = ResourceLifecycle::CLOSED;
}
}
}
}
}
} else if !direct_acquire && !direct_release {
let recv_name: &str = match info.call.receiver.as_deref() {
Some(r) if !r.contains('.') && !r.contains('(') => r,
_ => receiver_text,
};
for summary in self.resource_method_summaries {
if !summary.method_name.eq_ignore_ascii_case(method_suffix) {
continue;
}
let Some(sym) = self.get_sym(info, recv_name) else {
continue;
};
match summary.effect {
ResourceEffect::Acquire => {
state.resource.set(sym, ResourceLifecycle::OPEN);
state.receiver_class_group.insert(sym, summary.class_group);
state.proxy_acquire_spans.insert(sym, summary.original_span);
}
ResourceEffect::Release => {
if state.receiver_class_group.get(&sym) == Some(&summary.class_group) {
let current = state.resource.get(sym);
if current.contains(ResourceLifecycle::OPEN) {
state.resource.set(sym, ResourceLifecycle::CLOSED);
}
}
}
}
}
}
}
let mut use_checked = false;
for pair in self.resource_pairs {
if pair.use_patterns.iter().any(|p| callee_matches(&callee, p)) {
use_checked = true;
for used in &info.taint.uses {
if let Some(sym) = self.get_sym(info, used) {
if state.resource.get(sym) == ResourceLifecycle::CLOSED {
events.push(TransferEvent {
kind: TransferEventKind::UseAfterClose,
node: node_idx,
var: sym,
});
}
}
}
}
}
if !use_checked {
let is_use = RESOURCE_USE_PATTERNS
.iter()
.any(|p| callee_matches(&callee, p));
if is_use {
for used in &info.taint.uses {
if let Some(sym) = self.get_sym(info, used) {
if state.resource.get(sym) == ResourceLifecycle::CLOSED {
events.push(TransferEvent {
kind: TransferEventKind::UseAfterClose,
node: node_idx,
var: sym,
});
}
}
}
}
}
let auth_rules = rules::auth_rules(self.lang);
let is_auth = auth_rules.iter().any(|rule| {
rule.matchers
.iter()
.any(|m| callee_matches(&callee, &m.to_ascii_lowercase()))
});
if is_auth {
let is_admin = ADMIN_PATTERNS.iter().any(|p| callee_matches(&callee, p));
let new_level = if is_admin {
AuthLevel::Admin
} else {
AuthLevel::Authed
};
if new_level > state.auth.auth_level {
state.auth.auth_level = new_level;
}
}
if is_guard_like(&callee) {
for used in &info.taint.uses {
if let Some(sym) = self.get_sym(info, used) {
state.auth.validated.insert(sym);
}
}
}
}
fn apply_if(&self, info: &NodeInfo, edge: Option<EdgeKind>, state: &mut ProductState) {
let is_positive_edge = if info.condition_negated {
matches!(edge, Some(EdgeKind::False))
} else {
matches!(edge, Some(EdgeKind::True))
};
if !is_positive_edge && is_simple_truth_check(info) {
for var in &info.condition_vars {
if let Some(sym) = self.get_sym(info, var) {
let lc = state.resource.get(sym);
if lc.contains(ResourceLifecycle::OPEN) {
state
.resource
.set(sym, lc.difference(ResourceLifecycle::OPEN));
}
}
}
}
if !is_positive_edge {
return;
}
if let Some(ref cond) = info.condition_text {
let cond_lower = cond.to_ascii_lowercase();
let cond_inner = if info.condition_negated {
cond_lower.trim_start_matches('!').trim_start()
} else {
cond_lower.as_str()
};
let auth_rules = rules::auth_rules(self.lang);
let is_auth_cond = auth_rules.iter().any(|rule| {
rule.matchers
.iter()
.any(|m| condition_contains_auth_token(cond_inner, m))
});
if is_auth_cond {
let is_admin = ADMIN_PATTERNS
.iter()
.any(|p| condition_contains_auth_token(cond_inner, p));
let new_level = if is_admin {
AuthLevel::Admin
} else {
AuthLevel::Authed
};
if new_level > state.auth.auth_level {
state.auth.auth_level = new_level;
}
}
if self.lang == Lang::Go && is_go_map_boolean_guard(cond_inner) {
if AuthLevel::Authed > state.auth.auth_level {
state.auth.auth_level = AuthLevel::Authed;
}
}
if is_guard_like(cond_inner) {
for var in &info.condition_vars {
if let Some(sym) = self.get_sym(info, var) {
state.auth.validated.insert(sym);
}
}
}
}
}
fn apply_assignment(&self, _node_idx: NodeIndex, info: &NodeInfo, state: &mut ProductState) {
if let Some(ref def) = info.taint.defines
&& let Some(def_sym) = self.get_sym(info, def)
{
for used in &info.taint.uses {
if let Some(use_sym) = self.get_sym(info, used) {
let lc = state.resource.get(use_sym);
if lc.contains(ResourceLifecycle::OPEN) {
state.resource.set(def_sym, lc);
state.resource.set(use_sym, ResourceLifecycle::MOVED);
return;
}
}
}
}
}
}
pub fn callee_matches_pub(callee: &str, pattern: &str) -> bool {
callee_matches(callee, pattern)
}
fn callee_matches(callee: &str, pattern: &str) -> bool {
let pattern_lower = pattern.to_ascii_lowercase();
if pattern_lower.starts_with('.') {
callee.ends_with(&pattern_lower)
} else {
callee == pattern_lower || callee.ends_with(&pattern_lower)
}
}
fn is_guard_like(callee: &str) -> bool {
static GUARD_PREFIXES: &[&str] = &["validate", "sanitize", "check_", "verify_", "assert_"];
GUARD_PREFIXES.iter().any(|p| callee.starts_with(p))
}
fn is_simple_truth_check(info: &NodeInfo) -> bool {
if info.condition_vars.len() != 1 {
return false;
}
let var = &info.condition_vars[0];
let Some(text) = info.condition_text.as_deref() else {
return false;
};
let stripped = text.trim();
let stripped = stripped.trim_start_matches('!').trim();
let stripped = stripped.trim_matches(|c: char| c == '(' || c == ')').trim();
stripped == var
}
fn is_go_map_boolean_guard(cond: &str) -> bool {
let cond = cond.trim();
let Some(bracket_start) = cond.find('[') else {
return false;
};
if !cond.ends_with(']') {
return false;
}
let before = &cond[..bracket_start];
let inside = &cond[bracket_start + 1..cond.len() - 1];
!before.is_empty()
&& before
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_')
&& !inside.is_empty()
&& inside
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'.')
}
fn condition_contains_auth_token(cond: &str, matcher: &str) -> bool {
let matcher_lower = matcher.to_ascii_lowercase();
let is_ident_only = matcher_lower
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_');
if is_ident_only {
cond.split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
.filter(|s| !s.is_empty())
.any(|token| token == matcher_lower)
} else {
let hay = cond.as_bytes();
let needle = matcher_lower.as_bytes();
if needle.len() > hay.len() {
return false;
}
let mut start = 0;
while start + needle.len() <= hay.len() {
if let Some(pos) = cond[start..].find(&*matcher_lower) {
let abs = start + pos;
let end = abs + needle.len();
let left_ok = abs == 0 || {
let c = hay[abs - 1];
!c.is_ascii_alphanumeric() && c != b'_'
};
let right_ok = end >= hay.len() || {
let c = hay[end];
!c.is_ascii_alphanumeric() && c != b'_'
};
if left_ok && right_ok {
return true;
}
start = abs + 1;
} else {
break;
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cfg::{AstMeta, CallMeta, TaintMeta};
#[test]
fn callee_matches_exact() {
assert!(callee_matches("fopen", "fopen"));
assert!(!callee_matches("fopen", "fclose"));
}
#[test]
fn callee_matches_suffix() {
assert!(callee_matches("curlx_fclose", "fclose"));
}
#[test]
fn callee_matches_dot_prefix() {
assert!(callee_matches("file.close", ".close"));
assert!(!callee_matches("file.close", ".open"));
}
#[test]
fn callee_matches_js_fd_use_patterns() {
assert!(callee_matches("fs.readsync", "fs.readSync"));
assert!(callee_matches("fs.writesync", "fs.writeSync"));
assert!(!callee_matches("fs.readsync", "fs.writeSync"));
}
#[test]
fn callee_matches_stream_method_patterns() {
assert!(callee_matches("reader.pipe", ".pipe"));
assert!(callee_matches("stream.write", ".write"));
assert!(!callee_matches("readstream", ".read")); }
#[test]
fn callee_matches_dot_prefix_no_c_interference() {
assert!(!callee_matches("fread", ".read"));
assert!(!callee_matches("fwrite", ".write"));
assert!(!callee_matches("send", ".send"));
}
#[test]
fn acquire_sets_open() {
let mut interner = SymbolInterner::new();
let sym_f = interner.intern("f");
let transfer = DefaultTransfer {
lang: Lang::C,
resource_pairs: rules::resource_pairs(Lang::C),
interner: &interner,
resource_method_summaries: &[],
ptr_proxy_hints: None,
};
let info = NodeInfo {
kind: StmtKind::Call,
ast: AstMeta {
span: (0, 10),
..Default::default()
},
taint: TaintMeta {
defines: Some("f".into()),
..Default::default()
},
call: CallMeta {
callee: Some("fopen".into()),
..Default::default()
},
..Default::default()
};
let (state, events) =
transfer.apply(NodeIndex::new(0), &info, None, ProductState::initial());
assert!(events.is_empty());
assert_eq!(state.resource.get(sym_f), ResourceLifecycle::OPEN);
}
#[test]
fn close_after_open_sets_closed() {
let mut interner = SymbolInterner::new();
let sym_f = interner.intern("f");
let transfer = DefaultTransfer {
lang: Lang::C,
resource_pairs: rules::resource_pairs(Lang::C),
interner: &interner,
resource_method_summaries: &[],
ptr_proxy_hints: None,
};
let mut state = ProductState::initial();
state.resource.set(sym_f, ResourceLifecycle::OPEN);
let info = NodeInfo {
kind: StmtKind::Call,
ast: AstMeta {
span: (10, 20),
..Default::default()
},
taint: TaintMeta {
uses: vec!["f".into()],
..Default::default()
},
call: CallMeta {
callee: Some("fclose".into()),
..Default::default()
},
..Default::default()
};
let (state, events) = transfer.apply(NodeIndex::new(1), &info, None, state);
assert!(events.is_empty());
assert_eq!(state.resource.get(sym_f), ResourceLifecycle::CLOSED);
}
#[test]
fn double_close_emits_event() {
let mut interner = SymbolInterner::new();
let sym_f = interner.intern("f");
let transfer = DefaultTransfer {
lang: Lang::C,
resource_pairs: rules::resource_pairs(Lang::C),
interner: &interner,
resource_method_summaries: &[],
ptr_proxy_hints: None,
};
let mut state = ProductState::initial();
state.resource.set(sym_f, ResourceLifecycle::CLOSED);
let info = NodeInfo {
kind: StmtKind::Call,
ast: AstMeta {
span: (20, 30),
..Default::default()
},
taint: TaintMeta {
uses: vec!["f".into()],
..Default::default()
},
call: CallMeta {
callee: Some("fclose".into()),
..Default::default()
},
..Default::default()
};
let (_state, events) = transfer.apply(NodeIndex::new(2), &info, None, state);
assert_eq!(events.len(), 1);
assert_eq!(events[0].kind, TransferEventKind::DoubleClose);
assert_eq!(events[0].var, sym_f);
}
#[test]
fn use_after_close_emits_event() {
let mut interner = SymbolInterner::new();
let sym_f = interner.intern("f");
let transfer = DefaultTransfer {
lang: Lang::C,
resource_pairs: rules::resource_pairs(Lang::C),
interner: &interner,
resource_method_summaries: &[],
ptr_proxy_hints: None,
};
let mut state = ProductState::initial();
state.resource.set(sym_f, ResourceLifecycle::CLOSED);
let info = NodeInfo {
kind: StmtKind::Call,
ast: AstMeta {
span: (30, 40),
..Default::default()
},
taint: TaintMeta {
uses: vec!["f".into()],
..Default::default()
},
call: CallMeta {
callee: Some("fread".into()),
..Default::default()
},
..Default::default()
};
let (_state, events) = transfer.apply(NodeIndex::new(3), &info, None, state);
assert_eq!(events.len(), 1);
assert_eq!(events[0].kind, TransferEventKind::UseAfterClose);
}
#[test]
fn is_guard_like_check() {
assert!(is_guard_like("validate_input"));
assert!(is_guard_like("sanitize_html"));
assert!(is_guard_like("check_permission"));
assert!(!is_guard_like("open_file"));
}
#[test]
fn is_simple_truth_check_recognises_bare_identifier() {
let make = |text: &str, vars: Vec<&str>| NodeInfo {
kind: StmtKind::If,
ast: AstMeta::default(),
condition_text: Some(text.to_string()),
condition_vars: vars.into_iter().map(String::from).collect(),
..Default::default()
};
assert!(is_simple_truth_check(&make("f", vec!["f"])));
assert!(is_simple_truth_check(&make("!f", vec!["f"])));
assert!(is_simple_truth_check(&make("(f)", vec!["f"])));
assert!(is_simple_truth_check(&make("!(f)", vec!["f"])));
assert!(!is_simple_truth_check(&make("f != NULL", vec!["f"])));
assert!(!is_simple_truth_check(&make("f.is_valid()", vec!["f"])));
assert!(!is_simple_truth_check(&make("f && g", vec!["f", "g"])));
assert!(!is_simple_truth_check(&make("f", vec![])));
}
#[test]
fn null_check_clears_open_on_false_edge() {
let mut interner = SymbolInterner::new();
let sym_f = interner.intern("f");
let transfer = DefaultTransfer {
lang: Lang::C,
resource_pairs: rules::resource_pairs(Lang::C),
interner: &interner,
resource_method_summaries: &[],
ptr_proxy_hints: None,
};
let mut state = ProductState::initial();
state.resource.set(sym_f, ResourceLifecycle::OPEN);
let info = NodeInfo {
kind: StmtKind::If,
condition_text: Some("f".into()),
condition_vars: vec!["f".into()],
condition_negated: false,
..Default::default()
};
let (state_false, _) = transfer.apply(
NodeIndex::new(5),
&info,
Some(EdgeKind::False),
state.clone(),
);
assert!(
!state_false
.resource
.get(sym_f)
.contains(ResourceLifecycle::OPEN),
"OPEN should be cleared on the null edge of `if (f)`"
);
let (state_true, _) = transfer.apply(
NodeIndex::new(5),
&info,
Some(EdgeKind::True),
state.clone(),
);
assert!(
state_true
.resource
.get(sym_f)
.contains(ResourceLifecycle::OPEN),
"OPEN should be preserved on the non-null edge of `if (f)`"
);
}
#[test]
fn null_check_negated_clears_open_on_true_edge() {
let mut interner = SymbolInterner::new();
let sym_f = interner.intern("f");
let transfer = DefaultTransfer {
lang: Lang::C,
resource_pairs: rules::resource_pairs(Lang::C),
interner: &interner,
resource_method_summaries: &[],
ptr_proxy_hints: None,
};
let mut state = ProductState::initial();
state.resource.set(sym_f, ResourceLifecycle::OPEN);
let info = NodeInfo {
kind: StmtKind::If,
condition_text: Some("!f".into()),
condition_vars: vec!["f".into()],
condition_negated: true,
..Default::default()
};
let (state_true, _) = transfer.apply(
NodeIndex::new(5),
&info,
Some(EdgeKind::True),
state.clone(),
);
assert!(
!state_true
.resource
.get(sym_f)
.contains(ResourceLifecycle::OPEN),
"OPEN should be cleared on the null edge of `if (!f)` (true edge)"
);
let (state_false, _) = transfer.apply(
NodeIndex::new(5),
&info,
Some(EdgeKind::False),
state.clone(),
);
assert!(
state_false
.resource
.get(sym_f)
.contains(ResourceLifecycle::OPEN),
"OPEN should be preserved on the non-null edge of `if (!f)` (false edge)"
);
}
#[test]
fn callee_matches_js_end_release() {
assert!(callee_matches("conn.end", ".end"));
assert!(callee_matches("pool.end", ".end"));
assert!(!callee_matches("backend", ".end")); }
#[test]
fn callee_matches_go_sql_open() {
assert!(callee_matches("sql.open", "sql.Open")); }
#[test]
fn callee_matches_php_pg() {
assert!(callee_matches("pg_connect", "pg_connect"));
assert!(callee_matches("pg_close", "pg_close"));
assert!(!callee_matches("pg_query", "pg_connect"));
}
#[test]
fn callee_matches_java_prepare_statement() {
assert!(callee_matches("conn.preparestatement", "prepareStatement"));
assert!(callee_matches("preparestatement", "prepareStatement"));
}
#[test]
fn callee_matches_websocket() {
assert!(callee_matches("websocket", "WebSocket"));
}
#[test]
fn callee_matches_mysql_create_connection() {
assert!(callee_matches(
"mysql.createconnection",
"mysql.createConnection"
));
}
#[test]
fn callee_matches_finish_release() {
assert!(callee_matches("http.finish", ".finish"));
assert!(!callee_matches("finish_setup", ".finish")); }
#[test]
fn auth_token_exact_match() {
assert!(condition_contains_auth_token(
"is_authenticated",
"is_authenticated"
));
assert!(condition_contains_auth_token("is_admin", "is_admin"));
assert!(condition_contains_auth_token(
"require_auth",
"require_auth"
));
}
#[test]
fn auth_token_dotted_access() {
assert!(condition_contains_auth_token(
"req.is_authenticated()",
"is_authenticated"
));
assert!(condition_contains_auth_token(
"user.is_authenticated == true",
"is_authenticated"
));
assert!(condition_contains_auth_token(
"req.user.is_authenticated",
"is_authenticated"
));
assert!(condition_contains_auth_token("user.is_admin()", "is_admin"));
}
#[test]
fn auth_token_rejects_substring_regression() {
assert!(!condition_contains_auth_token(
"not_is_authenticated",
"is_authenticated"
));
assert!(!condition_contains_auth_token(
"cached_is_authenticated_flag",
"is_authenticated"
));
assert!(!condition_contains_auth_token(
"xis_authenticated",
"is_authenticated"
));
assert!(!condition_contains_auth_token(
"this_is_admin_panel",
"is_admin"
));
}
#[test]
fn auth_token_underscore_camel_boundary_cases() {
assert!(!condition_contains_auth_token(
"req.user_is_authenticated_flag",
"is_authenticated"
));
assert!(condition_contains_auth_token(
"req.user.is_authenticated",
"is_authenticated"
));
}
#[test]
fn auth_token_dotted_matcher() {
assert!(condition_contains_auth_token(
"middleware.auth()",
"middleware.auth"
));
assert!(condition_contains_auth_token(
"if middleware.auth(req)",
"middleware.auth"
));
assert!(!condition_contains_auth_token(
"xmiddleware.auth()",
"middleware.auth"
));
assert!(!condition_contains_auth_token(
"middleware.authz()",
"middleware.auth"
));
assert!(condition_contains_auth_token(
"middleware.auth.check()",
"middleware.auth"
));
}
#[test]
fn auth_token_jwt_verify() {
assert!(condition_contains_auth_token(
"jwt.verify(token)",
"jwt.verify"
));
assert!(!condition_contains_auth_token(
"jwt.verifyAsync(token)",
"jwt.verify"
));
}
#[test]
fn auth_token_passport() {
assert!(condition_contains_auth_token(
"passport.authenticate('local')",
"passport.authenticate"
));
}
#[test]
fn auth_token_generate_not_auth() {
assert!(!condition_contains_auth_token(
"generateToken(secret)",
"verify_token"
));
assert!(!condition_contains_auth_token(
"generateToken(secret)",
"validate_token"
));
assert!(!condition_contains_auth_token(
"generateToken(secret)",
"authenticate"
));
}
#[test]
fn auth_token_ensure_authenticated() {
assert!(condition_contains_auth_token(
"ensureauthenticated(req)",
"ensureAuthenticated"
));
}
#[test]
fn auth_token_require_role_not_substring() {
assert!(condition_contains_auth_token(
"requirerole('admin')",
"requireRole"
));
assert!(!condition_contains_auth_token(
"prerequirerole()",
"requireRole"
));
}
#[test]
fn auth_token_boolean_composition() {
assert!(condition_contains_auth_token(
"is_authenticated && is_admin",
"is_authenticated"
));
assert!(condition_contains_auth_token(
"is_authenticated && is_admin",
"is_admin"
));
assert!(condition_contains_auth_token(
"!is_authenticated && is_admin",
"is_authenticated"
));
assert!(condition_contains_auth_token(
"user == null || !user.is_authenticated",
"is_authenticated"
));
}
#[test]
fn try_chain_decompose_basic_two_dots() {
let (recv, method) = try_chain_decompose("c.mu.Lock").unwrap();
assert_eq!(recv, "c.mu");
assert_eq!(method, "Lock");
}
#[test]
fn try_chain_decompose_three_dots() {
let (recv, method) = try_chain_decompose("c.writer.header.set").unwrap();
assert_eq!(recv, "c.writer.header");
assert_eq!(method, "set");
}
#[test]
fn try_chain_decompose_one_dot_keeps_bare_receiver() {
let (recv, method) = try_chain_decompose("f.Close").unwrap();
assert_eq!(recv, "f");
assert_eq!(method, "Close");
}
#[test]
fn try_chain_decompose_no_dot_returns_none() {
assert!(try_chain_decompose("Close").is_none());
assert!(try_chain_decompose("fopen").is_none());
}
#[test]
fn try_chain_decompose_complex_tokens_returns_none() {
for s in [
"Foo::bar::baz", "ptr->field.f", "obj.f().g", "vec[0].field", "obj?.f.g", "obj.f g", "c.writer.header()", ] {
assert!(
try_chain_decompose(s).is_none(),
"expected bail on complex callee {s}"
);
}
}
#[test]
fn try_chain_decompose_rejects_empty_segments() {
for s in [".x.f", "x..f", "x.f.", "."] {
assert!(try_chain_decompose(s).is_none(), "expected bail on {s}");
}
}
#[test]
fn chain_proxy_acquire_records_chain_text_not_root() {
let mut interner = SymbolInterner::new();
let _sym_c = interner.intern_scoped(None, "c");
let lock = ResourceMethodSummary {
method_name: "Lock".into(),
effect: ResourceEffect::Acquire,
class_group: crate::cfg::BodyId(7),
original_span: (10, 20),
};
let transfer = DefaultTransfer {
lang: Lang::Go,
resource_pairs: rules::resource_pairs(Lang::Go),
interner: &interner,
resource_method_summaries: std::slice::from_ref(&lock),
ptr_proxy_hints: None,
};
let info = NodeInfo {
kind: StmtKind::Call,
ast: AstMeta {
span: (0, 30),
..Default::default()
},
taint: TaintMeta::default(),
call: CallMeta {
callee: Some("c.mu.Lock".into()),
..Default::default()
},
..Default::default()
};
let (state, events) =
transfer.apply(NodeIndex::new(0), &info, None, ProductState::initial());
assert!(events.is_empty());
assert!(
state.chain_proxies.contains_key("c.mu"),
"expected chain_proxies['c.mu'] entry; got {:?}",
state.chain_proxies.keys().collect::<Vec<_>>()
);
let entry = &state.chain_proxies["c.mu"];
assert_eq!(entry.lifecycle, ResourceLifecycle::OPEN);
assert_eq!(entry.class_group, crate::cfg::BodyId(7));
assert_eq!(entry.acquire_span, (10, 20));
assert!(
state.receiver_class_group.is_empty(),
"chain root must not inherit proxy state; receiver_class_group was {:?}",
state.receiver_class_group
);
}
#[test]
fn chain_proxy_release_after_acquire_transitions_to_closed() {
let mut interner = SymbolInterner::new();
let _sym_c = interner.intern_scoped(None, "c");
let class_group = crate::cfg::BodyId(11);
let summaries = vec![
ResourceMethodSummary {
method_name: "Lock".into(),
effect: ResourceEffect::Acquire,
class_group,
original_span: (0, 10),
},
ResourceMethodSummary {
method_name: "Unlock".into(),
effect: ResourceEffect::Release,
class_group,
original_span: (20, 30),
},
];
let transfer = DefaultTransfer {
lang: Lang::Go,
resource_pairs: rules::resource_pairs(Lang::Go),
interner: &interner,
resource_method_summaries: &summaries,
ptr_proxy_hints: None,
};
let lock_info = NodeInfo {
kind: StmtKind::Call,
ast: AstMeta {
span: (0, 10),
..Default::default()
},
taint: TaintMeta::default(),
call: CallMeta {
callee: Some("c.mu.Lock".into()),
..Default::default()
},
..Default::default()
};
let (state, _) =
transfer.apply(NodeIndex::new(0), &lock_info, None, ProductState::initial());
assert_eq!(
state.chain_proxies["c.mu"].lifecycle,
ResourceLifecycle::OPEN
);
let unlock_info = NodeInfo {
kind: StmtKind::Call,
ast: AstMeta {
span: (20, 30),
..Default::default()
},
taint: TaintMeta::default(),
call: CallMeta {
callee: Some("c.mu.Unlock".into()),
..Default::default()
},
..Default::default()
};
let (state, _) = transfer.apply(NodeIndex::new(1), &unlock_info, None, state);
assert_eq!(
state.chain_proxies["c.mu"].lifecycle,
ResourceLifecycle::CLOSED
);
}
#[test]
fn chain_proxy_distinct_chains_dont_collide() {
let interner = SymbolInterner::new();
let class_group = crate::cfg::BodyId(3);
let lock = ResourceMethodSummary {
method_name: "Lock".into(),
effect: ResourceEffect::Acquire,
class_group,
original_span: (0, 0),
};
let transfer = DefaultTransfer {
lang: Lang::Go,
resource_pairs: rules::resource_pairs(Lang::Go),
interner: &interner,
resource_method_summaries: std::slice::from_ref(&lock),
ptr_proxy_hints: None,
};
let mk_call = |callee: &str| NodeInfo {
kind: StmtKind::Call,
ast: AstMeta {
span: (0, 0),
..Default::default()
},
taint: TaintMeta::default(),
call: CallMeta {
callee: Some(callee.into()),
..Default::default()
},
..Default::default()
};
let (state, _) = transfer.apply(
NodeIndex::new(0),
&mk_call("c.mu.Lock"),
None,
ProductState::initial(),
);
let (state, _) = transfer.apply(NodeIndex::new(1), &mk_call("c.other.Lock"), None, state);
assert!(state.chain_proxies.contains_key("c.mu"));
assert!(state.chain_proxies.contains_key("c.other"));
assert_eq!(state.chain_proxies.len(), 2);
}
#[test]
fn single_dot_proxy_acquire_uses_symbol_id_path() {
let mut interner = SymbolInterner::new();
let sym_f = interner.intern_scoped(None, "f");
let class_group = crate::cfg::BodyId(2);
let acquire = ResourceMethodSummary {
method_name: "acquireMine".into(),
effect: ResourceEffect::Acquire,
class_group,
original_span: (0, 0),
};
let transfer = DefaultTransfer {
lang: Lang::Go,
resource_pairs: rules::resource_pairs(Lang::Go),
interner: &interner,
resource_method_summaries: std::slice::from_ref(&acquire),
ptr_proxy_hints: None,
};
let info = NodeInfo {
kind: StmtKind::Call,
ast: AstMeta {
span: (0, 0),
..Default::default()
},
taint: TaintMeta::default(),
call: CallMeta {
callee: Some("f.acquireMine".into()),
receiver: Some("f".into()),
..Default::default()
},
..Default::default()
};
let (state, _) = transfer.apply(NodeIndex::new(0), &info, None, ProductState::initial());
assert_eq!(
state.receiver_class_group.get(&sym_f),
Some(&class_group),
"single-dot must use SymbolId path"
);
assert!(
state.chain_proxies.is_empty(),
"single-dot must not populate chain_proxies; got {:?}",
state.chain_proxies.keys().collect::<Vec<_>>()
);
}
#[test]
fn complex_callee_does_not_record_proxy() {
let interner = SymbolInterner::new();
let class_group = crate::cfg::BodyId(0);
let lock = ResourceMethodSummary {
method_name: "Lock".into(),
effect: ResourceEffect::Acquire,
class_group,
original_span: (0, 0),
};
let transfer = DefaultTransfer {
lang: Lang::Go,
resource_pairs: rules::resource_pairs(Lang::Go),
interner: &interner,
resource_method_summaries: std::slice::from_ref(&lock),
ptr_proxy_hints: None,
};
for callee in ["c.writer.header().Lock", "Foo::bar::Lock", "c[i].mu.Lock"] {
let info = NodeInfo {
kind: StmtKind::Call,
ast: AstMeta {
span: (0, 0),
..Default::default()
},
taint: TaintMeta::default(),
call: CallMeta {
callee: Some(callee.into()),
..Default::default()
},
..Default::default()
};
let (state, _) =
transfer.apply(NodeIndex::new(0), &info, None, ProductState::initial());
assert!(
state.chain_proxies.is_empty() && state.receiver_class_group.is_empty(),
"complex callee {callee} should not record any proxy state; chain={:?} root={:?}",
state.chain_proxies.keys().collect::<Vec<_>>(),
state.receiver_class_group.keys().collect::<Vec<_>>()
);
}
}
#[test]
fn chain_proxy_lattice_join_unions_keys() {
use crate::state::lattice::Lattice;
let mut a = ProductState::initial();
let mut b = ProductState::initial();
a.chain_proxies.insert(
"c.mu".into(),
ChainProxyState {
lifecycle: ResourceLifecycle::OPEN,
class_group: crate::cfg::BodyId(1),
acquire_span: (0, 0),
},
);
b.chain_proxies.insert(
"c.other".into(),
ChainProxyState {
lifecycle: ResourceLifecycle::OPEN,
class_group: crate::cfg::BodyId(2),
acquire_span: (10, 20),
},
);
let joined = a.join(&b);
assert_eq!(joined.chain_proxies.len(), 2);
assert!(joined.chain_proxies.contains_key("c.mu"));
assert!(joined.chain_proxies.contains_key("c.other"));
}
#[test]
fn chain_proxy_lattice_join_merges_lifecycle() {
use crate::state::lattice::Lattice;
let mut a = ProductState::initial();
let mut b = ProductState::initial();
a.chain_proxies.insert(
"c.mu".into(),
ChainProxyState {
lifecycle: ResourceLifecycle::OPEN,
class_group: crate::cfg::BodyId(1),
acquire_span: (0, 0),
},
);
b.chain_proxies.insert(
"c.mu".into(),
ChainProxyState {
lifecycle: ResourceLifecycle::CLOSED,
class_group: crate::cfg::BodyId(1),
acquire_span: (0, 0),
},
);
let joined = a.join(&b);
assert_eq!(joined.chain_proxies.len(), 1);
let lc = joined.chain_proxies["c.mu"].lifecycle;
assert!(lc.contains(ResourceLifecycle::OPEN));
assert!(lc.contains(ResourceLifecycle::CLOSED));
}
#[test]
fn field_only_hint_routes_single_dot_acquire_to_chain_proxies() {
let mut interner = SymbolInterner::new();
let _sym_m = interner.intern_scoped(None, "m");
let class_group = crate::cfg::BodyId(2);
let acquire = ResourceMethodSummary {
method_name: "Lock".into(),
effect: ResourceEffect::Acquire,
class_group,
original_span: (0, 10),
};
let mut hints = std::collections::HashMap::new();
hints.insert("m".to_string(), crate::pointer::PtrProxyHint::FieldOnly);
let transfer = DefaultTransfer {
lang: Lang::Go,
resource_pairs: rules::resource_pairs(Lang::Go),
interner: &interner,
resource_method_summaries: std::slice::from_ref(&acquire),
ptr_proxy_hints: Some(&hints),
};
let info = NodeInfo {
kind: StmtKind::Call,
ast: AstMeta {
span: (0, 10),
..Default::default()
},
taint: TaintMeta::default(),
call: CallMeta {
callee: Some("m.Lock".into()),
receiver: Some("m".into()),
..Default::default()
},
..Default::default()
};
let (state, events) =
transfer.apply(NodeIndex::new(0), &info, None, ProductState::initial());
assert!(events.is_empty());
assert!(
state.chain_proxies.contains_key("m"),
"FieldOnly hint should route `m.Lock()` into chain_proxies; got {:?}",
state.chain_proxies.keys().collect::<Vec<_>>()
);
assert!(
state.receiver_class_group.is_empty(),
"FieldOnly hint must not record SymbolId proxy entry; got {:?}",
state.receiver_class_group.keys().collect::<Vec<_>>()
);
let entry = &state.chain_proxies["m"];
assert_eq!(entry.lifecycle, ResourceLifecycle::OPEN);
assert_eq!(entry.class_group, class_group);
}
#[test]
fn field_only_hint_release_transitions_chain_entry_to_closed() {
let mut interner = SymbolInterner::new();
let _sym_m = interner.intern_scoped(None, "m");
let class_group = crate::cfg::BodyId(11);
let summaries = vec![
ResourceMethodSummary {
method_name: "Lock".into(),
effect: ResourceEffect::Acquire,
class_group,
original_span: (0, 10),
},
ResourceMethodSummary {
method_name: "Unlock".into(),
effect: ResourceEffect::Release,
class_group,
original_span: (20, 30),
},
];
let mut hints = std::collections::HashMap::new();
hints.insert("m".to_string(), crate::pointer::PtrProxyHint::FieldOnly);
let transfer = DefaultTransfer {
lang: Lang::Go,
resource_pairs: rules::resource_pairs(Lang::Go),
interner: &interner,
resource_method_summaries: &summaries,
ptr_proxy_hints: Some(&hints),
};
let lock_info = NodeInfo {
kind: StmtKind::Call,
call: CallMeta {
callee: Some("m.Lock".into()),
receiver: Some("m".into()),
..Default::default()
},
..Default::default()
};
let (state, _) =
transfer.apply(NodeIndex::new(0), &lock_info, None, ProductState::initial());
assert_eq!(state.chain_proxies["m"].lifecycle, ResourceLifecycle::OPEN);
let unlock_info = NodeInfo {
kind: StmtKind::Call,
call: CallMeta {
callee: Some("m.Unlock".into()),
receiver: Some("m".into()),
..Default::default()
},
..Default::default()
};
let (state, _) = transfer.apply(NodeIndex::new(1), &unlock_info, None, state);
assert_eq!(
state.chain_proxies["m"].lifecycle,
ResourceLifecycle::CLOSED
);
}
#[test]
fn no_hint_falls_through_to_existing_symbol_id_path() {
let mut interner = SymbolInterner::new();
let sym_f = interner.intern_scoped(None, "f");
let class_group = crate::cfg::BodyId(3);
let acquire = ResourceMethodSummary {
method_name: "acquireMine".into(),
effect: ResourceEffect::Acquire,
class_group,
original_span: (0, 0),
};
let transfer = DefaultTransfer {
lang: Lang::Go,
resource_pairs: rules::resource_pairs(Lang::Go),
interner: &interner,
resource_method_summaries: std::slice::from_ref(&acquire),
ptr_proxy_hints: None,
};
let info = NodeInfo {
kind: StmtKind::Call,
call: CallMeta {
callee: Some("f.acquireMine".into()),
receiver: Some("f".into()),
..Default::default()
},
..Default::default()
};
let (state, _) = transfer.apply(NodeIndex::new(0), &info, None, ProductState::initial());
assert_eq!(
state.receiver_class_group.get(&sym_f),
Some(&class_group),
"no hint ⇒ SymbolId path"
);
assert!(state.chain_proxies.is_empty());
}
#[test]
fn empty_hint_map_does_not_redirect() {
let mut interner = SymbolInterner::new();
let sym_f = interner.intern_scoped(None, "f");
let class_group = crate::cfg::BodyId(3);
let acquire = ResourceMethodSummary {
method_name: "acquireMine".into(),
effect: ResourceEffect::Acquire,
class_group,
original_span: (0, 0),
};
let hints: std::collections::HashMap<String, crate::pointer::PtrProxyHint> =
std::collections::HashMap::new();
let transfer = DefaultTransfer {
lang: Lang::Go,
resource_pairs: rules::resource_pairs(Lang::Go),
interner: &interner,
resource_method_summaries: std::slice::from_ref(&acquire),
ptr_proxy_hints: Some(&hints),
};
let info = NodeInfo {
kind: StmtKind::Call,
call: CallMeta {
callee: Some("f.acquireMine".into()),
receiver: Some("f".into()),
..Default::default()
},
..Default::default()
};
let (state, _) = transfer.apply(NodeIndex::new(0), &info, None, ProductState::initial());
assert_eq!(state.receiver_class_group.get(&sym_f), Some(&class_group));
assert!(state.chain_proxies.is_empty());
}
}