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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ argus-languages = argus_languages.cli:main
@@ -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.