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 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)