gitrupt 0.1.0__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.
Files changed (48) hide show
  1. gitrupt/__init__.py +13 -0
  2. gitrupt/cli.py +546 -0
  3. gitrupt/config.py +269 -0
  4. gitrupt/git.py +590 -0
  5. gitrupt/hooks/__init__.py +7 -0
  6. gitrupt/hooks/install.py +255 -0
  7. gitrupt/hooks/pre_commit.py +103 -0
  8. gitrupt/hooks/pre_push.py +178 -0
  9. gitrupt/models.py +190 -0
  10. gitrupt/policy.py +36 -0
  11. gitrupt/reporting.py +316 -0
  12. gitrupt/risk.py +197 -0
  13. gitrupt/scanner.py +117 -0
  14. gitrupt/scanners/__init__.py +17 -0
  15. gitrupt/scanners/adapters.py +166 -0
  16. gitrupt/scanners/base.py +113 -0
  17. gitrupt/scanners/binaries.py +185 -0
  18. gitrupt/scanners/code_rules/__init__.py +36 -0
  19. gitrupt/scanners/code_rules/base.py +27 -0
  20. gitrupt/scanners/code_rules/go.py +65 -0
  21. gitrupt/scanners/code_rules/javascript.py +106 -0
  22. gitrupt/scanners/code_rules/php.py +71 -0
  23. gitrupt/scanners/code_rules/powershell.py +85 -0
  24. gitrupt/scanners/code_rules/python.py +153 -0
  25. gitrupt/scanners/code_rules/ruby.py +76 -0
  26. gitrupt/scanners/code_rules/rust.py +41 -0
  27. gitrupt/scanners/code_rules/shell.py +112 -0
  28. gitrupt/scanners/dependencies.py +244 -0
  29. gitrupt/scanners/ecosystems/__init__.py +30 -0
  30. gitrupt/scanners/ecosystems/base.py +60 -0
  31. gitrupt/scanners/ecosystems/node.py +128 -0
  32. gitrupt/scanners/ecosystems/python.py +157 -0
  33. gitrupt/scanners/entropy.py +123 -0
  34. gitrupt/scanners/forbidden_files.py +201 -0
  35. gitrupt/scanners/malware.py +219 -0
  36. gitrupt/scanners/osv_client.py +221 -0
  37. gitrupt/scanners/registry.py +66 -0
  38. gitrupt/scanners/secret_rules.py +368 -0
  39. gitrupt/scanners/secrets.py +558 -0
  40. gitrupt/scanners/suspicious_code.py +208 -0
  41. gitrupt/scanners/yara_loader.py +65 -0
  42. gitrupt/scanners/yara_rules_builtin.py +141 -0
  43. gitrupt-0.1.0.dist-info/METADATA +342 -0
  44. gitrupt-0.1.0.dist-info/RECORD +48 -0
  45. gitrupt-0.1.0.dist-info/WHEEL +5 -0
  46. gitrupt-0.1.0.dist-info/entry_points.txt +2 -0
  47. gitrupt-0.1.0.dist-info/licenses/LICENSE +23 -0
  48. gitrupt-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,185 @@
1
+ """
2
+ Binary heuristic analysis.
3
+
4
+ Detects:
5
+ - Executables (PE, ELF, Mach-O) in unexpected file extensions
6
+ - Known packer section names
7
+ - Encrypted / packed section patterns
8
+
9
+ Never executes any file. Pure byte inspection.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import math
15
+ from collections import Counter
16
+
17
+ from gitrupt.models import Finding, Severity
18
+
19
+ # ── File format magic bytes ─────────────────────────────────────────────────
20
+
21
+ PE_MAGIC = b"MZ"
22
+ ELF_MAGIC = b"\x7fELF"
23
+ MACHO_MAGICS = (
24
+ b"\xfe\xed\xfa\xce", # 32-bit big-endian
25
+ b"\xfe\xed\xfa\xcf", # 64-bit big-endian
26
+ b"\xce\xfa\xed\xfe", # 32-bit little-endian
27
+ b"\xcf\xfa\xed\xfe", # 64-bit little-endian
28
+ b"\xca\xfe\xba\xbe", # Universal / fat binary
29
+ )
30
+
31
+ # Extensions where an executable is *expected* and not suspicious by itself.
32
+ _EXECUTABLE_EXTENSIONS = frozenset({
33
+ ".exe", ".dll", ".sys", ".scr", ".com", ".msi", ".cpl", ".ocx",
34
+ ".so", ".dylib", ".bundle", ".ko", ".o", ".a",
35
+ ".elf", ".bin", ".out",
36
+ })
37
+
38
+ # Extensions that should NEVER contain a native executable.
39
+ # Presence of PE/ELF/Mach-O magic in these is highly suspicious.
40
+ _NEVER_EXECUTABLE_EXTENSIONS = frozenset({
41
+ ".txt", ".md", ".json", ".yaml", ".yml", ".toml", ".ini", ".cfg",
42
+ ".csv", ".tsv", ".log", ".xml", ".html", ".htm", ".css", ".js",
43
+ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".svg", ".webp",
44
+ ".mp3", ".mp4", ".wav", ".avi", ".mov", ".mkv",
45
+ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
46
+ })
47
+
48
+ # Packer section names — case-insensitive substring match.
49
+ _PACKER_SECTION_MARKERS = (
50
+ b"UPX0", b"UPX1", b"UPX2",
51
+ b".MPRESS1", b".MPRESS2",
52
+ b".aspack", b".adata",
53
+ b".petite",
54
+ b"pebundle",
55
+ b"FSG!",
56
+ b"PECompact2",
57
+ b".nsp0", b".nsp1", b".nsp2",
58
+ )
59
+
60
+ # How many bytes to sample for entropy.
61
+ _ENTROPY_SAMPLE = 4096
62
+
63
+
64
+ def detect_executable_format(data: bytes) -> str | None:
65
+ """Return 'pe', 'elf', 'macho', or None."""
66
+ if len(data) < 4:
67
+ return None
68
+ if data[:2] == PE_MAGIC:
69
+ # Confirm with the PE\0\0 header located via the DOS e_lfanew pointer.
70
+ # The header (4 bytes) must fit entirely inside the buffer:
71
+ # pe_off + 4 <= len(data) ⇔ pe_off <= len(data) - 4
72
+ if len(data) >= 0x40:
73
+ pe_off = int.from_bytes(data[0x3C:0x40], "little")
74
+ if 0 < pe_off <= len(data) - 4 and data[pe_off : pe_off + 4] == b"PE\x00\x00":
75
+ return "pe"
76
+ # MZ alone is weak; only treat as PE if header confirmed.
77
+ return None
78
+ if data[:4] == ELF_MAGIC:
79
+ return "elf"
80
+ if data[:4] in MACHO_MAGICS:
81
+ return "macho"
82
+ return None
83
+
84
+
85
+ def shannon_entropy(data: bytes) -> float:
86
+ """Byte-level Shannon entropy (0.0 – 8.0)."""
87
+ if not data:
88
+ return 0.0
89
+ counts = Counter(data)
90
+ total = len(data)
91
+ return -sum((c / total) * math.log2(c / total) for c in counts.values())
92
+
93
+
94
+ def find_packer_marker(data: bytes) -> str | None:
95
+ """Return the first packer marker found in the sample, or None."""
96
+ sample = data[:_ENTROPY_SAMPLE * 4]
97
+ for marker in _PACKER_SECTION_MARKERS:
98
+ if marker in sample:
99
+ return marker.decode("ascii", errors="replace")
100
+ return None
101
+
102
+
103
+ def scan_binary_heuristics(path: str, data: bytes) -> list[Finding]:
104
+ """
105
+ Analyze one file's bytes and return findings.
106
+
107
+ Never raises. Never executes.
108
+ """
109
+ findings: list[Finding] = []
110
+ if not data:
111
+ return findings
112
+
113
+ lowered = path.lower()
114
+ suffix = "." + lowered.rsplit(".", 1)[-1] if "." in lowered.rsplit("/", 1)[-1] else ""
115
+
116
+ # 1. Executable format in a non-executable extension
117
+ fmt = detect_executable_format(data)
118
+ if fmt and suffix and suffix not in _EXECUTABLE_EXTENSIONS:
119
+ severity = Severity.CRITICAL if suffix in _NEVER_EXECUTABLE_EXTENSIONS else Severity.HIGH
120
+ findings.append(
121
+ Finding(
122
+ scanner="malware",
123
+ rule_id=f"bin-{fmt}-wrong-extension",
124
+ severity=severity,
125
+ confidence=0.9,
126
+ file=path,
127
+ line=None,
128
+ message=f"{fmt.upper()} executable with '{suffix}' extension",
129
+ description=(
130
+ f"File is a native {fmt.upper()} binary but has extension '{suffix}'. "
131
+ "This is a common disguise technique."
132
+ ),
133
+ evidence=f"magic={data[:4].hex()}",
134
+ recommendation="Verify the file's origin; do not commit executables under false extensions.",
135
+ can_override=False,
136
+ )
137
+ )
138
+
139
+ # 2. Packer signature
140
+ marker = find_packer_marker(data)
141
+ if marker:
142
+ findings.append(
143
+ Finding(
144
+ scanner="malware",
145
+ rule_id="bin-packer-signature",
146
+ severity=Severity.HIGH,
147
+ confidence=0.85,
148
+ file=path,
149
+ line=None,
150
+ message=f"Packer signature detected ({marker})",
151
+ description=(
152
+ "The file contains a known executable packer signature. "
153
+ "Packers are commonly used to hide malicious payloads."
154
+ ),
155
+ evidence=f"marker={marker}",
156
+ recommendation="Inspect the file's origin; unpack and review if untrusted.",
157
+ can_override=False,
158
+ )
159
+ )
160
+
161
+ # 3. High entropy on an executable — likely encrypted/packed payload
162
+ if fmt:
163
+ sample = data[:_ENTROPY_SAMPLE]
164
+ entropy = shannon_entropy(sample)
165
+ if entropy > 7.2:
166
+ findings.append(
167
+ Finding(
168
+ scanner="malware",
169
+ rule_id="bin-high-entropy-section",
170
+ severity=Severity.MEDIUM,
171
+ confidence=0.6,
172
+ file=path,
173
+ line=None,
174
+ message=f"Very high entropy in {fmt.upper()} file ({entropy:.2f}/8.0)",
175
+ description=(
176
+ "Entropy above 7.2 suggests encrypted or compressed content. "
177
+ "Legitimate, unpacked executables normally fall below this."
178
+ ),
179
+ evidence=f"entropy={entropy:.2f}",
180
+ recommendation="If this file is unexpected, do not commit it.",
181
+ can_override=True,
182
+ )
183
+ )
184
+
185
+ return findings
@@ -0,0 +1,36 @@
1
+ """
2
+ Registry of suspicious-code rules.
3
+
4
+ Adding a language:
5
+ 1. Create a new module (e.g. rust.py) that exports a RULES list.
6
+ 2. Import it below and add it to _ALL_RULES.
7
+ 3. Done — the scanner picks it up automatically.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from gitrupt.scanners.code_rules.base import CodeRule
13
+ from gitrupt.scanners.code_rules.go import RULES as _GO
14
+ from gitrupt.scanners.code_rules.javascript import RULES as _JS
15
+ from gitrupt.scanners.code_rules.php import RULES as _PHP
16
+ from gitrupt.scanners.code_rules.powershell import RULES as _PS
17
+ from gitrupt.scanners.code_rules.python import RULES as _PY
18
+ from gitrupt.scanners.code_rules.ruby import RULES as _RB
19
+ from gitrupt.scanners.code_rules.shell import RULES as _SH
20
+ from gitrupt.scanners.code_rules.rust import RULES as _RS
21
+
22
+ _ALL_RULES: tuple[CodeRule, ...] = (
23
+ _PY + _JS + _SH + _PS + _PHP + _RB + _GO + _RS
24
+ )
25
+
26
+
27
+ def get_all_rules() -> tuple[CodeRule, ...]:
28
+ return _ALL_RULES
29
+
30
+
31
+ def get_rules_for_language(language: str) -> tuple[CodeRule, ...]:
32
+ return tuple(r for r in _ALL_RULES if r.language == language)
33
+
34
+
35
+ def supported_languages() -> tuple[str, ...]:
36
+ return tuple(sorted({r.language for r in _ALL_RULES}))
@@ -0,0 +1,27 @@
1
+ """Base model for language-specific suspicious-code rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+
8
+ from gitrupt.models import Severity
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class CodeRule:
13
+ """
14
+ A single suspicious-code rule.
15
+
16
+ Rules are data, not code. Adding a language = adding a module of CodeRule
17
+ objects. The scanner never needs to change.
18
+ """
19
+
20
+ rule_id: str
21
+ language: str # "python", "javascript", "shell", ...
22
+ pattern: re.Pattern # compiled regex, matched against a single added line
23
+ severity: Severity
24
+ confidence: float # 0.0 .. 1.0
25
+ message: str # short, shown next to file:line
26
+ description: str = "" # longer explanation
27
+ recommendation: str = ""
@@ -0,0 +1,65 @@
1
+ """Suspicious-code rules for Go."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from gitrupt.models import Severity
8
+ from gitrupt.scanners.code_rules.base import CodeRule
9
+
10
+ RULES: tuple[CodeRule, ...] = (
11
+ CodeRule(
12
+ rule_id="go-os-exec",
13
+ language="go",
14
+ pattern=re.compile(r"\bexec\.Command(?:Context)?\s*\("),
15
+ severity=Severity.LOW,
16
+ confidence=0.5,
17
+ message="os/exec Command call",
18
+ description="Executes an external process.",
19
+ recommendation="Pass an argument list — never build the command from untrusted strings.",
20
+ ),
21
+ CodeRule(
22
+ rule_id="go-shell-true-equivalent",
23
+ language="go",
24
+ pattern=re.compile(r"exec\.Command\s*\(\s*\"(?:sh|bash|cmd|powershell)\""),
25
+ severity=Severity.MEDIUM,
26
+ confidence=0.7,
27
+ message="Shell invocation via exec.Command",
28
+ description="Executing a shell interpreter re-exposes shell injection.",
29
+ recommendation="Call the target program directly instead of through a shell.",
30
+ ),
31
+ CodeRule(
32
+ rule_id="go-base64-decode",
33
+ language="go",
34
+ pattern=re.compile(r"base64\.StdEncoding\.DecodeString\s*\("),
35
+ severity=Severity.LOW,
36
+ confidence=0.5,
37
+ message="base64 decode in source",
38
+ description="Encoded payloads may indicate obfuscation.",
39
+ recommendation="Inspect what the decoded bytes are used for.",
40
+ ),
41
+
42
+
43
+
44
+
45
+ CodeRule(
46
+ rule_id="go-reverse-shell",
47
+ language="go",
48
+ pattern=re.compile(r"net\.Dial\s*\([^)]*\)[^\n]*exec\.Command"),
49
+ severity=Severity.CRITICAL,
50
+ confidence=0.85,
51
+ message="Go reverse shell pattern",
52
+ description="Outbound connection followed by exec.Command.",
53
+ recommendation="Audit carefully. This is a backdoor shape.",
54
+ ),
55
+ CodeRule(
56
+ rule_id="go-exec-sh",
57
+ language="go",
58
+ pattern=re.compile(r"exec\.Command\s*\(\s*\"(?:/bin/)?(?:ba)?sh\"\s*,\s*\"-c\""),
59
+ severity=Severity.HIGH,
60
+ confidence=0.8,
61
+ message="Shell invocation with -c",
62
+ description="Command injection risk if arguments are user-controlled.",
63
+ recommendation="Avoid shell wrappers; call the target binary directly.",
64
+ ),
65
+ )
@@ -0,0 +1,106 @@
1
+ """Suspicious-code rules for JavaScript / TypeScript / Node.js."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from gitrupt.models import Severity
8
+ from gitrupt.scanners.code_rules.base import CodeRule
9
+
10
+ RULES: tuple[CodeRule, ...] = (
11
+ CodeRule(
12
+ rule_id="js-eval-dynamic",
13
+ language="javascript",
14
+ pattern=re.compile(r"\beval\s*\(\s*(?!['\"])"),
15
+ severity=Severity.MEDIUM,
16
+ confidence=0.7,
17
+ message="Dynamic eval() call",
18
+ description="eval() on non-literal input executes arbitrary code.",
19
+ recommendation="Replace eval() with a safer parser or dispatch.",
20
+ ),
21
+ CodeRule(
22
+ rule_id="js-function-constructor",
23
+ language="javascript",
24
+ pattern=re.compile(r"\bnew\s+Function\s*\("),
25
+ severity=Severity.MEDIUM,
26
+ confidence=0.65,
27
+ message="new Function() constructor",
28
+ description="The Function constructor behaves like eval().",
29
+ recommendation="Avoid new Function(); use a normal function.",
30
+ ),
31
+ CodeRule(
32
+ rule_id="js-child-process-exec",
33
+ language="javascript",
34
+ pattern=re.compile(r"\bchild_process\.exec(?:Sync)?\s*\("),
35
+ severity=Severity.MEDIUM,
36
+ confidence=0.65,
37
+ message="child_process.exec() call",
38
+ description="child_process.exec runs a shell command.",
39
+ recommendation="Use execFile() or spawn() with an argument array.",
40
+ ),
41
+ CodeRule(
42
+ rule_id="js-curl-pipe-sh",
43
+ language="javascript",
44
+ pattern=re.compile(r"curl[^\"'`\n]*\|\s*(?:ba)?sh\b"),
45
+ severity=Severity.HIGH,
46
+ confidence=0.8,
47
+ message="curl piped to shell",
48
+ description="Download-and-execute one-liner.",
49
+ recommendation="Download, verify, then execute as separate steps.",
50
+ ),
51
+ CodeRule(
52
+ rule_id="js-base64-buffer",
53
+ language="javascript",
54
+ pattern=re.compile(r"Buffer\.from\s*\([^)]*['\"]base64['\"]"),
55
+ severity=Severity.LOW,
56
+ confidence=0.5,
57
+ message="base64 decode in source",
58
+ description="Encoded payload in source — verify intent.",
59
+ recommendation="Check what the decoded bytes are used for.",
60
+ ),
61
+
62
+
63
+
64
+
65
+
66
+ CodeRule(
67
+ rule_id="js-reverse-shell",
68
+ language="javascript",
69
+ pattern=re.compile(r"require\(['\"]net['\"]\)\s*\.\s*connect\s*\("),
70
+ severity=Severity.CRITICAL,
71
+ confidence=0.8,
72
+ message="Possible reverse shell via net.connect",
73
+ description="Raw TCP connection often used in Node.js backdoors.",
74
+ recommendation="Audit the connection target and surrounding logic.",
75
+ ),
76
+ CodeRule(
77
+ rule_id="js-process-env-dump",
78
+ language="javascript",
79
+ pattern=re.compile(r"JSON\.stringify\s*\(\s*process\.env\s*\)"),
80
+ severity=Severity.HIGH,
81
+ confidence=0.8,
82
+ message="process.env serialization — credential exfiltration",
83
+ description="Dumps all environment variables, including secrets.",
84
+ recommendation="Never serialize process.env; pass explicit values.",
85
+ ),
86
+ CodeRule(
87
+ rule_id="js-vm-runincontext",
88
+ language="javascript",
89
+ pattern=re.compile(r"\bvm\.runIn(?:This)?Context\s*\("),
90
+ severity=Severity.HIGH,
91
+ confidence=0.75,
92
+ message="vm.runInContext — sandbox escape risk",
93
+ description="Eval-like in a VM context.",
94
+ recommendation="Use vm2 or another isolated sandbox.",
95
+ ),
96
+ CodeRule(
97
+ rule_id="js-settimeout-string",
98
+ language="javascript",
99
+ pattern=re.compile(r"\bset(?:Timeout|Interval)\s*\(\s*['\"]"),
100
+ severity=Severity.MEDIUM,
101
+ confidence=0.7,
102
+ message="setTimeout/setInterval with string argument",
103
+ description="String form behaves like eval().",
104
+ recommendation="Pass a function, not a string.",
105
+ ),
106
+ )
@@ -0,0 +1,71 @@
1
+ """Suspicious-code rules for PHP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from gitrupt.models import Severity
8
+ from gitrupt.scanners.code_rules.base import CodeRule
9
+
10
+ RULES: tuple[CodeRule, ...] = (
11
+ CodeRule(
12
+ rule_id="php-shell-exec",
13
+ language="php",
14
+ pattern=re.compile(r"\b(?:shell_exec|passthru|proc_open|popen)\s*\("),
15
+ severity=Severity.HIGH,
16
+ confidence=0.8,
17
+ message="PHP shell execution function",
18
+ description="Executes an OS-level command.",
19
+ recommendation="Avoid shell functions on user input; sanitize strictly.",
20
+ ),
21
+ CodeRule(
22
+ rule_id="php-exec-system",
23
+ language="php",
24
+ pattern=re.compile(r"\b(?:exec|system)\s*\([^)]*\$"),
25
+ severity=Severity.HIGH,
26
+ confidence=0.75,
27
+ message="PHP exec/system with variable input",
28
+ description="Shell injection risk when passing untrusted input.",
29
+ recommendation="Use escapeshellarg() and validate input.",
30
+ ),
31
+ CodeRule(
32
+ rule_id="php-eval-dynamic",
33
+ language="php",
34
+ pattern=re.compile(r"\beval\s*\(\s*(?!['\"])"),
35
+ severity=Severity.HIGH,
36
+ confidence=0.75,
37
+ message="Dynamic eval() call",
38
+ description="eval() on non-literal input executes arbitrary code.",
39
+ recommendation="Replace eval() with explicit logic.",
40
+ ),
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+ CodeRule(
50
+ rule_id="php-reverse-shell",
51
+ language="php",
52
+ pattern=re.compile(
53
+ r"fsockopen\s*\([^)]*\)[^;]*;\s*(?:while|exec|system|passthru|shell_exec)"
54
+ ),
55
+ severity=Severity.CRITICAL,
56
+ confidence=0.9,
57
+ message="PHP reverse shell pattern",
58
+ description="Socket open followed by command execution.",
59
+ recommendation="Treat the file as malicious.",
60
+ ),
61
+ CodeRule(
62
+ rule_id="php-assert-dynamic",
63
+ language="php",
64
+ pattern=re.compile(r"\bassert\s*\(\s*(?!['\"])"),
65
+ severity=Severity.HIGH,
66
+ confidence=0.75,
67
+ message="assert() with non-literal argument",
68
+ description="assert() on a variable can execute arbitrary code in older PHP.",
69
+ recommendation="Replace assert() with explicit validation.",
70
+ ),
71
+ )
@@ -0,0 +1,85 @@
1
+ """Suspicious-code rules for PowerShell."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from gitrupt.models import Severity
8
+ from gitrupt.scanners.code_rules.base import CodeRule
9
+
10
+ RULES: tuple[CodeRule, ...] = (
11
+ CodeRule(
12
+ rule_id="ps-iex-downloadstring",
13
+ language="powershell",
14
+ pattern=re.compile(
15
+ r"\bIEX\b[^\n]*DownloadString|DownloadString[^\n]*\bIEX\b",
16
+ re.IGNORECASE,
17
+ ),
18
+ severity=Severity.CRITICAL,
19
+ confidence=0.9,
20
+ message="PowerShell download-and-execute",
21
+ description="IEX + DownloadString fetches and runs remote code.",
22
+ recommendation="Never run untrusted remote scripts.",
23
+ ),
24
+ CodeRule(
25
+ rule_id="ps-encoded-command",
26
+ language="powershell",
27
+ pattern=re.compile(r"-EncodedCommand\b|-enc\s+[A-Za-z0-9+/=]{20,}"),
28
+ severity=Severity.HIGH,
29
+ confidence=0.85,
30
+ message="PowerShell encoded command",
31
+ description="Base64-encoded PowerShell is a common obfuscation.",
32
+ recommendation="Decode and inspect the command before running.",
33
+ ),
34
+ CodeRule(
35
+ rule_id="ps-invoke-expression",
36
+ language="powershell",
37
+ pattern=re.compile(r"\bInvoke-Expression\b"),
38
+ severity=Severity.MEDIUM,
39
+ confidence=0.65,
40
+ message="Invoke-Expression",
41
+ description="Invoke-Expression runs a string as code.",
42
+ recommendation="Avoid Invoke-Expression; use direct command calls.",
43
+ ),
44
+
45
+
46
+
47
+
48
+
49
+ CodeRule(
50
+ rule_id="ps-defender-disable",
51
+ language="powershell",
52
+ pattern=re.compile(r"Set-MpPreference\s+[^\n]*-Disable(?:RealtimeMonitoring|BehaviorMonitoring|IOAVProtection)"),
53
+ severity=Severity.CRITICAL,
54
+ confidence=0.95,
55
+ message="Windows Defender disable attempt",
56
+ description="Attempts to turn off Defender protection.",
57
+ recommendation="Never run untrusted scripts containing this.",
58
+ ),
59
+ CodeRule(
60
+ rule_id="ps-invoke-webrequest-iex",
61
+ language="powershell",
62
+ pattern=re.compile(
63
+ r"(?:Invoke-WebRequest|iwr|DownloadString)[^\n]*\|\s*(?:IEX|Invoke-Expression)",
64
+ re.IGNORECASE,
65
+ ),
66
+ severity=Severity.CRITICAL,
67
+ confidence=0.9,
68
+ message="PowerShell download-pipe-execute",
69
+ description="Fetches remote content and pipes it to IEX.",
70
+ recommendation="Never execute remote scripts.",
71
+ ),
72
+ CodeRule(
73
+ rule_id="ps-amsi-bypass",
74
+ language="powershell",
75
+ pattern=re.compile(
76
+ r"(?:amsi|AmsiUtils|System\.Management\.Automation\.AmsiUtils)",
77
+ re.IGNORECASE,
78
+ ),
79
+ severity=Severity.HIGH,
80
+ confidence=0.7,
81
+ message="AMSI bypass indicator",
82
+ description="References to AMSI internals often indicate evasion.",
83
+ recommendation="Treat the script as hostile.",
84
+ ),
85
+ )