pub mod domain;
pub mod engine;
pub mod facts;
pub mod lattice;
pub mod symbol;
pub mod transfer;
use crate::cfg::{Cfg, FuncSummaries};
use crate::cfg_analysis::rules;
use crate::summary::GlobalSummaries;
use crate::symbol::Lang;
use domain::{AuthLevel, ProductState};
use engine::MAX_TRACKED_VARS;
use facts::StateFinding;
use petgraph::graph::NodeIndex;
use symbol::SymbolInterner;
use transfer::DefaultTransfer;
pub fn classify_auth_decorators(lang: Lang, decorators: &[String]) -> AuthLevel {
if decorators.is_empty() {
return AuthLevel::Unauthed;
}
let auth_rules = rules::auth_rules(lang);
let mut level = AuthLevel::Unauthed;
for dec in decorators {
let d = dec.to_ascii_lowercase();
if d.contains("admin") || d.contains("hasrole") || d.contains("superuser") {
return AuthLevel::Admin;
}
let matches = auth_rules.iter().any(|rule| {
rule.matchers.iter().any(|m| {
let ml = m.to_ascii_lowercase();
d == ml || d.ends_with(&ml)
})
});
if matches && level < AuthLevel::Authed {
level = AuthLevel::Authed;
}
}
level
}
#[allow(clippy::too_many_arguments)]
pub fn run_state_analysis(
cfg: &Cfg,
entry: NodeIndex,
lang: Lang,
_source_bytes: &[u8],
func_summaries: &FuncSummaries,
_global_summaries: Option<&GlobalSummaries>,
enable_auth: bool,
resource_method_summaries: &[transfer::ResourceMethodSummary],
auth_decorators: &[String],
path_safe_suppressed_sink_spans: &std::collections::HashSet<(usize, usize)>,
ptr_proxy_hints: Option<&std::collections::HashMap<String, crate::pointer::PtrProxyHint>>,
closure_released_var_names: Option<&std::collections::HashSet<String>>,
) -> Vec<StateFinding> {
let _span = tracing::debug_span!("run_state_analysis").entered();
let interner = SymbolInterner::from_cfg_scoped(cfg);
if interner.len() > MAX_TRACKED_VARS {
tracing::warn!(
symbols = interner.len(),
max = MAX_TRACKED_VARS,
"state analysis: too many variables, capping tracking"
);
}
let resource_pairs = rules::resource_pairs(lang);
let transfer = DefaultTransfer {
lang,
resource_pairs,
interner: &interner,
resource_method_summaries,
ptr_proxy_hints,
};
let mut initial = ProductState::initial();
initial.auth.auth_level = classify_auth_decorators(lang, auth_decorators);
let result = engine::run_forward(cfg, entry, &transfer, initial);
facts::extract_findings(
&result,
cfg,
&interner,
lang,
func_summaries,
enable_auth,
path_safe_suppressed_sink_spans,
closure_released_var_names,
)
}
pub fn collect_closure_released_var_names(
bodies: &[crate::cfg::BodyCfg],
lang: Lang,
) -> std::collections::HashMap<crate::cfg::BodyId, std::collections::HashSet<String>> {
use crate::cfg::{BodyId, StmtKind};
use petgraph::visit::IntoNodeReferences;
let pairs = rules::resource_pairs(lang);
let mut per_body: std::collections::HashMap<BodyId, std::collections::HashSet<String>> =
std::collections::HashMap::new();
for body in bodies {
if body.meta.parent_body_id.is_none() {
continue;
}
let mut local = std::collections::HashSet::new();
for (_idx, info) in body.graph.node_references() {
if info.kind != StmtKind::Call {
continue;
}
let Some(callee) = info.call.callee.as_deref() else {
continue;
};
let cl = callee.to_ascii_lowercase();
let is_release = pairs.iter().any(|p| {
p.release.iter().any(|r| {
let rl = r.to_ascii_lowercase();
if let Some(method) = rl.strip_prefix('.') {
cl.ends_with(&format!(".{method}"))
} else {
cl == rl || cl.ends_with(&format!(".{rl}"))
}
})
});
if !is_release {
continue;
}
if let Some(rcv) = info.call.receiver.as_deref() {
local.insert(rcv.to_string());
} else if let Some((rcv, _)) = callee.rsplit_once('.')
&& !rcv.is_empty()
{
local.insert(rcv.to_string());
}
}
if !local.is_empty() {
per_body.insert(body.meta.id, local);
}
}
let mut rollup: std::collections::HashMap<BodyId, std::collections::HashSet<String>> =
std::collections::HashMap::new();
let by_id: std::collections::HashMap<BodyId, &crate::cfg::BodyCfg> =
bodies.iter().map(|b| (b.meta.id, b)).collect();
for body in bodies {
let Some(local) = per_body.get(&body.meta.id) else {
continue;
};
let mut cur = body.meta.parent_body_id;
while let Some(pid) = cur {
rollup.entry(pid).or_default().extend(local.iter().cloned());
cur = by_id.get(&pid).and_then(|b| b.meta.parent_body_id);
}
}
rollup
}
pub fn build_resource_method_summaries(
bodies: &[crate::cfg::BodyCfg],
lang: Lang,
) -> Vec<transfer::ResourceMethodSummary> {
use petgraph::visit::IntoNodeReferences;
let resource_pairs = rules::resource_pairs(lang);
let mut summaries = Vec::new();
for body in bodies {
let method_name = match &body.meta.name {
Some(name) => name.clone(),
None => continue,
};
let class_group = match body.meta.parent_body_id {
Some(pid) => pid,
None => continue, };
for (_, info) in body.graph.node_references() {
if !matches!(
info.kind,
crate::cfg::StmtKind::Call | crate::cfg::StmtKind::Seq
) {
continue;
}
let callee = match &info.call.callee {
Some(c) => c.to_ascii_lowercase(),
None => continue,
};
for pair in resource_pairs {
if pair
.acquire
.iter()
.any(|a| transfer::callee_matches_pub(&callee, a))
{
summaries.push(transfer::ResourceMethodSummary {
method_name: method_name.clone(),
effect: transfer::ResourceEffect::Acquire,
class_group,
original_span: info.ast.span,
});
}
if pair
.release
.iter()
.any(|r| transfer::callee_matches_pub(&callee, r))
{
summaries.push(transfer::ResourceMethodSummary {
method_name: method_name.clone(),
effect: transfer::ResourceEffect::Release,
class_group,
original_span: info.ast.span,
});
}
}
}
}
summaries
}