1use std::borrow::Borrow;
2use std::collections::{HashMap, HashSet};
3use std::fmt::Display;
4use std::fs::File;
5use std::io::BufReader;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9use memchr::memmem;
10use nonempty::NonEmpty;
11use regex::Regex;
12use rustc_hash::FxHashSet;
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15use tree_sitter::Tree;
16use walkdir::WalkDir;
17use weggli::query::QueryTree;
18use weggli::result::QueryResult;
19use weggli::RegexMap;
20
21#[derive(Debug, Error)]
22pub enum RuleError {
23 #[error(transparent)]
24 Check(#[from] CheckError),
25 #[error("cannot parse rule: {0}")]
26 Parse(#[from] serde_yaml::Error),
27 #[error("cannot parse rule file {}: {1}", _0.display())]
28 ParseFile(PathBuf, anyhow::Error),
29 #[error("rule has no checks")]
30 NoChecks,
31 #[error("rule has no identifier")]
32 NoId,
33 #[error("rule has multiple checks with the same name")]
34 MultipleChecksWithSameName,
35 #[error(transparent)]
36 Regex(#[from] RegexError),
37}
38
39#[derive(Debug, Error)]
40pub enum CheckError {
41 #[error("check has no name")]
42 NoCheckName,
43 #[error("check has no patterns")]
44 NoCheckPatterns,
45 #[error("regex constraint has an invalid query variable `{0}`")]
46 InvalidQueryVariable(String),
47 #[error("invalid pattern: {0}")]
48 Pattern(#[from] weggli::WeggliError),
49 #[error(transparent)]
50 Regex(#[from] RegexError),
51}
52
53#[derive(Debug, Error)]
54pub enum RegexError {
55 #[error("`{0}` is not in the format `var=regex`")]
56 InvalidFormat(String),
57 #[error("invalid regex: {0}")]
58 InvalidRegex(#[from] regex::Error),
59}
60
61#[derive(Clone)]
62pub struct RuleSet {
63 rules: Arc<[(String, Arc<Rule>)]>,
64}
65
66impl RuleSet {
67 pub fn from_directory(root: impl AsRef<Path>, ignore_errors: bool) -> Result<Self, RuleError> {
68 let walker = WalkDir::new(root);
69 let mut rules = Vec::new();
70
71 for dirent in walker
72 .into_iter()
73 .filter_entry(|e| {
74 e.file_type().is_dir() || {
75 matches!(e.path().extension(), Some(x) if
76 ["yml", "yaml"].contains(&x.to_string_lossy().as_ref()))
77 }
78 })
79 .filter_map(Result::ok)
80 {
81 if dirent.file_type().is_dir() {
82 continue;
83 }
84
85 let path = dirent.path();
86 match Rule::from_file(path) {
87 Ok(rule) => {
88 rules.push((path.display().to_string(), Arc::new(rule)));
89 }
90 Err(e) => {
91 if !ignore_errors {
92 return Err(e);
93 }
94 }
95 }
96 }
97
98 Ok(Self {
99 rules: Arc::from(rules),
100 })
101 }
102
103 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, RuleError> {
104 let path = path.as_ref();
105 Ok(Self {
106 rules: Arc::from(vec![(
107 path.display().to_string(),
108 Arc::new(Rule::from_file(path)?),
109 )]),
110 })
111 }
112
113 pub fn from_str(rule: impl AsRef<str>) -> Result<Self, RuleError> {
114 Ok(Self {
115 rules: Arc::from(vec![(
116 String::from("default"),
117 Arc::new(Rule::from_str(rule)?),
118 )]),
119 })
120 }
121
122 pub fn get(&self, index: usize) -> Option<Arc<Rule>> {
123 self.rules.get(index).map(|(_, r)| r.clone())
124 }
125
126 pub fn get_ref(&self, index: usize) -> Option<&Rule> {
127 self.rules.get(index).map(|(_, r)| r.as_ref())
128 }
129
130 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &Rule)> {
131 self.rules.iter().map(|(p, r)| (p.as_str(), r.as_ref()))
132 }
133
134 pub fn viable_checkers(
135 &self,
136 source: impl AsRef<str>,
137 ) -> Vec<(usize, Arc<Rule>, usize, &Checker)> {
138 let source = source.as_ref();
139
140 self.rules
141 .iter()
142 .enumerate()
143 .map(|(rule_id, (_, rule))| {
144 rule.checks()
145 .iter()
146 .enumerate()
147 .filter_map(move |(i, checker)| {
148 if checker.can_match(source) {
149 Some((rule_id, rule.clone(), i, checker))
150 } else {
151 None
152 }
153 })
154 })
155 .flatten()
156 .collect()
157 }
158
159 pub fn is_empty(&self) -> bool {
160 self.rules.is_empty()
161 }
162
163 pub fn len(&self) -> usize {
164 self.rules.len()
165 }
166}
167
168#[derive(
169 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize,
170)]
171#[serde(rename_all = "lowercase")]
172pub enum Severity {
173 #[default]
174 None,
175 Low,
176 Medium,
177 High,
178 Critical,
179}
180
181impl Display for Severity {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 f.write_str(match self {
184 Self::None => "n/a",
185 Self::Low => "low",
186 Self::Medium => "medium",
187 Self::High => "high",
188 Self::Critical => "critical",
189 })
190 }
191}
192
193pub struct Rule {
194 id: String,
195 author: String,
196 description: String,
197 severity: Severity,
198 tags: FxHashSet<String>,
199 checks: Box<[Checker]>,
200}
201
202impl Rule {
203 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, RuleError> {
204 let path = path.as_ref();
205 let file = File::open(path).map_err(|e| RuleError::ParseFile(path.to_owned(), e.into()))?;
206 serde_yaml::from_reader(BufReader::new(file))
207 .map_err(|e| RuleError::ParseFile(path.to_owned(), e.into()))
208 }
209
210 pub fn from_str(rule: impl AsRef<str>) -> Result<Self, RuleError> {
211 serde_yaml::from_str(rule.as_ref()).map_err(RuleError::from)
212 }
213
214 pub fn id(&self) -> &str {
215 &self.id
216 }
217
218 pub fn author(&self) -> Option<&str> {
219 if self.author.is_empty() {
220 None
221 } else {
222 Some(&self.author)
223 }
224 }
225
226 pub fn description(&self) -> Option<&str> {
227 if self.description.is_empty() {
228 None
229 } else {
230 Some(&self.description)
231 }
232 }
233
234 pub fn severity(&self) -> Severity {
235 self.severity
236 }
237
238 pub fn tags(&self) -> &FxHashSet<String> {
239 &self.tags
240 }
241
242 pub fn has_tag(&self, tag: impl Borrow<str>) -> bool {
243 self.tags.contains(tag.borrow())
244 }
245
246 pub fn checks(&self) -> &[Checker] {
247 &self.checks
248 }
249}
250
251impl<'de> Deserialize<'de> for Rule {
252 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
253 where
254 D: serde::Deserializer<'de>,
255 {
256 #[derive(Deserialize)]
257 struct RuleT {
258 id: String,
259 #[serde(default)]
260 author: String,
261 #[serde(default)]
262 description: String,
263 #[serde(default)]
264 severity: Severity,
265 #[serde(default)]
266 tags: FxHashSet<String>,
267 #[serde(
268 rename = "check patterns",
269 alias = "check-patterns",
270 alias = "check pattern",
271 alias = "check-pattern"
272 )]
273 checks: OneOrMany<CheckerT>,
274 }
275
276 let rule = RuleT::deserialize(deserializer)?;
277
278 if rule.id.is_empty() {
279 return Err(<D::Error as serde::de::Error>::custom(RuleError::NoId));
280 }
281
282 let checks = Vec::try_from(rule.checks)
283 .map_err(<D::Error as serde::de::Error>::custom)?
284 .into_boxed_slice();
285
286 Ok(Rule {
287 id: rule.id,
288 author: rule.author,
289 description: rule.description,
290 severity: rule.severity,
291 tags: rule.tags,
292 checks,
293 })
294 }
295}
296
297#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
298pub enum CheckerLanguage {
299 #[serde(rename = "c")]
300 #[default]
301 C,
302 #[serde(rename = "c++")]
303 Cplusplus,
304}
305
306impl CheckerLanguage {
307 pub fn is_c(&self) -> bool {
308 matches!(self, Self::C)
309 }
310
311 pub fn is_cxx(&self) -> bool {
312 matches!(self, Self::Cplusplus)
313 }
314}
315
316pub struct Checker {
317 name: Arc<str>,
318 language: CheckerLanguage,
319 pattern: QueryTree,
320 identifiers: Box<[String]>,
321 limit: bool,
322 unique: bool,
323}
324
325impl Checker {
326 pub fn name(&self) -> &str {
327 &self.name
328 }
329
330 pub fn name_for_match(&self) -> Arc<str> {
331 self.name.clone()
332 }
333
334 pub fn language(&self) -> CheckerLanguage {
335 self.language
336 }
337
338 pub fn pattern(&self) -> &QueryTree {
339 &self.pattern
340 }
341
342 pub fn limit(&self) -> bool {
343 self.limit
344 }
345
346 pub fn unique(&self) -> bool {
347 self.unique
348 }
349
350 pub fn can_match(&self, source: &str) -> bool {
351 self.identifiers
352 .iter()
353 .all(|ident| memmem::find(source.as_ref(), ident.as_ref()).is_some())
354 }
355
356 pub fn check_match(&self, tree: &Tree, source: &str) -> Vec<QueryResult> {
357 let matches = self.pattern.matches(tree.root_node(), source);
358 if matches.is_empty() {
359 return Vec::with_capacity(0);
360 }
361
362 let check_unique = |m: &QueryResult| {
363 !self.unique || {
364 let mut seen = FxHashSet::default();
365 m.vars
366 .keys()
367 .filter_map(|k| m.value(k, &source))
368 .all(|x| seen.insert(x))
369 }
370 };
371
372 let mut skip_set = FxHashSet::default();
373 let mut check_limit = |m: &QueryResult| !self.limit || skip_set.insert(m.start_offset());
374
375 matches
376 .into_iter()
377 .filter(|v| check_unique(v) && check_limit(v))
378 .collect()
379 }
380}
381
382impl<'de> Deserialize<'de> for Checker {
383 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
384 where
385 D: serde::Deserializer<'de>,
386 {
387 let checker = CheckerT::deserialize(deserializer)?
388 .try_into()
389 .map_err(<D::Error as serde::de::Error>::custom)?;
390
391 Ok(checker)
392 }
393}
394
395#[derive(Debug, Deserialize)]
396#[serde(untagged)]
397enum OneOrMany<T> {
398 Many(NonEmpty<T>),
399 One(T),
400}
401
402impl<T> From<OneOrMany<T>> for Vec<T> {
403 fn from(value: OneOrMany<T>) -> Self {
404 match value {
405 OneOrMany::One(v) => vec![v],
406 OneOrMany::Many(vs) => vs.into(),
407 }
408 }
409}
410
411#[derive(Debug, Deserialize)]
412struct CheckerT {
413 #[serde(default = "default_check_name")]
414 name: String,
415 #[serde(default)]
416 language: CheckerLanguage,
417 pattern: String,
418 #[serde(alias = "regex", default)]
419 regexes: Option<OneOrMany<String>>,
420 #[serde(default)]
421 limit: bool,
422 #[serde(default)]
423 unique: bool,
424}
425
426fn default_check_name() -> String {
427 String::from("default")
428}
429
430fn validate_checker(checker: CheckerT) -> Result<CheckerT, CheckError> {
431 if checker.name.is_empty() {
432 return Err(CheckError::NoCheckName);
433 }
434
435 Ok(checker)
436}
437
438fn build_pattern(
439 input: String,
440 constraints: &RegexMap,
441 cxx: bool,
442) -> Result<(QueryTree, HashSet<String>), CheckError> {
443 let pattern = weggli::parse_search_pattern(&input, cxx, false, Some(constraints.clone()))?;
444 let variables = pattern.variables();
445
446 Ok((pattern, variables))
447}
448
449fn build_regex_mapping(regexes: Option<OneOrMany<String>>) -> Result<RegexMap, CheckError> {
451 let mut result = HashMap::new();
452
453 let Some(regexes) = regexes.map(Vec::from) else {
454 return Ok(RegexMap::new(result));
455 };
456
457 for r in regexes {
458 let (var, raw_regex) = r
459 .split_once('=')
460 .ok_or_else(|| RegexError::InvalidFormat(r.to_owned()))?;
461
462 let var = var.trim();
463 let raw_regex = raw_regex.trim();
464
465 let mut normalised_var = if var.starts_with('$') {
466 var.to_owned()
467 } else {
468 format!("${var}")
469 };
470
471 let negative = normalised_var.ends_with('!');
472
473 if negative {
474 normalised_var.pop(); }
476
477 let regex = Regex::new(raw_regex).map_err(RegexError::from)?;
478
479 result.insert(normalised_var, (negative, regex));
480 }
481
482 Ok(RegexMap::new(result))
483}
484
485impl TryFrom<CheckerT> for Checker {
486 type Error = CheckError;
487
488 fn try_from(c: CheckerT) -> Result<Self, Self::Error> {
489 let regexes = build_regex_mapping(c.regexes)?;
490 let (pattern, variables) = build_pattern(c.pattern.into(), ®exes, c.language.is_cxx())?;
491
492 for v in regexes.variables() {
493 if !variables.contains(v) {
494 return Err(CheckError::InvalidQueryVariable(v.to_owned()));
495 }
496 }
497
498 Ok(Self {
499 name: Arc::from(c.name),
500 language: c.language,
501 identifiers: pattern.identifiers().into_boxed_slice(),
502 pattern,
503 limit: c.limit,
504 unique: c.unique,
505 })
506 }
507}
508
509impl TryFrom<OneOrMany<CheckerT>> for Vec<Checker> {
510 type Error = RuleError;
511
512 fn try_from(value: OneOrMany<CheckerT>) -> Result<Self, Self::Error> {
513 match value {
514 OneOrMany::One(checker) => {
515 let checker = validate_checker(checker)?;
516 Ok(vec![checker.try_into()?])
517 }
518 OneOrMany::Many(checkers) => {
519 let mut names = FxHashSet::default();
520 let mut checks = Vec::new();
521
522 for checker in checkers {
523 let checker = validate_checker(checker)?;
524
525 if !names.insert(checker.name.to_owned()) {
526 return Err(RuleError::MultipleChecksWithSameName);
527 }
528
529 checks.push(checker.try_into()?);
530 }
531
532 Ok(checks)
533 }
534 }
535 }
536}
537
538#[cfg(test)]
539mod test {
540 use super::*;
541
542 #[test]
543 fn test_rule_parse() -> Result<(), RuleError> {
544 let rule1 = r#"
545id: call-to-unbounded-copy-functions
546check pattern:
547 regex: func=^gets$
548 pattern: '{$func();}'
549"#;
550 let rule = Rule::from_str(rule1)?;
551
552 assert_eq!(rule.id(), "call-to-unbounded-copy-functions");
553 assert_eq!(rule.checks().len(), 1);
554
555 let rule2 = r#"
556id: call-to-unbounded-copy-functions
557description: call to unbounded copy functions
558severity: medium
559tags:
560- CWE-120
561- CWE-242
562- CWE-676
563check-patterns:
564- name: gets
565 regex: func=^gets$
566 pattern: |
567 { $func(); }
568
569- name: st(r|p)(cpy|cat)
570 regex: func=st(r|p)(cpy|cat)$
571 pattern: '{$func();}'
572
573- name: wc(r|p)(cpy|cat)
574 regex: func=wc(r|p)(cpy|cat)$
575 pattern: '{$func();}'
576
577- name: sprintf
578 regex: func=sprintf$
579 pattern: '{$func();}'
580
581- name: scanf
582 regex: func=scanf$
583 pattern: '{$func();}'
584"#;
585
586 let rule = Rule::from_str(rule2)?;
587
588 assert_eq!(rule.id(), "call-to-unbounded-copy-functions");
589 assert_eq!(rule.checks().len(), 5);
590
591 Ok(())
592 }
593}