libsast 3.1.9__tar.gz → 3.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {libsast-3.1.9 → libsast-3.2.0}/PKG-INFO +79 -18
  2. {libsast-3.1.9 → libsast-3.2.0}/README.md +64 -15
  3. {libsast-3.1.9 → libsast-3.2.0}/libsast/__init__.py +3 -1
  4. {libsast-3.1.9 → libsast-3.2.0}/libsast/__main__.py +15 -2
  5. {libsast-3.1.9 → libsast-3.2.0}/libsast/core_matcher/choice_matcher.py +52 -53
  6. {libsast-3.1.9 → libsast-3.2.0}/libsast/core_matcher/helpers.py +40 -36
  7. {libsast-3.1.9 → libsast-3.2.0}/libsast/core_matcher/matchers.py +21 -17
  8. {libsast-3.1.9 → libsast-3.2.0}/libsast/core_matcher/pattern_matcher.py +60 -29
  9. {libsast-3.1.9 → libsast-3.2.0}/libsast/core_sgrep/helpers.py +3 -0
  10. libsast-3.2.0/libsast/core_treesitter/__init__.py +5 -0
  11. libsast-3.2.0/libsast/core_treesitter/languages.py +160 -0
  12. libsast-3.2.0/libsast/core_treesitter/matchers.py +469 -0
  13. libsast-3.2.0/libsast/core_treesitter/ts_matcher.py +223 -0
  14. {libsast-3.1.9 → libsast-3.2.0}/libsast/scanner.py +15 -3
  15. {libsast-3.1.9 → libsast-3.2.0}/libsast/standards.py +2 -0
  16. libsast-3.2.0/pyproject.toml +67 -0
  17. libsast-3.1.9/pyproject.toml +0 -43
  18. {libsast-3.1.9 → libsast-3.2.0}/LICENSE +0 -0
  19. {libsast-3.1.9 → libsast-3.2.0}/libsast/common.py +0 -0
  20. {libsast-3.1.9 → libsast-3.2.0}/libsast/core_matcher/__init__.py +0 -0
  21. {libsast-3.1.9 → libsast-3.2.0}/libsast/core_sgrep/__init__.py +0 -0
  22. {libsast-3.1.9 → libsast-3.2.0}/libsast/core_sgrep/semantic_sgrep.py +0 -0
  23. {libsast-3.1.9 → libsast-3.2.0}/libsast/exceptions.py +0 -0
  24. {libsast-3.1.9 → libsast-3.2.0}/libsast/standards/cwe.yaml +0 -0
  25. {libsast-3.1.9 → libsast-3.2.0}/libsast/standards/owasp_masvs.yaml +0 -0
  26. {libsast-3.1.9 → libsast-3.2.0}/libsast/standards/owasp_mobile_top10_2016.yaml +0 -0
  27. {libsast-3.1.9 → libsast-3.2.0}/libsast/standards/owasp_mobile_top10_2024.yaml +0 -0
  28. {libsast-3.1.9 → libsast-3.2.0}/libsast/standards/owasp_web_top10_2017.yaml +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: libsast
3
- Version: 3.1.9
4
- Summary: A generic SAST library built on top of semgrep and regex
3
+ Version: 3.2.0
4
+ Summary: A generic SAST library built on top of tree-sitter, semgrep, and regex
5
5
  License: LGPL-3.0-or-later
6
6
  License-File: LICENSE
7
- Keywords: libsast,SAST,Python SAST,SAST API,Regex SAST,Pattern Matcher
7
+ Keywords: libsast,SAST,Python SAST,SAST API,Tree-sitter SAST,Regex SAST,Pattern Matcher
8
8
  Author: Ajin Abraham
9
9
  Author-email: ajin@opensecurity.in
10
10
  Requires-Python: >=3.10,<4.0
@@ -20,9 +20,21 @@ Classifier: Programming Language :: Python :: 3.14
20
20
  Classifier: Programming Language :: Python :: 3.15
21
21
  Classifier: Topic :: Security
22
22
  Classifier: Topic :: Software Development :: Quality Assurance
23
+ Provides-Extra: js
24
+ Provides-Extra: mobile
25
+ Provides-Extra: treesitter
23
26
  Requires-Dist: billiard (>=4.2.1,<5.0.0)
24
27
  Requires-Dist: pyyaml (>=6.0)
25
28
  Requires-Dist: requests
29
+ Requires-Dist: tree-sitter (>=0.25.0)
30
+ Requires-Dist: tree-sitter-c-sharp (>=0.23.0) ; extra == "mobile"
31
+ Requires-Dist: tree-sitter-java (>=0.23.0) ; extra == "mobile"
32
+ Requires-Dist: tree-sitter-javascript (>=0.23.0) ; extra == "js" or extra == "mobile"
33
+ Requires-Dist: tree-sitter-kotlin (>=1.0.0) ; extra == "mobile"
34
+ Requires-Dist: tree-sitter-language-pack (>=0.13.0) ; (python_version >= "3.11") and (extra == "treesitter")
35
+ Requires-Dist: tree-sitter-objc (>=3.0.0) ; extra == "mobile"
36
+ Requires-Dist: tree-sitter-swift (>=0.0.1) ; extra == "mobile"
37
+ Requires-Dist: tree-sitter-typescript (>=0.23.2) ; extra == "js" or extra == "mobile"
26
38
  Project-URL: Bug Tracker, https://github.com/ajinabraham/libsast/issues
27
39
  Project-URL: Documentation, https://github.com/ajinabraham/libsast/blob/master/README.md
28
40
  Project-URL: Repository, https://github.com/ajinabraham/libsast
@@ -30,7 +42,7 @@ Description-Content-Type: text/markdown
30
42
 
31
43
  # libsast
32
44
 
33
- Generic SAST for Security Engineers. Powered by regex based pattern matcher and semantic aware [semgrep](https://github.com/returntocorp/semgrep).
45
+ Generic SAST for Security Engineers. Powered by [tree-sitter](https://tree-sitter.github.io/tree-sitter/) AST pattern matcher, regex based pattern matcher, and semantic aware [semgrep](https://github.com/returntocorp/semgrep).
34
46
 
35
47
  Made with ![Love](https://cloud.githubusercontent.com/assets/4301109/16754758/82e3a63c-4813-11e6-9430-6015d98aeaab.png) in India [![Tweet](https://img.shields.io/twitter/url?url=https://github.com/ajinabraham/libsast)](https://twitter.com/intent/tweet/?text=Generic%20SAST%20for%20Security%20Engineers.%20Powered%20by%20regex%20based%20pattern%20matcher%20and%20semantic%20aware%20semgrep%20by%20%40ajinabraham%20%40OpenSecurity_IN&url=https://github.com/ajinabraham/libsast)
36
48
 
@@ -48,17 +60,26 @@ Made with ![Love](https://cloud.githubusercontent.com/assets/4301109/16754758/82
48
60
  ## Install
49
61
 
50
62
  ```bash
51
- pip install semgrep==1.172.0 #For semgrep support
52
63
  pip install libsast
64
+ # Optional Tree-sitter language grammar extras:
65
+ pip install "libsast[treesitter]" # 165+ languages via tree-sitter-language-pack
66
+ pip install "libsast[mobile]" # JS, TS/TSX, Java, Kotlin, Swift, ObjC, C#
67
+ pip install "libsast[js]" # JavaScript and TypeScript/TSX
68
+ # Or install any individual tree-sitter-<lang> package alongside libsast:
69
+ pip install libsast tree-sitter-python
70
+ # Optional Semgrep support (macOS/Linux):
71
+ pip install semgrep==1.172.0
53
72
  ```
54
73
 
55
- Pattern Matcher is cross-platform, but Semgrep supports only Mac and Linux.
74
+ Pattern Matcher and Tree-sitter Matcher are cross-platform (Windows, macOS, and Linux). Semgrep supports macOS and Linux.
56
75
 
57
76
  ## Command Line Options
58
77
 
59
78
  ```bash
60
79
  $ libsast
61
- usage: libsast [-h] [-o OUTPUT] [-p PATTERN_FILE] [-s SGREP_PATTERN_FILE]
80
+ usage: libsast [-h] [-o OUTPUT] [-p PATTERN_FILE] [-t TS_PATTERN_FILE]
81
+ [-s SGREP_PATTERN_FILE]
82
+ [--ts-file-extensions TS_FILE_EXTENSIONS [TS_FILE_EXTENSIONS ...]]
62
83
  [--sgrep-file-extensions SGREP_FILE_EXTENSIONS [SGREP_FILE_EXTENSIONS ...]]
63
84
  [--file-extensions FILE_EXTENSIONS [FILE_EXTENSIONS ...]]
64
85
  [--ignore-filenames IGNORE_FILENAMES [IGNORE_FILENAMES ...]]
@@ -76,8 +97,13 @@ options:
76
97
  Output filename to save JSON report.
77
98
  -p PATTERN_FILE, --pattern-file PATTERN_FILE
78
99
  YAML pattern file, directory or url
100
+ -t TS_PATTERN_FILE, --ts-pattern-file TS_PATTERN_FILE
101
+ Tree-sitter YAML pattern file, directory or url
79
102
  -s SGREP_PATTERN_FILE, --sgrep-pattern-file SGREP_PATTERN_FILE
80
103
  sgrep rules directory
104
+ --ts-file-extensions TS_FILE_EXTENSIONS [TS_FILE_EXTENSIONS ...]
105
+ File extensions that should be scanned with
106
+ tree-sitter matcher
81
107
  --sgrep-file-extensions SGREP_FILE_EXTENSIONS [SGREP_FILE_EXTENSIONS ...]
82
108
  File extensions that should be scanned with semantic
83
109
  grep
@@ -303,23 +329,23 @@ $ libsast -s tests/assets/rules/semantic_grep/ -p tests/assets/rules/pattern_mat
303
329
 
304
330
  ```python
305
331
  >>> from libsast import Scanner
306
- >>> options = {'match_rules': '/Users/ajinabraham/Code/njsscan/njsscan/rules/pattern_matcher', 'sgrep_rules': '/Users/ajinabraham/Code/njsscan/njsscan/rules/semantic_grep', 'sgrep_extensions': {'', '.js'}, 'match_extensions': {'.hbs', '.sh', '.ejs', '.toml', '.mustache', '.tmpl', '.jade', '.json', '.ect', '.vue', '.yml', '.hdbs', '.tl', '.html', '.haml', '.dust', '.pug', '.tpl'}, 'ignore_filenames': {'bootstrap.min.js', '.DS_Store', 'bootstrap-tour.js', 'd3.min.js', 'tinymce.js', 'codemirror.js', 'tinymce.min.js', 'react-dom.production.min.js', 'react.js', 'jquery.min.js', 'react.production.min.js', 'codemirror-compressed.js', 'axios.min.js', 'angular.min.js', 'raphael-min.js', 'vue.min.js'}, 'ignore_extensions': {'.7z', '.exe', '.rar', '.zip', '.a', '.o', '.tz'}, 'ignore_paths': {'__MACOSX', 'jquery', 'fixtures', 'node_modules', 'bower_components', 'example', 'spec'}, 'show_progress': False}
332
+ >>> options = {'match_rules': '/Users/ajinabraham/Code/njsscan/njsscan/rules/pattern_matcher', 'sgrep_rules': '/Users/ajinabraham/Code/njsscan/njsscan/rules/semantic_grep', 'sgrep_extensions': {'', '.js'}, 'match_extensions': {'.hbs', '.sh', '.ejs', '.toml', '.mustache', '.tmpl', '.jade', '.json', '.ect', '.vue', '.yml', '.hdbs', '.tl', '.html', '.haml', '.dust', '.pug', '.tpl'}, 'ignore_filenames': {'bootstrap.min.js', '.DS_Store', 'bootstrap-tour.js', 'd3.min.js', 'tinymce.js', 'codemirror.js', 'tinymce.min.js', 'react-dom.production.min.js', 'react.js', 'jquery.min.js', 'react.production.min.js', 'codemirror-compressed.js', 'axios.min.js', 'angular.min.js', 'raphael-min.js', 'vue.min.js'}, 'ignore_paths': {'__MACOSX', 'jquery', 'fixtures', 'node_modules', 'bower_components', 'example', 'spec'}, 'show_progress': False}
307
333
  >>> paths = ['../njsscan/tests/assets/dot_njsscan/']
308
334
  >>> scanner = Scanner(options, paths)
309
335
  >>> scanner.scan()
310
- {'pattern_matcher': {'handlebar_mustache_template': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/ignore_ext.hbs', 'match_string': '{{{html}}}', 'match_position': (52, 62), 'match_lines': (1, 1)}], 'metadata': {'id': 'handlebar_mustache_template', 'description': 'The Handlebar.js/Mustache.js template has an unescaped variable. Untrusted user input passed to this variable results in Cross Site Scripting (XSS).', 'type': 'Regex', 'pattern': '{{{.+}}}|{{[ ]*&[\\w]+.*}}', 'severity': 'ERROR', 'input_case': 'exact', 'cwe': "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')", 'owasp': 'A1: Injection'}}}, 'semantic_grep': {'matches': {'node_aes_ecb': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/lorem_scan.js', 'match_position': (16, 87), 'match_lines': (14, 14), 'match_string': "let decipher = crypto.createDecipheriv('aes-128-ecb', Buffer.from(ENCRYPTION_KEY), iv);"}], 'metadata': {'owasp': 'A9: Using Components with Known Vulnerabilities', 'cwe': 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm', 'description': 'AES with ECB mode is deterministic in nature and not suitable for encrypting large amount of repetitive data.', 'severity': 'ERROR'}}, 'node_tls_reject': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/skip_dir/skip_me.js', 'match_position': (9, 58), 'match_lines': (9, 9), 'match_string': " process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';"}, {'file_path': '../njsscan/tests/assets/dot_njsscan/skip_dir/skip_me.js', 'match_position': (9, 55), 'match_lines': (18, 18), 'match_string': ' process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";'}], 'metadata': {'owasp': 'A6: Security Misconfiguration', 'cwe': 'CWE-295: Improper Certificate Validation', 'description': "Setting 'NODE_TLS_REJECT_UNAUTHORIZED' to 0 will allow node server to accept self signed certificates and is not a secure behaviour.", 'severity': 'ERROR'}}, 'node_curl_ssl_verify_disable': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/skip_dir/skip_me.js', 'match_position': (5, 11), 'match_lines': (45, 51), 'match_string': ' curl(url,\n\n {\n\n SSL_VERIFYPEER: 0\n\n },\n\n function (err) {\n\n response.end(this.body);\n\n })'}], 'metadata': {'owasp': 'A6: Security Misconfiguration', 'cwe': 'CWE-599: Missing Validation of OpenSSL Certificate', 'description': 'SSL Certificate verification for node-curl is disabled.', 'severity': 'ERROR'}}, 'regex_injection_dos': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/lorem_scan.js', 'match_position': (5, 37), 'match_lines': (25, 27), 'match_string': ' var key = req.param("key");\n\n // Regex created from user input\n\n var re = new RegExp("\\\\b" + key);'}], 'metadata': {'owasp': 'A1: Injection', 'cwe': 'CWE-400: Uncontrolled Resource Consumption', 'description': 'User controlled data in RegExp() can make the application vulnerable to layer 7 DoS.', 'severity': 'ERROR'}}, 'express_xss': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/skip.js', 'match_position': (9, 55), 'match_lines': (7, 10), 'match_string': ' var str = new Buffer(req.cookies.profile, \'base64\').toString();\n\n var obj = serialize.unserialize(str);\n\n if (obj.username) {\n\n res.send("Hello " + escape(obj.username));'}], 'metadata': {'owasp': 'A1: Injection', 'cwe': "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')", 'description': 'Untrusted User Input in Response will result in Reflected Cross Site Scripting Vulnerability.', 'severity': 'ERROR'}}, 'generic_path_traversal': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/lorem_scan.js', 'match_position': (5, 35), 'match_lines': (36, 37), 'match_string': " var filePath = path.join(__dirname, '/' + req.query.load);\n\n fileSystem.readFile(filePath); // ignore: generic_path_traversal"}, {'file_path': '../njsscan/tests/assets/dot_njsscan/lorem_scan.js', 'match_position': (5, 35), 'match_lines': (42, 43), 'match_string': " var filePath = path.join(__dirname, '/' + req.query.load);\n\n fileSystem.readFile(filePath); // detect this"}], 'metadata': {'owasp': 'A5: Broken Access Control', 'cwe': 'CWE-23: Relative Path Traversal', 'description': 'Untrusted user input in readFile()/readFileSync() can endup in Directory Traversal Attacks.', 'severity': 'ERROR'}}, 'express_open_redirect': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/lorem_scan.js', 'match_position': (5, 26), 'match_lines': (49, 51), 'match_string': ' var target = req.param("target");\n\n // BAD: sanitization doesn\'t apply here\n\n res.redirect(target); //ignore: express_open_redirect'}], 'metadata': {'owasp': 'A1: Injection', 'cwe': "CWE-601: URL Redirection to Untrusted Site ('Open Redirect')", 'description': 'Untrusted user input in redirect() can result in Open Redirect vulnerability.', 'severity': 'ERROR'}}, 'node_deserialize': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/skip.js', 'match_position': (19, 45), 'match_lines': (8, 8), 'match_string': ' var obj = serialize.unserialize(str);'}], 'metadata': {'owasp': 'A8: Insecure Deserialization', 'cwe': 'CWE-502: Deserialization of Untrusted Data', 'description': "User controlled data in 'unserialize()' or 'deserialize()' function can result in Object Injection or Remote Code Injection.", 'severity': 'ERROR'}}}, 'errors': [{'type': 'SourceParseError', 'code': 3, 'short_msg': 'parse error', 'long_msg': 'Could not parse .njsscan as javascript', 'level': 'warn', 'spans': [{'start': {'line': 2, 'col': 20}, 'end': {'line': 2, 'col': 21}, 'source_hash': 'c60298be568bfb1325d92cbb3c0bc1450a25b85bb2e4000bdc3267c05f1c8c73', 'file': '.njsscan', 'context_start': None, 'context_end': None}], 'help': 'If the code appears to be valid, this may be a semgrep bug.'}, {'type': 'SourceParseError', 'code': 3, 'short_msg': 'parse error', 'long_msg': 'Could not parse no_ext_scan as javascript', 'level': 'warn', 'spans': [{'start': {'line': 1, 'col': 3}, 'end': {'line': 1, 'col': 5}, 'source_hash': 'f002e2a715be216987dd1b134e7b9fa6eef28e3caa82dead0109c4cdc489e089', 'file': 'no_ext_scan', 'context_start': None, 'context_end': None}], 'help': 'If the code appears to be valid, this may be a semgrep bug.'}]}}
311
336
  ```
312
337
 
313
- ## Write you own Static Analysis tool
338
+ ## Write your own Static Analysis tool
314
339
 
315
- With libsast, you can write your own static analysis tools. libsast provides two matching engines:
340
+ With libsast, you can write your own static analysis tools. libsast provides three matching engines:
316
341
 
317
- 1. **Pattern Matcher**
318
- 2. **Semantic Grep**
342
+ 1. **Pattern Matcher (Regex)**
343
+ 2. **Tree-sitter Matcher (AST)**
344
+ 3. **Semantic Grep (Semgrep)**
319
345
 
320
346
  ### Pattern Matcher
321
347
 
322
- Currently Pattern Matcher supports any language.
348
+ Pattern Matcher supports any language.
323
349
 
324
350
  Use [Regex 101](https://regex101.com/r/nGbAay/1) to write simple Python Regex rule patterns.
325
351
 
@@ -339,15 +365,15 @@ A sample rule looks like
339
365
  reference: http://foo.bar
340
366
  foo: Some extra metadata
341
367
  ```
342
- A rule consist of
368
+ A rule consists of
343
369
 
344
370
  * `id` : A unique id for the rule.
345
371
  * `message`: A description for the rule.
346
372
  * `input_case`: It can be `exact`, `upper` or `lower`. Data will be converted to lower case/upper case/as it is before comparing with the regex.
347
373
  * `pattern`: List of patterns depends on `type`.
348
374
  * `severity`: It can be `error`, `warning` or `info`.
349
- * `type`: Pattern Matcher supports `Regex`, `RegexAnd`, `RegexOr`, `RegexAndOr`, `RegexAndNot`.
350
- * `metadata` (optional): Define your own custom fields that you can use as metadata along with standard mappings.
375
+ * `type`: Pattern Matcher supports `Regex`, `RegexAnd`, `RegexOr`, `RegexAndOr`, `RegexAndNot` (as well as `TreeSitter`, `TreeSitterAnd`, `TreeSitterOr`, `TreeSitterAndOr`, `TreeSitterAndNot`).
376
+ * `metadata` (optional): Define your own custom fields that you can use as metadata along with standard mappings.
351
377
 
352
378
  ```bash
353
379
  1. Regex - if regex1 in input
@@ -362,11 +388,46 @@ Test your pattern matcher rules
362
388
 
363
389
  `$ libsast -p tests/assets/rules/pattern_matcher/patterns.yaml tests/assets/files/`
364
390
 
391
+ ### Tree-sitter Matcher
392
+
393
+ Tree-sitter Matcher provides language-agnostic, syntax-aware AST pattern matching across any language supported by [Tree-sitter](https://tree-sitter.github.io/tree-sitter/using-parsers/queries/1-syntax.html) (JavaScript, TypeScript/TSX, HTML/Vue `<script>`, Java, Kotlin, Swift, Objective-C, Dart, C#, Python, Go, Rust, C/C++, etc.).
394
+
395
+ Tree-sitter rules can be placed inside `--pattern-file` (`match_rules`) alongside Regex rules or passed separately via `-t` / `ts_rules`:
396
+
397
+ ```yaml
398
+ - id: rn_webview_universal_file_access
399
+ type: TreeSitter
400
+ languages: [javascript, tsx]
401
+ message: WebView allows universal access from file URLs
402
+ severity: error
403
+ pattern: |
404
+ (jsx_opening_element
405
+ name: (_) @_tag (#match? @_tag "WebView$")
406
+ attribute: (jsx_attribute
407
+ (property_identifier) @_prop (#eq? @_prop "allowUniversalAccessFromFileURLs")
408
+ (_ (true)))) @match
409
+ exclude_ancestor: |
410
+ (if_statement condition: (_) @_c (#match? @_c "__DEV__"))
411
+ metadata:
412
+ cwe: cwe-200
413
+ owasp-mobile: m1
414
+ masvs: platform-2
415
+ ```
416
+
417
+ Supported rule keys:
418
+
419
+ * `type`: `TreeSitter`, `TreeSitterAnd`, `TreeSitterOr`, `TreeSitterAndOr`, `TreeSitterAndNot`.
420
+ * `pattern`: Tree-sitter S-expression query (or list of queries) capturing `@match`.
421
+ * `languages` (optional): List of Tree-sitter language names the rule applies to.
422
+ * `exclude_ancestor` (optional): S-expression query; suppresses matches nested inside a matching ancestor node (e.g., `if (__DEV__)`).
423
+ * `within_ancestor` (optional): S-expression query; requires `@match` to be nested inside a matching ancestor node.
424
+ * `not_within_node` (optional): S-expression query; suppresses `@match` if the matched node itself contains a sub-node matching this query (e.g., missing-property checks).
425
+
365
426
  #### Inbuilt Standard Mapping Support
366
427
 
367
428
  Metadata fields also support [libsast standard mapping](https://github.com/ajinabraham/libsast/tree/master/libsast/standards).
368
429
 
369
- For example, the metadata field `owasp-web: a1` will get expanded at runtime as `owasp-web: 'A1: Injection'`.
430
+ For example, the metadata field `owasp-web: a1` will get expanded at runtime as `owasp-web: 'A1: Injection'`.
370
431
 
371
432
  *Currently Supports*
372
433
 
@@ -1,6 +1,6 @@
1
1
  # libsast
2
2
 
3
- Generic SAST for Security Engineers. Powered by regex based pattern matcher and semantic aware [semgrep](https://github.com/returntocorp/semgrep).
3
+ Generic SAST for Security Engineers. Powered by [tree-sitter](https://tree-sitter.github.io/tree-sitter/) AST pattern matcher, regex based pattern matcher, and semantic aware [semgrep](https://github.com/returntocorp/semgrep).
4
4
 
5
5
  Made with ![Love](https://cloud.githubusercontent.com/assets/4301109/16754758/82e3a63c-4813-11e6-9430-6015d98aeaab.png) in India [![Tweet](https://img.shields.io/twitter/url?url=https://github.com/ajinabraham/libsast)](https://twitter.com/intent/tweet/?text=Generic%20SAST%20for%20Security%20Engineers.%20Powered%20by%20regex%20based%20pattern%20matcher%20and%20semantic%20aware%20semgrep%20by%20%40ajinabraham%20%40OpenSecurity_IN&url=https://github.com/ajinabraham/libsast)
6
6
 
@@ -18,17 +18,26 @@ Made with ![Love](https://cloud.githubusercontent.com/assets/4301109/16754758/82
18
18
  ## Install
19
19
 
20
20
  ```bash
21
- pip install semgrep==1.172.0 #For semgrep support
22
21
  pip install libsast
22
+ # Optional Tree-sitter language grammar extras:
23
+ pip install "libsast[treesitter]" # 165+ languages via tree-sitter-language-pack
24
+ pip install "libsast[mobile]" # JS, TS/TSX, Java, Kotlin, Swift, ObjC, C#
25
+ pip install "libsast[js]" # JavaScript and TypeScript/TSX
26
+ # Or install any individual tree-sitter-<lang> package alongside libsast:
27
+ pip install libsast tree-sitter-python
28
+ # Optional Semgrep support (macOS/Linux):
29
+ pip install semgrep==1.172.0
23
30
  ```
24
31
 
25
- Pattern Matcher is cross-platform, but Semgrep supports only Mac and Linux.
32
+ Pattern Matcher and Tree-sitter Matcher are cross-platform (Windows, macOS, and Linux). Semgrep supports macOS and Linux.
26
33
 
27
34
  ## Command Line Options
28
35
 
29
36
  ```bash
30
37
  $ libsast
31
- usage: libsast [-h] [-o OUTPUT] [-p PATTERN_FILE] [-s SGREP_PATTERN_FILE]
38
+ usage: libsast [-h] [-o OUTPUT] [-p PATTERN_FILE] [-t TS_PATTERN_FILE]
39
+ [-s SGREP_PATTERN_FILE]
40
+ [--ts-file-extensions TS_FILE_EXTENSIONS [TS_FILE_EXTENSIONS ...]]
32
41
  [--sgrep-file-extensions SGREP_FILE_EXTENSIONS [SGREP_FILE_EXTENSIONS ...]]
33
42
  [--file-extensions FILE_EXTENSIONS [FILE_EXTENSIONS ...]]
34
43
  [--ignore-filenames IGNORE_FILENAMES [IGNORE_FILENAMES ...]]
@@ -46,8 +55,13 @@ options:
46
55
  Output filename to save JSON report.
47
56
  -p PATTERN_FILE, --pattern-file PATTERN_FILE
48
57
  YAML pattern file, directory or url
58
+ -t TS_PATTERN_FILE, --ts-pattern-file TS_PATTERN_FILE
59
+ Tree-sitter YAML pattern file, directory or url
49
60
  -s SGREP_PATTERN_FILE, --sgrep-pattern-file SGREP_PATTERN_FILE
50
61
  sgrep rules directory
62
+ --ts-file-extensions TS_FILE_EXTENSIONS [TS_FILE_EXTENSIONS ...]
63
+ File extensions that should be scanned with
64
+ tree-sitter matcher
51
65
  --sgrep-file-extensions SGREP_FILE_EXTENSIONS [SGREP_FILE_EXTENSIONS ...]
52
66
  File extensions that should be scanned with semantic
53
67
  grep
@@ -273,23 +287,23 @@ $ libsast -s tests/assets/rules/semantic_grep/ -p tests/assets/rules/pattern_mat
273
287
 
274
288
  ```python
275
289
  >>> from libsast import Scanner
276
- >>> options = {'match_rules': '/Users/ajinabraham/Code/njsscan/njsscan/rules/pattern_matcher', 'sgrep_rules': '/Users/ajinabraham/Code/njsscan/njsscan/rules/semantic_grep', 'sgrep_extensions': {'', '.js'}, 'match_extensions': {'.hbs', '.sh', '.ejs', '.toml', '.mustache', '.tmpl', '.jade', '.json', '.ect', '.vue', '.yml', '.hdbs', '.tl', '.html', '.haml', '.dust', '.pug', '.tpl'}, 'ignore_filenames': {'bootstrap.min.js', '.DS_Store', 'bootstrap-tour.js', 'd3.min.js', 'tinymce.js', 'codemirror.js', 'tinymce.min.js', 'react-dom.production.min.js', 'react.js', 'jquery.min.js', 'react.production.min.js', 'codemirror-compressed.js', 'axios.min.js', 'angular.min.js', 'raphael-min.js', 'vue.min.js'}, 'ignore_extensions': {'.7z', '.exe', '.rar', '.zip', '.a', '.o', '.tz'}, 'ignore_paths': {'__MACOSX', 'jquery', 'fixtures', 'node_modules', 'bower_components', 'example', 'spec'}, 'show_progress': False}
290
+ >>> options = {'match_rules': '/Users/ajinabraham/Code/njsscan/njsscan/rules/pattern_matcher', 'sgrep_rules': '/Users/ajinabraham/Code/njsscan/njsscan/rules/semantic_grep', 'sgrep_extensions': {'', '.js'}, 'match_extensions': {'.hbs', '.sh', '.ejs', '.toml', '.mustache', '.tmpl', '.jade', '.json', '.ect', '.vue', '.yml', '.hdbs', '.tl', '.html', '.haml', '.dust', '.pug', '.tpl'}, 'ignore_filenames': {'bootstrap.min.js', '.DS_Store', 'bootstrap-tour.js', 'd3.min.js', 'tinymce.js', 'codemirror.js', 'tinymce.min.js', 'react-dom.production.min.js', 'react.js', 'jquery.min.js', 'react.production.min.js', 'codemirror-compressed.js', 'axios.min.js', 'angular.min.js', 'raphael-min.js', 'vue.min.js'}, 'ignore_paths': {'__MACOSX', 'jquery', 'fixtures', 'node_modules', 'bower_components', 'example', 'spec'}, 'show_progress': False}
277
291
  >>> paths = ['../njsscan/tests/assets/dot_njsscan/']
278
292
  >>> scanner = Scanner(options, paths)
279
293
  >>> scanner.scan()
280
- {'pattern_matcher': {'handlebar_mustache_template': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/ignore_ext.hbs', 'match_string': '{{{html}}}', 'match_position': (52, 62), 'match_lines': (1, 1)}], 'metadata': {'id': 'handlebar_mustache_template', 'description': 'The Handlebar.js/Mustache.js template has an unescaped variable. Untrusted user input passed to this variable results in Cross Site Scripting (XSS).', 'type': 'Regex', 'pattern': '{{{.+}}}|{{[ ]*&[\\w]+.*}}', 'severity': 'ERROR', 'input_case': 'exact', 'cwe': "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')", 'owasp': 'A1: Injection'}}}, 'semantic_grep': {'matches': {'node_aes_ecb': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/lorem_scan.js', 'match_position': (16, 87), 'match_lines': (14, 14), 'match_string': "let decipher = crypto.createDecipheriv('aes-128-ecb', Buffer.from(ENCRYPTION_KEY), iv);"}], 'metadata': {'owasp': 'A9: Using Components with Known Vulnerabilities', 'cwe': 'CWE-327: Use of a Broken or Risky Cryptographic Algorithm', 'description': 'AES with ECB mode is deterministic in nature and not suitable for encrypting large amount of repetitive data.', 'severity': 'ERROR'}}, 'node_tls_reject': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/skip_dir/skip_me.js', 'match_position': (9, 58), 'match_lines': (9, 9), 'match_string': " process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';"}, {'file_path': '../njsscan/tests/assets/dot_njsscan/skip_dir/skip_me.js', 'match_position': (9, 55), 'match_lines': (18, 18), 'match_string': ' process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";'}], 'metadata': {'owasp': 'A6: Security Misconfiguration', 'cwe': 'CWE-295: Improper Certificate Validation', 'description': "Setting 'NODE_TLS_REJECT_UNAUTHORIZED' to 0 will allow node server to accept self signed certificates and is not a secure behaviour.", 'severity': 'ERROR'}}, 'node_curl_ssl_verify_disable': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/skip_dir/skip_me.js', 'match_position': (5, 11), 'match_lines': (45, 51), 'match_string': ' curl(url,\n\n {\n\n SSL_VERIFYPEER: 0\n\n },\n\n function (err) {\n\n response.end(this.body);\n\n })'}], 'metadata': {'owasp': 'A6: Security Misconfiguration', 'cwe': 'CWE-599: Missing Validation of OpenSSL Certificate', 'description': 'SSL Certificate verification for node-curl is disabled.', 'severity': 'ERROR'}}, 'regex_injection_dos': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/lorem_scan.js', 'match_position': (5, 37), 'match_lines': (25, 27), 'match_string': ' var key = req.param("key");\n\n // Regex created from user input\n\n var re = new RegExp("\\\\b" + key);'}], 'metadata': {'owasp': 'A1: Injection', 'cwe': 'CWE-400: Uncontrolled Resource Consumption', 'description': 'User controlled data in RegExp() can make the application vulnerable to layer 7 DoS.', 'severity': 'ERROR'}}, 'express_xss': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/skip.js', 'match_position': (9, 55), 'match_lines': (7, 10), 'match_string': ' var str = new Buffer(req.cookies.profile, \'base64\').toString();\n\n var obj = serialize.unserialize(str);\n\n if (obj.username) {\n\n res.send("Hello " + escape(obj.username));'}], 'metadata': {'owasp': 'A1: Injection', 'cwe': "CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')", 'description': 'Untrusted User Input in Response will result in Reflected Cross Site Scripting Vulnerability.', 'severity': 'ERROR'}}, 'generic_path_traversal': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/lorem_scan.js', 'match_position': (5, 35), 'match_lines': (36, 37), 'match_string': " var filePath = path.join(__dirname, '/' + req.query.load);\n\n fileSystem.readFile(filePath); // ignore: generic_path_traversal"}, {'file_path': '../njsscan/tests/assets/dot_njsscan/lorem_scan.js', 'match_position': (5, 35), 'match_lines': (42, 43), 'match_string': " var filePath = path.join(__dirname, '/' + req.query.load);\n\n fileSystem.readFile(filePath); // detect this"}], 'metadata': {'owasp': 'A5: Broken Access Control', 'cwe': 'CWE-23: Relative Path Traversal', 'description': 'Untrusted user input in readFile()/readFileSync() can endup in Directory Traversal Attacks.', 'severity': 'ERROR'}}, 'express_open_redirect': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/lorem_scan.js', 'match_position': (5, 26), 'match_lines': (49, 51), 'match_string': ' var target = req.param("target");\n\n // BAD: sanitization doesn\'t apply here\n\n res.redirect(target); //ignore: express_open_redirect'}], 'metadata': {'owasp': 'A1: Injection', 'cwe': "CWE-601: URL Redirection to Untrusted Site ('Open Redirect')", 'description': 'Untrusted user input in redirect() can result in Open Redirect vulnerability.', 'severity': 'ERROR'}}, 'node_deserialize': {'files': [{'file_path': '../njsscan/tests/assets/dot_njsscan/skip.js', 'match_position': (19, 45), 'match_lines': (8, 8), 'match_string': ' var obj = serialize.unserialize(str);'}], 'metadata': {'owasp': 'A8: Insecure Deserialization', 'cwe': 'CWE-502: Deserialization of Untrusted Data', 'description': "User controlled data in 'unserialize()' or 'deserialize()' function can result in Object Injection or Remote Code Injection.", 'severity': 'ERROR'}}}, 'errors': [{'type': 'SourceParseError', 'code': 3, 'short_msg': 'parse error', 'long_msg': 'Could not parse .njsscan as javascript', 'level': 'warn', 'spans': [{'start': {'line': 2, 'col': 20}, 'end': {'line': 2, 'col': 21}, 'source_hash': 'c60298be568bfb1325d92cbb3c0bc1450a25b85bb2e4000bdc3267c05f1c8c73', 'file': '.njsscan', 'context_start': None, 'context_end': None}], 'help': 'If the code appears to be valid, this may be a semgrep bug.'}, {'type': 'SourceParseError', 'code': 3, 'short_msg': 'parse error', 'long_msg': 'Could not parse no_ext_scan as javascript', 'level': 'warn', 'spans': [{'start': {'line': 1, 'col': 3}, 'end': {'line': 1, 'col': 5}, 'source_hash': 'f002e2a715be216987dd1b134e7b9fa6eef28e3caa82dead0109c4cdc489e089', 'file': 'no_ext_scan', 'context_start': None, 'context_end': None}], 'help': 'If the code appears to be valid, this may be a semgrep bug.'}]}}
281
294
  ```
282
295
 
283
- ## Write you own Static Analysis tool
296
+ ## Write your own Static Analysis tool
284
297
 
285
- With libsast, you can write your own static analysis tools. libsast provides two matching engines:
298
+ With libsast, you can write your own static analysis tools. libsast provides three matching engines:
286
299
 
287
- 1. **Pattern Matcher**
288
- 2. **Semantic Grep**
300
+ 1. **Pattern Matcher (Regex)**
301
+ 2. **Tree-sitter Matcher (AST)**
302
+ 3. **Semantic Grep (Semgrep)**
289
303
 
290
304
  ### Pattern Matcher
291
305
 
292
- Currently Pattern Matcher supports any language.
306
+ Pattern Matcher supports any language.
293
307
 
294
308
  Use [Regex 101](https://regex101.com/r/nGbAay/1) to write simple Python Regex rule patterns.
295
309
 
@@ -309,15 +323,15 @@ A sample rule looks like
309
323
  reference: http://foo.bar
310
324
  foo: Some extra metadata
311
325
  ```
312
- A rule consist of
326
+ A rule consists of
313
327
 
314
328
  * `id` : A unique id for the rule.
315
329
  * `message`: A description for the rule.
316
330
  * `input_case`: It can be `exact`, `upper` or `lower`. Data will be converted to lower case/upper case/as it is before comparing with the regex.
317
331
  * `pattern`: List of patterns depends on `type`.
318
332
  * `severity`: It can be `error`, `warning` or `info`.
319
- * `type`: Pattern Matcher supports `Regex`, `RegexAnd`, `RegexOr`, `RegexAndOr`, `RegexAndNot`.
320
- * `metadata` (optional): Define your own custom fields that you can use as metadata along with standard mappings.
333
+ * `type`: Pattern Matcher supports `Regex`, `RegexAnd`, `RegexOr`, `RegexAndOr`, `RegexAndNot` (as well as `TreeSitter`, `TreeSitterAnd`, `TreeSitterOr`, `TreeSitterAndOr`, `TreeSitterAndNot`).
334
+ * `metadata` (optional): Define your own custom fields that you can use as metadata along with standard mappings.
321
335
 
322
336
  ```bash
323
337
  1. Regex - if regex1 in input
@@ -332,11 +346,46 @@ Test your pattern matcher rules
332
346
 
333
347
  `$ libsast -p tests/assets/rules/pattern_matcher/patterns.yaml tests/assets/files/`
334
348
 
349
+ ### Tree-sitter Matcher
350
+
351
+ Tree-sitter Matcher provides language-agnostic, syntax-aware AST pattern matching across any language supported by [Tree-sitter](https://tree-sitter.github.io/tree-sitter/using-parsers/queries/1-syntax.html) (JavaScript, TypeScript/TSX, HTML/Vue `<script>`, Java, Kotlin, Swift, Objective-C, Dart, C#, Python, Go, Rust, C/C++, etc.).
352
+
353
+ Tree-sitter rules can be placed inside `--pattern-file` (`match_rules`) alongside Regex rules or passed separately via `-t` / `ts_rules`:
354
+
355
+ ```yaml
356
+ - id: rn_webview_universal_file_access
357
+ type: TreeSitter
358
+ languages: [javascript, tsx]
359
+ message: WebView allows universal access from file URLs
360
+ severity: error
361
+ pattern: |
362
+ (jsx_opening_element
363
+ name: (_) @_tag (#match? @_tag "WebView$")
364
+ attribute: (jsx_attribute
365
+ (property_identifier) @_prop (#eq? @_prop "allowUniversalAccessFromFileURLs")
366
+ (_ (true)))) @match
367
+ exclude_ancestor: |
368
+ (if_statement condition: (_) @_c (#match? @_c "__DEV__"))
369
+ metadata:
370
+ cwe: cwe-200
371
+ owasp-mobile: m1
372
+ masvs: platform-2
373
+ ```
374
+
375
+ Supported rule keys:
376
+
377
+ * `type`: `TreeSitter`, `TreeSitterAnd`, `TreeSitterOr`, `TreeSitterAndOr`, `TreeSitterAndNot`.
378
+ * `pattern`: Tree-sitter S-expression query (or list of queries) capturing `@match`.
379
+ * `languages` (optional): List of Tree-sitter language names the rule applies to.
380
+ * `exclude_ancestor` (optional): S-expression query; suppresses matches nested inside a matching ancestor node (e.g., `if (__DEV__)`).
381
+ * `within_ancestor` (optional): S-expression query; requires `@match` to be nested inside a matching ancestor node.
382
+ * `not_within_node` (optional): S-expression query; suppresses `@match` if the matched node itself contains a sub-node matching this query (e.g., missing-property checks).
383
+
335
384
  #### Inbuilt Standard Mapping Support
336
385
 
337
386
  Metadata fields also support [libsast standard mapping](https://github.com/ajinabraham/libsast/tree/master/libsast/standards).
338
387
 
339
- For example, the metadata field `owasp-web: a1` will get expanded at runtime as `owasp-web: 'A1: Injection'`.
388
+ For example, the metadata field `owasp-web: a1` will get expanded at runtime as `owasp-web: 'A1: Injection'`.
340
389
 
341
390
  *Currently Supports*
342
391
 
@@ -5,6 +5,7 @@ from datetime import datetime
5
5
  from .core_matcher.pattern_matcher import PatternMatcher
6
6
  from .core_matcher.choice_matcher import ChoiceMatcher
7
7
  from .core_sgrep.semantic_sgrep import SemanticGrep
8
+ from .core_treesitter.ts_matcher import TreeSitterMatcher
8
9
  from .scanner import Scanner
9
10
 
10
11
 
@@ -12,13 +13,14 @@ year = str(datetime.now().year)
12
13
  __title__ = 'libsast'
13
14
  __authors__ = 'Ajin Abraham'
14
15
  __copyright__ = f'Copyright {year} Ajin Abraham, opensecurity.in'
15
- __version__ = '3.1.9'
16
+ __version__ = '3.2.0'
16
17
  __version_info__ = tuple(int(i) for i in __version__.split('.'))
17
18
  __all__ = [
18
19
  'Scanner',
19
20
  'ChoiceMatcher',
20
21
  'PatternMatcher',
21
22
  'SemanticGrep',
23
+ 'TreeSitterMatcher',
22
24
  '__title__',
23
25
  '__authors__',
24
26
  '__copyright__',
@@ -27,7 +27,8 @@ def output(out, scan_results):
27
27
  if scan_results:
28
28
  sgrep_out = scan_results.get('semantic_grep', {}).get('matches')
29
29
  matcher_out = scan_results.get('pattern_matcher')
30
- if sgrep_out or matcher_out:
30
+ ts_out = scan_results.get('treesitter_matcher')
31
+ if sgrep_out or matcher_out or ts_out:
31
32
  sys.exit(1)
32
33
  sys.exit(0)
33
34
 
@@ -45,9 +46,17 @@ def main():
45
46
  parser.add_argument('-p', '--pattern-file',
46
47
  help='YAML pattern file, directory or url',
47
48
  required=False)
49
+ parser.add_argument('-t', '--ts-pattern-file',
50
+ help='Tree-sitter YAML pattern file, directory or url',
51
+ required=False)
48
52
  parser.add_argument('-s', '--sgrep-pattern-file',
49
53
  help='sgrep rules directory',
50
54
  required=False)
55
+ parser.add_argument('--ts-file-extensions',
56
+ nargs='+',
57
+ help=('File extensions that should be scanned'
58
+ ' with tree-sitter matcher'),
59
+ required=False)
51
60
  parser.add_argument('--sgrep-file-extensions',
52
61
  nargs='+',
53
62
  help=('File extensions that should be scanned'
@@ -89,10 +98,14 @@ def main():
89
98
  required=False,
90
99
  action='store_true')
91
100
  args = parser.parse_args()
92
- if args.path and (args.pattern_file or args.sgrep_pattern_file):
101
+ has_rules = bool(
102
+ args.pattern_file or args.ts_pattern_file or args.sgrep_pattern_file)
103
+ if args.path and has_rules:
93
104
  options = {
94
105
  'sgrep_rules': args.sgrep_pattern_file,
95
106
  'sgrep_extensions': args.sgrep_file_extensions,
107
+ 'ts_rules': args.ts_pattern_file,
108
+ 'ts_extensions': args.ts_file_extensions,
96
109
  'match_rules': args.pattern_file,
97
110
  'match_extensions': args.file_extensions,
98
111
  'ignore_filenames': args.ignore_filenames,
@@ -1,6 +1,5 @@
1
1
  # -*- coding: utf_8 -*-
2
2
  """Choice Matcher."""
3
- import re
4
3
  from pathlib import Path
5
4
  from concurrent.futures import (
6
5
  ProcessPoolExecutor,
@@ -13,6 +12,7 @@ from libsast.core_matcher.helpers import (
13
12
  is_file_valid,
14
13
  strip_comments,
15
14
  )
15
+ from libsast.core_matcher.matchers import get_compiled_pattern
16
16
  from libsast import (
17
17
  common,
18
18
  exceptions,
@@ -39,30 +39,51 @@ class ChoiceMatcher:
39
39
  file_contents = self.read_file_contents(paths)
40
40
  return self.regex_scan(file_contents)
41
41
 
42
+ def _load_formatted_paths(self, unique_paths):
43
+ """Read and format all unique candidate paths in parallel."""
44
+ path_list = list(unique_paths)
45
+ with ThreadPoolExecutor() as io_executor:
46
+ formatted_list = list(io_executor.map(
47
+ self._read_formatted_path, path_list))
48
+ return {
49
+ p: d for p, d in zip(path_list, formatted_list) if d is not None
50
+ }
51
+
42
52
  def read_file_contents(self, paths: list) -> list:
43
- """Load file(s) content."""
44
- if not paths:
53
+ """Load file(s) content once and pair with each choice rule."""
54
+ if not paths or not self.scan_rules:
45
55
  return []
46
56
 
47
- choice_args = []
57
+ alt_path = Path(self.alternative_path) if self.alternative_path else None
58
+ rule_paths = []
59
+ unique_paths = set()
48
60
  for rule in self.scan_rules:
49
- scan_paths = paths
50
- if rule['type'] != 'code' and self.alternative_path:
51
- # Scan only alternative path
52
- scan_paths = [Path(self.alternative_path)]
53
- choice_args.append((scan_paths, rule))
54
- if not choice_args:
55
- return []
56
-
57
- # Use ThreadPoolExecutor for file reading
58
- with ThreadPoolExecutor() as io_executor:
59
- # Submit file reading tasks and wait for results
60
- futures = []
61
- for args_tuple in choice_args:
62
- future = io_executor.submit(
63
- self._read_file_contents, args_tuple)
64
- futures.append(future)
65
- return [future.result() for future in futures]
61
+ scan_paths = [alt_path] if (
62
+ rule['type'] != 'code' and alt_path) else paths
63
+ rule_paths.append((scan_paths, rule))
64
+ unique_paths.update(scan_paths)
65
+
66
+ path_to_data = self._load_formatted_paths(unique_paths)
67
+ return [
68
+ [
69
+ (path_to_data[p], rule)
70
+ for p in scan_paths
71
+ if p in path_to_data
72
+ ]
73
+ for scan_paths, rule in rule_paths
74
+ ]
75
+
76
+ def _read_formatted_path(self, sfile):
77
+ """Read and strip comments for a single file once."""
78
+ if not is_file_valid(sfile, self.exts, 5):
79
+ return None
80
+ try:
81
+ return self._format_content(
82
+ sfile.read_text('utf-8', 'ignore'),
83
+ sfile.suffix.lower())
84
+ except Exception as e:
85
+ raise exceptions.RuleProcessingError(
86
+ f'Error reading file: {sfile}') from e
66
87
 
67
88
  def regex_scan(self, file_contents: list, rules=None) -> dict:
68
89
  """Process regex matches on the file contents."""
@@ -72,26 +93,25 @@ class ChoiceMatcher:
72
93
  return {}
73
94
  self.validate_rules()
74
95
 
96
+ chunksize = max(1, len(file_contents) // ((self.cpu or 1) * 4))
75
97
  if self.multiprocessing == 'billiard':
76
- # Use billiard's pool for regex (support queues)
77
98
  from billiard import Pool
78
99
  with Pool(processes=self.cpu) as pool:
79
- # Run regex on file data
80
100
  results = pool.map(
81
101
  self.choice_matcher,
82
- file_contents)
102
+ file_contents,
103
+ chunksize=chunksize)
83
104
  elif self.multiprocessing == 'thread':
84
- # Use a ThreadPool for regex check
85
105
  with ThreadPoolExecutor() as io_executor:
86
106
  results = list(io_executor.map(
87
107
  self.choice_matcher,
88
108
  file_contents))
89
109
  else:
90
- # Use ProcessPoolExecutor for regex processing
91
110
  with ProcessPoolExecutor(max_workers=self.cpu) as cpu_executor:
92
111
  results = list(cpu_executor.map(
93
112
  self.choice_matcher,
94
- file_contents))
113
+ file_contents,
114
+ chunksize=chunksize))
95
115
 
96
116
  self.add_finding(results)
97
117
  return self.findings
@@ -113,40 +133,21 @@ class ChoiceMatcher:
113
133
  raise exceptions.PatternKeyMissingError(
114
134
  f'The rule is missing the key "{key}"')
115
135
 
116
- def _read_file_contents(self, args_tuple):
117
- """Read file contents for the given paths and rule."""
118
- scan_paths, rule = args_tuple
119
- results = []
120
- for sfile in scan_paths:
121
- if not is_file_valid(sfile, self.exts, 5):
122
- continue
123
- try:
124
- data = self._format_content(
125
- sfile.read_text('utf-8', 'ignore'),
126
- sfile.suffix.lower())
127
- results.append((data, rule))
128
- except Exception as e:
129
- raise exceptions.RuleProcessingError(
130
- 'Error reading file: {}'.format(sfile)) from e
131
- return results
132
-
133
136
  def find_choices(self, data, rule):
134
- """Find Choices."""
137
+ """Find Choices using cached compiled patterns."""
135
138
  all_matches = set()
136
139
  for idx, choice in enumerate(rule['choice']):
137
140
  typ = rule['choice_type']
138
141
  if typ == 'and':
139
- # Return on first and choice
140
- if all(re.compile(and_regx).search(data) for and_regx in choice[0]):
142
+ if all(get_compiled_pattern(and_regx).search(data)
143
+ for and_regx in choice[0]):
141
144
  return (all_matches, [idx])
142
- elif re.compile(choice[0]).search(data):
145
+ elif get_compiled_pattern(choice[0]).search(data):
143
146
  if typ == 'or':
144
- # Return on first or choice
145
147
  return (all_matches, [idx])
146
148
  elif typ == 'all':
147
- # Extract all choice(s) from all files
148
149
  all_matches.add(choice[1])
149
- return (all_matches, None) # None means no matches found.
150
+ return (all_matches, None)
150
151
 
151
152
  def choice_matcher(self, file_contents):
152
153
  """Process regex matches on the file contents."""
@@ -175,7 +176,6 @@ class ChoiceMatcher:
175
176
  all_matches = match_dict['all_matches']
176
177
  matches = match_dict['matches']
177
178
 
178
- # Determine selection string
179
179
  if all_matches:
180
180
  selection = rule['selection'].format(list(all_matches))
181
181
  elif matches:
@@ -184,7 +184,6 @@ class ChoiceMatcher:
184
184
  else:
185
185
  selection = rule['selection'].format(rule.get('else', ''))
186
186
 
187
- # Create metadata dictionary
188
187
  meta_dict = {
189
188
  'choice': selection,
190
189
  'description': rule['message'],