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.
- gitrupt/__init__.py +13 -0
- gitrupt/cli.py +546 -0
- gitrupt/config.py +269 -0
- gitrupt/git.py +590 -0
- gitrupt/hooks/__init__.py +7 -0
- gitrupt/hooks/install.py +255 -0
- gitrupt/hooks/pre_commit.py +103 -0
- gitrupt/hooks/pre_push.py +178 -0
- gitrupt/models.py +190 -0
- gitrupt/policy.py +36 -0
- gitrupt/reporting.py +316 -0
- gitrupt/risk.py +197 -0
- gitrupt/scanner.py +117 -0
- gitrupt/scanners/__init__.py +17 -0
- gitrupt/scanners/adapters.py +166 -0
- gitrupt/scanners/base.py +113 -0
- gitrupt/scanners/binaries.py +185 -0
- gitrupt/scanners/code_rules/__init__.py +36 -0
- gitrupt/scanners/code_rules/base.py +27 -0
- gitrupt/scanners/code_rules/go.py +65 -0
- gitrupt/scanners/code_rules/javascript.py +106 -0
- gitrupt/scanners/code_rules/php.py +71 -0
- gitrupt/scanners/code_rules/powershell.py +85 -0
- gitrupt/scanners/code_rules/python.py +153 -0
- gitrupt/scanners/code_rules/ruby.py +76 -0
- gitrupt/scanners/code_rules/rust.py +41 -0
- gitrupt/scanners/code_rules/shell.py +112 -0
- gitrupt/scanners/dependencies.py +244 -0
- gitrupt/scanners/ecosystems/__init__.py +30 -0
- gitrupt/scanners/ecosystems/base.py +60 -0
- gitrupt/scanners/ecosystems/node.py +128 -0
- gitrupt/scanners/ecosystems/python.py +157 -0
- gitrupt/scanners/entropy.py +123 -0
- gitrupt/scanners/forbidden_files.py +201 -0
- gitrupt/scanners/malware.py +219 -0
- gitrupt/scanners/osv_client.py +221 -0
- gitrupt/scanners/registry.py +66 -0
- gitrupt/scanners/secret_rules.py +368 -0
- gitrupt/scanners/secrets.py +558 -0
- gitrupt/scanners/suspicious_code.py +208 -0
- gitrupt/scanners/yara_loader.py +65 -0
- gitrupt/scanners/yara_rules_builtin.py +141 -0
- gitrupt-0.1.0.dist-info/METADATA +342 -0
- gitrupt-0.1.0.dist-info/RECORD +48 -0
- gitrupt-0.1.0.dist-info/WHEEL +5 -0
- gitrupt-0.1.0.dist-info/entry_points.txt +2 -0
- gitrupt-0.1.0.dist-info/licenses/LICENSE +23 -0
- 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
|
+
)
|