use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
pub struct UnsafeApiAnalyzer;
impl Plugin for UnsafeApiAnalyzer {
fn name(&self) -> &str {
"unsafe_api"
}
fn smells(&self) -> Vec<String> {
vec!["unsafe_api".into()]
}
fn description(&self) -> &str {
"Dangerous function calls (eval/exec/system)"
}
fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
let (Some(tree), Some(lang)) = (ctx.tree, ctx.ts_language) else {
return vec![];
};
let patterns = queries_for(&ctx.model.language);
if patterns.is_empty() {
return vec![];
}
let source = ctx.file.content.as_bytes();
let mut findings = Vec::new();
for (pattern, label, msg) in patterns {
for matches in crate::query::run_query(tree, lang, source, pattern) {
let Some(cap) = matches.iter().find(|c| c.capture_name == "site") else {
continue;
};
findings.push(Finding {
smell_name: "unsafe_api".into(),
category: SmellCategory::Security,
severity: Severity::Warning,
location: Location {
path: ctx.file.path.clone(),
start_line: cap.start_line as usize,
start_col: cap.start_col as usize,
end_line: cap.end_line as usize,
end_col: cap.end_col as usize,
name: None,
},
message: format!("Potentially dangerous: `{label}` — {msg}"),
suggested_refactorings: vec!["Use a safe alternative".into()],
..Default::default()
});
}
}
findings
}
}
fn queries_for(lang: &str) -> Vec<(&'static str, &'static str, &'static str)> {
match lang {
"rust" => vec![
(
"(unsafe_block) @site",
"unsafe block",
"unsafe block — review for memory safety",
),
(
"(function_modifiers \"unsafe\") @site",
"unsafe fn",
"unsafe fn — review for memory safety",
),
],
"python" => vec![
(
r#"(call function: (identifier) @n (#eq? @n "eval")) @site"#,
"eval",
"eval() executes arbitrary code",
),
(
r#"(call function: (identifier) @n (#eq? @n "exec")) @site"#,
"exec",
"exec() executes arbitrary code",
),
(
r#"(call function: (attribute object: (identifier) @o attribute: (identifier) @a) (#eq? @o "os") (#eq? @a "system")) @site"#,
"os.system",
"os.system() is vulnerable to shell injection",
),
(
r#"(call function: (attribute object: (identifier) @o attribute: (identifier) @a) (#eq? @o "subprocess") (#eq? @a "call")) @site"#,
"subprocess.call",
"prefer subprocess.run with shell=False",
),
(
r#"(call function: (attribute object: (identifier) @o attribute: (identifier) @a) (#eq? @o "pickle") (#match? @a "^(load|loads)$")) @site"#,
"pickle.load",
"pickle deserialization can execute arbitrary code",
),
],
"typescript" => vec![
(
r#"(call_expression function: (identifier) @n (#eq? @n "eval")) @site"#,
"eval",
"eval() executes arbitrary code",
),
(
r#"(member_expression property: (property_identifier) @p (#eq? @p "innerHTML")) @site"#,
"innerHTML",
"innerHTML can lead to XSS",
),
(
r#"(jsx_attribute (property_identifier) @p (#eq? @p "dangerouslySetInnerHTML")) @site"#,
"dangerouslySetInnerHTML",
"React escape hatch — review for XSS",
),
(
r#"(call_expression function: (member_expression object: (identifier) @o property: (property_identifier) @p) (#eq? @o "document") (#eq? @p "write")) @site"#,
"document.write",
"document.write can lead to XSS",
),
],
"c" | "cpp" => vec![
(
r#"(call_expression function: (identifier) @n (#eq? @n "gets")) @site"#,
"gets",
"gets() has no bounds checking — use fgets()",
),
(
r#"(call_expression function: (identifier) @n (#eq? @n "sprintf")) @site"#,
"sprintf",
"sprintf() has no bounds checking — use snprintf()",
),
(
r#"(call_expression function: (identifier) @n (#eq? @n "strcpy")) @site"#,
"strcpy",
"strcpy() has no bounds checking — use strncpy()",
),
(
r#"(call_expression function: (identifier) @n (#eq? @n "strcat")) @site"#,
"strcat",
"strcat() has no bounds checking — use strncat()",
),
(
r#"(call_expression function: (identifier) @n (#eq? @n "system")) @site"#,
"system",
"system() is vulnerable to shell injection",
),
],
"go" => vec![
(
r#"(call_expression function: (selector_expression operand: (identifier) @o field: (field_identifier) @f) (#eq? @o "exec") (#eq? @f "Command")) @site"#,
"exec.Command",
"review for command injection",
),
(
r#"(call_expression function: (selector_expression operand: (identifier) @o field: (field_identifier) @f) (#eq? @o "template") (#eq? @f "HTML")) @site"#,
"template.HTML",
"bypasses HTML escaping — review for XSS",
),
],
_ => vec![],
}
}