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";
|
} from "../scanJobs.js";
|
||||||
import type { Device } from "@launchpad/shared";
|
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
|
* Scannt EIN Gerät (Ports, Titel/Favicon, Softwareerkennung) und schreibt die
|
||||||
* Ergebnisse in die Datenbank. Ausgelagert aus der Route, damit sowohl der
|
* Ergebnisse in die Datenbank. Ausgelagert aus der Route, damit sowohl der
|
||||||
@@ -113,7 +140,7 @@ async function performDeviceScan(device: Device, fullScan = false) {
|
|||||||
type: "device",
|
type: "device",
|
||||||
targetId: device.id,
|
targetId: device.id,
|
||||||
level: "info",
|
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 {
|
return {
|
||||||
|
|||||||
@@ -10,10 +10,19 @@ export interface HttpxResult {
|
|||||||
|
|
||||||
let httpxConfirmedUnavailable = false;
|
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 {
|
export function isHttpxConfirmedUnavailable(): boolean {
|
||||||
return httpxConfirmedUnavailable;
|
return httpxConfirmedUnavailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getHttpxLastError(): string | null {
|
||||||
|
return httpxLastError;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nutzt httpx (ProjectDiscovery, falls im Container installiert - siehe
|
* Nutzt httpx (ProjectDiscovery, falls im Container installiert - siehe
|
||||||
* Dockerfile) um von einer Liste offener Ports (z. B. das Ergebnis von
|
* 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) => {
|
child.on("error", (err) => {
|
||||||
|
httpxLastError = err.message.slice(0, 400);
|
||||||
// ENOENT (Programm fehlt) - künftig direkt auf den Fallback gehen,
|
// ENOENT (Programm fehlt) - künftig direkt auf den Fallback gehen,
|
||||||
// statt bei jedem Scan erneut (dann garantiert erfolglos) zu versuchen.
|
// statt bei jedem Scan erneut (dann garantiert erfolglos) zu versuchen.
|
||||||
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
||||||
@@ -77,6 +87,7 @@ export function probeWithHttpx(ip: string, ports: number[]): Promise<Map<number,
|
|||||||
});
|
});
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
|
httpxLastError = "Zeitüberschreitung (30s) - httpx hat nicht rechtzeitig geantwortet";
|
||||||
child.kill();
|
child.kill();
|
||||||
finish(null);
|
finish(null);
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
@@ -84,11 +95,14 @@ export function probeWithHttpx(ip: string, ports: number[]): Promise<Map<number,
|
|||||||
child.on("close", (code) => {
|
child.on("close", (code) => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
if (code !== 0 && stdout.trim().length === 0) {
|
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;
|
if (/not found|no such file/i.test(stderr)) httpxConfirmedUnavailable = true;
|
||||||
finish(null);
|
finish(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
httpxLastError = null; // erfolgreicher Lauf - alten Fehler nicht mehr anzeigen
|
||||||
|
|
||||||
const results = new Map<number, HttpxResult>();
|
const results = new Map<number, HttpxResult>();
|
||||||
for (const line of stdout.split("\n")) {
|
for (const line of stdout.split("\n")) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ import { execFile } from "node:child_process";
|
|||||||
|
|
||||||
let masscanConfirmedUnavailable = false;
|
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
|
* Nutzt masscan (falls im Container installiert, siehe Dockerfile) für den
|
||||||
* vollständigen Portscan (1-65535) - masscan verschickt SYN-Pakete
|
* vollständigen Portscan (1-65535) - masscan verschickt SYN-Pakete
|
||||||
@@ -28,6 +38,13 @@ export async function scanWithMasscan(
|
|||||||
{ timeout: 60_000 },
|
{ timeout: 60_000 },
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
if (error) {
|
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. ä.
|
// ENOENT (Programm fehlt) oder fehlende Rechte (EPERM/"PCAP" o. ä.
|
||||||
// in stderr) - beides bedeutet: masscan für diese Installation
|
// in stderr) - beides bedeutet: masscan für diese Installation
|
||||||
// dauerhaft nicht nutzbar, künftig direkt auf den Fallback gehen
|
// 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);
|
if (Number.isFinite(port)) ports.push(port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
masscanLastError = null; // erfolgreicher Lauf - alten Fehler nicht mehr anzeigen
|
||||||
resolve(ports);
|
resolve(ports);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { resolveHostname, reverseLookup } from "./dns.js";
|
import { resolveHostname, reverseLookup } from "./dns.js";
|
||||||
import { isPortOpen, scanPortsInBatches, fullPortRange, TYPICAL_PORTS } from "./ports.js";
|
import { isPortOpen, scanPortsInBatches, fullPortRange, TYPICAL_PORTS } from "./ports.js";
|
||||||
import { scanWithMasscan } from "./masscan.js";
|
import { scanWithMasscan, getMasscanLastError } from "./masscan.js";
|
||||||
import { probeWithHttpx } from "./httpx.js";
|
import { probeWithHttpx, getHttpxLastError } from "./httpx.js";
|
||||||
import { probeHttp } from "./http.js";
|
import { probeHttp } from "./http.js";
|
||||||
import { detectSoftware } from "./softwareDetection.js";
|
import { detectSoftware } from "./softwareDetection.js";
|
||||||
import { findBestIconMatch } from "./iconDb.js";
|
import { findBestIconMatch } from "./iconDb.js";
|
||||||
@@ -86,7 +86,18 @@ export interface DeviceScanResult {
|
|||||||
* gegriffen hat oder (mangels Installation/Rechten) auf die eingebaute
|
* gegriffen hat oder (mangels Installation/Rechten) auf die eingebaute
|
||||||
* Methode zurückgefallen wurde.
|
* 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
|
// Ports"-Liste (~30 Ports) macht masscan kaum einen Unterschied, das
|
||||||
// eingebaute Scanning reicht dafür völlig.
|
// eingebaute Scanning reicht dafür völlig.
|
||||||
let usedMasscan = false;
|
let usedMasscan = false;
|
||||||
|
let masscanError: string | null = null;
|
||||||
|
const masscanAttempted = candidatePorts.length > 1000;
|
||||||
let openPorts: number[];
|
let openPorts: number[];
|
||||||
if (candidatePorts.length > 1000) {
|
if (masscanAttempted) {
|
||||||
const masscanResult = await scanWithMasscan(device.ip, "1-65535");
|
const masscanResult = await scanWithMasscan(device.ip, "1-65535");
|
||||||
usedMasscan = masscanResult !== null;
|
usedMasscan = masscanResult !== null;
|
||||||
|
if (!usedMasscan) masscanError = getMasscanLastError();
|
||||||
openPorts = masscanResult ?? (await scanPortsInBatches(device.ip, candidatePorts));
|
openPorts = masscanResult ?? (await scanPortsInBatches(device.ip, candidatePorts));
|
||||||
} else {
|
} else {
|
||||||
openPorts = await scanPortsInBatches(device.ip, candidatePorts);
|
openPorts = await scanPortsInBatches(device.ip, candidatePorts);
|
||||||
@@ -180,6 +194,7 @@ export async function scanDeviceServices(
|
|||||||
// geprüft, Protokoll wird pro Port erraten).
|
// geprüft, Protokoll wird pro Port erraten).
|
||||||
const httpxResults = openPorts.length > 0 ? await probeWithHttpx(device.ip, openPorts) : new Map();
|
const httpxResults = openPorts.length > 0 ? await probeWithHttpx(device.ip, openPorts) : new Map();
|
||||||
const usingHttpx = httpxResults !== null;
|
const usingHttpx = httpxResults !== null;
|
||||||
|
const httpxError = !usingHttpx ? getHttpxLastError() : null;
|
||||||
if (usingHttpx) {
|
if (usingHttpx) {
|
||||||
openPorts = Array.from(httpxResults!.keys());
|
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