403Webshell
Server IP : 202.61.199.114  /  Your IP : 216.73.217.139
Web Server : nginx/1.22.1
System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64
User : web20 ( 1018)
PHP Version : 8.4.23
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : ON
Directory :  /tmp/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /tmp//js_origin.js
/**
 * SafeMyIdent – IP-Check Pro (Frontend).
 * Ruft den /api-Service parallel ab, führt die Daten zusammen, berechnet einen
 * Risiko-Score und rendert Tabs/Karten. Vanilla JS, barrierearm.
 */
(function () {
	'use strict';

	var CFG = window.SMI_IPCHECK || { apiBase: '/api/v1', modules: {}, pdf: true, lang: 'de' };
	var __ = (window.wp && wp.i18n && wp.i18n.__) ? wp.i18n.__ : function (s) { return s; };
	function t(s) { return __(s, 'smi-ipcheck'); }
	function sprintf() {
		var a = arguments, i = 1;
		return String(a[0]).replace(/%s/g, function () { return a[i++]; });
	}

	var root = document.getElementById('smi-ipcheck-app');
	if (!root) { return; }

	var elForm   = document.getElementById('smi-ipc-form');
	var elInput  = document.getElementById('smi-ipc-input');
	var elSelf   = document.getElementById('smi-ipc-self');
	var elMyip   = document.getElementById('smi-ipc-myip');
	var elError  = document.getElementById('smi-ipc-error');
	var elResults= document.getElementById('smi-ipc-results');
	var state = {};

	// ---- Helfer -------------------------------------------------------------
	function el(tag, cls, html) {
		var e = document.createElement(tag);
		if (cls) { e.className = cls; }
		if (html != null) { e.innerHTML = html; }
		return e;
	}
	function esc(s) {
		return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
			return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
		});
	}
	function api(path, params) {
		var qs = Object.keys(params || {}).map(function (k) { return k + '=' + encodeURIComponent(params[k]); }).join('&');
		return fetch(CFG.apiBase + '/' + path + (qs ? '?' + qs : ''), { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } })
			.then(function (r) { return r.json(); })
			.then(function (j) { if (!j || !j.ok) { throw new Error((j && j.error) || t('Es ist ein Fehler aufgetreten.')); } return j.data; });
	}
	function countryName(cc) {
		if (!cc) { return ''; }
		try { return new Intl.DisplayNames([CFG.lang || 'de'], { type: 'region' }).of(cc) || cc; }
		catch (e) { return cc; }
	}
	function flag(cc) {
		if (!cc || cc.length !== 2) { return ''; }
		return cc.toUpperCase().replace(/./g, function (c) { return String.fromCodePoint(127397 + c.charCodeAt(0)); });
	}
	function badge(text, cls) { return '<span class="smi-ipc__pill ' + (cls || '') + '">' + esc(text) + '</span>'; }
	function providerLabel(slug) {
		var m = { 'abuseipdb': 'AbuseIPDB', 'proxycheck': 'proxycheck.io', 'ip-api': 'ip-api.com', 'abusech': 'abuse.ch', 'greynoise': 'GreyNoise', 'ipqs': 'IPQualityScore', 'ipqualityscore': 'IPQualityScore', 'shodan': 'Shodan', 'internetdb': 'Shodan InternetDB', 'shodan-internetdb': 'Shodan InternetDB', 'otx': 'AlienVault OTX', 'alienvault': 'AlienVault OTX', 'crowdsec': 'CrowdSec', 'virustotal': 'VirusTotal', 'dshield': 'SANS ISC / DShield', 'isc': 'SANS ISC / DShield', 'stopforumspam': 'StopForumSpam', 'sfs': 'StopForumSpam', 'blocklistde': 'BlockList.de', 'blocklist-de': 'BlockList.de', 'ipinfo': 'IPinfo.io', 'pulsedive': 'Pulsedive', 'scamalytics': 'Scamalytics' };
		return m[String(slug).toLowerCase()] || slug;
	}
	function yesno(v) { return v ? badge(t('Ja'), 'is-ok') : badge(t('Nein'), 'is-bad'); }

	// Key/Value-Tabelle bauen. rows: [[label, valueHTML, note?], ...]
	function kv(rows) {
		var h = '<dl class="smi-ipc__kv">';
		rows.forEach(function (r) {
			if (r === null) { return; }
			h += '<div class="smi-ipc__kv-row"><dt>' + esc(r[0]) + '</dt><dd>' + (r[1] == null || r[1] === '' ? '<span class="smi-ipc__muted">—</span>' : r[1]) + (r[2] ? '<small class="smi-ipc__note">' + esc(r[2]) + '</small>' : '') + '</dd></div>';
		});
		return h + '</dl>';
	}
	function card(title, inner, icon) {
		return '<div class="smi-ipc__card"><h3 class="smi-ipc__card-title">' + (icon ? '<i class="bi bi-' + icon + '" aria-hidden="true"></i> ' : '') + esc(title) + '</h3>' + inner + '</div>';
	}
	function explain(text, level) { return '<p class="smi-ipc__explain smi-ipc__explain--' + (level || 'info') + '">' + esc(text) + '</p>'; }

	function setPanel(key, html) {
		var p = document.getElementById('smi-ipc-panel-' + key);
		if (p) { p.innerHTML = html; }
	}
	function skeleton() { return '<div class="smi-ipc__skeleton" aria-hidden="true"><span></span><span></span><span></span></div>'; }
	function loading(key) { setPanel(key, skeleton()); }

	// Fortschritts-/Status-Balken.
	var elProg = document.getElementById('smi-ipc-progress');
	var elProgFill = document.getElementById('smi-ipc-progress-fill');
	var elProgText = document.getElementById('smi-ipc-progress-text');
	function progressStart(label) { if (!elProg) { return; } elProg.hidden = false; elProg.classList.remove('is-done'); elProgFill.style.width = '8%'; elProgText.textContent = label || t('Analyse läuft …'); }
	function progressSet(done, total, label) { if (!elProgFill) { return; } var pct = total ? Math.max(8, Math.round(done / total * 100)) : 8; elProgFill.style.width = pct + '%'; if (label != null) { elProgText.textContent = label; } }
	function progressDone() { if (!elProg) { return; } elProgFill.style.width = '100%'; elProg.classList.add('is-done'); elProgText.textContent = t('Analyse abgeschlossen'); setTimeout(function () { if (elProg.classList.contains('is-done')) { elProg.hidden = true; } }, 1400); }
	function progressHide() { if (elProg) { elProg.hidden = true; } }

	function showError(msg) {
		elError.textContent = msg;
		elError.hidden = false;
	}
	function clearError() { elError.hidden = true; elError.textContent = ''; }

	// ---- Tab-Navigation (ARIA) ---------------------------------------------
	var tabs = Array.prototype.slice.call(root.querySelectorAll('.smi-ipc__tab'));
	tabs.forEach(function (tab) {
		tab.addEventListener('click', function () { activateTab(tab); });
		tab.addEventListener('keydown', function (e) {
			if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') { return; }
			e.preventDefault();
			var vis = tabs.filter(function (x) { return !x.hidden; });
			var idx = vis.indexOf(tab);
			if (idx < 0) { return; }
			var n = e.key === 'ArrowRight' ? (idx + 1) % vis.length : (idx - 1 + vis.length) % vis.length;
			vis[n].focus(); activateTab(vis[n]);
		});
	});
	function activateTab(tab) {
		if (!tab || tab.hidden) { tab = tabs.filter(function (x) { return !x.hidden; })[0]; }
		if (!tab) { return; }
		tabs.forEach(function (x) {
			var on = x === tab;
			x.classList.toggle('is-active', on);
			x.setAttribute('aria-selected', on ? 'true' : 'false');
			x.tabIndex = on ? 0 : -1;
			var panel = document.getElementById('smi-ipc-panel-' + x.dataset.tab);
			if (panel) { panel.hidden = !on; panel.classList.toggle('is-active', on); }
		});
	}

	// Tab (Button + Panel) ein-/ausblenden. Aktiven Tab bei Bedarf umschalten.
	function setTabVisible(key, visible) {
		var tab = document.getElementById('smi-ipc-tab-' + key);
		var panel = document.getElementById('smi-ipc-panel-' + key);
		if (tab) { tab.hidden = !visible; }
		if (panel && !visible) { panel.hidden = true; panel.classList.remove('is-active'); }
		if (!visible && tab && tab.classList.contains('is-active')) { activateTab(null); }
	}

	// ---- Risiko-Engine ------------------------------------------------------
	function computeRisk(s) {
		var score = 0, reasons = [];
		function add(pts, why) { score += pts; if (why) { reasons.push({ pts: pts, text: why }); } }

		if (s.blacklist && s.blacklist.supported) {
			var listed = s.blacklist.listed || 0;
			if (listed > 0) { add(Math.min(55, 18 + listed * 12), sprintf(t('Auf %s Blacklist(s) gelistet.'), listed)); }
		}
		if (s.core && s.core.detection) {
			if (s.core.detection.tor) { add(35, t('Tor-Exit-Node erkannt.')); }
			if (s.core.detection.is_hosting) { add(12, sprintf(t('Hosting/Rechenzentrum (%s).'), s.core.detection.provider || s.core.detection.type)); }
		}
		// Externe Reputation (Gateway).
		var intel = s.core && s.core.intel;
		if (intel && intel.ok) {
			if (intel.blacklisted) { add(60, t('Auf der Reputations-Blacklist des Gateways gelistet.')); }
			if (intel.verdict === 'malicious') { add(45, t('Reputationsdienst stuft die IP als bösartig ein.')); }
			else if (intel.verdict === 'suspicious') { add(22, t('Reputationsdienst stuft die IP als auffällig ein.')); }
			if (intel.score != null && intel.score >= 25) { add(Math.min(30, Math.round(intel.score / 3)), sprintf(t('Externer Reputations-Score: %s/100.'), intel.score)); }
		}
		if (s.core) {
			if (!s.core.reverse_dns) { add(8, t('Kein Reverse-DNS (PTR) vorhanden.')); }
			else if (s.core.fcrdns === false) { add(6, t('Vorwärts/Rückwärts-DNS stimmen nicht überein.')); }
		}
		if (s.mail && s.mail.has_mx) {
			if (!s.mail.spf.present) { add(6, t('Kein SPF-Record.')); }
			if (!s.mail.dmarc.present) { add(6, t('Kein DMARC-Record.')); }
			else if (s.mail.dmarc.policy === 'none') { add(3, t('DMARC nur im Überwachungsmodus (p=none).')); }
			if (s.mail.spf.too_open) { add(4, t('SPF zu offen (+all).')); }
		}
		if (s.url && s.url.ok) {
			if (!s.url.https) { add(10, t('Keine HTTPS-Verschlüsselung.')); }
			if (s.url.cert_valid === false) { add(15, t('SSL-Zertifikat ungültig/abgelaufen.')); }
			if (s.url.https && !s.url.hsts) { add(3, t('Kein HSTS.')); }
		}
		score = Math.max(0, Math.min(100, Math.round(score)));
		var level = score >= 65 ? 'red' : score >= 35 ? 'orange' : score >= 15 ? 'yellow' : 'green';
		var labelMap = { green: t('Unauffällig'), yellow: t('Hinweise'), orange: t('Erhöhtes Risiko'), red: t('Kritisch') };
		return { score: score, level: level, label: labelMap[level], reasons: reasons };
	}

	function renderScore(s) {
		var r = computeRisk(s);
		state.risk = r;
		var num = document.getElementById('smi-ipc-score-num');
		var gauge = document.getElementById('smi-ipc-gauge');
		var badgeEl = document.getElementById('smi-ipc-score-badge');
		var target = document.getElementById('smi-ipc-score-target');
		var desc = document.getElementById('smi-ipc-score-desc');
		var scoreBox = document.getElementById('smi-ipc-score');
		num.textContent = r.score;
		scoreBox.setAttribute('data-level', r.level);
		var circ = 2 * Math.PI * 52;
		gauge.style.strokeDasharray = circ;
		gauge.style.strokeDashoffset = circ * (1 - r.score / 100);
		badgeEl.className = 'smi-ipc__score-badge is-' + r.level;
		badgeEl.textContent = r.label;
		var tgt = (s.core && s.core.target_ip) || '';
		var host = (s.core && s.core.input && (s.core.input.domain || s.core.input.host)) || '';
		target.textContent = host && host !== tgt ? host + ' → ' + tgt : (tgt || host);
		desc.innerHTML = r.reasons.length
			? esc(sprintf(t('Bewertung „%s“, weil:'), r.label)) + ' ' + r.reasons.map(function (x) { return esc(x.text); }).join(' ')
			: esc(t('Keine auffälligen Merkmale gefunden. Diese IP/Domain wirkt unauffällig.'));
		var pdfBtn = document.getElementById('smi-ipc-pdf');
		if (pdfBtn && CFG.pdf) { pdfBtn.hidden = false; }
	}

	// ---- Renderer je Tab ----------------------------------------------------
	function highlight(label, value, cls) {
		return '<div class="smi-ipc__hl"><span class="smi-ipc__hl-label">' + esc(label) + '</span><span class="smi-ipc__hl-val ' + (cls || '') + '">' + value + '</span></div>';
	}
	function renderOverview(s) {
		var c = s.core; if (!c) { return; }
		var d = c.detection || {};
		var g = c.geo || {}, n = c.network || {};
		var typeLabel = { cloud: t('Cloud-Anbieter'), cdn: 'CDN', hosting: t('Hosting/Rechenzentrum'), isp: t('Privat / Business (ISP)'), unknown: t('Unbekannt') }[d.type] || d.type;

		// Wichtigste Kennzahlen als Kacheln.
		var hs = '';
		hs += highlight(t('Risiko'), (s.risk ? s.risk.score : '–') + ' / 100', s.risk ? 'is-' + s.risk.level : '');
		hs += highlight(t('IP-Version'), 'IPv' + c.ip_version);
		hs += highlight(t('Standort'), g.country_code ? flag(g.country_code) + ' ' + esc(g.country_code) : '—');
		hs += highlight(t('Einordnung'), esc(typeLabel));
		if (s.blacklist && s.blacklist.supported) { hs += highlight(t('Blacklist'), s.blacklist.listed > 0 ? (s.blacklist.listed + '×') : t('sauber'), s.blacklist.listed > 0 ? 'is-bad' : 'is-ok'); }
		if (s.mail && s.mail.has_mx) { var mok = s.mail.spf.present && s.mail.dmarc.present; hs += highlight('Mail', mok ? t('abgesichert') : t('Lücken'), mok ? 'is-ok' : 'is-warn'); }
		if (s.url && s.url.ok) { hs += highlight('HTTPS', s.url.https ? t('aktiv') : t('fehlt'), s.url.https ? 'is-ok' : 'is-bad'); }
		if (d.tor) { hs += highlight('Tor', t('Exit-Node'), 'is-bad'); }
		if (d.proxy) { hs += highlight('Proxy/VPN', t('erkannt'), 'is-warn'); }
		if (intelHasData(c.intel)) { var vm = { clean: [t('Unbedenklich'), 'is-ok'], suspicious: [t('Auffällig'), 'is-warn'], malicious: [t('Bösartig'), 'is-bad'] }[c.intel.verdict] || [t('Unbekannt'), 'is-neutral']; hs += highlight(t('Reputation'), vm[0], vm[1]); }

		var rows = [
			[t('Geprüfte IP'), '<strong>' + esc(c.target_ip) + '</strong> ' + badge('IPv' + c.ip_version, 'is-neutral') + (c.target_ips && c.target_ips.all.length > 1 ? ' <span class="smi-ipc__muted">' + sprintf(t('(+%s weitere)'), c.target_ips.all.length - 1) + '</span>' : '')],
			c.input && c.input.domain ? [t('Domain'), esc(c.input.domain)] : null,
			c.input && c.input.url ? [t('URL'), '<a href="' + esc(c.input.url) + '" target="_blank" rel="noopener nofollow">' + esc(c.input.url) + '</a>'] : null,
			[t('Reverse DNS'), c.reverse_dns ? esc(c.reverse_dns) + ' ' + (c.fcrdns ? badge('FCrDNS ✓', 'is-ok') : (c.fcrdns === false ? badge(t('nicht bestätigt'), 'is-warn') : '')) : '<span class="smi-ipc__muted">' + t('nicht gesetzt') + '</span>', c.reverse_dns ? '' : t('Für Webseiten meist unkritisch, für Mailserver problematisch.')],
			[t('Provider / ISP'), esc(n.org || g.isp || n.as_name || '')],
			[t('ASN'), n.asn ? esc(n.asn + (n.as_name ? ' · ' + n.as_name : '')) : ''],
			[t('Standort'), (g.country_code) ? flag(g.country_code) + ' ' + esc(countryName(g.country_code)) + (g.city ? ', ' + esc(g.city) : '') + (g.region ? ' (' + esc(g.region) + ')' : '') : ''],
			[t('Einordnung'), badge(typeLabel, d.is_hosting ? 'is-warn' : 'is-neutral') + (d.tor ? ' ' + badge('Tor-Exit', 'is-bad') : '') + (d.proxy ? ' ' + badge('Proxy/VPN', 'is-warn') : '') + (d.mobile ? ' ' + badge(t('Mobilfunk'), 'is-neutral') : '')]
		];
		var expl = '';
		if (d.tor) { expl += explain(t('Diese IP ist ein bekannter Tor-Exit-Node. Datenverkehr wird anonymisiert – bei sicherheitskritischen Diensten oft blockiert.'), 'warn'); }
		else if (d.proxy) { expl += explain(t('Diese IP wird als Proxy/VPN eingestuft. Das kann legitim sein (Datenschutz), bei manchen Diensten aber Zusatzprüfungen auslösen.'), 'info'); }
		if (d.is_hosting) { expl += explain(t('Die IP gehört vermutlich zu einem Rechenzentrum/Cloud-Anbieter. Das ist nicht automatisch gefährlich, kann aber bei Logins oder Mailversand auffällig wirken.'), 'info'); }
		if (!c.reverse_dns) { expl += explain(t('Reverse DNS fehlt. Für normale Webseiten meist unkritisch, für Mailserver aber problematisch.'), 'info'); }

		setPanel('overview', '<div class="smi-ipc__highlights">' + hs + '</div>' + card(t('Zusammenfassung der wichtigsten Daten'), kv(rows), 'clipboard-data') + expl);
	}

	function renderLocation(s) {
		var c = s.core; if (!c) { return; }
		var g = c.geo || {};
		var rows = [
			[t('Kontinent'), esc(g.continent)],
			[t('Land'), g.country_code ? flag(g.country_code) + ' ' + esc(countryName(g.country_code)) + ' (' + esc(g.country_code) + ')' : ''],
			[t('Region / Bundesland'), esc(g.region)],
			[t('Stadt'), esc(g.city)],
			[t('Postleitzahl'), esc(g.postal)],
			[t('Zeitzone'), esc(g.timezone)],
			[t('Koordinaten'), (g.latitude != null && g.longitude != null) ? esc(g.latitude.toFixed(4) + ', ' + g.longitude.toFixed(4)) : ''],
			g.map_url ? [t('Karte'), '<a href="' + esc(g.map_url) + '" target="_blank" rel="noopener noreferrer">' + t('Auf OpenStreetMap ansehen') + ' ↗</a>'] : null,
			[t('ISP'), esc(g.isp || (c.network && c.network.org))],
			[t('Organisation'), esc((c.network && (c.network.org || c.network.netname)) || '')],
			[t('Anschlusstyp'), esc({ cloud: t('Cloud'), cdn: 'CDN', hosting: t('Hosting/Rechenzentrum'), isp: t('Privat/Business (ISP)'), unknown: t('unbekannt') }[(c.detection || {}).type] || '') + ((c.detection || {}).mobile ? ' · ' + t('Mobilfunk') : '')]
		];
		setPanel('location', card(t('Standort & Provider'), kv(rows), 'geo-alt') + explain(t('IP-Standorte sind nur Näherungswerte und können vom tatsächlichen Aufenthaltsort abweichen.'), 'info'));
	}

	function renderNetwork(s) {
		var c = s.core; if (!c) { return; }
		var n = c.network || {}, w = c.whois_ip || {}, ro = c.routing || {};
		var rpkiBadge = n.rpki === 'valid' ? badge('RPKI ' + t('gültig'), 'is-ok') : (n.rpki === 'invalid' ? badge('RPKI ' + t('ungültig'), 'is-bad') : (n.rpki === 'unknown' ? badge('RPKI ' + t('unbekannt'), 'is-neutral') : ''));
		var announced = ro.announced != null ? ro.announced : (ro.visibility != null ? ro.visibility > 0 : null);
		var rows = [
			[t('ASN'), n.asn ? '<strong>' + esc(n.asn) + '</strong>' : ''],
			[t('AS-Name'), esc(n.as_name)],
			[t('Netzbereich (CIDR)'), esc(n.prefix) + (rpkiBadge ? ' ' + rpkiBadge : '')],
			[t('Netzname'), esc(n.netname || w.netname)],
			[t('Beschreibung'), esc(n.descr)],
			[t('Organisation'), esc(n.org || w.org)],
			[t('Zuständige Registry (RIR)'), esc(n.rir_name || n.rir)],
			[t('Zuteilungsdatum'), esc(n.alloc_date)],
			[t('Bereich'), (w.start && w.end) ? esc(w.start + ' – ' + w.end) : ''],
			[t('Angekündigt (BGP)'), announced == null ? '' : (announced ? badge(t('sichtbar'), 'is-ok') + (ro.visibility != null ? ' <span class="smi-ipc__muted">' + sprintf(t('%s Peers'), ro.visibility) + '</span>' : '') : badge(t('nicht sichtbar'), 'is-warn'))],
			ro.last_seen ? [t('Zuletzt gesehen'), esc(String(ro.last_seen).replace('T', ' ').replace(/:00$/, ''))] : null,
			[t('Registriert'), esc(n.created)],
			[t('Zuletzt geändert'), esc(n.modified)],
			[t('Admin-C'), esc(n.admin_c)],
			[t('Tech-C'), esc(n.tech_c)],
			[t('Verwaltet von (mnt-by)'), esc(n.mnt_by)],
			[t('Abuse-Kontakt'), n.abuse ? '<a href="mailto:' + esc(n.abuse) + '">' + esc(n.abuse) + '</a>' : '']
		];
		var raw = '';
		if (s.whoisRaw && s.whoisRaw.length) {
			raw = '<details class="smi-ipc__raw"><summary>' + t('WHOIS-Rohdaten anzeigen') + '</summary><pre>' + esc(s.whoisRaw.map(function (r) { return r.key + ': ' + r.value; }).join('\n')) + '</pre></details>';
		}
		setPanel('network', card(t('Netzwerk & WHOIS'), kv(rows), 'diagram-3') + raw);
	}

	function renderDns(s) {
		// Ohne Domain (reine IP) gibt es keinen DNS-Inhalt → Panel leer (Tab ist ausgeblendet).
		if (!s.core || !s.core.input || !s.core.input.domain) {
			setPanel('dns', '');
			return;
		}
		var d = s.dns; if (!d) { loading('dns'); return; }
		function recTable(title, recs, fmt) {
			if (!recs || !recs.length) { return ''; }
			var h = '<h4 class="smi-ipc__sub">' + esc(title) + '</h4><table class="smi-ipc__table"><tbody>';
			recs.forEach(function (r) { h += '<tr><td>' + fmt(r) + '</td><td class="smi-ipc__ttl">' + (r.ttl != null ? 'TTL ' + r.ttl : '') + '</td></tr>'; });
			return h + '</tbody></table>';
		}
		var rec = d.records || {};
		var html = '';
		html += recTable(t('A (IPv4)'), rec.A, function (r) { return esc(r.value); });
		html += recTable(t('AAAA (IPv6)'), rec.AAAA, function (r) { return esc(r.value); });
		html += recTable('CNAME', rec.CNAME, function (r) { return esc(r.value); });
		html += recTable('MX', rec.MX, function (r) { return esc(r.value); });
		html += recTable('NS', rec.NS, function (r) { return esc(r.value); });
		html += recTable('TXT', rec.TXT, function (r) { return '<code>' + esc(r.value) + '</code>'; });
		if (rec.SOA && rec.SOA[0]) {
			var so = rec.SOA[0];
			html += '<h4 class="smi-ipc__sub">SOA</h4>' + kv([[t('Primärer NS'), esc(so.primary)], [t('Verantwortlich'), esc(so.email)], ['Serial', esc(so.serial)]]);
		}
		var meta = kv([
			['SPF', d.spf ? '<code>' + esc(d.spf) + '</code>' : badge(t('fehlt'), 'is-warn')],
			['DMARC', d.dmarc ? '<code>' + esc(d.dmarc) + '</code>' : badge(t('fehlt'), 'is-warn')]
		]);
		var fcr = '';
		if (d.fcrdns && d.fcrdns.length) {
			fcr = '<h4 class="smi-ipc__sub">' + t('Reverse-DNS-Abgleich') + '</h4>' + kv(d.fcrdns.map(function (f) {
				return [f.ip, f.ptr ? esc(f.ptr) + ' ' + (f.confirmed ? badge('✓', 'is-ok') : badge('≠', 'is-warn')) : badge(t('kein PTR'), 'is-warn')];
			}));
		}
		setPanel('dns', card(t('DNS-Einträge') + ' – ' + esc(d.domain), html || explain(t('Keine DNS-Einträge gefunden.'), 'info'), 'hdd-network') + card(t('E-Mail-Absicherung (DNS)'), meta + fcr, 'shield-lock'));
	}

	function renderSecurity(s) {
		var r = state.risk || computeRisk(s);
		var reasons = r.reasons.length
			? '<ul class="smi-ipc__reasons">' + r.reasons.map(function (x) { return '<li>' + esc(x.text) + '</li>'; }).join('') + '</ul>'
			: explain(t('Es wurden keine negativen Reputationssignale gefunden.'), 'ok');
		var pos = [];
		if (s.blacklist && s.blacklist.supported && s.blacklist.listed === 0) { pos.push(t('Auf keiner der geprüften Spam-Blacklists gelistet.')); }
		if (s.core && s.core.reverse_dns && s.core.fcrdns) { pos.push(t('Reverse-DNS ist gesetzt und bestätigt (FCrDNS).')); }
		if (s.url && s.url.https && s.url.cert_valid) { pos.push(t('Gültiges SSL-Zertifikat, HTTPS aktiv.')); }
		var posHtml = pos.length ? '<ul class="smi-ipc__reasons smi-ipc__reasons--ok">' + pos.map(function (p) { return '<li>' + esc(p) + '</li>'; }).join('') + '</ul>' : '';
		var urlCard = '';
		if (s.url && s.url.ok) { urlCard = renderUrlCard(s.url); }
		var intelCard = renderIntelCard(s.core && s.core.intel);
		setPanel('security',
			card(sprintf(t('Risiko-Score: %s / 100'), r.score) + ' – ' + r.label, reasons, 'shield-exclamation') +
			intelCard +
			(posHtml ? card(t('Positive Signale'), posHtml, 'check-circle') : '') +
			urlCard);
	}

	function intelHasData(intel) {
		return intel && intel.ok && ((intel.providers && intel.providers.length > 0) || intel.blacklisted || (intel.verdict && intel.verdict !== 'unknown'));
	}
	function renderIntelCard(intel) {
		if (!intelHasData(intel)) { return ''; }
		var verdictMap = { clean: [t('Unbedenklich'), 'is-ok'], suspicious: [t('Auffällig'), 'is-warn'], malicious: [t('Bösartig'), 'is-bad'], unknown: [t('Unbekannt'), 'is-neutral'] };
		var vm = verdictMap[intel.verdict] || verdictMap.unknown;
		var head = kv([
			[t('Gesamt-Verdict'), badge(vm[0], vm[1])],
			intel.score != null ? [t('Reputations-Score'), '<strong>' + esc(intel.score) + '</strong> / 100'] : null,
			[t('Blacklist (Gateway)'), intel.blacklisted ? badge(t('gelistet'), 'is-bad') + (intel.blacklist && intel.blacklist.reason ? ' <span class="smi-ipc__muted">' + esc(intel.blacklist.reason) + '</span>' : '') : badge(t('nicht gelistet'), 'is-ok')]
		]);
		var prov = '';
		if (intel.providers && intel.providers.length) {
			prov = '<h4 class="smi-ipc__sub">' + t('Anbieter-Signale') + '</h4><table class="smi-ipc__table"><tbody>' + intel.providers.map(function (p) {
				var flags = [];
				if (p.is_tor) { flags.push(badge('Tor', 'is-bad')); }
				if (p.is_vpn) { flags.push(badge('VPN', 'is-warn')); }
				if (p.is_proxy) { flags.push(badge('Proxy', 'is-warn')); }
				if (p.is_hosting) { flags.push(badge(t('Hosting'), 'is-neutral')); }
				// abuse.ch/Threat-Feeds: positiver Score = Malware-/C2-Treffer.
				if (/abusech|threatfox|feodo/i.test(p.provider) && p.score != null && p.score > 0) { flags.unshift(badge(t('Malware-C2'), 'is-bad')); }
				var scoreHtml = '';
				if (p.score != null) {
					var sc = p.score >= 75 ? 'is-bad' : (p.score >= 25 ? 'is-warn' : 'is-ok');
					scoreHtml = badge(esc(p.score) + '/100', sc);
				}
				var val = p.error ? badge(t('nicht erreichbar'), 'is-warn') : (scoreHtml + ' ' + flags.join(' ') + (p.country ? ' ' + flag(p.country) : '')).trim();
				return '<tr><td>' + esc(providerLabel(p.provider)) + '</td><td>' + (val || '<span class="smi-ipc__muted">' + t('keine Auffälligkeiten') + '</span>') + '</td></tr>';
			}).join('') + '</tbody></table>';
		}
		return card(t('Externe IP-Reputation (VPN-geroutet)'), head + prov, 'shield-fill-check') + explain(t('Diese Werte stammen aus dem hauseigenen IP-Intel-Gateway (mehrere Reputationsdienste über VPN-Egress). Blacklist-Treffer und ein hoher Score erhöhen das Gesamt-Risiko.'), 'info');
	}

	function renderUrlCard(u) {
		var rows = [
			[t('Finale URL'), '<a href="' + esc(u.final_url) + '" target="_blank" rel="noopener noreferrer nofollow">' + esc(u.final_url) + '</a>'],
			[t('HTTP-Status'), esc(u.final_status)],
			[t('Weiterleitungen'), esc(u.redirects)],
			['HTTPS', yesno(u.https)],
			[t('SSL-Zertifikat'), u.cert && u.cert.issuer ? esc(u.cert.issuer) + (u.cert_days_left != null ? ' · ' + sprintf(t('%s Tage gültig'), u.cert_days_left) : '') + ' ' + (u.cert_valid ? badge('✓', 'is-ok') : badge('✗', 'is-bad')) : ''],
			['HSTS', yesno(u.hsts)],
			[t('Server'), esc(u.server)],
			[t('Ladezeit'), u.time_ms != null ? esc(u.time_ms) + ' ms' : '']
		];
		var sec = '<h4 class="smi-ipc__sub">Security-Header</h4><table class="smi-ipc__table"><tbody>';
		Object.keys(u.security).forEach(function (k) {
			sec += '<tr><td><code>' + esc(k) + '</code></td><td>' + (u.security[k] ? badge('✓', 'is-ok') : badge('—', 'is-warn')) + '</td></tr>';
		});
		sec += '</tbody></table>';
		var warn = !u.https ? explain(t('Diese Seite nutzt kein HTTPS. Eingaben können unverschlüsselt übertragen werden.'), 'warn') : '';
		return card(t('URL-/HTTP-/TLS-Analyse'), kv(rows) + sec + warn, 'globe2');
	}

	function renderBlacklist(s) {
		var b = s.blacklist;
		if (!b) { loading('blacklist'); return; }
		if (!b.supported) { setPanel('blacklist', card(t('Blacklist-Check'), explain(t('Blacklist-Prüfung ist nur für öffentliche IPv4-Adressen verfügbar.'), 'info'), 'list-check')); return; }
		var head = '<div class="smi-ipc__blsum">' + badge(sprintf(t('%s geprüft'), b.checked), 'is-neutral')
			+ ' ' + (b.listed > 0 ? badge(sprintf(t('%s gelistet'), b.listed), 'is-bad') : badge(t('nicht gelistet'), 'is-ok'))
			+ (b.failed > 0 ? ' ' + badge(sprintf(t('%s fehlgeschlagen'), b.failed), 'is-warn') : '') + '</div>';
		var rows = '<table class="smi-ipc__table smi-ipc__bltable"><thead><tr><th>' + t('Liste') + '</th><th>' + t('Status') + '</th></tr></thead><tbody>';
		b.results.forEach(function (r) {
			var st = r.status === 'listed'
				? badge(t('gelistet'), 'is-bad') + (r.site ? ' <a href="' + esc(r.site) + '" target="_blank" rel="noopener noreferrer">' + t('Delisting') + ' ↗</a>' : '')
				: (r.status === 'error' ? badge(t('Prüfung fehlgeschlagen'), 'is-warn') : badge(t('sauber'), 'is-ok'));
			rows += '<tr><td>' + esc(r.name) + '<br><small class="smi-ipc__muted">' + esc(r.zone) + '</small></td><td>' + st + (r.reason ? '<br><small class="smi-ipc__muted">' + esc(r.reason) + '</small>' : '') + '</td></tr>';
		});
		rows += '</tbody></table>';
		var note = b.listed > 0
			? explain(t('Einträge können veraltet sein (False Positives). Nutze die Delisting-Links, um eine Entfernung zu beantragen.'), 'warn')
			: explain(t('Deine/diese IP ist auf keiner der geprüften Spam-Blacklists gelistet.'), 'ok');
		setPanel('blacklist', card(t('Spam-/Blacklist-Prüfung'), head + rows, 'list-check') + note);
	}

	function renderMail(s) {
		if (!s.core || !s.core.input || !s.core.input.domain) {
			setPanel('mail', '');
			return;
		}
		var m = s.mail; if (!m) { loading('mail'); return; }
		if (!m.has_mx) { setPanel('mail', card(t('Mailserver-Check'), explain(t('Diese Domain hat keinen MX-Record und empfängt vermutlich keine E-Mails.'), 'info'), 'envelope')); return; }
		var mx = '<h4 class="smi-ipc__sub">MX-Server</h4><table class="smi-ipc__table"><tbody>' + m.mx.map(function (x) {
			return '<tr><td>' + esc(x.pri) + ' ' + esc(x.host) + '</td><td>' + (x.resolves ? badge(t('erreichbar'), 'is-ok') : badge(t('löst nicht auf'), 'is-bad')) + '</td></tr>';
		}).join('') + '</tbody></table>';
		var rows = kv([
			['SPF', m.spf.present ? (m.spf.too_open ? badge(t('vorhanden, aber zu offen'), 'is-warn') : badge(t('vorhanden'), 'is-ok')) : badge(t('fehlt'), 'is-bad'), m.spf.present ? '' : t('Ohne SPF ist Absender-Fälschung leichter möglich.')],
			['DKIM', m.dkim.found ? badge(sprintf(t('Hinweis gefunden (Selektor %s)'), m.dkim.selector), 'is-ok') : badge(t('nicht gefunden'), 'is-warn')],
			['DMARC', m.dmarc.present ? badge('p=' + (m.dmarc.policy || '?'), m.dmarc.policy === 'reject' ? 'is-ok' : (m.dmarc.policy === 'none' ? 'is-warn' : 'is-neutral')) : badge(t('fehlt'), 'is-bad'), m.dmarc.present ? '' : t('Ohne DMARC lassen sich gefälschte E-Mails schwerer verhindern.')],
			['MTA-STS', yesno(m.mta_sts.present)],
			['TLS-RPT', yesno(m.tls_rpt.present)],
			['BIMI', yesno(m.bimi.present)],
			['DNSSEC', m.dnssec == null ? '' : yesno(m.dnssec)]
		]);
		var warns = (m.warnings || []).map(function (w) { return explain(w.text, w.level === 'error' ? 'warn' : (w.level === 'warn' ? 'warn' : 'info')); }).join('');
		setPanel('mail', card(t('Mailserver & E-Mail-Sicherheit') + ' – ' + esc(m.domain), mx + rows, 'envelope-check') + warns);
	}

	function renderDetails(s) {
		var c = s.core || {};
		var b = navigator || {};
		var d = c.detection || {};
		function tri(v) { return v == null ? '<span class="smi-ipc__muted">' + t('unbekannt') + '</span>' : yesno(v); }
		var rows = [
			[t('Zeitpunkt der Prüfung'), esc(new Date().toLocaleString(CFG.lang || 'de'))],
			[t('Alle Ziel-IPs'), c.target_ips ? esc((c.target_ips.all || []).join(', ')) : ''],
			[t('Proxy / VPN erkannt'), tri(d.proxy)],
			[t('Mobilfunk-Anschluss'), tri(d.mobile)],
			[t('Rechenzentrum / Hosting'), tri(d.hosting_flag != null ? d.hosting_flag : d.is_hosting)],
			[t('Tor-Exit-Node'), tri(d.tor)],
			[t('Browser-Sprache'), esc((b.language || '') + (b.languages && b.languages.length > 1 ? ' (' + b.languages.join(', ') + ')' : ''))],
			[t('User-Agent'), '<code class="smi-ipc__ua">' + esc(b.userAgent || '') + '</code>'],
			[t('Plattform'), esc(b.platform || '')]
		];
		var raw = '';
		if (s.url && s.url.chain) {
			raw = card(t('Redirect-Kette'), '<ol class="smi-ipc__chain">' + s.url.chain.map(function (h) { return '<li>' + esc(h.url) + ' → ' + (h.ok ? esc(h.status) : esc(h.error)) + '</li>'; }).join('') + '</ol>', 'arrow-repeat');
		}
		setPanel('details', card(t('Technische Details'), kv(rows), 'gear') + raw);
	}

	function renderAll(s) {
		renderScore(s);
		renderOverview(s);
		renderLocation(s);
		renderNetwork(s);
		renderDns(s);
		renderSecurity(s);
		renderBlacklist(s);
		renderMail(s);
		renderDetails(s);
	}

	// ---- Orchestrierung -----------------------------------------------------
	function mod(name) { return CFG.modules[name] !== false; }
	function assignInto(dst, src) { if (src) { for (var k in src) { if (Object.prototype.hasOwnProperty.call(src, k)) { dst[k] = src[k]; } } } return dst; }

	function resetScoreCard(target) {
		var sn = document.getElementById('smi-ipc-score-num'); if (sn) { sn.textContent = '…'; }
		var sb = document.getElementById('smi-ipc-score-badge'); if (sb) { sb.textContent = ''; sb.className = 'smi-ipc__score-badge'; }
		var st = document.getElementById('smi-ipc-score-target'); if (st) { st.textContent = target; }
		var sd = document.getElementById('smi-ipc-score-desc'); if (sd) { sd.textContent = t('Analyse läuft …'); }
		var sbox = document.getElementById('smi-ipc-score'); if (sbox) { sbox.removeAttribute('data-level'); }
		var g = document.getElementById('smi-ipc-gauge'); if (g) { g.style.strokeDashoffset = 2 * Math.PI * 52; }
		var pdfB = document.getElementById('smi-ipc-pdf'); if (pdfB) { pdfB.hidden = true; }
	}

	// ---- Scan-Anzeige (animiert) --------------------------------------------
	var elScan = document.getElementById('smi-ipc-scan');
	var elScoreCard = document.getElementById('smi-ipc-scorecard');
	function scanStart(target, checks) {
		if (!elScan) { return; }
		var items = checks.map(function (c) {
			return '<li data-check="' + esc(c.key) + '"><span class="smi-ipc__check-ico" aria-hidden="true"></span><span>' + esc(c.label) + '</span></li>';
		}).join('');
		elScan.innerHTML =
			'<div class="smi-ipc__scanner">' +
				'<div class="smi-ipc__radar" aria-hidden="true"><span class="smi-ipc__radar-grid"></span><span class="smi-ipc__radar-sweep"></span><span class="smi-ipc__radar-dot"></span></div>' +
				'<div class="smi-ipc__scan-info">' +
					'<h2 class="smi-ipc__scan-title">' + t('Scanne') + ' <b>' + esc(target) + '</b> …</h2>' +
					'<ul class="smi-ipc__scan-list">' + items + '</ul>' +
				'</div>' +
			'</div>';
		elScan.hidden = false;
		if (elScoreCard) { elScoreCard.hidden = true; }
	}
	function scanStep(key, status) {
		if (!elScan) { return; }
		var li = elScan.querySelector('li[data-check="' + key + '"]');
		if (li) { li.classList.remove('is-running', 'is-done', 'is-failed'); li.classList.add('is-' + status); }
	}
	function scanEnd() {
		if (elScan) { elScan.hidden = true; elScan.innerHTML = ''; }
		if (elScoreCard) { elScoreCard.hidden = false; elScoreCard.classList.remove('is-reveal'); void elScoreCard.offsetWidth; elScoreCard.classList.add('is-reveal'); }
	}

	// ---- Orchestrierung: auflösen → parallele Einzelabfragen ----------------
	function analyze(query, isSelf) {
		clearError();
		elResults.hidden = false;
		progressStart(t('Löse Ziel auf …'));
		['overview', 'location', 'network', 'dns', 'security', 'blacklist', 'mail', 'details'].forEach(loading);
		setTabVisible('dns', false); setTabVisible('mail', false);
		activateTab(tabs[0]);
		state = {};
		resetScoreCard(isSelf ? t('Deine IP-Adresse') : query);
		if (elScoreCard) { elScoreCard.hidden = false; }
		elResults.scrollIntoView({ behavior: 'smooth', block: 'start' });

		var rp = isSelf ? { self: '1', lang: CFG.lang || 'de' } : { q: query, lang: CFG.lang || 'de' };
		api('resolve.php', rp).then(function (rez) {
			state.core = {
				input: rez.input, target_ip: rez.target_ip, ip_version: rez.ip_version,
				target_ips: rez.target_ips, reverse_dns: rez.reverse_dns, fcrdns: rez.fcrdns, is_self: rez.is_self,
				geo: {}, network: {}, detection: {}, whois_ip: {}, routing: {}, intel: null
			};
			if (isSelf) { state.myip = rez.target_ip; }
			var domain = rez.input.domain, ip = rez.target_ip, isV4 = rez.ip_version === 4;
			setTabVisible('dns', !!domain); setTabVisible('mail', !!domain);
			renderOverview(state); renderLocation(state); renderNetwork(state); renderDetails(state);

			// Scan-Checkliste je nach Eingabetyp.
			var checks = [];
			if (mod('geo')) { checks.push({ key: 'geo', label: t('Standort & Provider') }); }
			if (mod('asn')) { checks.push({ key: 'network', label: t('Netzwerk & ASN') }); }
			if (mod('rdap')) { checks.push({ key: 'whois', label: t('WHOIS & Routing') }); }
			if (CFG.intel) { checks.push({ key: 'reputation', label: t('IP-Reputation') }); }
			if (mod('blacklist') && isV4) { checks.push({ key: 'blacklist', label: t('Spam-Blacklists') }); }
			if (domain && mod('dns')) { checks.push({ key: 'dns', label: t('DNS-Einträge') }); }
			if (domain && mod('mail')) { checks.push({ key: 'mail', label: t('Mailserver') }); }
			if (rez.input.type === 'url' && rez.input.url && mod('url')) { checks.push({ key: 'url', label: t('URL & TLS') }); }
			scanStart(domain || rez.input.host || ip, checks);

			var total = checks.length, done = 0;
			function stepDone(key, ok) { scanStep(key, ok ? 'done' : 'failed'); done++; progressSet(done, total, done >= total ? t('Auswertung …') : sprintf(t('Scanne Datenpunkte … %s/%s'), done, total)); }
			var tasks = [];
			function add(key, promise, onData) {
				scanStep(key, 'running');
				tasks.push(promise.then(function (d) { try { onData(d); } catch (e) {} stepDone(key, true); }, function () { stepDone(key, false); }));
			}

			if (mod('geo')) { add('geo', api('geoloc.php', { ip: ip, lang: CFG.lang || 'de' }), function (d) { state.core.geo = d.geo; assignInto(state.core.detection, d.detection); renderLocation(state); renderDetails(state); renderOverview(state); }); }
			if (mod('asn')) { add('network', api('network.php', { ip: ip }), function (d) { assignInto(state.core.network, d.network); assignInto(state.core.detection, d.detection); renderNetwork(state); renderOverview(state); renderScore(state); }); }
			if (mod('rdap')) { add('whois', api('ipwhois.php', { ip: ip }), function (d) { mergeWhois(d); renderNetwork(state); renderOverview(state); }); }
			if (CFG.intel) { add('reputation', api('reputation.php', { ip: ip }), function (d) { state.core.intel = d.intel; applyIntelDetection(); renderScore(state); renderSecurity(state); renderOverview(state); }); }
			if (mod('blacklist') && isV4) { add('blacklist', api('blacklist.php', { ip: ip }), function (d) { state.blacklist = d; renderBlacklist(state); renderScore(state); renderSecurity(state); renderOverview(state); }); }
			else { setPanel('blacklist', card(t('Blacklist-Check'), explain(t('Blacklist-Prüfung ist nur für öffentliche IPv4-Adressen verfügbar.'), 'info'), 'list-check')); }
			if (domain) {
				if (mod('dns')) { add('dns', api('dnsx.php', { domain: domain }), function (d) { state.dns = d; renderDns(state); }); } else { renderDns(state); }
				if (mod('mail')) { add('mail', api('mailsec.php', { domain: domain }), function (d) { state.mail = d; renderMail(state); renderScore(state); renderSecurity(state); renderOverview(state); }); } else { renderMail(state); }
			} else { renderDns(state); renderMail(state); }
			if (rez.input.type === 'url' && rez.input.url && mod('url')) { add('url', api('urlcheck.php', { url: rez.input.url }), function (d) { state.url = d; renderScore(state); renderSecurity(state); renderDetails(state); renderOverview(state); }); }

			return Promise.all(tasks);
		}).then(function () {
			scanEnd();
			renderScore(state); renderSecurity(state); renderOverview(state);
			progressDone();
			var pdfB = document.getElementById('smi-ipc-pdf'); if (pdfB && CFG.pdf) { pdfB.hidden = false; }
		}).catch(function (e) {
			progressHide(); scanEnd(); elResults.hidden = true;
			showError(e.message || t('Es ist ein Fehler aufgetreten.'));
		});
	}
	function mergeWhois(d) {
		var w = d.whois || {}, c = state.core;
		c.whois_ip = { netname: w.netname, org: w.org, country: w.country, start: w.start, end: w.end, abuse: w.abuse };
		assignInto(c.network, {
			org: w.org || c.network.org, netname: w.netname || c.network.netname, descr: w.descr,
			admin_c: w.admin_c, tech_c: w.tech_c, mnt_by: w.mnt_by, created: w.created, modified: w.modified,
			abuse: w.abuse || c.network.abuse, rpki: w.rpki, route: w.route, country: c.network.country || w.country
		});
		c.routing = d.routing || {};
	}
	function applyIntelDetection() {
		var it = state.core.intel; if (!it || !it.ok) { return; }
		(it.providers || []).forEach(function (p) {
			if (p.is_tor) { state.core.detection.tor = true; }
			if (p.is_vpn || p.is_proxy) { state.core.detection.proxy = true; }
			if (p.is_hosting) { state.core.detection.is_hosting = true; }
		});
	}

	// ---- Events -------------------------------------------------------------
	elForm.addEventListener('submit', function (e) {
		e.preventDefault();
		var q = elInput.value.trim();
		if (!q) { showError(t('Bitte gib eine IP-Adresse, Domain oder URL ein.')); return; }
		analyze(q, false);
	});
	elSelf.addEventListener('click', function () {
		if (state.myip) { elInput.value = state.myip; }
		analyze(null, true);
	});

	// Eigene IP beim Laden ermitteln (nur Anzeige, schneller resolve-Endpoint).
	api('resolve.php', { self: '1' }).then(function (core) {
		state.myip = core.target_ip;
		elMyip.innerHTML = '<i class="bi bi-hdd-network" aria-hidden="true"></i> ' + t('Deine öffentliche IP:') + ' <strong>' + esc(core.target_ip) + '</strong> ' + badge('IPv' + core.ip_version, 'is-neutral');
	}).catch(function () {});

	// Auto-Scan über ?scan=<ziel> (z. B. aus dem PDF-QR-Code „diese IP neu scannen").
	(function () {
		try {
			var m = /[?&]scan=([^&]+)/.exec(location.search);
			if (m) {
				var q = decodeURIComponent(m[1].replace(/\+/g, ' ')).trim().slice(0, 2048);
				if (q) { elInput.value = q; analyze(q, false); }
			}
		} catch (e) {}
	})();

	// PDF-Bericht: echtes PDF via jsPDF (lokal gebündelt, lazy geladen).
	var pdfBtn = document.getElementById('smi-ipc-pdf');
	if (pdfBtn) { pdfBtn.addEventListener('click', downloadPdf); }

	function loadScript(src) {
		return new Promise(function (resolve, reject) {
			var el = document.createElement('script'); el.src = src;
			el.onload = resolve; el.onerror = function () { reject(new Error('load ' + src)); };
			document.head.appendChild(el);
		});
	}
	var _pdfLibReady = false;
	function ensurePdfLib() {
		if (_pdfLibReady && window.jspdf && window.jspdf.jsPDF) { return Promise.resolve(); }
		var base = CFG.vendorUrl || '';
		return loadScript(base + 'jspdf.umd.min.js')
			.then(function () { return loadScript(base + 'jspdf.plugin.autotable.min.js'); })
			.then(function () { return loadScript(base + 'qrcode.min.js').catch(function () {}); })
			.then(function () { _pdfLibReady = true; });
	}
	// QR-Code in ein PDF zeichnen (schwarze Module auf weißem Grund).
	function drawQr(doc, text, x, y, size) {
		if (!window.qrcode) { return; }
		try {
			var qr = window.qrcode(0, 'M'); qr.addData(String(text)); qr.make();
			var n = qr.getModuleCount(), cell = size / n;
			doc.setFillColor(0, 0, 0);
			for (var r = 0; r < n; r++) { for (var c = 0; c < n; c++) { if (qr.isDark(r, c)) { doc.rect(x + c * cell, y + r * cell, cell + 0.03, cell + 0.03, 'F'); } } }
		} catch (e) {}
	}
	function qrBox(doc, text, x, y, size, pad) {
		pad = pad == null ? 1.6 : pad;
		doc.setFillColor(255, 255, 255); doc.roundedRect(x - pad, y - pad, size + 2 * pad, size + 2 * pad, 1.2, 1.2, 'F');
		drawQr(doc, text, x, y, size);
	}
	function toolUrl() { return location.origin + location.pathname; }
	function downloadPdf() {
		if (!state.core) { return; }
		var b = pdfBtn, old = b ? b.innerHTML : '';
		if (b) { b.disabled = true; b.innerHTML = '<i class="bi bi-hourglass-split" aria-hidden="true"></i> ' + t('PDF wird erstellt …'); }
		ensurePdfLib().then(buildPdf).catch(function () { showError(t('PDF-Erstellung fehlgeschlagen.')); })
			.then(function () { if (b) { b.disabled = false; b.innerHTML = old; } });
	}

	function typeLabelPdf(d) {
		return ({ cloud: 'Cloud-Anbieter', cdn: 'CDN', hosting: 'Hosting/Rechenzentrum', isp: 'Privat/Business (ISP)', unknown: 'unbekannt' }[d.type] || d.type || '') + (d.tor ? ' · Tor-Exit' : '') + (d.proxy ? ' · Proxy/VPN' : '');
	}
	function buildSummary(s, r) {
		var c = s.core, g = c.geo || {}, n = c.network || {}, d = c.detection || {}, parts = [];
		parts.push('Die geprüfte Adresse ' + c.target_ip + ' gehört zu ' + (n.org || n.as_name || 'einem unbekannten Netz') + (g.country_code ? ' und wird ' + (g.city ? g.city + ', ' : '') + countryName(g.country_code) + ' zugeordnet' : '') + '.');
		parts.push('Einordnung: ' + typeLabelPdf(d) + '.');
		if (s.blacklist && s.blacklist.supported) { parts.push(s.blacklist.listed > 0 ? ('Sie ist auf ' + s.blacklist.listed + ' von ' + s.blacklist.checked + ' geprüften Spam-Blacklists gelistet.') : ('Sie ist auf keiner der ' + s.blacklist.checked + ' geprüften Spam-Blacklists gelistet.')); }
		parts.push('Der Gesamt-Risiko-Score liegt bei ' + r.score + '/100 – Einstufung: ' + r.label + '.');
		parts.push({ green: 'Kurz gesagt: hier ist alles im grünen Bereich.', yellow: 'Kurz gesagt: nichts Dramatisches, aber ein Blick auf die Details lohnt sich.', orange: 'Kurz gesagt: diese Adresse solltest du im Auge behalten.', red: 'Kurz gesagt: hier ist Vorsicht geboten.' }[r.level]);
		return parts.join(' ');
	}
	function buildRecommendations(s) {
		var c = s.core, d = c.detection || {}, out = [];
		if (s.blacklist && s.blacklist.listed > 0) { out.push('Diese IP ist auf Spam-Blacklists gelistet. Kläre die Ursache (offener Mailserver, kompromittiertes Gerät) und nutze die Delisting-Formulare der betroffenen Listen.'); }
		if (!c.reverse_dns) { out.push('Es ist kein Reverse-DNS (PTR) gesetzt. Für Mailserver unbedingt einrichten – sonst landen E-Mails schneller im Spam.'); }
		if (s.mail && s.mail.has_mx) {
			if (!s.mail.spf.present) { out.push('Kein SPF-Record: SPF einrichten, um Absender-Fälschung zu erschweren.'); }
			if (!s.mail.dmarc.present) { out.push('Kein DMARC-Record: DMARC ergänzen (mindestens p=quarantine), um Spoofing zu verhindern.'); }
			else if (s.mail.dmarc.policy === 'none') { out.push('DMARC steht auf p=none (nur Monitoring). Verschärfe die Policy auf quarantine oder reject.'); }
		}
		if (s.url && s.url.ok && !s.url.https) { out.push('Die geprüfte URL nutzt kein HTTPS. Stelle auf verschlüsselte Verbindungen (TLS) um.'); }
		if (d.tor || d.proxy) { out.push('Die IP wird als Tor/Proxy/VPN erkannt. Bei Logins oder Mailversand kann das Zusatzprüfungen oder Blockaden auslösen.'); }
		if (!out.length) { out.push('Kein akuter Handlungsbedarf – die Adresse ist unauffällig. Schütze deine eigene Identität trotzdem mit starken Passwörtern und einem VPN.'); }
		out.push('Mehr kostenlose Sicherheits-Tools findest du auf www.safemyident.de.');
		return out;
	}

	function buildPdf() {
		var s = state, c = s.core, g = c.geo || {}, n = c.network || {}, d = c.detection || {};
		var r = s.risk || computeRisk(s);
		var doc = new window.jspdf.jsPDF({ unit: 'mm', format: 'a4' });
		var W = 210, M = 14, CW = W - 2 * M;
		var COL = { navy: [20, 36, 60], accent: [255, 194, 71], ink: [26, 35, 51], muted: [110, 125, 150], line: [224, 229, 238], soft: [247, 248, 251], green: [46, 190, 115], yellow: [224, 170, 30], orange: [240, 150, 20], red: [233, 70, 90] };
		var lvl = { green: COL.green, yellow: COL.yellow, orange: COL.orange, red: COL.red }[r.level];
		var sF = function (a) { doc.setFillColor(a[0], a[1], a[2]); };
		var sT = function (a) { doc.setTextColor(a[0], a[1], a[2]); };
		var sD = function (a) { doc.setDrawColor(a[0], a[1], a[2]); };
		var target = (c.input && (c.input.domain || c.input.url)) || c.target_ip;

		// Header-Band.
		sF(COL.navy); doc.rect(0, 0, W, 30, 'F');
		sT(COL.accent); doc.setFont('helvetica', 'bold'); doc.setFontSize(17); doc.text('SafeMyIdent.de', M, 14);
		sT([255, 255, 255]); doc.setFont('helvetica', 'normal'); doc.setFontSize(8.5); doc.text('IP-, WHOIS-, DNS- & Sicherheitsanalyse', M, 20);
		// QR oben rechts → zum IP-Checker.
		var hqr = 17, hqrX = W - M - hqr, hqrY = 6;
		qrBox(doc, toolUrl(), hqrX, hqrY, hqr, 1.3);
		sT([255, 255, 255]); doc.setFont('helvetica', 'bold'); doc.setFontSize(11.5); doc.text('IP-SICHERHEITSBERICHT', hqrX - 4, 13, { align: 'right' });
		doc.setFont('helvetica', 'normal'); doc.setFontSize(7.5); sT([180, 195, 215]); doc.text(new Date().toLocaleString('de-DE'), hqrX - 4, 19, { align: 'right' });
		doc.setFontSize(6.5); doc.text('Zum Checker »', hqrX + hqr / 2, hqrY + hqr + 3, { align: 'center' });

		// Clickbait-Headline.
		var headlines = { green: 'Entwarnung: Alles im grünen Bereich', yellow: 'Ein paar Dinge solltest du wissen', orange: 'Achtung: erhöhtes Risiko erkannt', red: 'Kritisch! Diese Adresse fällt negativ auf' };
		var y = 42;
		sT(COL.ink); doc.setFont('helvetica', 'bold'); doc.setFontSize(17);
		var hl = doc.splitTextToSize(headlines[r.level] || r.label, CW); doc.text(hl, M, y); y += hl.length * 7 + 2;
		sT(COL.muted); doc.setFont('helvetica', 'normal'); doc.setFontSize(10.5);
		doc.text('Geprüftes Ziel: ' + target, M, y); y += 8;

		// Score-Hero.
		var hY = y, hH = 30;
		sD(COL.line); sF(COL.soft); doc.roundedRect(M, hY, CW, hH, 3, 3, 'FD');
		var cx = M + 18, cy = hY + hH / 2;
		sF(lvl); doc.circle(cx, cy, 12, 'F');
		sT([255, 255, 255]); doc.setFont('helvetica', 'bold'); doc.setFontSize(18); doc.text(String(r.score), cx, cy + 1, { align: 'center' });
		doc.setFont('helvetica', 'normal'); doc.setFontSize(6.5); doc.text('von 100', cx, cy + 6, { align: 'center' });
		var tx = M + 36;
		sT(COL.ink); doc.setFont('helvetica', 'bold'); doc.setFontSize(13); doc.text('Risiko: ' + r.label, tx, hY + 10);
		var barY = hY + 15, barW = CW - (tx - M) - 8;
		sF([230, 233, 240]); doc.roundedRect(tx, barY, barW, 4, 2, 2, 'F');
		sF(lvl); doc.roundedRect(tx, barY, Math.max(4, barW * r.score / 100), 4, 2, 2, 'F');
		sT(COL.muted); doc.setFont('helvetica', 'normal'); doc.setFontSize(8.5); doc.text('0 = unauffällig    ·    100 = kritisch', tx, barY + 9);
		y = hY + hH + 7;

		// Executive Summary.
		sT(COL.ink); doc.setFont('helvetica', 'bold'); doc.setFontSize(11); doc.text('Das Wichtigste in Kürze', M, y); y += 5;
		sT([70, 80, 100]); doc.setFont('helvetica', 'normal'); doc.setFontSize(9.5);
		var sum = doc.splitTextToSize(buildSummary(s, r), CW); doc.text(sum, M, y); y += sum.length * 4.6 + 5;

		function section(title, rows) {
			rows = rows.filter(function (x) { return x && x[1] !== '' && x[1] != null; });
			if (!rows.length) { return; }
			doc.autoTable({
				startY: y, head: [[{ content: title, colSpan: 2 }]], body: rows, theme: 'grid',
				styles: { fontSize: 9, cellPadding: 1.8, textColor: COL.ink, lineColor: COL.line, lineWidth: 0.1, overflow: 'linebreak' },
				headStyles: { fillColor: COL.navy, textColor: [255, 255, 255], fontSize: 10, fontStyle: 'bold', cellPadding: 2.2, halign: 'left' },
				alternateRowStyles: { fillColor: [250, 251, 253] },
				columnStyles: { 0: { fontStyle: 'bold', textColor: COL.muted, cellWidth: 52 }, 1: { cellWidth: 'auto' } },
				margin: { left: M, right: M }
			});
			y = doc.lastAutoTable.finalY + 5;
		}

		section('Standort & Provider', [
			['Land', g.country_code ? countryName(g.country_code) + ' (' + g.country_code + ')' : ''],
			['Region / Stadt', [g.region, g.city].filter(Boolean).join(' / ')],
			['Zeitzone', g.timezone], ['ISP / Organisation', n.org || g.isp || n.as_name],
			['Einordnung', typeLabelPdf(d)]
		]);
		section('Netzwerk & WHOIS', [
			['Geprüfte IP', c.target_ip + ' (IPv' + c.ip_version + ')'],
			['Reverse DNS', c.reverse_dns ? c.reverse_dns + (c.fcrdns ? ' (bestätigt)' : '') : 'nicht gesetzt'],
			['ASN', (n.asn || '') + (n.as_name ? ' – ' + n.as_name : '')],
			['Netzbereich (CIDR)', n.prefix], ['Registry (RIR)', n.rir_name || n.rir], ['Abuse-Kontakt', n.abuse]
		]);
		section('Sicherheit & Reputation', [
			['Reputations-Score', r.score + ' / 100 – ' + r.label],
			(s.blacklist && s.blacklist.supported) ? ['Blacklist', s.blacklist.listed > 0 ? (s.blacklist.listed + ' von ' + s.blacklist.checked + ' Listen') : ('auf keiner von ' + s.blacklist.checked + ' Listen')] : null,
			(c.intel && c.intel.ok && c.intel.verdict !== 'unknown') ? ['Externe Reputation', c.intel.verdict + (c.intel.score != null ? ' (' + c.intel.score + '/100)' : '')] : null
		]);
		// Bewertungsgründe.
		if (y > 250) { doc.addPage(); y = 20; }
		sT(COL.ink); doc.setFont('helvetica', 'bold'); doc.setFontSize(9.5); doc.text('Bewertungsgründe:', M, y); y += 4.5;
		sT([70, 80, 100]); doc.setFont('helvetica', 'normal'); doc.setFontSize(9);
		(r.reasons.length ? r.reasons : [{ text: 'Keine negativen Signale gefunden.' }]).forEach(function (x) {
			var ln = doc.splitTextToSize('•  ' + x.text, CW - 2); doc.text(ln, M + 1, y); y += ln.length * 4.3;
		});
		y += 4;

		if (s.mail && s.mail.has_mx) {
			section('Mailserver-Sicherheit (' + (s.mail.domain || '') + ')', [
				['SPF', s.mail.spf.present ? (s.mail.spf.too_open ? 'vorhanden, zu offen' : 'vorhanden') : 'fehlt'],
				['DKIM', s.mail.dkim.found ? 'Hinweis gefunden' : 'nicht gefunden'],
				['DMARC', s.mail.dmarc.present ? 'p=' + (s.mail.dmarc.policy || '?') : 'fehlt'],
				['MTA-STS', s.mail.mta_sts.present ? 'ja' : 'nein'],
				['DNSSEC', s.mail.dnssec == null ? '' : (s.mail.dnssec ? 'ja' : 'nein')]
			]);
		}
		if (s.url && s.url.ok) {
			section('URL / TLS', [
				['Finale URL', s.url.final_url], ['HTTP-Status', String(s.url.final_status)],
				['HTTPS', s.url.https ? 'aktiv' : 'fehlt'],
				['SSL-Zertifikat', (s.url.cert && s.url.cert.issuer) ? (s.url.cert.issuer + (s.url.cert_days_left != null ? ' (' + s.url.cert_days_left + ' Tage gültig)' : '')) : ''],
				['HSTS', s.url.hsts ? 'ja' : 'nein']
			]);
		}

		// Empfehlungen-Box.
		var recs = buildRecommendations(s);
		var boxH = 11 + recs.reduce(function (a, rc) { return a + doc.splitTextToSize('•  ' + rc, CW - 8).length * 4.6; }, 0) + 3;
		if (y + boxH > 280) { doc.addPage(); y = 20; }
		sF([255, 248, 230]); sD(COL.accent); doc.setLineWidth(0.3); doc.roundedRect(M, y, CW, boxH, 2, 2, 'FD');
		sT(COL.ink); doc.setFont('helvetica', 'bold'); doc.setFontSize(11); doc.text('So gehst du jetzt vor', M + 4, y + 7);
		var ry = y + 13; sT([60, 70, 90]); doc.setFont('helvetica', 'normal'); doc.setFontSize(9.5);
		recs.forEach(function (rc) { var ln = doc.splitTextToSize('•  ' + rc, CW - 8); doc.text(ln, M + 4, ry); ry += ln.length * 4.6; });

		// ---- Marketing-Teaser mit Re-Scan-QR ----
		var teaserH = 48;
		if (y + teaserH > 284) { doc.addPage(); y = 20; }
		sF(COL.navy); doc.roundedRect(M, y, CW, teaserH, 3, 3, 'F');
		sF(COL.accent); doc.roundedRect(M, y, 2.6, teaserH, 1.3, 1.3, 'F');
		var rq = 27, rqX = M + CW - rq - 7, rqY = y + (teaserH - rq) / 2 - 1;
		qrBox(doc, toolUrl() + '?scan=' + encodeURIComponent(target), rqX, rqY, rq, 1.6);
		sT([225, 232, 244]); doc.setFont('helvetica', 'bold'); doc.setFontSize(7.3); doc.text('Diese IP neu scannen', rqX + rq / 2, rqY + rq + 4, { align: 'center' });
		var tX = M + 9, tW = rqX - tX - 6, yy = y + 9;
		sT(COL.accent); doc.setFont('helvetica', 'bold'); doc.setFontSize(13);
		var hl2 = doc.splitTextToSize('IP erneut checken? In 60 Sekunden Klarheit.', tW); doc.text(hl2, tX, yy); yy += hl2.length * 5.4 + 1.5;
		sT([215, 224, 238]); doc.setFont('helvetica', 'normal'); doc.setFontSize(8.6);
		var pl = doc.splitTextToSize('Deine IP-Reputation ist kein Standbild: Blacklists, Bedrohungen und Provider-Daten ändern sich täglich. Scanne genau diese Adresse jederzeit neu – 100 % kostenlos, ohne Anmeldung, in Sekunden.', tW); doc.text(pl, tX, yy); yy += pl.length * 4 + 2;
		sT([150, 165, 190]); doc.setFontSize(7.4);
		var pvl = doc.splitTextToSize('Datenschutz: Wir speichern deine Eingaben nicht – der Bericht lebt nur in deinem Browser und in dieser PDF. Zum Entfernen einfach die Datei löschen; für frische Ergebnisse den QR-Code scannen.', tW); doc.text(pvl, tX, yy); yy += pvl.length * 3.5 + 1;
		sT(COL.accent); doc.setFont('helvetica', 'bold'); doc.setFontSize(8.8); doc.text('› Jetzt kostenlos prüfen auf SafeMyIdent.de', tX, Math.min(yy + 2.5, y + teaserH - 3.5));
		y += teaserH + 6;

		// Footer auf allen Seiten.
		var pages = doc.internal.getNumberOfPages();
		for (var p = 1; p <= pages; p++) {
			doc.setPage(p); sD(COL.line); doc.setLineWidth(0.2); doc.line(M, 287, W - M, 287);
			sT(COL.muted); doc.setFont('helvetica', 'normal'); doc.setFontSize(7.5);
			doc.text('Erstellt mit SafeMyIdent · Daten aus öffentlichen Quellen, können abweichen. IP-Standorte sind Näherungswerte.', M, 291);
			doc.text('Seite ' + p + '/' + pages, W - M, 291, { align: 'right' });
		}
		doc.save('SafeMyIdent-IP-Bericht-' + String(target).replace(/[^a-z0-9.\-]/gi, '_').slice(0, 40) + '.pdf');
	}
})();

Youez - 2016 - github.com/yon3zu
LinuXploit