weggli_ruleset/
matcher.rs1use std::fmt::Debug;
2use std::path::Path;
3use std::sync::Arc;
4
5use thiserror::Error;
6use tree_sitter::Parser;
7use weggli::result::QueryResult;
8
9use crate::rule::{Checker, Rule, RuleError, RuleSet};
10
11pub struct RuleMatcher {
12 rules: RuleSet,
13 c_parser: Parser,
14 cxx_parser: Parser,
15}
16
17pub struct RuleMatch {
18 rule: Arc<Rule>,
19 rule_id: usize,
20 checker_id: usize,
21 source: Arc<str>,
22 result: QueryResult,
23}
24
25impl RuleMatch {
26 pub fn rule(&self) -> &Rule {
27 &self.rule
28 }
29
30 pub fn rule_id(&self) -> usize {
31 self.rule_id
32 }
33
34 pub fn checker_id(&self) -> usize {
35 self.checker_id
36 }
37
38 pub fn checker(&self) -> &Checker {
39 &self.rule().checks()[self.checker_id]
40 }
41
42 pub fn source(&self) -> Arc<str> {
43 self.source.clone()
44 }
45
46 pub fn source_ref(&self) -> &str {
47 &self.source
48 }
49
50 pub fn result(&self) -> &QueryResult {
51 &self.result
52 }
53
54 pub fn into_result(self) -> QueryResult {
55 self.result
56 }
57
58 pub fn display(&self, before: usize, after: usize, line_numbers: bool) -> String {
59 self.result
60 .display(&self.source, before, after, line_numbers)
61 }
62}
63
64impl Debug for RuleMatch {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 let mut m = f.debug_struct("RuleMatch");
67
68 m.field("rule", &self.rule.id() as _);
69 m.field("checker", &self.checker().name() as _);
70
71 if let Some(ref description) = self.rule().description() {
72 m.field("description", description as _);
73 }
74
75 m.field("tags", self.rule().tags() as _);
76 m.field("severity", &self.rule().severity() as _);
77
78 m.field("match", &self.result as _);
79
80 m.finish_non_exhaustive()
81 }
82}
83
84#[derive(Debug, Error)]
85pub enum RuleMatcherError {
86 #[error("cannot construct parser")]
87 Parser(weggli::WeggliError),
88 #[error(transparent)]
89 Rules(#[from] RuleError),
90}
91
92impl RuleMatcher {
93 pub fn new(rules: RuleSet) -> Result<Self, RuleMatcherError> {
94 Ok(Self {
95 rules,
96 c_parser: weggli::get_parser(false).map_err(RuleMatcherError::Parser)?,
97 cxx_parser: weggli::get_parser(true).map_err(RuleMatcherError::Parser)?,
98 })
99 }
100
101 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, RuleMatcherError> {
102 Self::new(RuleSet::from_file(path)?)
103 }
104
105 pub fn from_str(rule: impl AsRef<str>) -> Result<Self, RuleMatcherError> {
106 Self::new(RuleSet::from_str(rule)?)
107 }
108
109 pub fn from_directory(root: impl AsRef<Path>) -> Result<Self, RuleMatcherError> {
110 Self::from_directory_with(root, true)
111 }
112
113 pub fn from_directory_with(
114 root: impl AsRef<Path>,
115 ignore_errors: bool,
116 ) -> Result<Self, RuleMatcherError> {
117 Self::new(RuleSet::from_directory(root, ignore_errors)?)
118 }
119
120 pub fn rules(&self) -> &RuleSet {
121 &self.rules
122 }
123
124 pub fn matches(&mut self, source: impl AsRef<str>) -> Result<Vec<RuleMatch>, RuleMatcherError> {
125 self.matches_with(source, false)
126 }
127
128 pub fn matches_with(
129 &mut self,
130 source: impl AsRef<str>,
131 is_cxx: bool,
132 ) -> Result<Vec<RuleMatch>, RuleMatcherError> {
133 let source = source.as_ref();
134
135 let checkers = self.rules.viable_checkers(source);
136
137 if checkers.is_empty() {
138 return Ok(Vec::with_capacity(0));
139 }
140
141 let tree = if is_cxx {
142 self.cxx_parser.parse(source.as_bytes(), None)
143 } else {
144 self.c_parser.parse(source.as_bytes(), None)
145 };
146
147 let Some(tree) = tree else {
149 return Ok(Vec::with_capacity(0));
150 };
151
152 let source = Arc::<str>::from(source);
153
154 let results = checkers
155 .into_iter()
156 .flat_map(|(rule_id, rule, checker_id, checker)| {
157 let source = source.clone();
158 checker
159 .check_match(&tree, &source)
160 .into_iter()
161 .map(move |result| RuleMatch {
162 rule: rule.clone(),
163 rule_id,
164 checker_id,
165 source: source.clone(),
166 result,
167 })
168 })
169 .collect();
170
171 Ok(results)
172 }
173}
174
175#[cfg(test)]
176mod test {
177 use super::RuleMatcher;
178 use std::fs;
179
180 #[test]
181 fn test_strcpy() -> Result<(), Box<dyn std::error::Error>> {
182 let decompiler_output = r#"
183char *__fastcall sub_XYZ(char *s, size_t a2)
184{
185 char *v2; // rbx
186 char *v3; // rax
187 const char *v4; // r15
188 char *v5; // rax
189 const char *v6; // r15
190
191 v2 = s;
192 v3 = j__secure_getenv("ZZZ");
193 if ( !v3 || (v4 = v3, !*v3) )
194 {
195 v5 = j__secure_getenv("HOME");
196 if ( v5 )
197 {
198 v6 = v5;
199 if ( *v5 )
200 {
201 if ( strlen(v5) + 6 < a2 )
202 {
203 strcpy(s, v6);
204 *(_WORD *)&s[strlen(s)] = 47;
205 strcat(s, ".rnd");
206 return v2;
207 }
208 }
209 }
210 return 0LL;
211 }
212 if ( strlen(v3) + 1 >= a2 )
213 return 0LL;
214 strcpy(s, v4);
215 return v2;
216}
217"#;
218
219 let rule = r#"
220id: call-to-unbounded-copy-functions
221description: call to unbounded copy functions
222severity: medium
223tags:
224- CWE-120
225- CWE-242
226- CWE-676
227check-patterns:
228- name: gets
229 regex: func=^gets$
230 pattern: |
231 { $func(); }
232- name: st(r|p)(cpy|cat)
233 regex: func=st(r|p)(cpy|cat)$
234 pattern: |
235 { $func(); }
236- name: wc(r|p)(cpy|cat)
237 regex: func=wc(r|p)(cpy|cat)$
238 pattern: |
239 { $func(); }
240- name: sprintf
241 regex: func=sprintf$
242 pattern: |
243 { $func(); }
244- name: scanf
245 regex: func=scanf$
246 pattern: |
247 { $func(); }
248"#;
249
250 let mut matcher = RuleMatcher::from_str(rule)?;
251
252 let matches = matcher.matches_with(decompiler_output, false)?;
253
254 println!("{matches:#?}");
255
256 for m in matches {
257 println!("\n\n{}", m.display(5, 5, true));
258 }
259
260 Ok(())
261 }
262
263 #[test]
264 fn test_decomp_ls() -> Result<(), Box<dyn std::error::Error>> {
265 let rule1 = r#"
266id: call-to-unbounded-copy-functions
267check pattern:
268- name: st(r|p)(cpy|cat)
269 regex: func=st(r|p)(cpy|cat)$
270 pattern: |
271 { $func(); }
272"#;
273 let mut matcher = RuleMatcher::from_str(rule1)?;
274 let input = fs::read_to_string("tests/ls-main.c")?;
275
276 let matches = matcher.matches_with(&input, false)?;
277
278 assert_eq!(matches.len(), 1);
279
280 Ok(())
281 }
282
283 #[test]
284 fn test_decomp_objdump() -> Result<(), Box<dyn std::error::Error>> {
285 let rule1 = r#"
286id: simple-check
287check pattern:
288- name: check-var
289 pattern: |
290 { unsigned char $var; }
291"#;
292 let input = fs::read_to_string("tests/objdump-disas.c")?;
293
294 let mut matcher = RuleMatcher::from_str(rule1)?;
295 let matches = matcher.matches_with(&input, false)?;
296
297 assert_eq!(matches.len(), 2);
298
299 for m in matches {
300 println!("\n\n{}", m.display(5, 5, true));
301 }
302
303 Ok(())
304 }
305}