diff --git a/backend/app/main.py b/backend/app/main.py
index e92fad8..7f7d9ec 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -35,6 +35,7 @@
from app.services.diagnostics import build_system_diagnostics, test_export_dependencies
from app.services.executive_risk import build_executive_risk
from app.services.nmap_audit import audit_service_versions
+from app.services.dnsdumpster_like_inventory import build_dnsdumpster_like_inventory
from app.services.legal_terms import legal_payload, create_acceptance, validate_acceptance
from app.services.attack_graph import build_attack_graph
@@ -155,6 +156,14 @@ async def create_audit(payload: AuditRequest, request: Request, db=Depends(get_d
passive_cves = detect_passive_cves(web_result)
cti_result = audit_cti(ip_inventory, domain)
service_scan = await asyncio.to_thread(audit_service_versions, domain, ip_inventory)
+ dnsdumpster_like = await build_dnsdumpster_like_inventory(
+ domain,
+ dns_result,
+ mail_result,
+ subdomains_result,
+ ip_inventory,
+ service_scan,
+ )
raw_findings = collect_findings(
dns_result,
@@ -193,6 +202,7 @@ async def create_audit(payload: AuditRequest, request: Request, db=Depends(get_d
"ip_inventory": ip_inventory,
"passive_cves": passive_cves,
"service_scan": service_scan,
+ "dnsdumpster_like": dnsdumpster_like,
"cti": cti_result,
"patching_sla": patching_sla,
"executive_risk": executive_risk,
@@ -275,6 +285,17 @@ async def create_audit(payload: AuditRequest, request: Request, db=Depends(get_d
"items": passive_cves.get("items", [])[:30],
"note": passive_cves.get("note"),
},
+ "dnsdumpster_like": {
+ "system_locations": dnsdumpster_like.get("system_locations", {}),
+ "hosting_networks": dnsdumpster_like.get("hosting_networks", [])[:50],
+ "services_banners": dnsdumpster_like.get("services_banners", {}),
+ "a_records": dnsdumpster_like.get("a_records", [])[:80],
+ "mx_records": dnsdumpster_like.get("mx_records", [])[:50],
+ "ns_records": dnsdumpster_like.get("ns_records", [])[:50],
+ "txt_records": dnsdumpster_like.get("txt_records", [])[:80],
+ "hosts": dnsdumpster_like.get("hosts", [])[:80],
+ "policy": dnsdumpster_like.get("policy", {}),
+ },
"service_scan": {
"enabled": service_scan.get("enabled"),
"mode": service_scan.get("mode"),
diff --git a/backend/app/reports/excel_report.py b/backend/app/reports/excel_report.py
index e9f6032..6526423 100644
--- a/backend/app/reports/excel_report.py
+++ b/backend/app/reports/excel_report.py
@@ -42,6 +42,7 @@ def generate_excel_report(audit: dict) -> str:
ws.title = "Synthese Direction"
_sheet_summary(ws, audit)
+ _sheet_dnsdumpster_like(wb, audit)
_sheet_action_plan(wb, audit)
_sheet_findings(wb, audit)
_sheet_exposure(wb, audit)
@@ -136,6 +137,100 @@ def _sheet_summary(ws, audit: dict):
_format(ws, freeze=None)
+
+def _sheet_dnsdumpster_like(wb, audit):
+ dd = audit.get("dnsdumpster_like", {}) or {}
+ summary = wb.create_sheet("DNSDumpster-like Summary")
+ summary_rows = [
+ ["Domain", dd.get("domain") or audit.get("domain")],
+ ["System Locations", len(dd.get("system_locations", {}) or {})],
+ ["Hosting / Networks", len(dd.get("hosting_networks", []) or [])],
+ ["Services / Banners", len(dd.get("services_banners", {}) or {})],
+ ["A Records enrichis", len(dd.get("a_records", []) or [])],
+ ["MX Records enrichis", len(dd.get("mx_records", []) or [])],
+ ["NS Records enrichis", len(dd.get("ns_records", []) or [])],
+ ["TXT Records", len(dd.get("txt_records", []) or [])],
+ ["Note", "Nmap est une source complémentaire ; HTTP/HTTPS/TLS sont intégrés même si Nmap est vide."],
+ ]
+ _table_sheet(summary, "DNSDUMPSTER-LIKE SUMMARY", ["Indicateur", "Valeur"], summary_rows)
+
+ ws = wb.create_sheet("System Locations")
+ rows = [[country, count] for country, count in (dd.get("system_locations", {}) or {}).items()]
+ _table_sheet(ws, "SYSTEM LOCATIONS", ["Country", "Count"], rows or [["Unknown", 0]])
+
+ ws = wb.create_sheet("Hosting Networks")
+ rows = [[h.get("asn"), h.get("network"), h.get("asn_name"), h.get("country"), h.get("count")] for h in dd.get("hosting_networks", [])]
+ _table_sheet(ws, "HOSTING / NETWORKS", ["ASN", "Network", "ASN Name", "Country", "Hosts"], rows)
+
+ ws = wb.create_sheet("Services Banners")
+ rows = [[banner, count] for banner, count in (dd.get("services_banners", {}) or {}).items()]
+ _table_sheet(ws, "SERVICES / BANNERS", ["Banner", "Count"], rows)
+
+ for title, key, headers, fn in [
+ ("A Records", "a_records", _host_headers(), _host_row),
+ ("MX Records", "mx_records", ["Priority"] + _host_headers(), lambda h: [h.get("priority")] + _host_row(h)),
+ ("NS Records", "ns_records", _host_headers(), _host_row),
+ ("Host Inventory", "hosts", _host_headers(), _host_row),
+ ]:
+ ws = wb.create_sheet(title)
+ _table_sheet(ws, title.upper(), headers, [fn(h) for h in dd.get(key, [])])
+
+ ws = wb.create_sheet("TXT Records")
+ rows = []
+ for item in dd.get("txt_records", []) or []:
+ spf = item.get("spf") or {}
+ rows.append([item.get("type"), item.get("value"), "\n".join(spf.get("includes", [])), "\n".join(spf.get("ip4", [])), "\n".join(spf.get("ip6", [])), "\n".join(spf.get("providers", [])), ", ".join(item.get("sources", []))])
+ _table_sheet(ws, "TXT RECORDS", ["Type", "Value", "SPF Includes", "SPF ip4", "SPF ip6", "Providers", "Sources"], rows)
+
+
+def _host_headers():
+ return ["Host", "IP", "ASN", "Network", "ASN Name", "Country", "Provider", "Open Services", "HTTP Status", "HTTPS Status", "Final URL", "Title", "Server Banner", "TLS CN", "TLS SAN", "TLS Issuer", "TLS Expiration", "Technologies", "Nmap Services", "RevIP", "Sources"]
+
+
+def _host_row(h):
+ http = h.get("http") or {}
+ https = h.get("https") or {}
+ tls = h.get("tls") or {}
+ services = _services_text(h.get("open_services", [])) or "Non détecté"
+ server_banner = "; ".join(dict.fromkeys([s.get("banner") for s in h.get("open_services", []) if s.get("banner")]))
+ title = https.get("title") or http.get("title") or next((s.get("title") for s in h.get("open_services", []) if s.get("title") and s.get("title") != "Non détecté"), "Non détecté")
+ return [
+ h.get("host"), h.get("ip"), h.get("asn"), h.get("network"), h.get("asn_name"), h.get("country"), h.get("provider"),
+ services, http.get("status_code") or "Non détecté", https.get("status_code") or "Non détecté", https.get("final_url") or http.get("final_url") or "Non détecté", title,
+ server_banner or "Non détecté", tls.get("cn") or "Non détecté", "\n".join(tls.get("san", []) or []), tls.get("issuer") or "Non détecté", tls.get("expires_at") or "Non détecté",
+ _tech_text(h.get("technologies", [])) or "Non détecté", _nmap_text(h.get("nmap_services", [])) or "Non détecté par Nmap", h.get("revip_count", 0), ", ".join(h.get("sources", [])),
+ ]
+
+
+def _services_text(services):
+ parts = []
+ for s in services or []:
+ seg = f"{s.get('scheme')}: {s.get('banner') or s.get('service') or 'unknown server'}"
+ if s.get("title") and s.get("title") != "Non détecté":
+ seg += f"; title: {s.get('title')}"
+ parts.append(seg)
+ return "\n".join(parts)
+
+
+def _tech_text(technologies):
+ out = []
+ for t in technologies or []:
+ name = t.get("name") or ""
+ if t.get("version"):
+ name += f":{t.get('version')}"
+ if t.get("note"):
+ name += f" ({t.get('note')})"
+ out.append(name)
+ return "\n".join(dict.fromkeys(out))
+
+
+def _nmap_text(services):
+ out = []
+ for s in services or []:
+ version = s.get("version") or "Version non exposée"
+ out.append(f"{s.get('port')}/{s.get('protocol', 'tcp')} {s.get('name') or s.get('service')} {s.get('product') or ''} {version}".strip())
+ return "\n".join(out)
+
def _sheet_action_plan(wb, audit):
ws = wb.create_sheet("Plan Action")
rows = []
@@ -259,7 +354,7 @@ def _sheet_limits(wb, audit):
def _sheet_raw(wb, audit):
- ws = wb.create_sheet("Resume JSON")
+ ws = wb.create_sheet("Raw JSON Summary")
rows = [
["id", audit.get("id")],
["domain", audit.get("domain")],
@@ -267,6 +362,7 @@ def _sheet_raw(wb, audit):
["mode", audit.get("mode")],
["score", json.dumps(audit.get("score", {}), ensure_ascii=False, default=str)],
["executive_risk", json.dumps(audit.get("executive_risk", {}), ensure_ascii=False, default=str)[:32000]],
+ ["dnsdumpster_like", json.dumps(audit.get("dnsdumpster_like", {}), ensure_ascii=False, default=str)[:32000]],
]
_table_sheet(ws, "RÉSUMÉ JSON", ["Clé", "Valeur"], rows)
diff --git a/backend/app/reports/html_report.py b/backend/app/reports/html_report.py
index 351507a..f273db0 100644
--- a/backend/app/reports/html_report.py
+++ b/backend/app/reports/html_report.py
@@ -370,9 +370,142 @@ def _render_graph(audit: dict) -> str:
)
+
+def _render_dnsdumpster_like(audit: dict) -> str:
+ dd = audit.get("dnsdumpster_like") or {}
+ note = '
Nmap est une source complémentaire. Les services HTTP/HTTPS peuvent être détectés par fingerprint web même lorsque Nmap ne remonte pas de service.
'
+ metrics = ('' + _metric("System Locations", len(dd.get("system_locations", {}) or {})) + _metric("Hosting / Networks", len(dd.get("hosting_networks", []) or [])) + _metric("Services / Banners", len(dd.get("services_banners", {}) or {})) + _metric("A Records enrichis", len(dd.get("a_records", []) or [])) + _metric("MX Records enrichis", len(dd.get("mx_records", []) or [])) + _metric("NS Records enrichis", len(dd.get("ns_records", []) or [])) + '
')
+ return note + metrics
+
+
+def _render_system_locations(audit: dict) -> str:
+ rows = [[country, count] for country, count in (safe_get(audit, "dnsdumpster_like.system_locations", {}) or {}).items()]
+ return render_table(["Country", "Count"], rows or [["Unknown", 0]])
+
+
+def _render_hosting_networks(audit: dict) -> str:
+ rows = [[h.get("asn"), h.get("network"), h.get("asn_name"), h.get("country"), h.get("count")] for h in safe_get(audit, "dnsdumpster_like.hosting_networks", []) or []]
+ return render_table(["ASN", "Network", "ASN Name", "Country", "Hosts"], rows)
+
+
+def _render_services_banners(audit: dict) -> str:
+ rows = [[banner, count] for banner, count in (safe_get(audit, "dnsdumpster_like.services_banners", {}) or {}).items()]
+ return render_table(["Banner", "Count"], rows)
+
+
+def _render_a_records_filterable(audit: dict) -> str:
+ records = safe_get(audit, "dnsdumpster_like.a_records", []) or []
+ controls = """
+
+
+
+
+
+
+
+
"""
+ headers = ["Host", "IP", "ASN", "Network", "ASN Name", "Country", "Open Services observed", "RevIP", "Sources"]
+ rows = []
+ for h in records:
+ tech = _tech_text(h)
+ services = _open_services_html(h)
+ badges = _host_badges(h)
+ detail = _host_detail_html(h)
+ attrs = {"host": h.get("host", ""), "asn": h.get("asn", ""), "country": h.get("country", ""), "tech": tech, "service": _plain_services(h), "webnonmap": str(bool(h.get("open_services") and not h.get("nmap_services"))).lower(), "guard": str(bool(h.get("guardrail"))).lower()}
+ attr_str = " ".join(f'data-{k}="{esc(v).lower()}"' for k, v in attrs.items())
+ host_cell = SafeHtml(f'{esc(h.get("host"))} {badges}
{detail} ')
+ rows.append(SafeHtml(f'| {host_cell} | {esc(h.get("ip"))} | {esc(h.get("asn"))} | {esc(h.get("network"))} | {esc(h.get("asn_name"))} | {esc(h.get("country"))} | {services} | {esc(h.get("revip_count", 0))} | {esc(", ".join(h.get("sources", []) or []))} |
'))
+ if not rows:
+ return controls + 'Aucun A record enrichi disponible.
'
+ head = "".join(f"{esc(h)} | " for h in headers)
+ return controls + f''
+
+
+def _render_mx_records(audit: dict) -> str:
+ rows = []
+ for h in safe_get(audit, "dnsdumpster_like.mx_records", []) or []:
+ rows.append([h.get("priority"), h.get("host"), h.get("ip"), h.get("asn"), h.get("network"), h.get("asn_name"), h.get("country"), SafeHtml(_open_services_html(h)), _tech_text(h), _join(h.get("sources", []))])
+ return render_table(["Priority", "Host", "IP", "ASN", "Network", "ASN Name", "Country", "Open Services observed", "Technologies", "Sources"], rows)
+
+
+def _render_ns_records(audit: dict) -> str:
+ rows = []
+ for h in safe_get(audit, "dnsdumpster_like.ns_records", []) or []:
+ rows.append([h.get("host"), h.get("ip"), h.get("asn"), h.get("network"), h.get("asn_name"), h.get("country"), SafeHtml(_open_services_html(h)), _join(h.get("sources", []))])
+ return render_table(["Host", "IP", "ASN", "Network", "ASN Name", "Country", "Open Services observed", "Sources"], rows)
+
+
+def _render_txt_records(audit: dict) -> str:
+ rows = []
+ for item in safe_get(audit, "dnsdumpster_like.txt_records", []) or []:
+ spf = item.get("spf") or {}
+ rows.append([item.get("type"), SafeHtml(f'{esc(item.get("value"))}
'), _join(spf.get("providers", []), 10), _join(spf.get("includes", []), 15), _join((spf.get("ip4", []) or []) + (spf.get("ip6", []) or []), 20)])
+ return render_table(["Type", "Value", "SPF providers", "SPF includes", "SPF IPs"], rows)
+
+
+def _render_host_inventory_detail(audit: dict) -> str:
+ cards = []
+ for h in safe_get(audit, "dnsdumpster_like.hosts", []) or []:
+ cards.append(f"{esc(h.get('host'))} - {esc(h.get('ip'))} {_host_badges(h)}
{_host_detail_html(h)} ")
+ return "".join(cards) or 'Aucun host inventory détaillé disponible.
'
+
+
+def _host_badges(h: dict) -> str:
+ badges = []
+ if any(s.get("scheme") == "http" for s in h.get("open_services", []) or []): badges.append("HTTP")
+ if any(s.get("scheme") == "https" for s in h.get("open_services", []) or []): badges.append("HTTPS")
+ if (h.get("tls") or {}).get("cn") and (h.get("tls") or {}).get("cn") != "Non détecté": badges.append("TLS")
+ if h.get("nmap_services"): badges.append("Nmap")
+ if h.get("asn") and h.get("asn") != "Non détecté": badges.append("ASN")
+ if h.get("technologies"): badges.append("Tech")
+ if h.get("guardrail"): badges.append("Bloqué par garde-fou")
+ if not h.get("open_services"): badges.append("Non détecté")
+ return " ".join(f'{esc(b)}' for b in badges)
+
+
+def _open_services_html(h: dict) -> str:
+ services = h.get("open_services", []) or []
+ if not services:
+ return 'Bloqué par garde-fou' if h.get("guardrail") else 'Non détecté'
+ parts = []
+ for svc in services:
+ text = f"{svc.get('scheme')}: {svc.get('banner') or svc.get('service') or 'unknown server'}"
+ if svc.get("title") and svc.get("title") != "Non détecté": text += f"; title: {svc.get('title')}"
+ parts.append(esc(text))
+ tls = h.get("tls") or {}
+ if tls.get("cn") and tls.get("cn") != "Non détecté": parts.append(f"cn: {esc(tls.get('cn'))}")
+ tech = _tech_text(h)
+ if tech: parts.append(f"tech: {esc(tech)}")
+ return "
".join(parts)
+
+
+def _plain_services(h: dict) -> str:
+ return " ".join([str(s.get("banner") or s.get("service") or "") for s in h.get("open_services", []) or []])
+
+
+def _tech_text(h: dict) -> str:
+ out = []
+ for t in h.get("technologies", []) or []:
+ name = t.get("name") or ""
+ if t.get("version"): name += f":{t.get('version')}"
+ if t.get("note"): name += f" ({t.get('note')})"
+ out.append(name)
+ return ", ".join(dict.fromkeys(out))
+
+
+def _host_detail_html(h: dict) -> str:
+ tls = h.get("tls") or {}
+ guard = h.get("guardrail") or {}
+ data = [["DNS", h.get("host")], ["IP", h.get("ip")], ["ASN", h.get("asn")], ["Network", h.get("network")], ["ASN Name", h.get("asn_name")], ["Country", h.get("country")], ["Provider", h.get("provider")], ["HTTP", h.get("http", {}).get("status_code") or "Non détecté"], ["HTTPS", h.get("https", {}).get("status_code") or "Non détecté"], ["TLS", f"CN={tls.get('cn', 'Non détecté')} SAN={', '.join(tls.get('san', []) or [])} issuer={tls.get('issuer', 'Non détecté')} exp={tls.get('expires_at', 'Non détecté')}"], ["Technologies", _tech_text(h) or "Non détecté"], ["Nmap", _nmap_services_text(h) or "Non détecté par Nmap"], ["Sources", ", ".join(h.get("sources", []) or [])], ["Erreurs / garde-fous", guard.get("reason") or _join(h.get("resolution_errors", []))]]
+ return render_table(["Champ", "Valeur"], data, max_rows=80)
+
+
+def _nmap_services_text(h: dict) -> str:
+ return "; ".join(f"{s.get('port')}/{s.get('protocol', 'tcp')} {s.get('name') or s.get('service') or ''} {s.get('product') or ''} {s.get('version') or 'Version non exposée'}".strip() for s in h.get("nmap_services", []) or [])
+
def _css() -> str:
return """
-:root{--bg:#07070a;--bg2:#09090d;--card:#101018;--card2:#151521;--red:#ff3045;--red2:#dc143c;--darkred:#65000b;--text:#f5f5f5;--muted:#a1a1aa;--border:rgba(255,48,69,.25);--green:#34d399;--orange:#fb923c}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:radial-gradient(circle at 70% 0,rgba(220,20,60,.22),transparent 32%),linear-gradient(135deg,var(--bg),var(--bg2));color:var(--text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Arial,sans-serif;line-height:1.55}.watermark{position:fixed;inset:auto -4vw 8vh auto;font-size:13vw;font-weight:900;letter-spacing:.08em;color:rgba(255,255,255,.025);transform:rotate(-10deg);pointer-events:none;z-index:0}.hero{position:relative;z-index:1;min-height:72vh;padding:72px 7vw 52px;border-bottom:1px solid var(--border);background:linear-gradient(120deg,rgba(255,48,69,.12),transparent 55%)}.hero:before{content:"";position:absolute;inset:24px;border:1px solid rgba(255,48,69,.18);border-radius:32px;pointer-events:none}.eyebrow{color:var(--red);text-transform:uppercase;letter-spacing:.28em;font-size:.78rem;font-weight:800}.hero h1{margin:.1em 0;font-size:clamp(3.6rem,9vw,8.8rem);line-height:.88;letter-spacing:-.08em}.hero h2{margin:0;color:#fff;font-size:clamp(1.15rem,2vw,2rem);font-weight:500}.hero-meta{display:flex;flex-wrap:wrap;gap:12px;margin:28px 0}.badge{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:999px;padding:5px 10px;background:rgba(255,48,69,.09);color:#fff;font-weight:800;font-size:.76rem;text-transform:uppercase;letter-spacing:.06em}.layout{position:relative;z-index:1;display:grid;grid-template-columns:280px minmax(0,1fr);gap:28px;padding:36px 5vw}.toc{position:sticky;top:20px;align-self:start;background:rgba(16,16,24,.86);backdrop-filter:blur(10px);border:1px solid var(--border);border-radius:20px;padding:18px;max-height:calc(100vh - 40px);overflow:auto}.toc strong{display:block;margin-bottom:12px;color:#fff}.toc a{display:block;color:var(--muted);text-decoration:none;padding:8px 10px;border-radius:10px;font-size:.92rem}.toc a:hover{background:rgba(255,48,69,.12);color:#fff}.content{min-width:0}.metric-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin-top:28px}.metric-card,.cardlet{background:linear-gradient(180deg,var(--card),rgba(16,16,24,.76));border:1px solid var(--border);border-radius:18px;padding:18px;box-shadow:0 18px 50px rgba(0,0,0,.24)}.metric-card span{display:block;color:var(--muted);font-size:.82rem;text-transform:uppercase;letter-spacing:.08em}.metric-card strong{display:block;margin-top:7px;font-size:1.7rem;color:#fff}.metric-card small{color:var(--muted)}.section{margin:0 0 28px;padding:26px;background:rgba(16,16,24,.78);border:1px solid var(--border);border-radius:24px;box-shadow:0 20px 70px rgba(0,0,0,.26)}.section-title{display:flex;align-items:center;gap:14px;border-bottom:1px solid rgba(255,48,69,.18);padding-bottom:14px;margin-bottom:18px}.section-title span{color:var(--red);font-weight:900}.section h2{margin:0;color:#fff;font-size:1.55rem}.section-subtitle,.notice{color:var(--muted)}.lead{font-size:1.06rem;color:#e7e7e9}.two-col{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px}.cardlet h3{margin-top:0;color:var(--red)}.table-scroll{overflow:auto;border:1px solid rgba(255,48,69,.16);border-radius:16px}.table{width:100%;border-collapse:collapse;min-width:760px;background:rgba(7,7,10,.55)}.table th,.table td{padding:11px 12px;border-bottom:1px solid rgba(255,255,255,.07);text-align:left;vertical-align:top}.table th{color:#fff;background:rgba(101,0,11,.5);font-size:.78rem;text-transform:uppercase;letter-spacing:.08em}.table td{color:#d8d8dc;font-size:.9rem}.finding{border:1px solid rgba(255,48,69,.22);border-radius:18px;padding:18px;margin:14px 0;background:linear-gradient(135deg,rgba(255,48,69,.05),rgba(255,255,255,.02))}.finding-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.finding h3{margin:0;color:#fff}.finding-meta{display:grid;grid-template-columns:140px 1fr;gap:8px 12px}.finding-meta dt{color:var(--red);font-weight:800}.finding-meta dd{margin:0;color:#dcdce1}.severity-critical{background:rgba(101,0,11,.88);border-color:rgba(255,48,69,.75)}.severity-high{background:rgba(220,20,60,.55)}.severity-medium{background:rgba(251,146,60,.28);border-color:rgba(251,146,60,.45)}.severity-low{background:rgba(161,161,170,.18)}.severity-info{background:rgba(59,130,246,.16);border-color:rgba(59,130,246,.35)}.progress-wrap{margin:14px 0}.progress-label{display:flex;justify-content:space-between;color:#fff;font-weight:800}.progress{height:12px;border:1px solid var(--border);border-radius:999px;background:#09090d;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,var(--darkred),var(--red));border-radius:999px}.code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:#08080c;border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:8px;color:#f1f1f3;white-space:pre-wrap;word-break:break-word}@media(max-width:880px){.layout{grid-template-columns:1fr}.toc{position:relative;top:0}.hero{padding:48px 6vw}.section{padding:20px}}@media print{body{background:#09090d}.toc{position:relative}.layout{display:block}.section{break-inside:avoid}}
+:root{--bg:#07070a;--bg2:#09090d;--card:#101018;--card2:#151521;--red:#ff3045;--red2:#dc143c;--darkred:#65000b;--text:#f5f5f5;--muted:#a1a1aa;--border:rgba(255,48,69,.25);--green:#34d399;--orange:#fb923c}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:radial-gradient(circle at 70% 0,rgba(220,20,60,.22),transparent 32%),linear-gradient(135deg,var(--bg),var(--bg2));color:var(--text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Arial,sans-serif;line-height:1.55}.watermark{position:fixed;inset:auto -4vw 8vh auto;font-size:13vw;font-weight:900;letter-spacing:.08em;color:rgba(255,255,255,.025);transform:rotate(-10deg);pointer-events:none;z-index:0}.hero{position:relative;z-index:1;min-height:72vh;padding:72px 7vw 52px;border-bottom:1px solid var(--border);background:linear-gradient(120deg,rgba(255,48,69,.12),transparent 55%)}.hero:before{content:"";position:absolute;inset:24px;border:1px solid rgba(255,48,69,.18);border-radius:32px;pointer-events:none}.eyebrow{color:var(--red);text-transform:uppercase;letter-spacing:.28em;font-size:.78rem;font-weight:800}.hero h1{margin:.1em 0;font-size:clamp(3.6rem,9vw,8.8rem);line-height:.88;letter-spacing:-.08em}.hero h2{margin:0;color:#fff;font-size:clamp(1.15rem,2vw,2rem);font-weight:500}.hero-meta{display:flex;flex-wrap:wrap;gap:12px;margin:28px 0}.badge{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:999px;padding:5px 10px;background:rgba(255,48,69,.09);color:#fff;font-weight:800;font-size:.76rem;text-transform:uppercase;letter-spacing:.06em}.layout{position:relative;z-index:1;display:grid;grid-template-columns:280px minmax(0,1fr);gap:28px;padding:36px 5vw}.toc{position:sticky;top:20px;align-self:start;background:rgba(16,16,24,.86);backdrop-filter:blur(10px);border:1px solid var(--border);border-radius:20px;padding:18px;max-height:calc(100vh - 40px);overflow:auto}.toc strong{display:block;margin-bottom:12px;color:#fff}.toc a{display:block;color:var(--muted);text-decoration:none;padding:8px 10px;border-radius:10px;font-size:.92rem}.toc a:hover{background:rgba(255,48,69,.12);color:#fff}.content{min-width:0}.metric-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin-top:28px}.metric-card,.cardlet{background:linear-gradient(180deg,var(--card),rgba(16,16,24,.76));border:1px solid var(--border);border-radius:18px;padding:18px;box-shadow:0 18px 50px rgba(0,0,0,.24)}.metric-card span{display:block;color:var(--muted);font-size:.82rem;text-transform:uppercase;letter-spacing:.08em}.metric-card strong{display:block;margin-top:7px;font-size:1.7rem;color:#fff}.metric-card small{color:var(--muted)}.section{margin:0 0 28px;padding:26px;background:rgba(16,16,24,.78);border:1px solid var(--border);border-radius:24px;box-shadow:0 20px 70px rgba(0,0,0,.26)}.section-title{display:flex;align-items:center;gap:14px;border-bottom:1px solid rgba(255,48,69,.18);padding-bottom:14px;margin-bottom:18px}.section-title span{color:var(--red);font-weight:900}.section h2{margin:0;color:#fff;font-size:1.55rem}.section-subtitle,.notice{color:var(--muted)}.lead{font-size:1.06rem;color:#e7e7e9}.two-col{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px}.cardlet h3{margin-top:0;color:var(--red)}.table-scroll{overflow:auto;border:1px solid rgba(255,48,69,.16);border-radius:16px}.table{width:100%;border-collapse:collapse;min-width:760px;background:rgba(7,7,10,.55)}.table th,.table td{padding:11px 12px;border-bottom:1px solid rgba(255,255,255,.07);text-align:left;vertical-align:top}.table th{color:#fff;background:rgba(101,0,11,.5);font-size:.78rem;text-transform:uppercase;letter-spacing:.08em}.table td{color:#d8d8dc;font-size:.9rem}.finding{border:1px solid rgba(255,48,69,.22);border-radius:18px;padding:18px;margin:14px 0;background:linear-gradient(135deg,rgba(255,48,69,.05),rgba(255,255,255,.02))}.finding-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.finding h3{margin:0;color:#fff}.finding-meta{display:grid;grid-template-columns:140px 1fr;gap:8px 12px}.finding-meta dt{color:var(--red);font-weight:800}.finding-meta dd{margin:0;color:#dcdce1}.severity-critical{background:rgba(101,0,11,.88);border-color:rgba(255,48,69,.75)}.severity-high{background:rgba(220,20,60,.55)}.severity-medium{background:rgba(251,146,60,.28);border-color:rgba(251,146,60,.45)}.severity-low{background:rgba(161,161,170,.18)}.severity-info{background:rgba(59,130,246,.16);border-color:rgba(59,130,246,.35)}.progress-wrap{margin:14px 0}.progress-label{display:flex;justify-content:space-between;color:#fff;font-weight:800}.progress{height:12px;border:1px solid var(--border);border-radius:999px;background:#09090d;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,var(--darkred),var(--red));border-radius:999px}.code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:#08080c;border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:8px;color:#f1f1f3;white-space:pre-wrap;word-break:break-word}.filters{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin:14px 0}.filters input{background:#08080c;color:#fff;border:1px solid var(--border);border-radius:12px;padding:10px}.filters label{color:#d8d8dc;font-size:.9rem}.badge.mini{font-size:.64rem;padding:3px 7px;margin:2px}.host-card{border:1px solid rgba(255,48,69,.2);border-radius:16px;margin:12px 0;padding:12px;background:rgba(7,7,10,.35)}details summary{cursor:pointer;color:#fff;font-weight:800}@media(max-width:880px){.layout{grid-template-columns:1fr}.toc{position:relative;top:0}.hero{padding:48px 6vw}.section{padding:20px}}@media print{body{background:#09090d}.toc{position:relative}.layout{display:block}.section{break-inside:avoid}}
"""
@@ -386,11 +519,11 @@ def generate_html_report(audit: dict) -> str:
subdomains = safe_get(audit, "subdomains.subdomains", [])
cve_count = (safe_get(audit, "passive_cves.count", 0) or 0) + (safe_get(audit, "service_scan.count_cves", 0) or 0)
toc = [
- ("synthese", "Synthèse exécutive"), ("scoring", "Scoring"), ("plan", "Plan d’action priorisé"),
- ("findings", "Findings"), ("dns", "DNS"), ("messagerie", "Messagerie"), ("web", "Web / Headers"),
- ("tls", "TLS / SSL"), ("subdomains", "Sous-domaines"), ("ips", "IP publiques"),
- ("nmap", "Nmap service/version/port"), ("cve", "CVE"), ("cti", "CTI / réputation"),
- ("graph", "Graph Explorer"), ("limites", "Limites et responsabilité"),
+ ("synthese", "Synthèse"), ("system_locations", "System Locations"), ("hosting_networks", "Hosting / Networks"),
+ ("services_banners", "Services / Banners"), ("a_records", "A Records / Subdomains"),
+ ("mx_records", "MX Records"), ("ns_records", "NS Records"), ("txt_records", "TXT Records"),
+ ("host_inventory", "Host Inventory détaillé"), ("nmap", "Nmap en annexe"), ("cve", "CVE"),
+ ("limites", "Limites et sources"),
]
hero_metrics = (
''
@@ -416,21 +549,36 @@ def generate_html_report(audit: dict) -> str:
-{section('synthese','Synthèse exécutive',_build_executive_summary(audit))}
-{section('scoring','Scoring',_render_scoring(audit),'Scores consolidés et jauges visuelles CSS.')}
-{section('plan','Plan d’action priorisé',_render_action_plan(audit))}
-{section('findings','Findings détaillés',_render_findings(audit))}
-{section('dns','DNS',_render_dns(audit))}
-{section('messagerie','Messagerie',_render_mail(audit))}
-{section('web','Web / Headers',_render_web(audit))}
-{section('tls','TLS / SSL',_render_tls(audit))}
-{section('subdomains','Sous-domaines',_render_subdomains(audit))}
-{section('ips','IP publiques',_render_ips(audit))}
-{section('nmap','Nmap service/version/port',_render_nmap(audit),'Rappel : collecte non exploitante, sans scripts intrusifs.')}
+{section('synthese','Synthèse',_build_executive_summary(audit) + _render_dnsdumpster_like(audit))}
+{section('system_locations','System Locations',_render_system_locations(audit))}
+{section('hosting_networks','Hosting / Networks',_render_hosting_networks(audit))}
+{section('services_banners','Services / Banners',_render_services_banners(audit))}
+{section('a_records','A Records / Subdomains from dataset',_render_a_records_filterable(audit),'Table filtrable : host, ASN, pays, technologie, service, HTTP/HTTPS sans Nmap, garde-fou.')}
+{section('mx_records','MX Records',_render_mx_records(audit))}
+{section('ns_records','NS Records',_render_ns_records(audit))}
+{section('txt_records','TXT Records',_render_txt_records(audit))}
+{section('host_inventory','Host Inventory détaillé',_render_host_inventory_detail(audit))}
+{section('nmap','Nmap en annexe',_render_nmap(audit),'Rappel : collecte non exploitante, sans scripts intrusifs.')}
{section('cve','CVE',_render_cves(audit))}
-{section('cti','CTI / réputation',_render_cti(audit))}
-{section('graph','Graph Explorer',_render_graph(audit))}
-{section('limites','Limites et responsabilité',limits)}
-