forked from eddie4/geoip-attack-map
-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathupdate_hashes.py
More file actions
executable file
·282 lines (236 loc) · 10.2 KB
/
Copy pathupdate_hashes.py
File metadata and controls
executable file
·282 lines (236 loc) · 10.2 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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
#!/usr/bin/env python3
"""
T-Pot Attack Map - Integrity Hash Updater
==========================================
Automatically updates SHA384 integrity hashes for all static assets in index.html
Usage:
python3 update_hashes.py # Update all hashes
python3 update_hashes.py --check # Check which files need updating
python3 update_hashes.py --check-vendor # Verify static/vendor.lock (offline)
python3 update_hashes.py --verbose # Show detailed output
Integrity hierarchy (HANDOFF-v2 §14.4):
1. static/vendor.lock + committed hashes — the authoritative build-integrity
mechanism, verified locally by --check-vendor.
2. SRI on ordinary <script>/<link> tags (including <link rel="modulepreload">,
which the discovery regex matches) — an additional browser-side control.
3. modulepreload integrity — a browser-dependent optimisation; dynamic-import
correctness never depends on it.
"""
import re
import hashlib
import base64
import sys
import os
from pathlib import Path
from typing import List, Tuple, Dict
# ANSI color codes for pretty output
class Colors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
def calculate_sha384(file_path: str) -> str:
"""
Calculate SHA384 hash for a file and return as base64-encoded string.
Args:
file_path: Path to the file to hash
Returns:
Base64-encoded SHA384 hash in format: sha384-<hash>
"""
try:
with open(file_path, 'rb') as f:
file_data = f.read()
sha384_hash = hashlib.sha384(file_data).digest()
base64_hash = base64.b64encode(sha384_hash).decode('utf-8')
return f"sha384-{base64_hash}"
except FileNotFoundError:
print(f"{Colors.FAIL}✗ File not found: {file_path}{Colors.ENDC}")
return None
except Exception as e:
print(f"{Colors.FAIL}✗ Error calculating hash for {file_path}: {e}{Colors.ENDC}")
return None
def extract_integrity_entries(html_content: str) -> List[Tuple[str, str, str]]:
"""
Extract all integrity attribute entries from HTML content.
Args:
html_content: The HTML file content
Returns:
List of tuples: (file_path, current_hash, full_match_string)
"""
# Pattern to match both <link> and <script> tags with integrity attributes
# Handles various formats: src="...", href="...", integrity="..."
pattern = r'(?:src|href)="(static/[^"]+)"[^>]*?integrity="(sha384-[^"]+)"'
matches = []
for match in re.finditer(pattern, html_content):
file_path = match.group(1)
current_hash = match.group(2)
full_match = match.group(0)
matches.append((file_path, current_hash, full_match))
return matches
def update_integrity_hashes(html_file: str, check_only: bool = False, verbose: bool = False) -> bool:
"""
Update all integrity hashes in the HTML file.
Args:
html_file: Path to the HTML file
check_only: If True, only check which files need updating without modifying
verbose: If True, show detailed output for all files
Returns:
True if successful (or if check_only and no changes needed), False otherwise
"""
# Read the HTML file
try:
with open(html_file, 'r', encoding='utf-8') as f:
html_content = f.read()
except FileNotFoundError:
print(f"{Colors.FAIL}✗ HTML file not found: {html_file}{Colors.ENDC}")
return False
# Get the directory of the HTML file for resolving relative paths
html_dir = Path(html_file).parent
# Extract all integrity entries
entries = extract_integrity_entries(html_content)
if not entries:
print(f"{Colors.WARNING}⚠ No integrity attributes found in {html_file}{Colors.ENDC}")
return False
print(f"{Colors.HEADER}{Colors.BOLD}T-Pot Attack Map - Integrity Hash Updater{Colors.ENDC}")
print(f"{Colors.HEADER}{'=' * 60}{Colors.ENDC}\n")
if check_only:
print(f"{Colors.OKBLUE}🔍 Checking integrity hashes...{Colors.ENDC}\n")
else:
print(f"{Colors.OKBLUE}🔄 Updating integrity hashes...{Colors.ENDC}\n")
# Track statistics
stats = {
'total': len(entries),
'updated': 0,
'unchanged': 0,
'errors': 0
}
# Store updates to apply
updates: Dict[str, str] = {}
# Process each entry
for file_path, current_hash, full_match in entries:
# Remove query parameters from file path (e.g., ?v=5)
clean_file_path = file_path.split('?')[0]
# Resolve the full path
# If HTML file is already in static/, don't add static/ again
if 'static' in str(html_dir).lower() and clean_file_path.startswith('static/'):
# Remove 'static/' prefix since we're already in the static directory
relative_path = clean_file_path.replace('static/', '', 1)
full_path = html_dir / relative_path
else:
full_path = html_dir / clean_file_path
# Calculate new hash
new_hash = calculate_sha384(str(full_path))
if new_hash is None:
stats['errors'] += 1
continue
# Check if hash changed
if current_hash == new_hash:
stats['unchanged'] += 1
if verbose:
print(f"{Colors.OKGREEN}✓ {file_path}{Colors.ENDC}")
print(f" Hash: {Colors.OKCYAN}{current_hash}{Colors.ENDC}")
print()
else:
stats['updated'] += 1
print(f"{Colors.WARNING}⚡ {file_path}{Colors.ENDC}")
print(f" Old: {Colors.FAIL}{current_hash}{Colors.ENDC}")
print(f" New: {Colors.OKGREEN}{new_hash}{Colors.ENDC}")
print()
# Store the update
old_pattern = full_match.replace(current_hash, r'sha384-[A-Za-z0-9+/=]+')
new_text = full_match.replace(current_hash, new_hash)
updates[full_match] = new_text
# Print summary
print(f"{Colors.HEADER}{'=' * 60}{Colors.ENDC}")
print(f"{Colors.BOLD}Summary:{Colors.ENDC}")
print(f" Total files: {stats['total']}")
print(f" {Colors.OKGREEN}Unchanged: {stats['unchanged']}{Colors.ENDC}")
print(f" {Colors.WARNING}Need updating: {stats['updated']}{Colors.ENDC}")
print(f" {Colors.FAIL}Errors: {stats['errors']}{Colors.ENDC}")
print()
# Apply updates if not check_only mode
if not check_only and updates:
try:
updated_content = html_content
for old_text, new_text in updates.items():
updated_content = updated_content.replace(old_text, new_text)
# Write back to file
with open(html_file, 'w', encoding='utf-8') as f:
f.write(updated_content)
print(f"{Colors.OKGREEN}✓ Successfully updated {html_file}{Colors.ENDC}")
return True
except Exception as e:
print(f"{Colors.FAIL}✗ Error writing to {html_file}: {e}{Colors.ENDC}")
return False
elif check_only and stats['updated'] > 0:
print(f"{Colors.WARNING}⚠ Run without --check to update the hashes{Colors.ENDC}")
return False
return stats['updated'] == 0
def check_vendor_lock(repo_root: Path, verbose: bool = False) -> bool:
"""
Verify every entry of static/vendor.lock against the working tree
(HANDOFF-v2 §14.4). Fully offline. The manifest covers all four
provenance types (vendored / generated / local / legacy) by hash.
Returns True when every listed file exists and matches its hash.
"""
lock_file = repo_root / 'static' / 'vendor.lock'
if not lock_file.exists():
print(f"{Colors.FAIL}✗ Missing manifest: {lock_file}{Colors.ENDC}")
return False
ok = True
entries = 0
for lineno, line in enumerate(lock_file.read_text(encoding='utf-8').splitlines(), 1):
if not line.strip() or line.startswith('#'):
continue
parts = line.split('\t')
if len(parts) != 5:
print(f"{Colors.FAIL}✗ vendor.lock:{lineno}: malformed record ({len(parts)} fields){Colors.ENDC}")
ok = False
continue
rel_path, expected_hash, provenance, source, version = parts
entries += 1
target = repo_root / rel_path
if not target.is_file():
print(f"{Colors.FAIL}✗ missing file: {rel_path}{Colors.ENDC}")
ok = False
continue
actual_hash = calculate_sha384(str(target))
if actual_hash != expected_hash:
print(f"{Colors.FAIL}✗ hash mismatch: {rel_path} ({provenance}){Colors.ENDC}")
print(f" expected: {expected_hash}")
print(f" actual: {actual_hash}")
ok = False
elif verbose:
print(f"{Colors.OKGREEN}✓ {rel_path} ({provenance}, {source} {version}){Colors.ENDC}")
if ok:
print(f"{Colors.OKGREEN}✓ vendor.lock verified: {entries} entries match the working tree{Colors.ENDC}")
else:
print(f"{Colors.FAIL}✗ vendor.lock verification FAILED — regenerate via "
f"tools/vendor_frontend.sh --write-lock only if the change is intentional{Colors.ENDC}")
return ok
def main():
"""Main entry point."""
# Parse command line arguments
check_only = '--check' in sys.argv
check_vendor = '--check-vendor' in sys.argv
verbose = '--verbose' in sys.argv or '-v' in sys.argv
# Show help if requested
if '--help' in sys.argv or '-h' in sys.argv:
print(__doc__)
return
script_dir = Path(__file__).parent
if check_vendor:
sys.exit(0 if check_vendor_lock(script_dir, verbose=verbose) else 1)
# Determine HTML file path
html_file = script_dir / 'static' / 'index.html'
# Run the updater
success = update_integrity_hashes(str(html_file), check_only=check_only, verbose=verbose)
# Exit with appropriate code
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()