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.
- {libsast-3.1.9 → libsast-3.2.0}/PKG-INFO +79 -18
- {libsast-3.1.9 → libsast-3.2.0}/README.md +64 -15
- {libsast-3.1.9 → libsast-3.2.0}/libsast/__init__.py +3 -1
- {libsast-3.1.9 → libsast-3.2.0}/libsast/__main__.py +15 -2
- {libsast-3.1.9 → libsast-3.2.0}/libsast/core_matcher/choice_matcher.py +52 -53
- {libsast-3.1.9 → libsast-3.2.0}/libsast/core_matcher/helpers.py +40 -36
- {libsast-3.1.9 → libsast-3.2.0}/libsast/core_matcher/matchers.py +21 -17
- {libsast-3.1.9 → libsast-3.2.0}/libsast/core_matcher/pattern_matcher.py +60 -29
- {libsast-3.1.9 → libsast-3.2.0}/libsast/core_sgrep/helpers.py +3 -0
- libsast-3.2.0/libsast/core_treesitter/__init__.py +5 -0
- libsast-3.2.0/libsast/core_treesitter/languages.py +160 -0
- libsast-3.2.0/libsast/core_treesitter/matchers.py +469 -0
- libsast-3.2.0/libsast/core_treesitter/ts_matcher.py +223 -0
- {libsast-3.1.9 → libsast-3.2.0}/libsast/scanner.py +15 -3
- {libsast-3.1.9 → libsast-3.2.0}/libsast/standards.py +2 -0
- libsast-3.2.0/pyproject.toml +67 -0
- libsast-3.1.9/pyproject.toml +0 -43
- {libsast-3.1.9 → libsast-3.2.0}/LICENSE +0 -0
- {libsast-3.1.9 → libsast-3.2.0}/libsast/common.py +0 -0
- {libsast-3.1.9 → libsast-3.2.0}/libsast/core_matcher/__init__.py +0 -0
- {libsast-3.1.9 → libsast-3.2.0}/libsast/core_sgrep/__init__.py +0 -0
- {libsast-3.1.9 → libsast-3.2.0}/libsast/core_sgrep/semantic_sgrep.py +0 -0
- {libsast-3.1.9 → libsast-3.2.0}/libsast/exceptions.py +0 -0
- {libsast-3.1.9 → libsast-3.2.0}/libsast/standards/cwe.yaml +0 -0
- {libsast-3.1.9 → libsast-3.2.0}/libsast/standards/owasp_masvs.yaml +0 -0
- {libsast-3.1.9 → libsast-3.2.0}/libsast/standards/owasp_mobile_top10_2016.yaml +0 -0
- {libsast-3.1.9 → libsast-3.2.0}/libsast/standards/owasp_mobile_top10_2024.yaml +0 -0
- {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.
|
|
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  in India [](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 :
|
|
71
|
+
pip install semgrep==1.172.0
|
|
53
72
|
```
|
|
54
73
|
|
|
55
|
-
Pattern Matcher
|
|
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] [-
|
|
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'}, '
|
|
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
|
|
338
|
+
## Write your own Static Analysis tool
|
|
314
339
|
|
|
315
|
-
With libsast, you can write your own static analysis tools. libsast provides
|
|
340
|
+
With libsast, you can write your own static analysis tools. libsast provides three matching engines:
|
|
316
341
|
|
|
317
|
-
1. **Pattern Matcher**
|
|
318
|
-
2. **
|
|
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
|
-
|
|
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
|
|
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  in India [](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 :
|
|
29
|
+
pip install semgrep==1.172.0
|
|
23
30
|
```
|
|
24
31
|
|
|
25
|
-
Pattern Matcher
|
|
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] [-
|
|
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'}, '
|
|
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
|
|
296
|
+
## Write your own Static Analysis tool
|
|
284
297
|
|
|
285
|
-
With libsast, you can write your own static analysis tools. libsast provides
|
|
298
|
+
With libsast, you can write your own static analysis tools. libsast provides three matching engines:
|
|
286
299
|
|
|
287
|
-
1. **Pattern Matcher**
|
|
288
|
-
2. **
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
for
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
|
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)
|
|
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'],
|