1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3pub struct UnsafeApiAnalyzer;
14
15impl Plugin for UnsafeApiAnalyzer {
16 fn name(&self) -> &str {
17 "unsafe_api"
18 }
19
20 fn smells(&self) -> Vec<String> {
21 vec!["unsafe_api".into()]
22 }
23
24 fn description(&self) -> &str {
25 "Dangerous function calls (eval/exec/system)"
26 }
27
28 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
29 let (Some(tree), Some(lang)) = (ctx.tree, ctx.ts_language) else {
30 return vec![];
31 };
32 let patterns = queries_for(&ctx.model.language);
33 if patterns.is_empty() {
34 return vec![];
35 }
36 let source = ctx.file.content.as_bytes();
37 let mut findings = Vec::new();
38 for (pattern, label, msg) in patterns {
39 for matches in crate::query::run_query(tree, lang, source, pattern) {
40 let Some(cap) = matches.iter().find(|c| c.capture_name == "site") else {
41 continue;
42 };
43 findings.push(Finding {
44 smell_name: "unsafe_api".into(),
45 category: SmellCategory::Security,
46 severity: Severity::Warning,
47 location: Location {
48 path: ctx.file.path.clone(),
49 start_line: cap.start_line as usize,
50 start_col: cap.start_col as usize,
51 end_line: cap.end_line as usize,
52 end_col: cap.end_col as usize,
53 name: None,
54 },
55 message: format!("Potentially dangerous: `{label}` — {msg}"),
56 suggested_refactorings: vec!["Use a safe alternative".into()],
57 ..Default::default()
58 });
59 }
60 }
61 findings
62 }
63}
64
65fn queries_for(lang: &str) -> Vec<(&'static str, &'static str, &'static str)> {
71 match lang {
72 "rust" => vec![
73 (
74 "(unsafe_block) @site",
75 "unsafe block",
76 "unsafe block — review for memory safety",
77 ),
78 (
79 "(function_modifiers \"unsafe\") @site",
80 "unsafe fn",
81 "unsafe fn — review for memory safety",
82 ),
83 ],
84 "python" => vec![
85 (
86 r#"(call function: (identifier) @n (#eq? @n "eval")) @site"#,
87 "eval",
88 "eval() executes arbitrary code",
89 ),
90 (
91 r#"(call function: (identifier) @n (#eq? @n "exec")) @site"#,
92 "exec",
93 "exec() executes arbitrary code",
94 ),
95 (
96 r#"(call function: (attribute object: (identifier) @o attribute: (identifier) @a) (#eq? @o "os") (#eq? @a "system")) @site"#,
97 "os.system",
98 "os.system() is vulnerable to shell injection",
99 ),
100 (
101 r#"(call function: (attribute object: (identifier) @o attribute: (identifier) @a) (#eq? @o "subprocess") (#eq? @a "call")) @site"#,
102 "subprocess.call",
103 "prefer subprocess.run with shell=False",
104 ),
105 (
106 r#"(call function: (attribute object: (identifier) @o attribute: (identifier) @a) (#eq? @o "pickle") (#match? @a "^(load|loads)$")) @site"#,
107 "pickle.load",
108 "pickle deserialization can execute arbitrary code",
109 ),
110 ],
111 "typescript" => vec![
112 (
113 r#"(call_expression function: (identifier) @n (#eq? @n "eval")) @site"#,
114 "eval",
115 "eval() executes arbitrary code",
116 ),
117 (
118 r#"(member_expression property: (property_identifier) @p (#eq? @p "innerHTML")) @site"#,
119 "innerHTML",
120 "innerHTML can lead to XSS",
121 ),
122 (
123 r#"(jsx_attribute (property_identifier) @p (#eq? @p "dangerouslySetInnerHTML")) @site"#,
124 "dangerouslySetInnerHTML",
125 "React escape hatch — review for XSS",
126 ),
127 (
128 r#"(call_expression function: (member_expression object: (identifier) @o property: (property_identifier) @p) (#eq? @o "document") (#eq? @p "write")) @site"#,
129 "document.write",
130 "document.write can lead to XSS",
131 ),
132 ],
133 "c" | "cpp" => vec![
134 (
135 r#"(call_expression function: (identifier) @n (#eq? @n "gets")) @site"#,
136 "gets",
137 "gets() has no bounds checking — use fgets()",
138 ),
139 (
140 r#"(call_expression function: (identifier) @n (#eq? @n "sprintf")) @site"#,
141 "sprintf",
142 "sprintf() has no bounds checking — use snprintf()",
143 ),
144 (
145 r#"(call_expression function: (identifier) @n (#eq? @n "strcpy")) @site"#,
146 "strcpy",
147 "strcpy() has no bounds checking — use strncpy()",
148 ),
149 (
150 r#"(call_expression function: (identifier) @n (#eq? @n "strcat")) @site"#,
151 "strcat",
152 "strcat() has no bounds checking — use strncat()",
153 ),
154 (
155 r#"(call_expression function: (identifier) @n (#eq? @n "system")) @site"#,
156 "system",
157 "system() is vulnerable to shell injection",
158 ),
159 ],
160 "go" => vec![
161 (
162 r#"(call_expression function: (selector_expression operand: (identifier) @o field: (field_identifier) @f) (#eq? @o "exec") (#eq? @f "Command")) @site"#,
163 "exec.Command",
164 "review for command injection",
165 ),
166 (
167 r#"(call_expression function: (selector_expression operand: (identifier) @o field: (field_identifier) @f) (#eq? @o "template") (#eq? @f "HTML")) @site"#,
168 "template.HTML",
169 "bypasses HTML escaping — review for XSS",
170 ),
171 ],
172 _ => vec![],
173 }
174}