#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn build_pattern_only_findings(
ws: &Workspace,
sinks: &[RuleMatch],
pack: &Rulepack,
taint_sink_sites: &AHashSet<(String, String, u32, u32)>,
) -> Vec<FindingWithChain> {
let mut emitted: AHashSet<(String, String, u32, u32)> = AHashSet::new();
let mut out = Vec::new();
for snk in sinks {
let site_key = (snk.rule_id.clone(), snk.file.clone(), snk.line, snk.column);
if taint_sink_sites.contains(&site_key) || !emitted.insert(site_key) {
continue;
}
let chain_funcs: Vec<FuncId> = func_id_for_match(ws, snk).into_iter().collect();
if let Some(finding) = make_pattern_finding(ws, snk, pack, &chain_funcs) {
out.push(FindingWithChain { finding, chain_funcs });
}
}
out
}
fn make_pattern_finding(
ws: &Workspace,
snk: &RuleMatch,
pack: &Rulepack,
_chain_funcs: &[FuncId],
) -> Option<Finding> {
let sink_rule = pack.find_rule_by_id(&snk.rule_id)?;
let group_tokens = [
snk.rule_id.clone(),
snk.file.clone(),
snk.line.to_string(),
snk.column.to_string(),
];
let group_id = format!("G:{:016x}", bonsai_hash::fnv1a_names64(&group_tokens));
let flow_id = bonsai_inspect::compute_flow_id(&group_tokens);
let source_rule_id = format!("pattern:{}", sink_rule.id);
let finding_id = compute_finding_id(&source_rule_id, &sink_rule.id, &group_id, &snk.language);
let source = FindingMatch {
origin: MatchOrigin::Pattern,
rule_id: source_rule_id,
file: snk.file.clone(),
line: snk.line,
column: snk.column,
text: snk.match_text.clone(),
enclosing_fn: snk.enclosing_fn.clone(),
tag: Some("pattern".to_string()),
severity: None,
category: Some("pattern".to_string()),
trust: None,
payload_types: Vec::new(),
tainted_args: Vec::new(),
sanitised_arg_indices: Vec::new(),
};
let sink = FindingMatch::from_rule_match(snk, sink_rule);
let chain_display = snk
.enclosing_fn
.as_ref()
.map(|name| vec![name.clone()])
.unwrap_or_default();
Some(Finding {
finding_id,
language: snk.language.clone(),
source,
sink,
sanitizers_seen: Vec::new(),
taint_transforms_seen: Vec::new(),
group_id: Some(group_id.clone()),
representative_flow_id: Some(flow_id),
analysis_complete: true,
analysis_incomplete_reasons: Vec::new(),
chain_display,
taint_path: Vec::new(),
alternate_flows: Vec::new(),
hops: Vec::new(),
tag: sink_rule.tag.clone(),
severity: sink_rule.severity,
cwe: sink_rule.cwe.clone(),
owasp: sink_rule.owasp.clone(),
status: FindingStatus::Unsanitized,
from_test: path_is_test_file_with_root(
ws.db().workspace_root().as_deref(),
&snk.file,
&pack.metadata.test_path_patterns,
),
})
}
pub(super) fn rule_is_pattern_only_finding(rule: &Rule) -> bool {
rule.kind == RuleKind::Sink
&& rule.enabled
&& !rule_has_taint_predicate(rule)
&& (rule
.analysis_semantics
.as_ref()
.and_then(|semantics| semantics.non_taint_evaluation)
== Some(NonTaintEvaluation::Pattern)
|| rule.match_spec.kind == MatchKind::Missing)
}
pub(super) fn rule_is_non_taint_sink(rule: &Rule) -> bool {
rule_is_pattern_only_finding(rule)
|| (rule.kind == RuleKind::Sink
&& rule.enabled
&& !rule_has_taint_predicate(rule)
&& rule
.analysis_semantics
.as_ref()
.and_then(|semantics| semantics.non_taint_evaluation)
.is_some())
}
pub(super) fn rule_has_taint_predicate(rule: &Rule) -> bool {
rule.constraints.0.iter().any(|constraint| {
matches!(
constraint,
ConstraintKind::ArgTainted { .. }
| ConstraintKind::AnyArgTainted { .. }
| ConstraintKind::ReceiverTainted { .. }
| ConstraintKind::ReceiverOriginCallbackParamReachesCall { .. }
)
})
}
pub(super) struct FindingBuildContext<'a> {
pub(super) group_id: Option<String>,
pub(super) flow_id: Option<String>,
pub(super) source_func: FuncId,
pub(super) sink_func: FuncId,
pub(super) sanitizer_candidate_funcs: &'a [FuncId],
pub(super) chain_names: Vec<String>,
pub(super) san_by_func: &'a AHashMap<FuncId, Vec<&'a RuleMatch>>,
pub(super) ws: &'a Workspace,
pub(super) global: &'a Arc<GlobalIndex>,
pub(super) call_graph: &'a bonsai_callgraph::ResolvedCallGraph,
pub(super) static_provenance_call_graph: &'a bonsai_callgraph::ResolvedCallGraph,
pub(super) callback_invocations: &'a [bonsai_taint::CallbackInvocation],
pub(super) throwing_call_sites: &'a [Span],
pub(super) tainted_call_spans: &'a AHashSet<Span>,
pub(super) sink_tainted_args: Vec<TaintedArgInfo>,
pub(super) taint_path: Vec<TaintPropagationStep>,
pub(super) analysis_incomplete_reasons: Vec<String>,
}
pub(super) fn make_finding(
src: &RuleMatch,
snk: &RuleMatch,
pack: &Rulepack,
context: FindingBuildContext<'_>,
) -> Option<Finding> {
let skr = pack.find_rule_by_id(&snk.rule_id)?;
let attributed_sink = caller_composition_attribution_match(&context, snk, skr)
.or_else(|| callback_extension_attribution_match(context.ws, context.global.as_ref(), snk, skr))
.or_else(|| {
configured_receiver_factory_attribution_match(context.ws, context.global.as_ref(), snk, skr)
});
let report_sink = attributed_sink.as_ref().unwrap_or(snk);
let is_inferred = src.origin != MatchOrigin::Rulepack;
let source_rule = if is_inferred {
None
} else {
Some(pack.find_rule_by_id(&src.rule_id)?)
};
let src_match = if let Some(rule) = source_rule {
FindingMatch::from_rule_match(src, rule)
} else {
FindingMatch::from_inferred(src)
};
let src_rule_id_for_id = if is_inferred {
src.rule_id.as_str()
} else {
&src_match.rule_id
};
let group = context.group_id.unwrap_or_else(|| {
let tokens = [src.file.clone(), report_sink.file.clone()];
format!("G:{:016x}", bonsai_hash::fnv1a_names64(&tokens))
});
let source_identity = rule_match_identity_token(src_rule_id_for_id, src);
let sink_identity = rule_match_identity_token(&skr.id, report_sink);
let finding_id = compute_finding_id(&source_identity, &sink_identity, &group, &src.language);
let mut sanitizers_seen: Vec<FindingMatch> = Vec::new();
let mut taint_transforms_seen: Vec<FindingMatch> = Vec::new();
let mut seen_sanitizer_keys: AHashSet<(String, u32, u32)> = AHashSet::new();
let mut seen_transform_keys: AHashSet<(String, u32, u32)> = AHashSet::new();
let mut sanitizer_candidate_funcs = context.sanitizer_candidate_funcs.to_vec();
sanitizer_candidate_funcs.extend(helper_functions_reaching_tainted_sink_args(
context.ws,
context.call_graph,
context.sink_func,
snk,
&context.sink_tainted_args,
));
sanitizer_candidate_funcs.extend(predicate_helper_functions_before_sink(
context.ws,
context.call_graph,
context.sink_func,
snk,
));
sanitizer_candidate_funcs.sort_by_key(|function| function.raw());
sanitizer_candidate_funcs.dedup();
bonsai_diagnostics::debug_log!(
"security-taint",
"sanitizer_candidates sink={} functions={:?}",
snk.rule_id,
sanitizer_candidate_funcs
);
for hop_func in sanitizer_candidate_funcs {
let Some(sanitizer_hits) = context.san_by_func.get(&hop_func) else {
continue;
};
for sanitizer_match in sanitizer_hits {
let sanitizer_rule = pack.find_rule_by_id(&sanitizer_match.rule_id);
if sanitizer_rule.is_some_and(|rule| {
rule.taint_semantics
.as_ref()
.and_then(|semantics| semantics.finite_literal_map_selector.as_ref())
.is_some()
&& !finite_literal_map_selector_call_is_proven(
context.ws,
hop_func,
sanitizer_match,
rule,
)
}) {
continue;
}
let guard_connected = sanitizer_guard_feeds_sink_arg(
&SanitizerGuardContext {
ws: context.ws,
call_graph: context.call_graph,
sink_func: context.sink_func,
sink_tainted_args: &context.sink_tainted_args,
},
pack,
hop_func,
sanitizer_rule,
sanitizer_match,
sanitizer_hits,
snk,
);
if sanitizer_rule
.and_then(|rule| rule.analysis_semantics.as_ref())
.and_then(|semantics| semantics.sanitizer_guard.as_ref())
.is_some_and(|guard| guard.require_terminal_rejection)
&& !guard_connected
{
continue;
}
let nested_in_tainted_sink_arg =
sanitizer_is_nested_in_tainted_sink_arg(
context.ws,
context.sink_func,
src,
sanitizer_match,
snk,
&context.sink_tainted_args,
) || sanitizer_is_sole_nested_value_call_in_return(
context.ws,
context.sink_func,
sanitizer_match,
snk,
skr,
) || sanitizer_is_helper_return_reaching_tainted_sink_arg(HelperSanitizerReturnContext {
ws: context.ws,
call_graph: context.call_graph,
helper_func: hop_func,
sink_func: context.sink_func,
sanitizer: sanitizer_match,
sink: snk,
sink_tainted_args: &context.sink_tainted_args,
tainted_call_spans: context.tainted_call_spans,
});
let dataflow_connected =
sanitizer_call_overlaps_tainted_call(sanitizer_match, context.tainted_call_spans)
|| nested_in_tainted_sink_arg
|| sanitizer_assignment_output_feeds_sink_arg(
context.ws,
hop_func,
sanitizer_match,
snk,
skr,
&context.sink_tainted_args,
)
|| sanitizer_guarded_value_filter_output_feeds_sink_arg(
context.ws,
hop_func,
sanitizer_match,
snk,
skr,
&context.sink_tainted_args,
)
|| guard_connected
|| xxe_factory_hardening_sanitizes_sink(
context.ws,
context.sink_func,
&pack.metadata,
sanitizer_rule,
skr,
sanitizer_match,
snk,
);
let post_sink_path_construction_containment = dataflow_connected
&& post_sink_path_construction_containment_allowed(
&pack.metadata,
sanitizer_rule,
skr,
sanitizer_match,
snk,
);
if !sanitizer_can_attach(
src,
context.source_func,
sanitizer_match,
hop_func,
snk,
context.sink_func,
nested_in_tainted_sink_arg,
dataflow_connected,
post_sink_path_construction_containment,
) {
continue;
}
if !dataflow_connected {
continue;
}
let dedup_key = (
sanitizer_match.file.clone(),
sanitizer_match.line,
sanitizer_match.column,
);
if let Some(rule) = sanitizer_rule {
let matched = FindingMatch::from_rule_match(sanitizer_match, rule);
if rule_is_taint_preserving_transform(rule, &pack.metadata) {
if seen_transform_keys.insert(dedup_key) {
taint_transforms_seen.push(matched);
}
} else if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(matched);
}
}
}
}
if let Some(selection) = finite_literal_selection_sanitizer(
context.ws,
context.global.as_ref(),
context.call_graph,
context.sink_func,
snk,
skr,
&context.sink_tainted_args,
) {
let dedup_key = (selection.file.clone(), selection.line, selection.column);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(selection);
}
}
if let Some(type_guard) = runtime_type_rejection_guard_sanitizer(
context.ws,
context.sink_func,
snk,
skr,
&context.sink_tainted_args,
) {
let dedup_key = (type_guard.file.clone(), type_guard.line, type_guard.column);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(type_guard);
}
}
if let Some(parameterized_query) =
parameterized_query_guard_sanitizer(context.ws, context.sink_func, snk, skr)
{
let dedup_key = (
parameterized_query.file.clone(),
parameterized_query.line,
parameterized_query.column,
);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(parameterized_query);
}
}
if let Some(path_guard) = path_containment_guard_sanitizer(
context.ws,
context.sink_func,
snk,
skr,
&context.sink_tainted_args,
) {
let dedup_key = (path_guard.file.clone(), path_guard.line, path_guard.column);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(path_guard);
}
}
let path_consumer_context = PathConsumerGuardContext {
ws: context.ws,
global: context.global.as_ref(),
call_graph: context.call_graph,
static_provenance_call_graph: context.static_provenance_call_graph,
callback_invocations: context.callback_invocations,
throwing_call_sites: context.throwing_call_sites,
};
if let Some(path_guard) = path_consumer_containment_guard_sanitizer(
&path_consumer_context,
&context.taint_path,
context.sink_func,
snk,
skr,
) {
let dedup_key = (path_guard.file.clone(), path_guard.line, path_guard.column);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(path_guard);
}
}
if let Some(factory_guard) = receiver_factory_guard_sanitizer(context.ws, context.sink_func, snk, skr) {
let dedup_key = (
factory_guard.file.clone(),
factory_guard.line,
factory_guard.column,
);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(factory_guard);
}
}
if let Some(configuration_guard) =
receiver_configuration_guard_sanitizer(context.ws, context.sink_func, snk, skr)
{
let dedup_key = (
configuration_guard.file.clone(),
configuration_guard.line,
configuration_guard.column,
);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(configuration_guard);
}
}
if let Some(escape) =
character_escape_sanitizer(context.ws, context.call_graph, context.sink_func, snk, skr)
{
let dedup_key = (escape.file.clone(), escape.line, escape.column);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(escape);
}
}
let compiler_guard_context = CompilerGuardContext {
ws: context.ws,
call_graph: context.call_graph,
source: src,
source_rule,
source_func: context.source_func,
sink: snk,
sink_rule: skr,
candidate_funcs: context.sanitizer_candidate_funcs,
tainted_call_spans: context.tainted_call_spans,
taint_path: &context.taint_path,
sink_tainted_args: &context.sink_tainted_args,
};
if let Some(constraint) = character_constraint_sanitizer(&compiler_guard_context) {
let dedup_key = (constraint.file.clone(), constraint.line, constraint.column);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(constraint);
}
}
if let Some(constraint) = same_origin_path_constraint_sanitizer(&compiler_guard_context) {
let dedup_key = (constraint.file.clone(), constraint.line, constraint.column);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(constraint);
}
}
if let Some(path_guard) = relative_path_containment_guard_sanitizer(
context.ws,
context.global.as_ref(),
context.static_provenance_call_graph,
context.sink_func,
snk,
skr,
&context.sink_tainted_args,
) {
let dedup_key = (path_guard.file.clone(), path_guard.line, path_guard.column);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(path_guard);
}
}
if let Some(configured_factory_guard) = configured_argument_factory_guard_sanitizer(
context.ws,
context.call_graph,
context.sink_func,
snk,
skr,
) {
let dedup_key = (
configured_factory_guard.file.clone(),
configured_factory_guard.line,
configured_factory_guard.column,
);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(configured_factory_guard);
}
}
if let Some(configured_receiver_guard) =
configured_argument_receiver_guard_sanitizer(context.ws, context.sink_func, snk, skr)
{
let dedup_key = (
configured_receiver_guard.file.clone(),
configured_receiver_guard.line,
configured_receiver_guard.column,
);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(configured_receiver_guard);
}
}
if let Some(callback_receiver_guard) =
receiver_callback_configuration_guard_sanitizer(context.ws, context.sink_func, snk, skr)
{
let dedup_key = (
callback_receiver_guard.file.clone(),
callback_receiver_guard.line,
callback_receiver_guard.column,
);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(callback_receiver_guard);
}
}
if let Some(configured_call_guard) = configured_call_argument_guard_sanitizer(
context.ws,
context.sink_func,
snk,
skr,
&context.sink_tainted_args,
) {
let dedup_key = (
configured_call_guard.file.clone(),
configured_call_guard.line,
configured_call_guard.column,
);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(configured_call_guard);
}
}
if let Some(ssrf_guard) =
url_network_guard_sanitizer(context.ws, context.call_graph, context.sink_func, snk, skr)
{
let dedup_key = (ssrf_guard.file.clone(), ssrf_guard.line, ssrf_guard.column);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(ssrf_guard);
}
}
if let Some(reconstruction_guard) = url_reconstruction_guard_sanitizer(
context.ws,
context.call_graph,
context.sink_func,
snk,
skr,
&context.sink_tainted_args,
) {
let dedup_key = (
reconstruction_guard.file.clone(),
reconstruction_guard.line,
reconstruction_guard.column,
);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(reconstruction_guard);
}
}
if let Some(compiler_guard) = compiler_guard_sanitizer(context.ws, context.sink_func, snk, skr) {
let dedup_key = (
compiler_guard.file.clone(),
compiler_guard.line,
compiler_guard.column,
);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(compiler_guard);
}
}
if let Some(eq_guard) = nosql_eq_filter_wrapper_sanitizer(
context.ws,
context.sink_func,
src,
snk,
skr,
&context.sink_tainted_args,
) {
let dedup_key = (eq_guard.file.clone(), eq_guard.line, eq_guard.column);
if seen_sanitizer_keys.insert(dedup_key) {
sanitizers_seen.push(eq_guard);
}
}
if source_sink_pair_is_low_signal(&src_match, source_rule, skr) {
return None;
}
let status = compute_status(&pack.metadata, &sanitizers_seen, skr.tag.as_deref());
let mut sink_match = FindingMatch::from_rule_match(report_sink, skr);
sink_match.tainted_args = context.sink_tainted_args;
let root = context.ws.db().workspace_root();
let test_patterns = &pack.metadata.test_path_patterns;
let from_test = path_is_test_file_with_root(root.as_deref(), &src.file, test_patterns)
|| path_is_test_file_with_root(root.as_deref(), &report_sink.file, test_patterns)
|| context
.taint_path
.iter()
.any(|step| path_is_test_file_with_root(root.as_deref(), &step.file, test_patterns));
let severity = match (skr.severity, src_match.trust.as_deref()) {
(Some(sev), Some("local")) => Some(cap_local_trust_severity(sev)),
(Some(sev), Some("inferred")) => Some(demote_severity_one_tier(sev)),
(sev, _) => sev,
};
Some(Finding {
finding_id,
language: src.language.clone(),
source: src_match,
sink: sink_match,
sanitizers_seen,
taint_transforms_seen,
group_id: Some(group),
representative_flow_id: context.flow_id,
analysis_complete: context.analysis_incomplete_reasons.is_empty(),
analysis_incomplete_reasons: context.analysis_incomplete_reasons,
chain_display: context.chain_names,
taint_path: context.taint_path,
alternate_flows: Vec::new(),
hops: Vec::new(),
tag: skr.tag.clone(),
severity,
cwe: skr.cwe.clone(),
owasp: skr.owasp.clone(),
status,
from_test,
})
}
fn caller_composition_attribution_match(
context: &FindingBuildContext<'_>,
sink: &RuleMatch,
sink_rule: &Rule,
) -> Option<RuleMatch> {
let query_index = sink_rule
.analysis_semantics
.as_ref()?
.parameterized_query
.as_ref()?
.query_arg_index;
let sink_argument = context
.sink_tainted_args
.iter()
.find(|argument| argument.index == query_index)?;
let sink_place = sink_argument.place.as_deref()?.trim();
let sink_decl = context.ws.exact_decl(SymbolId::new(context.sink_func.raw()))?;
let parameter_index = sink_decl
.params
.iter()
.position(|parameter| parameter.trim() == sink_place)?;
for edge in context.call_graph.callers_of(context.sink_func) {
let Some(caller) = context.ws.exact_decl(SymbolId::new(edge.from.raw())) else {
continue;
};
let (edge_file, edge_line, _) = resolve_span_location(context.ws, edge.span);
let on_selected_path = context.taint_path.iter().any(|step| {
step.caller == caller.name
&& step.callee == sink_decl.name
&& step.line == edge_line
&& (step.file == edge_file
|| edge_file.ends_with(&step.file)
|| step.file.ends_with(&edge_file))
&& step
.tainted_args
.iter()
.any(|argument| argument.index == parameter_index)
});
if !on_selected_path {
continue;
}
let Some(FlowEvent::Call { args, .. }) = find_call_event_at(&caller.flow_events, edge.span) else {
continue;
};
let Some(caller_argument) = args.get(parameter_index) else {
continue;
};
let Some(caller_place) = caller_argument.place.as_deref().map(str::trim) else {
continue;
};
if caller_place.is_empty() {
continue;
}
let Some(file_index) = context
.ws
.db()
.decl_index_remapped_to_headers(context.global, edge.span.file)
else {
continue;
};
let Some(composition) = file_index
.string_compositions
.iter()
.filter(|composition| {
composition.target.as_deref() == Some(caller_place)
&& composition.container_span.start < edge.span.start
})
.max_by_key(|composition| composition.container_span.start)
else {
continue;
};
let Some(latest_assignment) = file_index
.assignment_values
.iter()
.filter(|assignment| {
assignment.target.as_deref() == Some(caller_place)
&& assignment.assignment_span.start < edge.span.start
})
.max_by_key(|assignment| assignment.assignment_span.start)
else {
continue;
};
if latest_assignment.assignment_span.file != composition.container_span.file
|| latest_assignment.assignment_span.start > composition.container_span.start
|| latest_assignment.assignment_span.end < composition.container_span.end
{
continue;
}
let has_literal = composition
.parts
.iter()
.any(|part| matches!(part, bonsai_lang_api::StringCompositionPart::Literal { .. }));
let mut dynamic_places = composition
.parts
.iter()
.filter_map(|part| match part {
bonsai_lang_api::StringCompositionPart::Place { place }
| bonsai_lang_api::StringCompositionPart::PlaceOrLiteral { place, .. } => {
Some(place.as_str())
}
_ => None,
})
.collect::<Vec<_>>();
dynamic_places.sort_unstable();
dynamic_places.dedup();
let [dynamic_place] = dynamic_places.as_slice() else {
continue;
};
if !has_literal {
continue;
}
if !latest_assignment
.value_flow
.source_names
.iter()
.any(|source| source == *dynamic_place)
{
continue;
}
let attribution_span = composition
.dynamic_anchor_span
.unwrap_or(composition.container_span);
let (file, line, column) = resolve_span_location(context.ws, attribution_span);
let text = context
.ws
.vfs()
.snapshot(composition.value_span.file)
.ok()
.and_then(|snapshot| {
snapshot
.text
.get(composition.value_span.start as usize..composition.value_span.end as usize)
.map(str::trim)
.map(str::to_string)
})
.unwrap_or_else(|| caller_place.to_string());
let mut attributed = sink.clone();
attributed.file = file;
attributed.line = line;
attributed.column = column;
attributed.span = attribution_span;
attributed.match_text = text;
attributed.enclosing_fn = Some(caller.name.clone());
return Some(attributed);
}
None
}
fn rule_is_taint_preserving_transform(rule: &Rule, metadata: &RulepackMetadata) -> bool {
let has_passthrough_semantics = rule.taint_semantics.as_ref().is_some_and(|semantics| {
!semantics.call_result_passthrough_args.is_empty()
|| semantics.call_result_passthrough_args_from.is_some()
|| semantics.call_result_passthrough_receiver
});
rule.analysis_semantics
.as_ref()
.and_then(|semantics| semantics.taint_preserving_transform)
.unwrap_or(has_passthrough_semantics)
&& rule
.tag
.as_deref()
.is_some_and(|tag| sanitizer_tag_is_recognized_non_crediting(metadata, tag))
}