generated from Dicken/dickendock
round48: masscan/httpx-Fehlermeldungen im Scan-Log sichtbar machen, masscan-Status nur bei vollstaendigem Portscan anzeigen
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user