#[allow(clippy::wildcard_imports)]
use super::*;
#[derive(Copy, Clone)]
pub(super) struct SourceSeedContext<'a> {
pub source: &'a RuleMatch,
pub output_args: &'a [usize],
pub output_args_from: Option<usize>,
pub callback_args: &'a [SourceCallbackArgSemantics],
pub callback_only: bool,
pub allow_text_only_match: bool,
}
pub(super) fn collect_source_seed_targets(
events: &[bonsai_lang_api::FlowEvent],
context: SourceSeedContext<'_>,
out: &mut TokenSet,
) {
let SourceSeedContext {
source: src,
output_args: source_output_args,
output_args_from: source_output_args_from,
callback_args: source_callback_args,
callback_only: source_callback_only,
allow_text_only_match: allow_text_only_source_match,
} = context;
use bonsai_lang_api::FlowEvent;
for event in events {
match event {
FlowEvent::Assign {
span,
target,
source_name,
source_call,
source_names,
source_call_args,
value_kind,
..
} => {
let source_text_matches = source_name
.as_deref()
.is_some_and(|n| security_text_matches_source_strict(n, &src.match_text))
|| source_call
.as_deref()
.is_some_and(|n| security_text_matches_source_strict(n, &src.match_text))
|| source_names
.iter()
.any(|n| security_text_matches_source_strict(n, &src.match_text))
|| source_call_args
.iter()
.any(|n| security_text_matches_source_strict(n, &src.match_text));
let source_site_matches = span_contains(*span, src.span)
|| spans_overlap(*span, src.span)
|| (allow_text_only_source_match && source_text_matches);
if source_site_matches {
if !source_output_args.is_empty() || source_output_args_from.is_some() {
seed_source_output_text_args(
out,
source_call_args,
source_output_args,
source_output_args_from,
);
continue;
}
if source_callback_only && matches!(value_kind, Some(AssignValueKind::CallResult)) {
continue;
}
let source_is_call_input = source_call.is_some()
&& matches!(value_kind, Some(AssignValueKind::CallResult))
&& !source_call
.as_deref()
.is_some_and(|n| security_text_matches_source_strict(n, &src.match_text))
&& (source_names
.iter()
.any(|n| security_text_matches_source_strict(n, &src.match_text))
|| source_call_args
.iter()
.any(|n| security_text_matches_source_strict(n, &src.match_text)));
let skip_target_seed = assign_is_callback_parameter_binding(
target,
source_name.as_deref(),
source_call.as_deref(),
source_names,
*value_kind,
);
if !skip_target_seed && !source_is_call_input && !target.is_empty() {
insert_taint_aliases(out, target);
if source_names_contain_descendant_of_source(source_names, &src.match_text) {
insert_descendant_taint_aliases(out, target);
}
}
let _ = source_call;
if let Some(source_name) = source_name.as_deref() {
if security_text_matches_source_strict(source_name, &src.match_text) {
insert_taint_aliases(out, source_name);
}
}
seed_descendant_aliases_for_qualified_source_reads(out, source_names, &src.match_text);
for name in source_names {
if security_text_matches_source_strict(name, &src.match_text) {
insert_taint_aliases(out, name);
}
}
for name in source_call_args {
if security_text_matches_source_strict(name, &src.match_text) {
insert_taint_aliases(out, name);
}
}
if target_is_destructuring_pattern(target) {
for name in source_names {
insert_taint_aliases(out, name);
}
}
}
}
FlowEvent::Call {
span,
name,
receiver,
args,
..
} => {
let text_only_call_match = security_text_matches_source_strict(name, &src.match_text);
let call_matches = span_contains(*span, src.span)
|| spans_overlap(*span, src.span)
|| (allow_text_only_source_match && text_only_call_match);
let _ = receiver;
if call_matches && (!source_output_args.is_empty() || source_output_args_from.is_some()) {
seed_source_output_call_args(out, args, source_output_args, source_output_args_from);
}
if call_matches
&& source_output_args.is_empty()
&& source_output_args_from.is_none()
&& !source_callback_only
&& !name.is_empty()
{
insert_taint_aliases(out, name);
}
if source_output_args.is_empty()
&& source_output_args_from.is_none()
&& source_callback_args.is_empty()
{
seed_source_arg_reads(out, args, src);
}
}
FlowEvent::Branch {
then_events,
else_events,
..
} => {
collect_source_seed_targets(then_events, context, out);
collect_source_seed_targets(else_events, context, out);
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
collect_source_seed_targets(body, context, out);
}
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
collect_source_seed_targets(body, context, out);
collect_source_seed_targets(catch_events, context, out);
collect_source_seed_targets(finally_events, context, out);
}
_ => {}
}
}
}
fn assign_is_callback_parameter_binding(
target: &str,
source_name: Option<&str>,
source_call: Option<&str>,
source_names: &[String],
value_kind: Option<AssignValueKind>,
) -> bool {
source_name.is_none()
&& source_call.is_none()
&& matches!(
value_kind,
Some(AssignValueKind::Compound | AssignValueKind::Unknown)
)
&& source_names.iter().any(|name| name == target)
&& source_names
.iter()
.any(|name| matches!(name.as_str(), "function" | "async"))
}
fn source_output_indices(explicit: &[usize], start: Option<usize>, arg_count: usize) -> Vec<usize> {
let mut indices = explicit.to_vec();
if let Some(start) = start {
indices.extend(start..arg_count);
}
indices.sort_unstable();
indices.dedup();
indices
}
fn seed_source_output_text_args(
out: &mut TokenSet,
args: &[String],
source_output_args: &[usize],
source_output_args_from: Option<usize>,
) {
for index in source_output_indices(source_output_args, source_output_args_from, args.len()) {
let Some(text) = args.get(index).map(|value| value.trim()) else {
continue;
};
if text.is_empty() || source_seed_text_is_literal(text) {
continue;
}
insert_taint_aliases(out, text);
insert_descendant_taint_aliases(out, text);
}
}
fn seed_source_arg_reads(out: &mut TokenSet, args: &[bonsai_lang_api::CallArg], src: &RuleMatch) {
for arg in args {
if !span_contains(arg.span, src.span) {
continue;
}
seed_descendant_aliases_for_qualified_source_reads(out, &arg.source_names, &src.match_text);
if let Some(place) = arg.place.as_deref() {
if security_text_matches_source_strict(place, &src.match_text) {
insert_taint_aliases(out, place);
insert_descendant_taint_aliases(out, place);
}
}
for name in &arg.source_names {
if security_text_matches_source_strict(name, &src.match_text) {
insert_taint_aliases(out, name);
}
}
}
}
fn seed_source_output_call_args(
out: &mut TokenSet,
args: &[bonsai_lang_api::CallArg],
source_output_args: &[usize],
source_output_args_from: Option<usize>,
) {
for index in source_output_indices(source_output_args, source_output_args_from, args.len()) {
let Some(arg) = args.get(index) else {
continue;
};
let Some(text) = arg.place.as_deref().map(str::trim) else {
continue;
};
if text.is_empty() || source_seed_text_is_literal(text) {
continue;
}
insert_taint_aliases(out, text);
insert_descendant_taint_aliases(out, text);
}
}
fn source_seed_text_is_literal(text: &str) -> bool {
let text = text.trim();
if text.len() < 2 {
return false;
}
let Some(first) = text.chars().next() else {
return false;
};
let Some(last) = text.chars().last() else {
return false;
};
matches!(first, '"' | '\'' | '`') && first == last
}
pub(super) fn seed_descendant_aliases_for_qualified_source_reads(
out: &mut TokenSet,
source_names: &[String],
source_text: &str,
) {
let source_normalised = security_normalise_qualified_text(source_text);
for name in source_names {
let normalised = security_normalise_qualified_text(name);
if !source_normalised.is_empty()
&& source_normalised.contains('.')
&& (normalised == source_normalised
|| normalised
.strip_prefix(source_normalised.as_str())
.is_some_and(|rest| rest.starts_with('.')))
{
insert_descendant_taint_aliases(out, source_text);
insert_descendant_taint_aliases(out, &source_normalised);
continue;
}
let Some((base, _)) = normalised.split_once('.') else {
continue;
};
if source_base_matches(base, source_text) {
insert_descendant_taint_aliases(out, base);
insert_descendant_taint_aliases(out, source_text);
}
}
}
fn source_names_contain_descendant_of_source(source_names: &[String], source_text: &str) -> bool {
let source = security_normalise_qualified_text(source_text);
if source.is_empty() {
return false;
}
source_names.iter().any(|name| {
let name = security_normalise_qualified_text(name);
name.strip_prefix(source.as_str())
.is_some_and(|rest| rest.starts_with('.') && rest.len() > 1)
})
}
fn source_base_matches(base: &str, source_text: &str) -> bool {
security_text_matches_source_strict(base, source_text)
|| security_text_matches_source_strict(
strip_security_sigils(base),
strip_security_sigils(source_text),
)
}
fn strip_security_sigils(text: &str) -> &str {
text.trim().trim_start_matches(bonsai_common::is_name_punctuation)
}
fn target_is_destructuring_pattern(target: &str) -> bool {
let target = target.trim();
target.contains(',')
|| target.starts_with('[')
|| target.starts_with('(')
|| target.starts_with('{')
|| target.contains(":=")
}
pub(super) fn insert_taint_aliases(out: &mut TokenSet, text: &str) {
let trimmed = text.trim();
if trimmed.is_empty() {
return;
}
out.insert(trimmed.to_string());
let normalised = security_normalise_qualified_text(trimmed);
if normalised != trimmed {
out.insert(normalised);
}
}
pub(super) fn insert_descendant_taint_aliases(out: &mut TokenSet, text: &str) {
let mut aliases = TokenSet::default();
insert_taint_aliases(&mut aliases, text);
for alias in aliases {
if alias.is_empty() || alias.contains('*') {
continue;
}
out.insert(alias.clone());
out.insert(format!("{alias}.*"));
}
}
pub(super) fn security_text_matches_source_strict(text: &str, source_text: &str) -> bool {
let text = text.trim();
let source_text = source_text.trim();
if text.is_empty() || source_text.is_empty() {
return false;
}
if text == source_text {
return true;
}
let text_norm = security_normalise_qualified_text(text);
let src_norm = security_normalise_qualified_text(source_text);
if text_norm == src_norm {
return true;
}
if source_qualified_segment_count(source_text) > 2 {
return false;
}
let text_tail = bonsai_common::short_qualified_tail(text);
let src_tail = bonsai_common::short_qualified_tail(source_text);
text_tail == src_tail && !text_tail.is_empty()
}
fn source_qualified_segment_count(text: &str) -> usize {
let normalized = security_normalise_qualified_text(text);
bonsai_common::qualified_name_segments(&normalized).len()
}
fn security_normalise_qualified_text(text: &str) -> String {
let compiler_name = bonsai_common::trim_leading_name_punctuation(text.trim());
bonsai_common::normalize_qualified_name(compiler_name)
}