round48: masscan/httpx-Fehlermeldungen im Scan-Log sichtbar machen, masscan-Status nur bei vollstaendigem Portscan anzeigen

This commit is contained in:
2026-07-26 08:29:11 +02:00
parent 2e15669098
commit 28fedb2572
4 changed files with 90 additions and 6 deletions

View File

@@ -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 {

View File

@@ -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<Map<number,
};
child.on("error", (err) => {
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<Map<number,
});
const timeout = setTimeout(() => {
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<Map<number,
child.on("close", (code) => {
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<number, HttpxResult>();
for (const line of stdout.split("\n")) {
const trimmed = line.trim();

View File

@@ -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);
}
);

View File

@@ -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,
},
};
}