reverify 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.
- reverify/__init__.py +31 -0
- reverify/boundary_auditor.py +434 -0
- reverify/cli.py +402 -0
- reverify/disasm.py +244 -0
- reverify/emulator.py +171 -0
- reverify/frida_bridge.py +140 -0
- reverify/mcp_server.py +219 -0
- reverify/pe_parser.py +353 -0
- reverify/protocol_parser.py +174 -0
- reverify/py.typed +0 -0
- reverify/verifier.py +358 -0
- reverify-0.1.0.dist-info/METADATA +139 -0
- reverify-0.1.0.dist-info/RECORD +17 -0
- reverify-0.1.0.dist-info/WHEEL +5 -0
- reverify-0.1.0.dist-info/entry_points.txt +3 -0
- reverify-0.1.0.dist-info/licenses/LICENSE +21 -0
- reverify-0.1.0.dist-info/top_level.txt +1 -0
reverify/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Reverse Engineering, Protocol Dissection, and Dynamic Instrumentation Toolkit."""
|
|
2
|
+
|
|
3
|
+
from .pe_parser import PEParser, BinaryParseError
|
|
4
|
+
from .disasm import Disassembler, Instruction, pattern_scan, create_patch
|
|
5
|
+
from .emulator import MicroEmulator, EmulatorError
|
|
6
|
+
from .protocol_parser import ProtobufDissector, TLVDissector, format_hexdump, decode_varint, encode_varint
|
|
7
|
+
from .frida_bridge import FridaScriptGenerator
|
|
8
|
+
from .verifier import Verifier, Claim, verify_claims, VERIFIED, REFUTED, INCONCLUSIVE
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"PEParser",
|
|
12
|
+
"BinaryParseError",
|
|
13
|
+
"Disassembler",
|
|
14
|
+
"Instruction",
|
|
15
|
+
"pattern_scan",
|
|
16
|
+
"create_patch",
|
|
17
|
+
"MicroEmulator",
|
|
18
|
+
"EmulatorError",
|
|
19
|
+
"ProtobufDissector",
|
|
20
|
+
"TLVDissector",
|
|
21
|
+
"format_hexdump",
|
|
22
|
+
"decode_varint",
|
|
23
|
+
"encode_varint",
|
|
24
|
+
"FridaScriptGenerator",
|
|
25
|
+
"Verifier",
|
|
26
|
+
"Claim",
|
|
27
|
+
"verify_claims",
|
|
28
|
+
"VERIFIED",
|
|
29
|
+
"REFUTED",
|
|
30
|
+
"INCONCLUSIVE",
|
|
31
|
+
]
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Security Boundary Auditor Module.
|
|
3
|
+
|
|
4
|
+
Provides defensive security checks and audits for:
|
|
5
|
+
1. Path Canonicalization & Symlink Containment (Filesystem Boundary)
|
|
6
|
+
2. Network Loopback, Private IP, DNS Rebinding & Metadata Filtering (SSRF Boundary)
|
|
7
|
+
3. State Serialization & Structured Snapshot Integrity (State Boundary)
|
|
8
|
+
4. Environment Variable & Secret Boundary Sanitization (Secret Boundary)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ipaddress
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import socket
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
18
|
+
from urllib.parse import urlparse
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PathBoundaryAuditor:
|
|
22
|
+
"""Audits and validates filesystem paths against containment rules."""
|
|
23
|
+
|
|
24
|
+
RESERVED_WINDOWS_NAMES = {
|
|
25
|
+
"CON", "PRN", "AUX", "NUL",
|
|
26
|
+
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
|
27
|
+
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def normalize_path(path_str: str) -> str:
|
|
32
|
+
"""Strip surrounding quotes and whitespace."""
|
|
33
|
+
s = str(path_str).strip()
|
|
34
|
+
while len(s) >= 2 and ((s[0] == '"' and s[-1] == '"') or (s[0] == "'" and s[-1] == "'")):
|
|
35
|
+
s = s[1:-1].strip()
|
|
36
|
+
return s
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def is_safe_path(cls, base_dir: Union[str, Path], target_path: Union[str, Path]) -> bool:
|
|
40
|
+
"""Check if target_path resolves strictly within base_dir without escaping."""
|
|
41
|
+
res = cls.audit_path(base_dir, target_path)
|
|
42
|
+
return res["is_safe"]
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def audit_path(cls, base_dir: Union[str, Path], target_path: Union[str, Path]) -> Dict[str, Any]:
|
|
46
|
+
"""Detailed path boundary security audit."""
|
|
47
|
+
base_str = cls.normalize_path(str(base_dir))
|
|
48
|
+
target_str = cls.normalize_path(str(target_path))
|
|
49
|
+
|
|
50
|
+
findings: List[str] = []
|
|
51
|
+
is_safe = True
|
|
52
|
+
|
|
53
|
+
# Check reserved DOS device names
|
|
54
|
+
path_obj = Path(target_str)
|
|
55
|
+
for part in path_obj.parts:
|
|
56
|
+
stem = part.split(".")[0].upper()
|
|
57
|
+
if stem in cls.RESERVED_WINDOWS_NAMES:
|
|
58
|
+
findings.append(f"Reserved DOS device name detected: {part}")
|
|
59
|
+
is_safe = False
|
|
60
|
+
|
|
61
|
+
# Check NT device namespace / UNC injection
|
|
62
|
+
if target_str.startswith(("\\\\.\\", "\\\\?\\", "//./", "//?/")):
|
|
63
|
+
findings.append("NT device namespace prefix detected")
|
|
64
|
+
is_safe = False
|
|
65
|
+
|
|
66
|
+
# Check NTFS Alternate Data Streams (ADS)
|
|
67
|
+
if ":" in path_obj.name and len(path_obj.parts) > 0:
|
|
68
|
+
if not (len(path_obj.parts) == 1 and path_obj.drive):
|
|
69
|
+
findings.append(f"Potential NTFS Alternate Data Stream detected in '{path_obj.name}'")
|
|
70
|
+
is_safe = False
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
resolved_base = Path(base_str).resolve()
|
|
74
|
+
if os.path.isabs(target_str):
|
|
75
|
+
resolved_target = Path(target_str).resolve()
|
|
76
|
+
else:
|
|
77
|
+
resolved_target = (resolved_base / target_str).resolve()
|
|
78
|
+
|
|
79
|
+
# Verify containment
|
|
80
|
+
try:
|
|
81
|
+
resolved_target.relative_to(resolved_base)
|
|
82
|
+
except ValueError:
|
|
83
|
+
findings.append(f"Path escape detected: '{resolved_target}' is outside base '{resolved_base}'")
|
|
84
|
+
is_safe = False
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
findings.append(f"Path resolution error: {str(e)}")
|
|
88
|
+
is_safe = False
|
|
89
|
+
resolved_base = Path(base_str)
|
|
90
|
+
resolved_target = Path(target_str)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"is_safe": is_safe,
|
|
94
|
+
"base_directory": str(resolved_base),
|
|
95
|
+
"target_path": str(target_str),
|
|
96
|
+
"resolved_target": str(resolved_target),
|
|
97
|
+
"findings": findings,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class NetworkBoundaryAuditor:
|
|
102
|
+
"""Audits network URLs and IP addresses to prevent SSRF and loopback leakage."""
|
|
103
|
+
|
|
104
|
+
CLOUD_METADATA_IPS = {
|
|
105
|
+
"169.254.169.254", # AWS/GCP/Azure Instance Metadata
|
|
106
|
+
"100.100.100.200", # Alibaba Cloud Metadata
|
|
107
|
+
"169.254.170.2", # AWS ECS Task Metadata
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
DNS_REBINDING_DOMAINS = {
|
|
111
|
+
"nip.io", "sslip.io", "traefik.me", "lvh.me", "localho.st",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
SENSITIVE_INTERNAL_PORTS = {
|
|
115
|
+
22: "SSH",
|
|
116
|
+
2375: "Docker Daemon (Unencrypted)",
|
|
117
|
+
2376: "Docker Daemon (TLS)",
|
|
118
|
+
6379: "Redis",
|
|
119
|
+
9200: "Elasticsearch",
|
|
120
|
+
10250: "Kubelet API",
|
|
121
|
+
27017: "MongoDB",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def decode_numeric_ip(cls, host: str) -> Optional[str]:
|
|
126
|
+
"""Decode decimal, hex, or octal IPv4 string notations to canonical dotted quad."""
|
|
127
|
+
# Decimal integer IP (e.g. 2130706433 -> 127.0.0.1)
|
|
128
|
+
if host.isdigit():
|
|
129
|
+
try:
|
|
130
|
+
val = int(host)
|
|
131
|
+
if 0 <= val <= 0xFFFFFFFF:
|
|
132
|
+
return str(ipaddress.IPv4Address(val))
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
# Hexadecimal IP (e.g. 0x7f000001)
|
|
137
|
+
if host.lower().startswith("0x"):
|
|
138
|
+
try:
|
|
139
|
+
val = int(host, 16)
|
|
140
|
+
if 0 <= val <= 0xFFFFFFFF:
|
|
141
|
+
return str(ipaddress.IPv4Address(val))
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
# Mixed / Octal notation (e.g. 0177.0.0.1 or 0x7f.0.0.1)
|
|
146
|
+
if "." in host:
|
|
147
|
+
parts = host.split(".")
|
|
148
|
+
if len(parts) == 4:
|
|
149
|
+
try:
|
|
150
|
+
octets = []
|
|
151
|
+
for p in parts:
|
|
152
|
+
if p.lower().startswith("0x"):
|
|
153
|
+
octets.append(int(p, 16))
|
|
154
|
+
elif p.startswith("0") and len(p) > 1 and p.isdigit():
|
|
155
|
+
octets.append(int(p, 8))
|
|
156
|
+
elif p.isdigit():
|
|
157
|
+
octets.append(int(p))
|
|
158
|
+
else:
|
|
159
|
+
return None
|
|
160
|
+
if all(0 <= o <= 255 for o in octets):
|
|
161
|
+
return ".".join(str(o) for o in octets)
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def is_private_or_loopback_ip(cls, ip_str: str) -> Tuple[bool, str]:
|
|
168
|
+
"""Determine if an IP is loopback, private, link-local, or cloud metadata."""
|
|
169
|
+
try:
|
|
170
|
+
ip = ipaddress.ip_address(ip_str)
|
|
171
|
+
# Handle IPv6-mapped IPv4 addresses (e.g. ::ffff:127.0.0.1)
|
|
172
|
+
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
|
|
173
|
+
ip = ip.ipv4_mapped
|
|
174
|
+
|
|
175
|
+
if str(ip) in cls.CLOUD_METADATA_IPS:
|
|
176
|
+
return True, "Cloud Instance Metadata Endpoint"
|
|
177
|
+
if ip.is_loopback:
|
|
178
|
+
return True, "Loopback Address (Localhost)"
|
|
179
|
+
if ip.is_private:
|
|
180
|
+
return True, "Private / RFC1918 Network Address"
|
|
181
|
+
if ip.is_link_local:
|
|
182
|
+
return True, "Link-Local Address"
|
|
183
|
+
if ip.is_multicast:
|
|
184
|
+
return True, "Multicast Address"
|
|
185
|
+
if ip.is_reserved:
|
|
186
|
+
return True, "Reserved Network Address"
|
|
187
|
+
return False, "Public / Routable Address"
|
|
188
|
+
except ValueError:
|
|
189
|
+
return False, "Invalid IP"
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def audit_url(cls, url_str: str, allow_local: bool = False) -> Dict[str, Any]:
|
|
193
|
+
"""Audit a URL against network boundary security policies."""
|
|
194
|
+
findings: List[str] = []
|
|
195
|
+
is_safe = True
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
parsed = urlparse(url_str)
|
|
199
|
+
scheme = parsed.scheme.lower()
|
|
200
|
+
hostname = parsed.hostname or ""
|
|
201
|
+
port = parsed.port
|
|
202
|
+
|
|
203
|
+
if scheme not in ("http", "https"):
|
|
204
|
+
findings.append(f"Disallowed URL scheme: '{scheme}' (only HTTP/HTTPS permitted)")
|
|
205
|
+
is_safe = False
|
|
206
|
+
|
|
207
|
+
if not hostname:
|
|
208
|
+
findings.append("Missing hostname in URL")
|
|
209
|
+
is_safe = False
|
|
210
|
+
return {
|
|
211
|
+
"is_safe": False,
|
|
212
|
+
"url": url_str,
|
|
213
|
+
"findings": findings,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Check port sensitivities
|
|
217
|
+
if port and port in cls.SENSITIVE_INTERNAL_PORTS:
|
|
218
|
+
service = cls.SENSITIVE_INTERNAL_PORTS[port]
|
|
219
|
+
findings.append(f"Target port {port} exposes sensitive internal service ({service})")
|
|
220
|
+
is_safe = False
|
|
221
|
+
|
|
222
|
+
# Check localhost aliases
|
|
223
|
+
lower_host = hostname.lower()
|
|
224
|
+
if lower_host in ("localhost", "localhost.localdomain", "127.0.0.1", "::1", "0.0.0.0"):
|
|
225
|
+
if not allow_local:
|
|
226
|
+
findings.append(f"Localhost access target detected: '{hostname}'")
|
|
227
|
+
is_safe = False
|
|
228
|
+
|
|
229
|
+
# Check DNS rebinding / wildcard domains
|
|
230
|
+
for rebind in cls.DNS_REBINDING_DOMAINS:
|
|
231
|
+
if lower_host == rebind or lower_host.endswith("." + rebind):
|
|
232
|
+
findings.append(f"DNS Rebinding wildcard domain detected: '{hostname}'")
|
|
233
|
+
is_safe = False
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
# Check encoded IP representations
|
|
237
|
+
decoded_ip = cls.decode_numeric_ip(hostname)
|
|
238
|
+
eval_ip = decoded_ip or hostname
|
|
239
|
+
|
|
240
|
+
# Evaluate IP classification
|
|
241
|
+
try:
|
|
242
|
+
is_priv, reason = cls.is_private_or_loopback_ip(eval_ip)
|
|
243
|
+
if is_priv and not allow_local:
|
|
244
|
+
findings.append(f"Restricted network boundary access: {reason} ({eval_ip})")
|
|
245
|
+
is_safe = False
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
findings.append(f"URL parse error: {str(e)}")
|
|
251
|
+
is_safe = False
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"is_safe": is_safe,
|
|
255
|
+
"url": url_str,
|
|
256
|
+
"findings": findings,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class EnvironmentBoundaryAuditor:
|
|
261
|
+
"""Audits and redacts secret tokens and credentials from environment snapshots."""
|
|
262
|
+
|
|
263
|
+
SECRET_KEY_PATTERNS = [
|
|
264
|
+
re.compile(r".*(?:api_key|apikey|secret|password|passwd|token|auth|credential|private_key).*", re.I),
|
|
265
|
+
re.compile(r"^(?:AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|GITHUB_TOKEN|GH_TOKEN|ANTHROPIC_API_KEY)$", re.I),
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
SECRET_VALUE_PATTERNS = [
|
|
269
|
+
re.compile(r"^sk-[A-Za-z0-9_\-]{20,}$"), # OpenAI / Service Keys
|
|
270
|
+
re.compile(r"^ghp_[A-Za-z0-9]{30,}$"), # GitHub Personal Access Token
|
|
271
|
+
re.compile(r"-----BEGIN [A-Z ]+ PRIVATE KEY-----"), # PEM Private Keys
|
|
272
|
+
re.compile(r"^Bearer\s+[A-Za-z0-9_\-\.]{20,}$", re.I), # JWT / Bearer Tokens
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def is_secret(cls, key: str, value: str) -> bool:
|
|
277
|
+
"""Identify whether a key-value pair constitutes sensitive credential data."""
|
|
278
|
+
for pat in cls.SECRET_KEY_PATTERNS:
|
|
279
|
+
if pat.match(key):
|
|
280
|
+
return True
|
|
281
|
+
for pat in cls.SECRET_VALUE_PATTERNS:
|
|
282
|
+
if pat.search(str(value)):
|
|
283
|
+
return True
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def sanitize_dict(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
288
|
+
"""Return a sanitized copy of a dictionary with sensitive keys redacted."""
|
|
289
|
+
sanitized = {}
|
|
290
|
+
for k, v in data.items():
|
|
291
|
+
if isinstance(v, dict):
|
|
292
|
+
sanitized[k] = cls.sanitize_dict(v)
|
|
293
|
+
elif isinstance(v, str) and cls.is_secret(k, v):
|
|
294
|
+
sanitized[k] = "[REDACTED_SECRET]"
|
|
295
|
+
else:
|
|
296
|
+
sanitized[k] = v
|
|
297
|
+
return sanitized
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def audit_environment(cls, env_dict: Dict[str, str]) -> Dict[str, Any]:
|
|
301
|
+
"""Audit environment snapshot and report exposed secret keys."""
|
|
302
|
+
leaked_keys: List[str] = []
|
|
303
|
+
for k, v in env_dict.items():
|
|
304
|
+
if cls.is_secret(k, v):
|
|
305
|
+
leaked_keys.append(k)
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
"is_safe": len(leaked_keys) == 0,
|
|
309
|
+
"secret_count": len(leaked_keys),
|
|
310
|
+
"leaked_keys": leaked_keys,
|
|
311
|
+
"sanitized_preview": cls.sanitize_dict(env_dict),
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class StateIntegrityAuditor:
|
|
316
|
+
"""Audits state serialization and receipt schemas."""
|
|
317
|
+
|
|
318
|
+
FORBIDDEN_PROPERTIES = {"__proto__", "constructor", "prototype"}
|
|
319
|
+
|
|
320
|
+
@classmethod
|
|
321
|
+
def audit_state_json(cls, data: Union[str, bytes, dict, list]) -> Dict[str, Any]:
|
|
322
|
+
"""Audit serialized state data for injection and integrity violations."""
|
|
323
|
+
findings: List[str] = []
|
|
324
|
+
is_safe = True
|
|
325
|
+
|
|
326
|
+
obj: Any = None
|
|
327
|
+
if isinstance(data, (str, bytes)):
|
|
328
|
+
try:
|
|
329
|
+
obj = json.loads(data)
|
|
330
|
+
except Exception as e:
|
|
331
|
+
return {
|
|
332
|
+
"is_safe": False,
|
|
333
|
+
"findings": [f"Invalid JSON format: {str(e)}"],
|
|
334
|
+
}
|
|
335
|
+
else:
|
|
336
|
+
obj = data
|
|
337
|
+
|
|
338
|
+
def traverse(node: Any, path: str = ""):
|
|
339
|
+
nonlocal is_safe
|
|
340
|
+
if isinstance(node, dict):
|
|
341
|
+
for k, v in node.items():
|
|
342
|
+
if k in cls.FORBIDDEN_PROPERTIES:
|
|
343
|
+
findings.append(f"Object prototype pollution key detected at '{path}.{k}'")
|
|
344
|
+
is_safe = False
|
|
345
|
+
if len(k) > 1024:
|
|
346
|
+
findings.append(f"Abnormally long dictionary key at '{path}' ({len(k)} chars)")
|
|
347
|
+
is_safe = False
|
|
348
|
+
traverse(v, f"{path}.{k}" if path else str(k))
|
|
349
|
+
elif isinstance(node, list):
|
|
350
|
+
if len(node) > 50000:
|
|
351
|
+
findings.append(f"List length exceeds security budget at '{path}' ({len(node)} items)")
|
|
352
|
+
is_safe = False
|
|
353
|
+
for idx, item in enumerate(node[:100]):
|
|
354
|
+
traverse(item, f"{path}[{idx}]")
|
|
355
|
+
|
|
356
|
+
traverse(obj)
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
"is_safe": is_safe,
|
|
360
|
+
"findings": findings,
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def run_full_security_audit(workspace_dir: str, target_urls: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
365
|
+
"""Execute a comprehensive boundary audit report across filesystem, network, and environment."""
|
|
366
|
+
report: Dict[str, Any] = {
|
|
367
|
+
"status": "PASS",
|
|
368
|
+
"workspace_root": str(Path(workspace_dir).resolve()),
|
|
369
|
+
"filesystem_audit": {},
|
|
370
|
+
"network_audit": [],
|
|
371
|
+
"environment_audit": {},
|
|
372
|
+
"state_audit": {},
|
|
373
|
+
"summary": "All security boundaries intact.",
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
# Filesystem audit on workspace
|
|
377
|
+
ws_path = Path(workspace_dir)
|
|
378
|
+
test_paths = [
|
|
379
|
+
"valid_sub/file.txt",
|
|
380
|
+
"../escaped.txt",
|
|
381
|
+
"../../Windows/System32/config/SAM",
|
|
382
|
+
"NUL",
|
|
383
|
+
]
|
|
384
|
+
fs_results = []
|
|
385
|
+
for p in test_paths:
|
|
386
|
+
res = PathBoundaryAuditor.audit_path(ws_path, p)
|
|
387
|
+
fs_results.append(res)
|
|
388
|
+
report["filesystem_audit"] = {
|
|
389
|
+
"traversal_blocked": not any(r["is_safe"] for r in fs_results if "escape" in str(r["findings"]) or "SAM" in r["target_path"]),
|
|
390
|
+
"tests_evaluated": len(fs_results),
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
# Network audit
|
|
394
|
+
urls_to_test = target_urls or [
|
|
395
|
+
"https://api.openai.com/v1",
|
|
396
|
+
"http://127.0.0.1:8080/admin",
|
|
397
|
+
"http://169.254.169.254/latest/meta-data/",
|
|
398
|
+
"http://[::1]:9000/metrics",
|
|
399
|
+
"http://2130706433:80/",
|
|
400
|
+
"http://attacker.127.0.0.1.nip.io/",
|
|
401
|
+
]
|
|
402
|
+
net_results = []
|
|
403
|
+
for u in urls_to_test:
|
|
404
|
+
net_results.append(NetworkBoundaryAuditor.audit_url(u, allow_local=False))
|
|
405
|
+
report["network_audit"] = net_results
|
|
406
|
+
|
|
407
|
+
# Environment audit
|
|
408
|
+
env_sample = {
|
|
409
|
+
"PATH": "C:\\Windows\\System32",
|
|
410
|
+
"USER": "developer",
|
|
411
|
+
"OPENAI_API_KEY": "sk-example-key-12345678901234567890",
|
|
412
|
+
}
|
|
413
|
+
env_res = EnvironmentBoundaryAuditor.audit_environment(env_sample)
|
|
414
|
+
report["environment_audit"] = {
|
|
415
|
+
"secrets_detected": env_res["secret_count"],
|
|
416
|
+
"sanitized_safe": env_res["sanitized_preview"]["OPENAI_API_KEY"] == "[REDACTED_SECRET]",
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
# Overall health
|
|
420
|
+
has_fs_breach = not report["filesystem_audit"]["traversal_blocked"]
|
|
421
|
+
has_net_leak = any(r["is_safe"] for r in net_results if "169.254" in r["url"] or "127.0.0.1" in r["url"] or "2130706433" in r["url"] or "nip.io" in r["url"])
|
|
422
|
+
|
|
423
|
+
if has_fs_breach or has_net_leak or not report["environment_audit"]["sanitized_safe"]:
|
|
424
|
+
report["status"] = "FAIL"
|
|
425
|
+
report["summary"] = "Security boundary violation detected."
|
|
426
|
+
|
|
427
|
+
return report
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
if __name__ == "__main__":
|
|
431
|
+
import pprint
|
|
432
|
+
current_dir = os.getcwd()
|
|
433
|
+
result = run_full_security_audit(current_dir)
|
|
434
|
+
pprint.pprint(result)
|