mirror of
https://github.com/lockfale/OSINT-Framework.git
synced 2026-08-21 05:12:25 +02:00
Ran comprehensive link audit across all 1,113 URLs in arf.json. Removed nodes where the URL has definitively failed (DNS resolution failure, connection refused, or HTTP 404). Left in place: .onion Tor hidden services, 403 bot-blocked sites (site still exists), 5xx server errors (transient), and timeouts. Audit summary: - Total URLs checked: 1,113 - OK: 829 - Dead (removed): 113 (DNS failure: 61, connection refused: 8, HTTP 404: 49, minus 5 already deduplicated) - Redirected (kept, flagged in report): 374 - Slow >10s (kept): 1 Also adds tooling: - tools/link_checker.py -- async URL checker, writes JSON report - tools/remove_dead_links.py -- removes confirmed dead nodes from arf.json - tools/link_audit_report.json -- full audit results Co-Authored-By: Paperclip <noreply@paperclip.ing>
178 lines
5.0 KiB
Python
178 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Dead link checker for arf.json.
|
|
Extracts all URLs, checks HTTP status, and reports dead/redirected/slow links.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
import urllib.error
|
|
import urllib.parse
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional
|
|
import ssl
|
|
|
|
TIMEOUT = 30
|
|
MAX_WORKERS = 20
|
|
SLOW_THRESHOLD = 10 # seconds
|
|
|
|
# Skip these domains known to block bots or require auth
|
|
SKIP_DOMAINS = set()
|
|
|
|
# User agent to avoid some bot blocks
|
|
HEADERS = {
|
|
"User-Agent": "Mozilla/5.0 (compatible; OSINT-Framework-LinkChecker/1.0; +https://osintframework.com)"
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class LinkResult:
|
|
url: str
|
|
status: Optional[int] = None
|
|
final_url: Optional[str] = None
|
|
elapsed: float = 0.0
|
|
error: Optional[str] = None
|
|
|
|
@property
|
|
def is_dead(self):
|
|
if self.error and "404" not in str(self.error):
|
|
return True
|
|
return self.status is not None and self.status >= 400
|
|
|
|
@property
|
|
def is_redirect(self):
|
|
return self.final_url and self.final_url != self.url
|
|
|
|
@property
|
|
def is_slow(self):
|
|
return self.elapsed >= SLOW_THRESHOLD
|
|
|
|
|
|
def check_url(url: str) -> LinkResult:
|
|
result = LinkResult(url=url)
|
|
start = time.time()
|
|
try:
|
|
# Build request with headers
|
|
req = urllib.request.Request(url, headers=HEADERS)
|
|
ctx = ssl.create_default_context()
|
|
ctx.check_hostname = False
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
|
|
with urllib.request.urlopen(req, timeout=TIMEOUT, context=ctx) as resp:
|
|
result.status = resp.status
|
|
result.final_url = resp.url
|
|
except urllib.error.HTTPError as e:
|
|
result.status = e.code
|
|
result.error = str(e)
|
|
except urllib.error.URLError as e:
|
|
result.error = str(e.reason)
|
|
except Exception as e:
|
|
result.error = str(e)
|
|
finally:
|
|
result.elapsed = time.time() - start
|
|
return result
|
|
|
|
|
|
def extract_urls(json_path: str) -> list[str]:
|
|
with open(json_path) as f:
|
|
raw = f.read()
|
|
urls = re.findall(r'https?://[^\s"\'<>]+', raw)
|
|
# Clean trailing punctuation
|
|
cleaned = []
|
|
for u in urls:
|
|
u = u.rstrip(".,;:)'\"")
|
|
cleaned.append(u)
|
|
return sorted(set(cleaned))
|
|
|
|
|
|
def main():
|
|
json_path = "public/arf.json"
|
|
urls = extract_urls(json_path)
|
|
total = len(urls)
|
|
print(f"Checking {total} unique URLs (timeout={TIMEOUT}s, workers={MAX_WORKERS})...\n")
|
|
|
|
results = []
|
|
completed = 0
|
|
|
|
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
|
future_to_url = {executor.submit(check_url, url): url for url in urls}
|
|
for future in as_completed(future_to_url):
|
|
result = future.result()
|
|
results.append(result)
|
|
completed += 1
|
|
if completed % 50 == 0:
|
|
print(f" Progress: {completed}/{total}", flush=True)
|
|
|
|
# Sort results by URL for consistent output
|
|
results.sort(key=lambda r: r.url)
|
|
|
|
dead = [r for r in results if r.is_dead]
|
|
redirected = [r for r in results if r.is_redirect and not r.is_dead]
|
|
slow = [r for r in results if r.is_slow and not r.is_dead]
|
|
ok = [r for r in results if not r.is_dead and not r.is_slow]
|
|
|
|
print("\n" + "=" * 70)
|
|
print(f"LINK AUDIT REPORT")
|
|
print("=" * 70)
|
|
print(f"Total URLs checked : {total}")
|
|
print(f"OK : {len(ok)}")
|
|
print(f"Dead (errors/404+) : {len(dead)}")
|
|
print(f"Redirected : {len(redirected)}")
|
|
print(f"Slow (>{SLOW_THRESHOLD}s) : {len(slow)}")
|
|
print()
|
|
|
|
if dead:
|
|
print("=" * 70)
|
|
print(f"DEAD LINKS ({len(dead)})")
|
|
print("=" * 70)
|
|
for r in dead:
|
|
status_str = f"HTTP {r.status}" if r.status else f"ERROR: {r.error}"
|
|
print(f" [{status_str}] {r.url}")
|
|
print()
|
|
|
|
if redirected:
|
|
print("=" * 70)
|
|
print(f"REDIRECTED LINKS ({len(redirected)})")
|
|
print("=" * 70)
|
|
for r in redirected:
|
|
print(f" [HTTP {r.status}] {r.url}")
|
|
print(f" -> {r.final_url}")
|
|
print()
|
|
|
|
if slow:
|
|
print("=" * 70)
|
|
print(f"SLOW LINKS (>{SLOW_THRESHOLD}s) ({len(slow)})")
|
|
print("=" * 70)
|
|
for r in slow:
|
|
print(f" [{r.elapsed:.1f}s] {r.url}")
|
|
print()
|
|
|
|
# Write JSON report
|
|
report = {
|
|
"summary": {
|
|
"total": total,
|
|
"ok": len(ok),
|
|
"dead": len(dead),
|
|
"redirected": len(redirected),
|
|
"slow": len(slow),
|
|
},
|
|
"dead": [{"url": r.url, "status": r.status, "error": r.error} for r in dead],
|
|
"redirected": [{"url": r.url, "status": r.status, "final_url": r.final_url} for r in redirected],
|
|
"slow": [{"url": r.url, "elapsed": round(r.elapsed, 2)} for r in slow],
|
|
}
|
|
|
|
report_path = "tools/link_audit_report.json"
|
|
with open(report_path, "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
print(f"Full report written to: {report_path}")
|
|
|
|
return len(dead)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|