Skip to main content

weggli_ruleset/
matcher.rs

1use 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        // parse failed...
148        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}