argus-languages 0.1.1__py3-none-any.whl
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.
- argus_languages/__init__.py +15 -0
- argus_languages/bundled_rules/__init__.py +1 -0
- argus_languages/bundled_rules/ansible.yaml +42 -0
- argus_languages/bundled_rules/common.yaml +65 -0
- argus_languages/bundled_rules/dart.yaml +57 -0
- argus_languages/bundled_rules/flutter.yaml +58 -0
- argus_languages/bundled_rules/java.yaml +35 -0
- argus_languages/bundled_rules/other.yaml +111 -0
- argus_languages/bundled_rules/php.yaml +35 -0
- argus_languages/bundled_rules/terraform.yaml +59 -0
- argus_languages/cli.py +46 -0
- argus_languages/discover.py +176 -0
- argus_languages/models.py +69 -0
- argus_languages/rules_loader.py +79 -0
- argus_languages/scanner.py +109 -0
- argus_languages-0.1.1.dist-info/METADATA +63 -0
- argus_languages-0.1.1.dist-info/RECORD +20 -0
- argus_languages-0.1.1.dist-info/WHEEL +4 -0
- argus_languages-0.1.1.dist-info/entry_points.txt +2 -0
- argus_languages-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Built-in multi-language security pattern scanner."""
|
|
2
|
+
|
|
3
|
+
from argus_languages.models import Finding, ScanResult, Severity
|
|
4
|
+
from argus_languages.scanner import SUPPORTED_LANGUAGES, scan_directory, scan_path
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Finding",
|
|
8
|
+
"ScanResult",
|
|
9
|
+
"Severity",
|
|
10
|
+
"SUPPORTED_LANGUAGES",
|
|
11
|
+
"scan_directory",
|
|
12
|
+
"scan_path",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.1"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Bundled YAML security rules."""
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
- id: ansible-shell-command
|
|
2
|
+
title: Ansible shell/command module — prefer ansible.builtin.command
|
|
3
|
+
severity: moderate
|
|
4
|
+
pattern: 'ansible\.builtin\.(shell|raw):|^\s*(shell|raw):\s'
|
|
5
|
+
flags: [m]
|
|
6
|
+
languages: [ansible]
|
|
7
|
+
|
|
8
|
+
- id: ansible-no-become-password
|
|
9
|
+
title: Hardcoded become/sudo password
|
|
10
|
+
severity: high
|
|
11
|
+
pattern: '(ansible_become_pass|ansible_sudo_pass|become_pass):\s*["''][^"'']+["'']'
|
|
12
|
+
languages: [ansible]
|
|
13
|
+
|
|
14
|
+
- id: ansible-ssl-verify-off
|
|
15
|
+
title: SSL certificate verification disabled
|
|
16
|
+
severity: high
|
|
17
|
+
pattern: '(validate_certs:\s*false|verify:\s*false|insecure:\s*true)'
|
|
18
|
+
languages: [ansible]
|
|
19
|
+
|
|
20
|
+
- id: ansible-world-perms
|
|
21
|
+
title: File permissions too open (777 or 666)
|
|
22
|
+
severity: moderate
|
|
23
|
+
pattern: 'mode:\s*[''"]?(0777|666|0o777)'
|
|
24
|
+
languages: [ansible]
|
|
25
|
+
|
|
26
|
+
- id: ansible-hardcoded-secret
|
|
27
|
+
title: Hardcoded password or token in playbook
|
|
28
|
+
severity: high
|
|
29
|
+
pattern: '(password|api_key|token|secret):\s*["''][^"'']{4,}["'']'
|
|
30
|
+
languages: [ansible]
|
|
31
|
+
|
|
32
|
+
- id: ansible-unquoted-var-shell
|
|
33
|
+
title: Unquoted Jinja variable in shell context
|
|
34
|
+
severity: moderate
|
|
35
|
+
pattern: '(shell|command):\s*[^\n]*\{\{[^}]+\}\}'
|
|
36
|
+
languages: [ansible]
|
|
37
|
+
|
|
38
|
+
- id: ansible-git-insecure
|
|
39
|
+
title: Git clone with accept_hostkey or force
|
|
40
|
+
severity: moderate
|
|
41
|
+
pattern: '(accept_hostkey:\s*true|force:\s*true)'
|
|
42
|
+
languages: [ansible]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
- id: injection-eval
|
|
2
|
+
title: Dynamic code execution (eval/exec) — injection risk
|
|
3
|
+
severity: high
|
|
4
|
+
pattern: '\beval\s*\(|\bexec\s*\(|\bFunction\s*\(|Runtime\.getRuntime\(\)\.exec'
|
|
5
|
+
languages: [javascript, typescript, python, java, php, ruby, perl, lua, vue]
|
|
6
|
+
|
|
7
|
+
- id: sql-concat
|
|
8
|
+
title: Possible SQL injection — string concatenation in query
|
|
9
|
+
severity: high
|
|
10
|
+
pattern: '(SELECT|INSERT|UPDATE|DELETE|query\s*\+|f["''].*SELECT|Statement\.execute\s*\([^)]*\+)'
|
|
11
|
+
flags: [i]
|
|
12
|
+
languages: [javascript, typescript, python, java, php, ruby, csharp, go, kotlin, scala]
|
|
13
|
+
|
|
14
|
+
- id: command-injection
|
|
15
|
+
title: Possible command injection — shell execution
|
|
16
|
+
severity: high
|
|
17
|
+
pattern: '(os\.system|subprocess\.(call|Popen|run)|shell_exec|exec\s*\(|passthru|system\s*\(|ProcessBuilder|child_process\.exec|Runtime\.getRuntime|os/exec\.Command)'
|
|
18
|
+
languages: [javascript, typescript, python, java, php, ruby, go, csharp, kotlin, scala, perl, shell]
|
|
19
|
+
|
|
20
|
+
- id: weak-crypto
|
|
21
|
+
title: Weak cryptography (MD5/SHA1/DES/ECB)
|
|
22
|
+
severity: moderate
|
|
23
|
+
pattern: '(MD5|SHA1|DES|ECB|createHash\s*\(\s*["'']md5|MessageDigest\.getInstance\s*\(\s*["'']MD5|hash\.md5)'
|
|
24
|
+
flags: [i]
|
|
25
|
+
|
|
26
|
+
- id: hardcoded-password
|
|
27
|
+
title: Hardcoded password or secret assignment
|
|
28
|
+
severity: high
|
|
29
|
+
pattern: '(password\s*=\s*["''][^"'']{4,}["'']|passwd\s*=\s*["'']|api_key\s*=\s*["''][^"'']+["'']|secret_key\s*=\s*["''][^"'']+["''])'
|
|
30
|
+
flags: [i]
|
|
31
|
+
|
|
32
|
+
- id: deserialization
|
|
33
|
+
title: Unsafe deserialization
|
|
34
|
+
severity: high
|
|
35
|
+
pattern: '(pickle\.loads|yaml\.load\s*\(|unserialize\s*\(|ObjectInputStream|readObject\s*\()'
|
|
36
|
+
languages: [python, java, php, ruby, csharp, kotlin, scala, rust]
|
|
37
|
+
|
|
38
|
+
- id: ssrf-fetch
|
|
39
|
+
title: Possible SSRF — URL fetch may use user-controlled input
|
|
40
|
+
severity: moderate
|
|
41
|
+
pattern: '(fetch\s*\([^)]*\+|requests\.(get|post)\s*\([^)]*\+|HttpClient.*\+|file_get_contents\s*\(\s*\$|urllib\.request\.urlopen\s*\([^)]*\+)'
|
|
42
|
+
languages: [javascript, typescript, python, php, ruby, java, go]
|
|
43
|
+
|
|
44
|
+
- id: xss-innerhtml
|
|
45
|
+
title: Possible XSS — unsafe HTML insertion
|
|
46
|
+
severity: high
|
|
47
|
+
pattern: '(innerHTML\s*=|dangerouslySetInnerHTML|document\.write\s*\()'
|
|
48
|
+
languages: [javascript, typescript, vue, php]
|
|
49
|
+
|
|
50
|
+
- id: nosql-injection
|
|
51
|
+
title: Possible NoSQL injection
|
|
52
|
+
severity: high
|
|
53
|
+
pattern: '(\$where|\$regex.*\+|find\s*\(\s*\{[^}]*\$)'
|
|
54
|
+
languages: [javascript, typescript, python]
|
|
55
|
+
|
|
56
|
+
- id: cors-wildcard
|
|
57
|
+
title: CORS allows all origins (*)
|
|
58
|
+
severity: moderate
|
|
59
|
+
pattern: '(Access-Control-Allow-Origin[''"]\s*,\s*[''"]\*|cors\s*\(\s*\{[^}]*origin\s*:\s*[''"]\*)'
|
|
60
|
+
languages: [javascript, typescript]
|
|
61
|
+
|
|
62
|
+
- id: path-traversal
|
|
63
|
+
title: Possible path traversal — user input in file path
|
|
64
|
+
severity: moderate
|
|
65
|
+
pattern: '(open\s*\([^)]*\+|readFile\s*\([^)]*\+|include\s*\(\s*\$|require\s*\(\s*\$|new File\s*\([^)]*\+)'
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
- id: dart-cleartext-http
|
|
2
|
+
title: Cleartext HTTP URL — use HTTPS
|
|
3
|
+
severity: high
|
|
4
|
+
pattern: 'http://[^\s"'']+'
|
|
5
|
+
languages: [dart]
|
|
6
|
+
|
|
7
|
+
- id: dart-hardcoded-secret
|
|
8
|
+
title: Hardcoded API key or secret in Dart source
|
|
9
|
+
severity: high
|
|
10
|
+
pattern: '(apiKey|api_key|secretKey|secret_key|accessToken|password)\s*=\s*[''"][^''"]{8,}[''"]'
|
|
11
|
+
flags: [i]
|
|
12
|
+
languages: [dart]
|
|
13
|
+
|
|
14
|
+
- id: dart-weak-hash
|
|
15
|
+
title: Weak hashing (MD5/SHA-1) in Dart crypto
|
|
16
|
+
severity: moderate
|
|
17
|
+
pattern: '(Digest\s*\(\s*[''"]SHA-1|Digest\s*\(\s*[''"]MD5|MD5Digest|SHA1Digest|md5\.convert|sha1\.convert)'
|
|
18
|
+
flags: [i]
|
|
19
|
+
languages: [dart]
|
|
20
|
+
|
|
21
|
+
- id: dart-bad-cert-callback
|
|
22
|
+
title: TLS certificate validation disabled (badCertificateCallback)
|
|
23
|
+
severity: high
|
|
24
|
+
pattern: 'badCertificateCallback\s*=>|badCertificateCallback\s*\('
|
|
25
|
+
languages: [dart]
|
|
26
|
+
|
|
27
|
+
- id: dart-insecure-storage
|
|
28
|
+
title: Sensitive data in SharedPreferences — prefer flutter_secure_storage
|
|
29
|
+
severity: moderate
|
|
30
|
+
pattern: '(SharedPreferences.*\.(setString|setBool).*(password|token|secret|pin|apiKey))'
|
|
31
|
+
flags: [i]
|
|
32
|
+
languages: [dart]
|
|
33
|
+
|
|
34
|
+
- id: dart-webview-js
|
|
35
|
+
title: WebView JavaScript enabled — XSS risk if loading untrusted content
|
|
36
|
+
severity: moderate
|
|
37
|
+
pattern: 'javascriptMode:\s*JavascriptMode\.unrestricted'
|
|
38
|
+
languages: [dart]
|
|
39
|
+
|
|
40
|
+
- id: dart-print-sensitive
|
|
41
|
+
title: Possible sensitive data logged via print/debugPrint
|
|
42
|
+
severity: low
|
|
43
|
+
pattern: '(print|debugPrint)\s*\([^)]*(password|token|secret|apiKey|credential)'
|
|
44
|
+
flags: [i]
|
|
45
|
+
languages: [dart]
|
|
46
|
+
|
|
47
|
+
- id: dart-sql-concat
|
|
48
|
+
title: Possible SQL injection — string concatenation in query
|
|
49
|
+
severity: high
|
|
50
|
+
pattern: '(rawQuery|execute)\s*\(\s*[''"][^''"]*[''"]\s*\+'
|
|
51
|
+
languages: [dart]
|
|
52
|
+
|
|
53
|
+
- id: dart-eval
|
|
54
|
+
title: Dynamic evaluation — code injection risk
|
|
55
|
+
severity: high
|
|
56
|
+
pattern: '(Isolate\.spawnUri|Function\.apply\s*\([^)]*user|dart:mirrors)'
|
|
57
|
+
languages: [dart]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
- id: flutter-debug-dependency
|
|
2
|
+
title: Debug-only package in production dependencies
|
|
3
|
+
severity: moderate
|
|
4
|
+
pattern: '^\s*(flutter_test|integration_test|mockito|build_runner):'
|
|
5
|
+
flags: [m]
|
|
6
|
+
languages: [flutter]
|
|
7
|
+
|
|
8
|
+
- id: flutter-http-dependency
|
|
9
|
+
title: Plain http package — prefer https and certificate pinning for production
|
|
10
|
+
severity: low
|
|
11
|
+
pattern: '^\s*http:\s'
|
|
12
|
+
flags: [m]
|
|
13
|
+
languages: [flutter]
|
|
14
|
+
|
|
15
|
+
- id: flutter-android-debuggable
|
|
16
|
+
title: Android app debuggable in release — set android:debuggable=false for production
|
|
17
|
+
severity: high
|
|
18
|
+
pattern: 'android:debuggable\s*=\s*["'']true["'']'
|
|
19
|
+
languages: [flutter]
|
|
20
|
+
|
|
21
|
+
- id: flutter-android-backup
|
|
22
|
+
title: Android allowBackup enabled — may expose app data
|
|
23
|
+
severity: moderate
|
|
24
|
+
pattern: 'android:allowBackup\s*=\s*["'']true["'']'
|
|
25
|
+
languages: [flutter]
|
|
26
|
+
|
|
27
|
+
- id: flutter-android-cleartext
|
|
28
|
+
title: Android cleartext traffic allowed
|
|
29
|
+
severity: high
|
|
30
|
+
pattern: 'usesCleartextTraffic\s*=\s*["'']true["'']'
|
|
31
|
+
languages: [flutter]
|
|
32
|
+
|
|
33
|
+
- id: flutter-android-exported
|
|
34
|
+
title: Android component exported without permission — review attack surface
|
|
35
|
+
severity: moderate
|
|
36
|
+
pattern: 'android:exported\s*=\s*["'']true["'']'
|
|
37
|
+
languages: [flutter]
|
|
38
|
+
|
|
39
|
+
- id: flutter-ios-arbitrary-loads
|
|
40
|
+
title: iOS App Transport Security disabled (allows arbitrary loads)
|
|
41
|
+
severity: high
|
|
42
|
+
pattern: '<key>NSAllowsArbitraryLoads</key>\s*<true\s*/>'
|
|
43
|
+
flags: [i]
|
|
44
|
+
languages: [flutter]
|
|
45
|
+
|
|
46
|
+
- id: flutter-ios-file-sharing
|
|
47
|
+
title: iOS UIFileSharingEnabled — app documents exposed via iTunes
|
|
48
|
+
severity: moderate
|
|
49
|
+
pattern: '<key>UIFileSharingEnabled</key>\s*<true\s*/>'
|
|
50
|
+
flags: [i]
|
|
51
|
+
languages: [flutter]
|
|
52
|
+
|
|
53
|
+
- id: flutter-hardcoded-secret-pubspec
|
|
54
|
+
title: Possible secret in pubspec or config file
|
|
55
|
+
severity: high
|
|
56
|
+
pattern: '(api[_-]?key|secret|password|token)\s*:\s*[''"][^''"]{8,}[''"]'
|
|
57
|
+
flags: [i]
|
|
58
|
+
languages: [flutter]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
- id: java-sql-statement-concat
|
|
2
|
+
title: Java SQL Statement built via string concatenation
|
|
3
|
+
severity: high
|
|
4
|
+
pattern: '(Statement\.execute\s*\(|createStatement\s*\(\).*\+|PreparedStatement.*\+.*\+)'
|
|
5
|
+
languages: [java, kotlin, scala]
|
|
6
|
+
|
|
7
|
+
- id: java-xxe
|
|
8
|
+
title: Possible XXE — XML parser without secure features
|
|
9
|
+
severity: high
|
|
10
|
+
pattern: '(DocumentBuilderFactory\.newInstance|SAXParserFactory\.newInstance|XMLInputFactory\.newInstance)'
|
|
11
|
+
languages: [java, kotlin, scala]
|
|
12
|
+
|
|
13
|
+
- id: java-ldap-injection
|
|
14
|
+
title: Possible LDAP injection — concatenated filter
|
|
15
|
+
severity: high
|
|
16
|
+
pattern: '(search\s*\([^)]*\+|DirContext\.search\s*\([^)]*\+)'
|
|
17
|
+
languages: [java, kotlin]
|
|
18
|
+
|
|
19
|
+
- id: java-path-traversal
|
|
20
|
+
title: Possible path traversal — user input in file path
|
|
21
|
+
severity: moderate
|
|
22
|
+
pattern: '(Paths\.get\s*\([^)]*\+|Files\.(read|write).*\+)'
|
|
23
|
+
languages: [java, kotlin, scala]
|
|
24
|
+
|
|
25
|
+
- id: spring-csrf-disabled
|
|
26
|
+
title: Spring CSRF protection disabled
|
|
27
|
+
severity: moderate
|
|
28
|
+
pattern: '\.csrf\s*\(\s*\)\.disable\s*\(\)'
|
|
29
|
+
languages: [java, kotlin]
|
|
30
|
+
|
|
31
|
+
- id: java-log-injection
|
|
32
|
+
title: Possible log injection — user input in log statement
|
|
33
|
+
severity: low
|
|
34
|
+
pattern: '(logger\.(info|warn|error|debug)\s*\([^)]*\+.*request\.|log\.(info|warn|error)\s*\([^)]*\+)'
|
|
35
|
+
languages: [java, kotlin]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
- id: python-debug-enabled
|
|
2
|
+
title: Debug mode enabled
|
|
3
|
+
severity: low
|
|
4
|
+
pattern: '(DEBUG\s*=\s*True|app\.run\s*\([^)]*debug\s*=\s*True)'
|
|
5
|
+
flags: [i]
|
|
6
|
+
languages: [python]
|
|
7
|
+
|
|
8
|
+
- id: python-flask-secret
|
|
9
|
+
title: Hardcoded Flask secret key
|
|
10
|
+
severity: high
|
|
11
|
+
pattern: 'SECRET_KEY\s*=\s*["''][^"'']+["'']'
|
|
12
|
+
languages: [python]
|
|
13
|
+
|
|
14
|
+
- id: python-django-allowed-hosts
|
|
15
|
+
title: Django ALLOWED_HOSTS allows all (*)
|
|
16
|
+
severity: moderate
|
|
17
|
+
pattern: 'ALLOWED_HOSTS\s*=\s*\[[^\]]*[''"]\*[''"]'
|
|
18
|
+
languages: [python]
|
|
19
|
+
|
|
20
|
+
- id: docker-secrets-env
|
|
21
|
+
title: Secret passed via ENV in Dockerfile
|
|
22
|
+
severity: high
|
|
23
|
+
pattern: '^ENV\s+.*(PASSWORD|SECRET|API_KEY|TOKEN)='
|
|
24
|
+
flags: [im]
|
|
25
|
+
languages: [docker]
|
|
26
|
+
|
|
27
|
+
- id: docker-privileged
|
|
28
|
+
title: Docker Compose privileged mode enabled
|
|
29
|
+
severity: high
|
|
30
|
+
pattern: 'privileged:\s*true'
|
|
31
|
+
languages: [docker]
|
|
32
|
+
|
|
33
|
+
- id: docker-host-network
|
|
34
|
+
title: Docker Compose uses host network mode
|
|
35
|
+
severity: moderate
|
|
36
|
+
pattern: 'network_mode:\s*["'']?host["'']?'
|
|
37
|
+
languages: [docker]
|
|
38
|
+
|
|
39
|
+
- id: docker-socket-mount
|
|
40
|
+
title: Docker socket mounted — container escape risk
|
|
41
|
+
severity: high
|
|
42
|
+
pattern: '/var/run/docker\.sock'
|
|
43
|
+
languages: [docker]
|
|
44
|
+
|
|
45
|
+
- id: k8s-privileged-container
|
|
46
|
+
title: Kubernetes container runs in privileged mode
|
|
47
|
+
severity: high
|
|
48
|
+
pattern: 'privileged:\s*true'
|
|
49
|
+
languages: [kubernetes]
|
|
50
|
+
|
|
51
|
+
- id: k8s-run-as-root
|
|
52
|
+
title: Kubernetes pod runs as root (runAsUser 0)
|
|
53
|
+
severity: moderate
|
|
54
|
+
pattern: 'runAsUser:\s*0'
|
|
55
|
+
languages: [kubernetes]
|
|
56
|
+
|
|
57
|
+
- id: k8s-host-network
|
|
58
|
+
title: Pod uses hostNetwork
|
|
59
|
+
severity: high
|
|
60
|
+
pattern: 'hostNetwork:\s*true'
|
|
61
|
+
languages: [kubernetes]
|
|
62
|
+
|
|
63
|
+
- id: k8s-host-pid
|
|
64
|
+
title: Pod uses hostPID
|
|
65
|
+
severity: high
|
|
66
|
+
pattern: 'hostPID:\s*true'
|
|
67
|
+
languages: [kubernetes]
|
|
68
|
+
|
|
69
|
+
- id: go-insecure-tls
|
|
70
|
+
title: TLS InsecureSkipVerify enabled
|
|
71
|
+
severity: high
|
|
72
|
+
pattern: 'InsecureSkipVerify:\s*true'
|
|
73
|
+
languages: [go]
|
|
74
|
+
|
|
75
|
+
- id: go-sql-sprintf
|
|
76
|
+
title: SQL built with fmt.Sprintf — use parameterized queries
|
|
77
|
+
severity: high
|
|
78
|
+
pattern: '(fmt\.Sprintf\s*\(\s*["''].*(SELECT|INSERT|UPDATE|DELETE))'
|
|
79
|
+
flags: [i]
|
|
80
|
+
languages: [go]
|
|
81
|
+
|
|
82
|
+
- id: shell-curl-pipe-bash
|
|
83
|
+
title: curl/wget piped to shell — supply chain risk
|
|
84
|
+
severity: high
|
|
85
|
+
pattern: '(curl|wget)[^\n|]*\|\s*(bash|sh|zsh)'
|
|
86
|
+
languages: [shell]
|
|
87
|
+
|
|
88
|
+
- id: sql-grant-all
|
|
89
|
+
title: GRANT ALL privileges
|
|
90
|
+
severity: moderate
|
|
91
|
+
pattern: 'GRANT\s+ALL'
|
|
92
|
+
flags: [i]
|
|
93
|
+
languages: [sql]
|
|
94
|
+
|
|
95
|
+
- id: csharp-binary-formatter
|
|
96
|
+
title: BinaryFormatter deserialization — RCE risk
|
|
97
|
+
severity: high
|
|
98
|
+
pattern: 'BinaryFormatter'
|
|
99
|
+
languages: [csharp]
|
|
100
|
+
|
|
101
|
+
- id: rust-unsafe-block
|
|
102
|
+
title: Unsafe Rust block — review memory safety
|
|
103
|
+
severity: low
|
|
104
|
+
pattern: '\bunsafe\s*\{'
|
|
105
|
+
languages: [rust]
|
|
106
|
+
|
|
107
|
+
- id: elixir-eval
|
|
108
|
+
title: Elixir Code.eval_string — code injection risk
|
|
109
|
+
severity: high
|
|
110
|
+
pattern: 'Code\.eval_string'
|
|
111
|
+
languages: [elixir]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
- id: php-xss-echo
|
|
2
|
+
title: Possible XSS — unescaped output
|
|
3
|
+
severity: high
|
|
4
|
+
pattern: '(echo\s+\$_|print\s+\$_|<\?=\s*\$_)'
|
|
5
|
+
languages: [php]
|
|
6
|
+
|
|
7
|
+
- id: php-include-user-input
|
|
8
|
+
title: Possible LFI — include/require with variable
|
|
9
|
+
severity: high
|
|
10
|
+
pattern: '((include|require)(_once)?\s*\(\s*\$)'
|
|
11
|
+
languages: [php]
|
|
12
|
+
|
|
13
|
+
- id: php-sql-mysql
|
|
14
|
+
title: Possible SQL injection — mysql_query with variable
|
|
15
|
+
severity: high
|
|
16
|
+
pattern: '(mysql_query\s*\([^)]*\$|mysqli_query\s*\([^)]*\$)'
|
|
17
|
+
languages: [php]
|
|
18
|
+
|
|
19
|
+
- id: php-deserialize
|
|
20
|
+
title: Unsafe unserialize() on user input
|
|
21
|
+
severity: high
|
|
22
|
+
pattern: 'unserialize\s*\(\s*\$_'
|
|
23
|
+
languages: [php]
|
|
24
|
+
|
|
25
|
+
- id: php-dangerous-functions
|
|
26
|
+
title: Dangerous function — eval/assert/create_function
|
|
27
|
+
severity: high
|
|
28
|
+
pattern: '\b(assert\s*\(|create_function\s*\(|preg_replace\s*\([^)]*\/e)'
|
|
29
|
+
languages: [php]
|
|
30
|
+
|
|
31
|
+
- id: php-open-redirect
|
|
32
|
+
title: Possible open redirect — header Location with user input
|
|
33
|
+
severity: moderate
|
|
34
|
+
pattern: 'header\s*\(\s*["'']Location:.*\$_(GET|POST|REQUEST)'
|
|
35
|
+
languages: [php]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
- id: tf-public-s3-acl
|
|
2
|
+
title: S3 bucket ACL set to public-read or public-read-write
|
|
3
|
+
severity: high
|
|
4
|
+
pattern: 'acl\s*=\s*"(public-read|public-read-write|authenticated-read)"'
|
|
5
|
+
languages: [terraform]
|
|
6
|
+
|
|
7
|
+
- id: tf-s3-public-access-block-off
|
|
8
|
+
title: S3 public access block disabled
|
|
9
|
+
severity: high
|
|
10
|
+
pattern: 'block_public_(acls|policy)\s*=\s*false'
|
|
11
|
+
languages: [terraform]
|
|
12
|
+
|
|
13
|
+
- id: tf-open-security-group
|
|
14
|
+
title: Security group allows ingress from 0.0.0.0/0
|
|
15
|
+
severity: high
|
|
16
|
+
pattern: '(cidr_blocks\s*=\s*\[[^\]]*["'']0\.0\.0\.0\/0["'']|0\.0\.0\.0\/0)'
|
|
17
|
+
languages: [terraform]
|
|
18
|
+
|
|
19
|
+
- id: tf-unencrypted-ebs
|
|
20
|
+
title: EBS volume encryption disabled
|
|
21
|
+
severity: moderate
|
|
22
|
+
pattern: 'encrypted\s*=\s*false'
|
|
23
|
+
languages: [terraform]
|
|
24
|
+
|
|
25
|
+
- id: tf-rds-public
|
|
26
|
+
title: RDS instance publicly accessible
|
|
27
|
+
severity: high
|
|
28
|
+
pattern: 'publicly_accessible\s*=\s*true'
|
|
29
|
+
languages: [terraform]
|
|
30
|
+
|
|
31
|
+
- id: tf-hardcoded-secret
|
|
32
|
+
title: Hardcoded secret in Terraform resource
|
|
33
|
+
severity: high
|
|
34
|
+
pattern: '(password\s*=\s*"[^"]{4,}"|secret_key\s*=\s*"[^"]+"|access_key\s*=\s*"AKIA)'
|
|
35
|
+
languages: [terraform]
|
|
36
|
+
|
|
37
|
+
- id: tf-iam-wildcard
|
|
38
|
+
title: IAM policy allows wildcard actions or resources
|
|
39
|
+
severity: high
|
|
40
|
+
pattern: '(Action\s*=\s*"\*"|Resource\s*=\s*"\*")'
|
|
41
|
+
languages: [terraform]
|
|
42
|
+
|
|
43
|
+
- id: tf-http-backend
|
|
44
|
+
title: Terraform HTTP backend without TLS verification
|
|
45
|
+
severity: moderate
|
|
46
|
+
pattern: '(skip_tls_verification\s*=\s*true|address\s*=\s*"http:\/\/)'
|
|
47
|
+
languages: [terraform]
|
|
48
|
+
|
|
49
|
+
- id: tf-azure-storage-public
|
|
50
|
+
title: Azure storage allows public blob access
|
|
51
|
+
severity: high
|
|
52
|
+
pattern: 'allow_blob_public_access\s*=\s*true'
|
|
53
|
+
languages: [terraform]
|
|
54
|
+
|
|
55
|
+
- id: tf-gcp-public-bucket
|
|
56
|
+
title: GCP bucket with allUsers or allAuthenticatedUsers
|
|
57
|
+
severity: high
|
|
58
|
+
pattern: '(allUsers|allAuthenticatedUsers)'
|
|
59
|
+
languages: [terraform]
|
argus_languages/cli.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from argus_languages import __version__, scan_directory
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(argv: list[str] | None = None) -> int:
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
prog="argus-languages",
|
|
13
|
+
description="Built-in multi-language security pattern scanner (Java, PHP, Terraform, Ansible, …)",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument("--version", action="version", version=f"argus-languages {__version__}")
|
|
16
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
17
|
+
|
|
18
|
+
scan_p = sub.add_parser("scan", help="Scan a file or directory")
|
|
19
|
+
scan_p.add_argument("target", help="Path to scan")
|
|
20
|
+
scan_p.add_argument(
|
|
21
|
+
"--format", "-f", choices=["table", "json"], default="table", help="Output format"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
args = parser.parse_args(argv)
|
|
25
|
+
if args.command != "scan":
|
|
26
|
+
return 2
|
|
27
|
+
|
|
28
|
+
result = scan_directory(args.target)
|
|
29
|
+
if args.format == "json":
|
|
30
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
31
|
+
else:
|
|
32
|
+
if result.errors:
|
|
33
|
+
for err in result.errors:
|
|
34
|
+
print(f"Note: {err}", file=sys.stderr)
|
|
35
|
+
if not result.findings:
|
|
36
|
+
print("No findings.")
|
|
37
|
+
for f in result.findings:
|
|
38
|
+
sev = f.severity.value.upper()
|
|
39
|
+
loc = f"{f.file}:{f.line}" if f.file else "?"
|
|
40
|
+
print(f"[{sev}] {loc} — {f.title} ({f.rule_id})")
|
|
41
|
+
|
|
42
|
+
return 1 if result.findings else 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
SKIP_DIRS = {
|
|
8
|
+
"node_modules", ".git", "dist", "build", ".next", "coverage", "vendor",
|
|
9
|
+
"__pycache__", "target", "bin", "obj", ".venv", "venv", ".terraform",
|
|
10
|
+
".idea", ".vscode", "Pods", "DerivedData", ".dart_tool", ".pub-cache",
|
|
11
|
+
".gradle",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Flutter platform folders under android/ are scanned; skip build artifacts only
|
|
15
|
+
FLUTTER_SKIP_DIR_NAMES = {".gradle", "build", "Pods", "DerivedData"}
|
|
16
|
+
|
|
17
|
+
GENERATED_DART_SUFFIXES = (".g.dart", ".freezed.dart", ".gr.dart", ".mocks.dart")
|
|
18
|
+
|
|
19
|
+
LanguageId = str
|
|
20
|
+
|
|
21
|
+
EXT_MAP: dict[str, LanguageId] = {
|
|
22
|
+
".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
|
|
23
|
+
".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
|
|
24
|
+
".py": "python", ".pyw": "python",
|
|
25
|
+
".java": "java", ".jsp": "java",
|
|
26
|
+
".php": "php", ".phtml": "php",
|
|
27
|
+
".go": "go",
|
|
28
|
+
".rb": "ruby", ".erb": "ruby",
|
|
29
|
+
".cs": "csharp",
|
|
30
|
+
".rs": "rust",
|
|
31
|
+
".tf": "terraform", ".tfvars": "terraform", ".hcl": "terraform",
|
|
32
|
+
".sh": "shell", ".bash": "shell", ".zsh": "shell",
|
|
33
|
+
".sql": "sql",
|
|
34
|
+
".kt": "kotlin", ".kts": "kotlin",
|
|
35
|
+
".swift": "swift",
|
|
36
|
+
".scala": "scala",
|
|
37
|
+
".pl": "perl", ".pm": "perl",
|
|
38
|
+
".lua": "lua",
|
|
39
|
+
".ex": "elixir", ".exs": "elixir",
|
|
40
|
+
".vue": "vue",
|
|
41
|
+
".dart": "dart",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ANSIBLE_PATH_MARKERS = (
|
|
45
|
+
"/roles/", "/playbooks/", "/tasks/", "/handlers/", "/vars/", "/defaults/",
|
|
46
|
+
"/group_vars/", "/host_vars/", "/inventory/",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ScannedFile:
|
|
52
|
+
path: Path
|
|
53
|
+
relative: str
|
|
54
|
+
language: LanguageId
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_ansible_yaml(rel: str, content: str) -> bool:
|
|
58
|
+
lower = rel.lower()
|
|
59
|
+
if any(m in lower for m in ANSIBLE_PATH_MARKERS):
|
|
60
|
+
return True
|
|
61
|
+
if re.search(r"ansible\.builtin|ansible\.legacy|- hosts:|become:|gather_facts:", content):
|
|
62
|
+
return True
|
|
63
|
+
return "playbook" in Path(lower).name
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_kubernetes_yaml(rel: str, content: str) -> bool:
|
|
67
|
+
lower = rel.lower()
|
|
68
|
+
if any(p in lower for p in ("/k8s/", "/kubernetes/", "/manifests/")):
|
|
69
|
+
return True
|
|
70
|
+
return bool(re.search(r"^\s*apiVersion:", content, re.M) and re.search(r"^\s*kind:", content, re.M))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_flutter_pubspec(name: str, content: str) -> bool:
|
|
74
|
+
if name != "pubspec.yaml":
|
|
75
|
+
return False
|
|
76
|
+
return "dependencies:" in content or "flutter:" in content
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_generated_dart(name: str) -> bool:
|
|
80
|
+
lower = name.lower()
|
|
81
|
+
return any(lower.endswith(suffix) for suffix in GENERATED_DART_SUFFIXES)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def classify_file(path: Path, root: Path, content: str) -> LanguageId | None:
|
|
85
|
+
rel = str(path.relative_to(root))
|
|
86
|
+
name = path.name.lower()
|
|
87
|
+
|
|
88
|
+
if _is_generated_dart(name):
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
if name == "androidmanifest.xml":
|
|
92
|
+
return "flutter"
|
|
93
|
+
if name == "info.plist" and ("ios" in rel.lower() or "macos" in rel.lower() or "CFBundle" in content):
|
|
94
|
+
return "flutter"
|
|
95
|
+
if _is_flutter_pubspec(name, content):
|
|
96
|
+
return "flutter"
|
|
97
|
+
|
|
98
|
+
if name == "dockerfile" or name.endswith(".dockerfile"):
|
|
99
|
+
return "docker"
|
|
100
|
+
if name.startswith("docker-compose") and name.endswith((".yml", ".yaml")):
|
|
101
|
+
return "docker"
|
|
102
|
+
|
|
103
|
+
ext = path.suffix.lower()
|
|
104
|
+
if ext in (".yaml", ".yml"):
|
|
105
|
+
if _is_ansible_yaml(rel, content):
|
|
106
|
+
return "ansible"
|
|
107
|
+
if _is_kubernetes_yaml(rel, content):
|
|
108
|
+
return "kubernetes"
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
if ext == ".json" and _is_kubernetes_yaml(rel, content):
|
|
112
|
+
return "kubernetes"
|
|
113
|
+
|
|
114
|
+
return EXT_MAP.get(ext)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def discover_files(target: Path) -> list[ScannedFile]:
|
|
118
|
+
root = target if target.is_dir() else target.parent
|
|
119
|
+
out: list[ScannedFile] = []
|
|
120
|
+
|
|
121
|
+
def should_skip_dir(entry: Path, name: str) -> bool:
|
|
122
|
+
if name in SKIP_DIRS:
|
|
123
|
+
return True
|
|
124
|
+
if name.startswith("."):
|
|
125
|
+
return True
|
|
126
|
+
# Keep android/ but skip nested Gradle build dirs
|
|
127
|
+
if name in FLUTTER_SKIP_DIR_NAMES and "android" in str(entry).lower():
|
|
128
|
+
return True
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
def walk(directory: Path, depth: int) -> None:
|
|
132
|
+
if depth > 16:
|
|
133
|
+
return
|
|
134
|
+
try:
|
|
135
|
+
entries = list(directory.iterdir())
|
|
136
|
+
except OSError:
|
|
137
|
+
return
|
|
138
|
+
for entry in entries:
|
|
139
|
+
if should_skip_dir(entry, entry.name):
|
|
140
|
+
continue
|
|
141
|
+
if entry.is_dir():
|
|
142
|
+
walk(entry, depth + 1)
|
|
143
|
+
continue
|
|
144
|
+
try:
|
|
145
|
+
content = entry.read_text(encoding="utf-8", errors="replace")
|
|
146
|
+
except OSError:
|
|
147
|
+
continue
|
|
148
|
+
language = classify_file(entry, root, content)
|
|
149
|
+
if not language:
|
|
150
|
+
continue
|
|
151
|
+
out.append(
|
|
152
|
+
ScannedFile(
|
|
153
|
+
path=entry,
|
|
154
|
+
relative=str(entry.relative_to(root)),
|
|
155
|
+
language=language,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if target.is_file():
|
|
160
|
+
try:
|
|
161
|
+
content = target.read_text(encoding="utf-8", errors="replace")
|
|
162
|
+
except OSError:
|
|
163
|
+
return []
|
|
164
|
+
language = classify_file(target, root, content)
|
|
165
|
+
if language:
|
|
166
|
+
out.append(ScannedFile(path=target, relative=target.name, language=language))
|
|
167
|
+
else:
|
|
168
|
+
walk(target, 0)
|
|
169
|
+
|
|
170
|
+
return out
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
SUPPORTED_LANGUAGES = sorted(
|
|
174
|
+
set(EXT_MAP.values())
|
|
175
|
+
| {"terraform", "ansible", "docker", "kubernetes", "shell", "sql", "flutter", "dart"}
|
|
176
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Severity(str, Enum):
|
|
9
|
+
CRITICAL = "critical"
|
|
10
|
+
HIGH = "high"
|
|
11
|
+
MEDIUM = "medium"
|
|
12
|
+
MODERATE = "moderate"
|
|
13
|
+
LOW = "low"
|
|
14
|
+
INFO = "info"
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def normalize(cls, value: str) -> Severity:
|
|
18
|
+
v = value.lower()
|
|
19
|
+
if v == "moderate":
|
|
20
|
+
return cls.MEDIUM
|
|
21
|
+
try:
|
|
22
|
+
return cls(v)
|
|
23
|
+
except ValueError:
|
|
24
|
+
return cls.INFO
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Finding:
|
|
29
|
+
title: str
|
|
30
|
+
severity: Severity
|
|
31
|
+
tool: str
|
|
32
|
+
file: str = ""
|
|
33
|
+
line: int = 0
|
|
34
|
+
rule_id: str = ""
|
|
35
|
+
description: str = ""
|
|
36
|
+
language: str = ""
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict[str, Any]:
|
|
39
|
+
sev = self.severity.value
|
|
40
|
+
if sev == "medium":
|
|
41
|
+
sev = "moderate"
|
|
42
|
+
return {
|
|
43
|
+
"title": self.title,
|
|
44
|
+
"severity": sev,
|
|
45
|
+
"tool": self.tool,
|
|
46
|
+
"file": self.file,
|
|
47
|
+
"line": self.line,
|
|
48
|
+
"rule_id": self.rule_id,
|
|
49
|
+
"description": self.description,
|
|
50
|
+
"language": self.language,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ScanResult:
|
|
56
|
+
tool: str
|
|
57
|
+
target: str
|
|
58
|
+
findings: list[Finding] = field(default_factory=list)
|
|
59
|
+
errors: list[str] = field(default_factory=list)
|
|
60
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
return {
|
|
64
|
+
"tool": self.tool,
|
|
65
|
+
"target": self.target,
|
|
66
|
+
"findings": [f.to_dict() for f in self.findings],
|
|
67
|
+
"errors": self.errors,
|
|
68
|
+
"metadata": self.metadata,
|
|
69
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from importlib import resources
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from argus_languages.models import Severity
|
|
12
|
+
|
|
13
|
+
RULE_FILES = (
|
|
14
|
+
"common.yaml",
|
|
15
|
+
"java.yaml",
|
|
16
|
+
"php.yaml",
|
|
17
|
+
"terraform.yaml",
|
|
18
|
+
"ansible.yaml",
|
|
19
|
+
"dart.yaml",
|
|
20
|
+
"flutter.yaml",
|
|
21
|
+
"other.yaml",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class LoadedRule:
|
|
27
|
+
id: str
|
|
28
|
+
title: str
|
|
29
|
+
severity: Severity
|
|
30
|
+
pattern: re.Pattern[str]
|
|
31
|
+
languages: list[str] | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _compile_pattern(raw: str, flags: list[str] | None) -> re.Pattern[str]:
|
|
35
|
+
flag_bits = 0
|
|
36
|
+
for f in flags or []:
|
|
37
|
+
if f.lower() == "i":
|
|
38
|
+
flag_bits |= re.IGNORECASE
|
|
39
|
+
elif f.lower() == "m":
|
|
40
|
+
flag_bits |= re.MULTILINE
|
|
41
|
+
return re.compile(raw, flag_bits)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parse_yaml_rules(data: Any) -> list[LoadedRule]:
|
|
45
|
+
if not isinstance(data, list):
|
|
46
|
+
return []
|
|
47
|
+
out: list[LoadedRule] = []
|
|
48
|
+
for item in data:
|
|
49
|
+
if not isinstance(item, dict):
|
|
50
|
+
continue
|
|
51
|
+
out.append(
|
|
52
|
+
LoadedRule(
|
|
53
|
+
id=str(item["id"]),
|
|
54
|
+
title=str(item["title"]),
|
|
55
|
+
severity=Severity.normalize(str(item.get("severity", "info"))),
|
|
56
|
+
pattern=_compile_pattern(str(item["pattern"]), item.get("flags")),
|
|
57
|
+
languages=[str(x) for x in item["languages"]] if item.get("languages") else None,
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_rules_from_dir(rules_dir: Path | None = None) -> list[LoadedRule]:
|
|
64
|
+
rules: list[LoadedRule] = []
|
|
65
|
+
if rules_dir is not None:
|
|
66
|
+
for path in sorted(rules_dir.glob("*.y*ml")):
|
|
67
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
68
|
+
rules.extend(_parse_yaml_rules(data))
|
|
69
|
+
return rules
|
|
70
|
+
|
|
71
|
+
base = resources.files("argus_languages").joinpath("bundled_rules")
|
|
72
|
+
for name in RULE_FILES:
|
|
73
|
+
resource = base.joinpath(name)
|
|
74
|
+
try:
|
|
75
|
+
text = resource.read_text(encoding="utf-8")
|
|
76
|
+
except (FileNotFoundError, OSError, AttributeError):
|
|
77
|
+
continue
|
|
78
|
+
rules.extend(_parse_yaml_rules(yaml.safe_load(text)))
|
|
79
|
+
return rules
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from argus_languages.discover import SUPPORTED_LANGUAGES, discover_files
|
|
6
|
+
from argus_languages.models import Finding, ScanResult, Severity
|
|
7
|
+
from argus_languages.rules_loader import LoadedRule, load_rules_from_dir
|
|
8
|
+
|
|
9
|
+
TOOL_NAME = "argus-languages"
|
|
10
|
+
|
|
11
|
+
COMMENT_PREFIX: dict[str, tuple[str, ...]] = {
|
|
12
|
+
"javascript": ("//", "/*"),
|
|
13
|
+
"typescript": ("//", "/*"),
|
|
14
|
+
"vue": ("//", "/*"),
|
|
15
|
+
"python": ("#",),
|
|
16
|
+
"java": ("//", "/*"),
|
|
17
|
+
"kotlin": ("//", "/*"),
|
|
18
|
+
"scala": ("//", "/*"),
|
|
19
|
+
"php": ("//", "#", "/*"),
|
|
20
|
+
"go": ("//",),
|
|
21
|
+
"ruby": ("#",),
|
|
22
|
+
"csharp": ("//", "/*"),
|
|
23
|
+
"rust": ("//",),
|
|
24
|
+
"terraform": ("#", "//"),
|
|
25
|
+
"ansible": ("#",),
|
|
26
|
+
"docker": ("#",),
|
|
27
|
+
"kubernetes": ("#",),
|
|
28
|
+
"shell": ("#",),
|
|
29
|
+
"sql": ("--", "/*"),
|
|
30
|
+
"dart": ("//",),
|
|
31
|
+
"flutter": ("#", "//", "<!--"),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _skip_line(line: str, language: str) -> bool:
|
|
36
|
+
stripped = line.strip()
|
|
37
|
+
for prefix in COMMENT_PREFIX.get(language, ("//", "#")):
|
|
38
|
+
if stripped.startswith(prefix):
|
|
39
|
+
return True
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _rule_applies(rule: LoadedRule, language: str) -> bool:
|
|
44
|
+
if rule.languages is None:
|
|
45
|
+
return True
|
|
46
|
+
return language in rule.languages
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _scan_content(
|
|
50
|
+
relative: str,
|
|
51
|
+
language: str,
|
|
52
|
+
content: str,
|
|
53
|
+
rules: list[LoadedRule],
|
|
54
|
+
) -> list[Finding]:
|
|
55
|
+
findings: list[Finding] = []
|
|
56
|
+
for i, line in enumerate(content.splitlines(), start=1):
|
|
57
|
+
if _skip_line(line, language):
|
|
58
|
+
continue
|
|
59
|
+
for rule in rules:
|
|
60
|
+
if not _rule_applies(rule, language):
|
|
61
|
+
continue
|
|
62
|
+
if rule.pattern.search(line):
|
|
63
|
+
findings.append(
|
|
64
|
+
Finding(
|
|
65
|
+
title=rule.title,
|
|
66
|
+
severity=rule.severity,
|
|
67
|
+
tool=TOOL_NAME,
|
|
68
|
+
file=relative,
|
|
69
|
+
line=i,
|
|
70
|
+
rule_id=rule.id,
|
|
71
|
+
description=f"Language: {language}",
|
|
72
|
+
language=language,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
return findings
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def scan_path(target: str | Path, rules: list[LoadedRule] | None = None) -> ScanResult:
|
|
79
|
+
path = Path(target).resolve()
|
|
80
|
+
result = ScanResult(tool=TOOL_NAME, target=str(path))
|
|
81
|
+
if not path.exists():
|
|
82
|
+
result.errors.append(f"Target not found: {path}")
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
loaded = rules if rules is not None else load_rules_from_dir()
|
|
86
|
+
files = discover_files(path)
|
|
87
|
+
if not files:
|
|
88
|
+
result.errors.append(
|
|
89
|
+
f"No scannable files found. Supported: {', '.join(SUPPORTED_LANGUAGES)}"
|
|
90
|
+
)
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
lang_counts: dict[str, int] = {}
|
|
94
|
+
for scanned in files:
|
|
95
|
+
lang_counts[scanned.language] = lang_counts.get(scanned.language, 0) + 1
|
|
96
|
+
try:
|
|
97
|
+
content = scanned.path.read_text(encoding="utf-8", errors="replace")
|
|
98
|
+
except OSError:
|
|
99
|
+
continue
|
|
100
|
+
result.findings.extend(_scan_content(scanned.relative, scanned.language, content, loaded))
|
|
101
|
+
|
|
102
|
+
result.metadata["files_scanned"] = len(files)
|
|
103
|
+
result.metadata["languages"] = lang_counts
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def scan_directory(target: str | Path) -> ScanResult:
|
|
108
|
+
"""Scan a directory or file for security patterns across all supported languages."""
|
|
109
|
+
return scan_path(target)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: argus-languages
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Built-in multi-language security pattern scanner — Java, PHP, Terraform, Ansible, and 15+ languages. No external tools required.
|
|
5
|
+
Project-URL: Homepage, https://github.com/OkiriGabriel/argus-codescan-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/OkiriGabriel/argus-codescan-mcp
|
|
7
|
+
Project-URL: Documentation, https://github.com/OkiriGabriel/argus-codescan-mcp/tree/main/packages/languages
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ansible,argus,code-scanning,devsecops,iac,java,php,sast,security,terraform
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: pyyaml>=6.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# argus-languages
|
|
21
|
+
|
|
22
|
+
Built-in security pattern scanner for **all major languages and IaC** — pure Python, no external tools.
|
|
23
|
+
|
|
24
|
+
Install on its own or as part of `argus-scan`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install argus-languages
|
|
28
|
+
argus-languages scan /path/to/project
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or via the full Argus CLI:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install argus-scan
|
|
35
|
+
argus scan code /path/to/project
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Supported languages
|
|
39
|
+
|
|
40
|
+
| Category | Languages / formats |
|
|
41
|
+
|----------|---------------------|
|
|
42
|
+
| **Web & app** | JavaScript, TypeScript, Python, Java, Kotlin, PHP, Go, Ruby, C#, Rust, Swift, Scala, Perl, Lua, Elixir, Vue, **Dart / Flutter** |
|
|
43
|
+
| **Mobile (Flutter)** | `.dart` source, `pubspec.yaml`, `AndroidManifest.xml`, `Info.plist` |
|
|
44
|
+
| **Infrastructure** | Terraform (`.tf`, `.hcl`), Ansible playbooks, Docker, Kubernetes manifests |
|
|
45
|
+
| **Shell & SQL** | Bash/Shell scripts, SQL |
|
|
46
|
+
|
|
47
|
+
Rules live in `src/argus_languages/bundled_rules/` as YAML so they can be shared across Python (and other packages later).
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from argus_languages import scan_directory
|
|
53
|
+
|
|
54
|
+
result = scan_directory("/path/to/repo")
|
|
55
|
+
for finding in result.findings:
|
|
56
|
+
print(finding.file, finding.line, finding.title)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## npm vs Python
|
|
60
|
+
|
|
61
|
+
- **`packages/npm`** — Node.js only (JS/TS SCA + eslint-security)
|
|
62
|
+
- **`packages/languages`** — all other languages (install via pip)
|
|
63
|
+
- **`packages/python`** — full Argus CLI/MCP; depends on `argus-languages`
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
argus_languages/__init__.py,sha256=XUmtPIIwj50HHoHpDg3c1T18EZNwbnQEM_MdIq0htxU,358
|
|
2
|
+
argus_languages/cli.py,sha256=TILmOlWxaFE0vgFlxeVbhWcHQb7BXnwkg0wfg0Z_eJM,1476
|
|
3
|
+
argus_languages/discover.py,sha256=4TeNyZmISwQSeq4zZb88uHm8SDccI4PMA_MiEw6ARLY,5465
|
|
4
|
+
argus_languages/models.py,sha256=ylcnePcvxWJhpK2YYe17X8a4U649SblzltbUJKpa5Mw,1646
|
|
5
|
+
argus_languages/rules_loader.py,sha256=2By8TgXxMgQRT1F-yQD4qYbde8e7A1UNzWBf0lIiZUM,2179
|
|
6
|
+
argus_languages/scanner.py,sha256=Yhj_UZu6zCrkz16LJWRGJ52etriK3-VMqlwzY6caR2k,3360
|
|
7
|
+
argus_languages/bundled_rules/__init__.py,sha256=RD0dhAfuvM2EbjeLQR7-l-4iQCdLY114d6LUe_UrTrM,35
|
|
8
|
+
argus_languages/bundled_rules/ansible.yaml,sha256=K7D__N3rOAICRxdFccLCI7rqj2q_W9BdN7e2j0X88k4,1315
|
|
9
|
+
argus_languages/bundled_rules/common.yaml,sha256=sFB3wl6utEpXce1if68Ypf9Cg2ZYijkBB6wqjoDa6Hc,2763
|
|
10
|
+
argus_languages/bundled_rules/dart.yaml,sha256=nrSsO054K_Svyh8-zII1lDlZs5ebqvAiZTbvL3Ee2L0,1854
|
|
11
|
+
argus_languages/bundled_rules/flutter.yaml,sha256=JF9C0tzo9uIZIPTX5613BFBqCDcHUl4HEpnPX-WtR8U,1848
|
|
12
|
+
argus_languages/bundled_rules/java.yaml,sha256=BWbBEX6WPBM8kspemHTw3ZRAaTi-vwqJm1O-juZHGXk,1277
|
|
13
|
+
argus_languages/bundled_rules/other.yaml,sha256=CSlFMVznyf29PgcbQZikoex5Nr0d_r7aUwNPISsYFf4,2788
|
|
14
|
+
argus_languages/bundled_rules/php.yaml,sha256=ONhzScgCOM_zqS5VkA7OZG1OW7dQ3SWxK0kqPPzp5ds,1043
|
|
15
|
+
argus_languages/bundled_rules/terraform.yaml,sha256=CWin5-bZfRaKLcYtU3-Wj98WMBbb4D3l6YomF3h9rZE,1803
|
|
16
|
+
argus_languages-0.1.1.dist-info/METADATA,sha256=W0S0QgVG7L7eRH76us4Fslv7-vKxsgqXg9ObRy9HYec,2270
|
|
17
|
+
argus_languages-0.1.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
18
|
+
argus_languages-0.1.1.dist-info/entry_points.txt,sha256=hBUcxS7N6C3wvKwRADcWdjQ5NUQ8YTiLifp27QbdTYc,61
|
|
19
|
+
argus_languages-0.1.1.dist-info/licenses/LICENSE,sha256=wQSjNH1sdVrIqz0TffGgFIhEqTtvJijBY3mLn9wFQ6Q,1085
|
|
20
|
+
argus_languages-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 codetesting-mcp contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|