Repository navigation
Expand file tree
/
Copy pathcollect.py
More file actions
122 lines (102 loc) · 4.15 KB
/
Copy pathcollect.py
File metadata and controls
122 lines (102 loc) · 4.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#!/usr/bin/env python3
"""
HExHTTP - Real-time scan result collector.
Thread-safe, URL-keyed. Findings are stored as they arrive, not at the end.
"""
import threading
from urllib.parse import urlsplit, urlunsplit
_lock = threading.Lock()
_results: dict[str, dict] = {} # keyed by base URL
def _resolve_url(url: str) -> str:
"""
Resolve a URL (possibly with cache buster) to the registered base URL.
Strips query string and tries to match against known URLs.
"""
# Exact match first
if url in _results:
return url
# Strip query string (?cb=xxx, ?CPDoS=xxx, etc.)
parts = urlsplit(url)
base = urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
# Try base without query
if base in _results:
return base
# Try base with trailing slash
if not base.endswith("/"):
base_slash = base + "/"
if base_slash in _results:
return base_slash
# Try matching by domain+path prefix (for URLs like https://x.com/page?cb=123)
for registered_url in _results:
if url.startswith(registered_url.rstrip("/")) or base.startswith(registered_url.rstrip("/")):
return registered_url
# No match found, return as-is
return url
def init_url(url: str, status_code: int = 0, response_size: int = 0,
technology: str = "Unknown", cache_headers: dict | None = None) -> None:
"""Call at the start of process_modules to register the URL."""
with _lock:
_results[url] = {
"url": url,
"status_code": status_code,
"response_size": response_size,
"technology": technology,
"cache_headers": cache_headers or {},
"findings": [],
"errors": [],
}
def update_url(url: str, status_code: int | None = None, response_size: int | None = None,
technology: str | None = None, cache_headers: dict | None = None) -> None:
"""Update URL metadata. Always overwrites with provided values."""
with _lock:
key = _resolve_url(url)
if key not in _results:
init_url(url, status_code or 0, response_size or 0,
technology or "Unknown", cache_headers)
return
r = _results[key]
if status_code is not None:
r["status_code"] = status_code
if response_size is not None:
r["response_size"] = response_size
if technology is not None and technology != "Unknown":
r["technology"] = technology
if cache_headers is not None:
r["cache_headers"].update(cache_headers)
def add_finding(url: str, finding: dict) -> None:
"""Add a finding to a URL. Thread-safe, real-time. Resolves cache-busted URLs."""
with _lock:
key = _resolve_url(url)
if key not in _results:
# Fallback: create entry with cleaned URL
parts = urlsplit(url)
clean = urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
_results[clean] = {
"url": clean, "status_code": 0, "response_size": 0,
"technology": "Unknown", "cache_headers": {},
"findings": [], "errors": [],
}
key = clean
_results[key]["findings"].append(finding)
def add_error(url: str, error: str) -> None:
"""Add an error to a URL."""
with _lock:
key = _resolve_url(url)
if key not in _results:
parts = urlsplit(url)
clean = urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
_results[clean] = {
"url": clean, "status_code": 0, "response_size": 0,
"technology": "Unknown", "cache_headers": {},
"findings": [], "errors": [],
}
key = clean
_results[key]["errors"].append(error)
def get_results() -> list[dict]:
"""Return all results as a list for html_report."""
with _lock:
return list(_results.values())
def reset() -> None:
"""Clear all results."""
with _lock:
_results.clear()