import json
import os
import sys
import urllib.request
UPSTREAM_BASE = "https://raw.githubusercontent.com/dochne/wappalyzer/main/src/technologies"
TECH_FILES = [chr(c) + ".json" for c in range(ord('a'), ord('z') + 1)]
TECH_FILES.append("_.json")
CATEGORIES_URL = "https://raw.githubusercontent.com/dochne/wappalyzer/main/src/categories.json"
def fetch_json(url):
try:
req = urllib.request.Request(url, headers={"User-Agent": "psnet-sync/1.0"})
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read().decode())
except Exception as e:
print(f" Warning: Could not fetch {url}: {e}", file=sys.stderr)
return None
def extract_header_sigs(tech_name, tech_data, categories):
sigs = []
cats = tech_data.get("cats", [])
category = "Technology"
for cat_id in cats:
cat_id_str = str(cat_id)
if cat_id_str in categories:
category = categories[cat_id_str].get("name", "Technology")
break
headers = tech_data.get("headers", {})
for header_name, pattern in headers.items():
if isinstance(pattern, str):
clean_pattern = clean_wappalyzer_pattern(pattern)
if clean_pattern is not None:
sig = {
"name": tech_name,
"category": category,
"header": header_name.lower(),
}
if clean_pattern:
sig["pattern"] = clean_pattern
sigs.append(sig)
meta = tech_data.get("meta", {})
for meta_name, pattern in meta.items():
if isinstance(pattern, str):
clean_pattern = clean_wappalyzer_pattern(pattern)
if clean_pattern is not None:
sig = {
"name": tech_name,
"category": category,
"header": f"x-meta-{meta_name.lower()}",
}
if clean_pattern:
sig["pattern"] = clean_pattern
sigs.append(sig)
return sigs
def clean_wappalyzer_pattern(pattern):
if not pattern:
return ""
if "\\;" in pattern:
pattern = pattern.split("\\;")[0]
pattern = pattern.strip()
if any(c in pattern for c in ['(', ')', '|', '[', '+', '*', '{', '?', '^', '$']):
if pattern.startswith('^') and not any(c in pattern[1:] for c in ['(', ')', '|', '[', '+', '*', '{', '?']):
return pattern[1:].rstrip('$').lower()
return None
return pattern.lower()
def main():
output_path = os.path.join(os.path.dirname(__file__), "..", "data", "wappalyzer.json")
existing = []
if os.path.exists(output_path):
with open(output_path, "r") as f:
existing = json.load(f)
print(f"Loaded {len(existing)} existing signatures")
print("Fetching Wappalyzer categories...")
categories = fetch_json(CATEGORIES_URL) or {}
all_sigs = []
seen = set()
for tech_file in TECH_FILES:
url = f"{UPSTREAM_BASE}/{tech_file}"
print(f" Fetching {tech_file}...", end=" ")
data = fetch_json(url)
if not data:
print("skipped")
continue
count = 0
for tech_name, tech_data in data.items():
if not isinstance(tech_data, dict):
continue
sigs = extract_header_sigs(tech_name, tech_data, categories)
for sig in sigs:
key = (sig["name"], sig["header"], sig.get("pattern", ""))
if key not in seen:
seen.add(key)
all_sigs.append(sig)
count += 1
print(f"{count} sigs")
existing_keys = set()
for sig in existing:
key = (sig["name"], sig["header"], sig.get("pattern", ""))
existing_keys.add(key)
upstream_keys = set()
for sig in all_sigs:
key = (sig["name"], sig["header"], sig.get("pattern", ""))
upstream_keys.add(key)
custom_kept = 0
for sig in existing:
key = (sig["name"], sig["header"], sig.get("pattern", ""))
if key not in upstream_keys:
all_sigs.append(sig)
custom_kept += 1
all_sigs.sort(key=lambda s: (s["category"], s["name"], s["header"]))
with open(output_path, "w") as f:
json.dump(all_sigs, f, indent=2)
print(f"\nWrote {len(all_sigs)} signatures to {output_path}")
print(f" From upstream: {len(all_sigs) - custom_kept}")
print(f" Custom kept: {custom_kept}")
print(f"\nRebuild psnet: cargo build")
if __name__ == "__main__":
main()