/** * Prueft den Geraete-Vergleich zweier Protokolle. * * ALLE Faelle arbeiten mit Geraeten OHNE MAC - genau so kommen sie beim Kunden * an (/proc/net/arp ist ab Android 10 gesperrt; im ausgelieferten Protokoll * ND2026-0015 waren bei allen 68 Geraeten MAC, Hostname und Hersteller leer). * Der Browser-Mock dagegen gibt jedem Geraet eine MAC - dort saehe auch ein * naiver MAC-Diff perfekt aus und wuerde nichts beweisen. * * Start: node tools/pruefe-geraetediff.mjs */ const { vergleicheGeraete, bewerteDiff, geraeteSchluessel } = await import('../src/lib/tools/netzwerk/geraete-diff.ts'); // Geraete wie sie real ankommen: keine MAC, oft kein Name. const kamera = { ip: '192.168.1.51', mdnsName: 'IPC-Hof', openPorts: [80, 554] }; const drucker = { ip: '192.168.1.20', mdnsName: 'Brother HL-L2350DW', openPorts: [80, 9100, 515] }; const pc = { ip: '192.168.1.30', netbiosName: 'BUERO-PC', openPorts: [139, 445] }; const nas = { ip: '192.168.1.10', hostname: 'nas.fritz.box', openPorts: [80, 445, 5000] }; const namenlos= { ip: '192.168.1.77', openPorts: [502] }; // Wechselrichter, Modbus const router = { ip: '192.168.1.1', hostname: 'fritz.box', openPorts: [80, 443, 53] }; let fehler = 0; function pruefe(name, ist, soll) { const ok = JSON.stringify(ist) === JSON.stringify(soll); if (!ok) fehler++; console.log(`${ok ? 'OK ' : 'FEHL'} ${name}`); if (!ok) console.log(` ist=${JSON.stringify(ist)} soll=${JSON.stringify(soll)}`); } // 1. Identisch -> keine Veraenderung let d = vergleicheGeraete([router, nas, kamera], [router, nas, kamera]); pruefe('Unveraendertes Netz -> 0 neu, 0 weg, 3 geblieben', [d.neu.length, d.verschwunden.length, d.geblieben.length], [0, 0, 3]); // 2. DER KERNFALL: dieselben Geraete OHNE MAC, aber mit neuen DHCP-Adressen. // Ein MAC-Diff haette hier "alles neu, alles verschwunden" gemeldet. const kameraNeueIp = { ...kamera, ip: '192.168.1.99' }; const druckerNeueIp = { ...drucker, ip: '192.168.1.98' }; d = vergleicheGeraete([kamera, drucker], [kameraNeueIp, druckerNeueIp]); pruefe('Beide Geraete mit neuer IP -> 0 neu, 0 weg, 2 mit IP-Wechsel', [d.neu.length, d.verschwunden.length, d.ipGewechselt.length], [0, 0, 2]); pruefe('IP-Wechsel nennt die fruehere Adresse', d.ipGewechselt.map((e) => e.frühereIp).sort(), ['192.168.1.20', '192.168.1.51']); // 3. Echtes neues Geraet d = vergleicheGeraete([router, nas], [router, nas, kamera]); pruefe('Ein Geraet dazugekommen', [d.neu.length, d.neu[0]?.name], [1, 'IPC-Hof']); // 4. Geraet verschwunden d = vergleicheGeraete([router, nas, kamera], [router, nas]); pruefe('Ein Geraet verschwunden', [d.verschwunden.length, d.verschwunden[0]?.name], [1, 'IPC-Hof']); // 5. Namenloses Geraet: gleiche IP + gleiche Ports -> wiedererkannt d = vergleicheGeraete([namenlos], [{ ...namenlos }]); pruefe('Namenlos, IP+Ports gleich -> wiedererkannt', [d.geblieben.length, d.neu.length], [1, 0]); // 6. Namenloses Geraet mit neuer IP -> NICHT zuordenbar, ehrlich als neu+weg d = vergleicheGeraete([namenlos], [{ ...namenlos, ip: '192.168.1.78' }]); pruefe('Namenlos mit neuer IP -> gilt als neu UND verschwunden (Grenze des Verfahrens)', [d.neu.length, d.verschwunden.length], [1, 1]); // 6b. DER FALL AUS DEM ERSTEN PDF: gleiche IP, anderes Portmuster. // Darf NICHT gleichzeitig als neu und verschwunden erscheinen. d = vergleicheGeraete( [{ ip: '10.0.2.2', openPorts: [80] }], [{ ip: '10.0.2.2', openPorts: [80, 443] }], ); pruefe('Gleiche IP, andere Ports -> eigene Kategorie statt "neu UND weg"', [d.neu.length, d.verschwunden.length, d.portsGeaendert.length], [0, 0, 1]); // 7. Erkennungsart wird korrekt benannt pruefe('Kaskade: mDNS schlaegt Hostname', geraeteSchluessel({ ip: '1.2.3.4', mdnsName: 'A', hostname: 'B' }).erkennung, 'mdns'); pruefe('Kaskade: NetBIOS vor Hostname', geraeteSchluessel({ ip: '1.2.3.4', netbiosName: 'A', hostname: 'B' }).erkennung, 'netbios'); pruefe('Kaskade: ohne alles -> ip+ports', geraeteSchluessel({ ip: '1.2.3.4', openPorts: [80] }).erkennung, 'ip+ports'); pruefe('Kaskade: MAC schlaegt alles (falls ausnahmsweise da)', geraeteSchluessel({ ip: '1.2.3.4', mac: 'AA:BB:CC:11:22:33', mdnsName: 'A' }).erkennung, 'mac'); // 8. Schreibweise: Gross/Klein und .local duerfen kein Unterschied sein d = vergleicheGeraete( [{ ip: '10.0.0.5', mdnsName: 'Drucker.local' }], [{ ip: '10.0.0.5', mdnsName: 'drucker' }], ); pruefe('mDNS "Drucker.local" == "drucker"', [d.geblieben.length, d.neu.length], [1, 0]); // 9. Portmuster-Reihenfolge darf egal sein d = vergleicheGeraete( [{ ip: '10.0.0.9', openPorts: [443, 80] }], [{ ip: '10.0.0.9', openPorts: [80, 443] }], ); pruefe('Portreihenfolge egal', [d.geblieben.length, d.neu.length], [1, 0]); // 10. Bewertung let b = bewerteDiff(vergleicheGeraete([router, nas], [router, nas]), 2); pruefe('Keine Veraenderung -> GRUEN', b.status, 0); b = bewerteDiff(vergleicheGeraete([router, nas, kamera], [router, nas]), 3); pruefe('Geraet fehlt -> GELB mit Einschraenkung', [b.status, /Standby/.test(b.hinweis || '')], [1, true]); b = bewerteDiff(vergleicheGeraete([], []), 0); pruefe('Kein Vorgaengerprotokoll -> NICHT MESSBAR', b.status, 3); b = bewerteDiff(vergleicheGeraete([namenlos], [namenlos]), 1); pruefe('Nur schwache Zuordnung -> GELB mit Warnung', [b.status, /IP und Portmuster/.test(b.hinweis || '')], [1, true]); b = bewerteDiff(vergleicheGeraete([router, nas], [router, nas, kamera]), 2); pruefe('Nur neue Geraete -> GRUEN (neu ist kein Fehler)', b.status, 0); b = bewerteDiff(vergleicheGeraete( [{ ip: '10.0.2.2', openPorts: [80] }], [{ ip: '10.0.2.2', openPorts: [80, 443] }]), 1); pruefe('Portwechsel wird im Label genannt', /andere.*Ports/.test(b.label), true); console.log(fehler === 0 ? 'ALLE FAELLE KORREKT' : `${fehler} FEHLER`); process.exit(fehler === 0 ? 0 : 1);