From 28fedb257233cf22392b326c7bff7d3bc80bac5f Mon Sep 17 00:00:00 2001 From: Dicken Date: Sun, 26 Jul 2026 08:29:11 +0200 Subject: [PATCH] round48: masscan/httpx-Fehlermeldungen im Scan-Log sichtbar machen, masscan-Status nur bei vollstaendigem Portscan anzeigen --- apps/backend/src/routes/scan.ts | 29 +++++++++++++++++- apps/backend/src/scanner/httpx.ts | 14 +++++++++ apps/backend/src/scanner/masscan.ts | 18 +++++++++++ apps/backend/src/scanner/networkScanner.ts | 35 ++++++++++++++++++---- 4 files changed, 90 insertions(+), 6 deletions(-) diff --git a/apps/backend/src/routes/scan.ts b/apps/backend/src/routes/scan.ts index 4bd29c8..ab493bf 100644 --- a/apps/backend/src/routes/scan.ts +++ b/apps/backend/src/routes/scan.ts @@ -21,6 +21,33 @@ import { } from "../scanJobs.js"; import type { Device } from "@launchpad/shared"; +/** + * Formatiert den masscan/httpx-Status für die Scan-Log-Zeile. Zeigt bei + * masscan "nicht relevant" statt "nicht genutzt", wenn es bei diesem Scan + * gar nicht versucht wurde (masscan läuft nur beim vollständigen Portscan, + * siehe networkScanner.ts) - und hängt bei echten Fehlschlägen die konkrete + * Fehlermeldung an, statt nur "nicht genutzt/verfügbar" ohne Erklärung. + */ +function formatToolsUsed(toolsUsed: { + masscan: boolean; + masscanAttempted: boolean; + masscanError: string | null; + httpx: boolean; + httpxError: string | null; +}): string { + const masscanPart = !toolsUsed.masscanAttempted + ? "masscan: nicht relevant (kein Volltest)" + : toolsUsed.masscan + ? "masscan: ✓ genutzt" + : `masscan: ✗ fehlgeschlagen${toolsUsed.masscanError ? ` (${toolsUsed.masscanError})` : " (Grund unbekannt)"}`; + + const httpxPart = toolsUsed.httpx + ? "httpx: ✓ genutzt" + : `httpx: ✗ fehlgeschlagen${toolsUsed.httpxError ? ` (${toolsUsed.httpxError})` : " (Grund unbekannt)"}`; + + return `${masscanPart}, ${httpxPart}`; +} + /** * Scannt EIN Gerät (Ports, Titel/Favicon, Softwareerkennung) und schreibt die * Ergebnisse in die Datenbank. Ausgelagert aus der Route, damit sowohl der @@ -113,7 +140,7 @@ async function performDeviceScan(device: Device, fullScan = false) { type: "device", targetId: device.id, level: "info", - message: `${device.hostname} (${device.ip}): ${discovered.length} Dienst(e) gefunden (Ports: ${ports.join(", ") || "keine"}), ${created} neu, ${updated} aktualisiert${staleServices.length > 0 ? `, ${staleServices.length} nicht mehr gefunden` : ""}${nameChanges.length > 0 ? `, ${nameChanges.length} Namensänderung(en) vorgeschlagen` : ""} [masscan: ${toolsUsed.masscan ? "✓ genutzt" : "nicht genutzt/verfügbar"}, httpx: ${toolsUsed.httpx ? "✓ genutzt" : "nicht genutzt/verfügbar"}]`, + message: `${device.hostname} (${device.ip}): ${discovered.length} Dienst(e) gefunden (Ports: ${ports.join(", ") || "keine"}), ${created} neu, ${updated} aktualisiert${staleServices.length > 0 ? `, ${staleServices.length} nicht mehr gefunden` : ""}${nameChanges.length > 0 ? `, ${nameChanges.length} Namensänderung(en) vorgeschlagen` : ""} [${formatToolsUsed(toolsUsed)}]`, }); return { diff --git a/apps/backend/src/scanner/httpx.ts b/apps/backend/src/scanner/httpx.ts index c82dc0d..4ec5e61 100644 --- a/apps/backend/src/scanner/httpx.ts +++ b/apps/backend/src/scanner/httpx.ts @@ -10,10 +10,19 @@ export interface HttpxResult { let httpxConfirmedUnavailable = false; +// Klartext-Grund des letzten Fehlschlags (stderr/Fehlermeldung) - wird im +// Scan-Log angezeigt, damit sichtbar ist WARUM httpx nicht greift, statt nur +// "nicht genutzt/verfügbar" ohne jede Erklärung. +let httpxLastError: string | null = null; + export function isHttpxConfirmedUnavailable(): boolean { return httpxConfirmedUnavailable; } +export function getHttpxLastError(): string | null { + return httpxLastError; +} + /** * Nutzt httpx (ProjectDiscovery, falls im Container installiert - siehe * Dockerfile) um von einer Liste offener Ports (z. B. das Ergebnis von @@ -61,6 +70,7 @@ export function probeWithHttpx(ip: string, ports: number[]): Promise { + httpxLastError = err.message.slice(0, 400); // ENOENT (Programm fehlt) - künftig direkt auf den Fallback gehen, // statt bei jedem Scan erneut (dann garantiert erfolglos) zu versuchen. if ((err as NodeJS.ErrnoException).code === "ENOENT") { @@ -77,6 +87,7 @@ export function probeWithHttpx(ip: string, ports: number[]): Promise { + httpxLastError = "Zeitüberschreitung (30s) - httpx hat nicht rechtzeitig geantwortet"; child.kill(); finish(null); }, 30_000); @@ -84,11 +95,14 @@ export function probeWithHttpx(ip: string, ports: number[]): Promise { clearTimeout(timeout); if (code !== 0 && stdout.trim().length === 0) { + httpxLastError = (stderr.trim() || `httpx beendet mit Exit-Code ${code}`).slice(0, 400); if (/not found|no such file/i.test(stderr)) httpxConfirmedUnavailable = true; finish(null); return; } + httpxLastError = null; // erfolgreicher Lauf - alten Fehler nicht mehr anzeigen + const results = new Map(); for (const line of stdout.split("\n")) { const trimmed = line.trim(); diff --git a/apps/backend/src/scanner/masscan.ts b/apps/backend/src/scanner/masscan.ts index 3bf3e64..a39b948 100644 --- a/apps/backend/src/scanner/masscan.ts +++ b/apps/backend/src/scanner/masscan.ts @@ -2,6 +2,16 @@ import { execFile } from "node:child_process"; let masscanConfirmedUnavailable = false; +// Klartext-Grund des letzten Fehlschlags (stderr/Fehlermeldung von masscan +// selbst) - wird im Scan-Log angezeigt, damit man sieht WARUM masscan nicht +// greift (z. B. fehlende Rechte trotz cap_add, kein Interface gefunden), +// statt nur "nicht genutzt/verfügbar" ohne jede Erklärung. +let masscanLastError: string | null = null; + +export function getMasscanLastError(): string | null { + return masscanLastError; +} + /** * Nutzt masscan (falls im Container installiert, siehe Dockerfile) für den * vollständigen Portscan (1-65535) - masscan verschickt SYN-Pakete @@ -28,6 +38,13 @@ export async function scanWithMasscan( { timeout: 60_000 }, (error, stdout, stderr) => { if (error) { + // Klartext-Fehler für den Aufrufer/das Scan-Log merken - stderr + // enthält bei masscan i. d. R. den eigentlichen Grund (z. B. + // "FAIL:.. Permission denied", "could not detect interface"), + // error.message oft nur den generischen Exit-Code. + const rawReason = (stderr?.trim() || error.message).slice(0, 400); + masscanLastError = rawReason; + // ENOENT (Programm fehlt) oder fehlende Rechte (EPERM/"PCAP" o. ä. // in stderr) - beides bedeutet: masscan für diese Installation // dauerhaft nicht nutzbar, künftig direkt auf den Fallback gehen @@ -57,6 +74,7 @@ export async function scanWithMasscan( if (Number.isFinite(port)) ports.push(port); } } + masscanLastError = null; // erfolgreicher Lauf - alten Fehler nicht mehr anzeigen resolve(ports); } ); diff --git a/apps/backend/src/scanner/networkScanner.ts b/apps/backend/src/scanner/networkScanner.ts index 74e9c2e..3f75900 100644 --- a/apps/backend/src/scanner/networkScanner.ts +++ b/apps/backend/src/scanner/networkScanner.ts @@ -1,7 +1,7 @@ import { resolveHostname, reverseLookup } from "./dns.js"; import { isPortOpen, scanPortsInBatches, fullPortRange, TYPICAL_PORTS } from "./ports.js"; -import { scanWithMasscan } from "./masscan.js"; -import { probeWithHttpx } from "./httpx.js"; +import { scanWithMasscan, getMasscanLastError } from "./masscan.js"; +import { probeWithHttpx, getHttpxLastError } from "./httpx.js"; import { probeHttp } from "./http.js"; import { detectSoftware } from "./softwareDetection.js"; import { findBestIconMatch } from "./iconDb.js"; @@ -86,7 +86,18 @@ export interface DeviceScanResult { * gegriffen hat oder (mangels Installation/Rechten) auf die eingebaute * Methode zurückgefallen wurde. */ - toolsUsed: { masscan: boolean; httpx: boolean }; + toolsUsed: { + masscan: boolean; + /** Ob masscan bei diesem Scan überhaupt versucht wurde (nur beim + * vollständigen Portscan, siehe unten) - bei "false" ist "masscan: + * nicht genutzt" also KEIN Fehler, sondern schlicht nicht relevant. */ + masscanAttempted: boolean; + /** Klartext-Fehlermeldung, falls masscan versucht wurde und fehlschlug. */ + masscanError: string | null; + httpx: boolean; + /** Klartext-Fehlermeldung, falls httpx fehlschlug. */ + httpxError: string | null; + }; } /** @@ -122,10 +133,13 @@ export async function scanDeviceServices( // Ports"-Liste (~30 Ports) macht masscan kaum einen Unterschied, das // eingebaute Scanning reicht dafür völlig. let usedMasscan = false; + let masscanError: string | null = null; + const masscanAttempted = candidatePorts.length > 1000; let openPorts: number[]; - if (candidatePorts.length > 1000) { + if (masscanAttempted) { const masscanResult = await scanWithMasscan(device.ip, "1-65535"); usedMasscan = masscanResult !== null; + if (!usedMasscan) masscanError = getMasscanLastError(); openPorts = masscanResult ?? (await scanPortsInBatches(device.ip, candidatePorts)); } else { openPorts = await scanPortsInBatches(device.ip, candidatePorts); @@ -180,6 +194,7 @@ export async function scanDeviceServices( // geprüft, Protokoll wird pro Port erraten). const httpxResults = openPorts.length > 0 ? await probeWithHttpx(device.ip, openPorts) : new Map(); const usingHttpx = httpxResults !== null; + const httpxError = !usingHttpx ? getHttpxLastError() : null; if (usingHttpx) { openPorts = Array.from(httpxResults!.keys()); } @@ -339,5 +354,15 @@ export async function scanDeviceServices( } } - return { services: found, suggestedHostname, toolsUsed: { masscan: usedMasscan, httpx: usingHttpx } }; + return { + services: found, + suggestedHostname, + toolsUsed: { + masscan: usedMasscan, + masscanAttempted, + masscanError, + httpx: usingHttpx, + httpxError, + }, + }; }