round26: Ping vor Portscan, Liste neuer Dienste, Suchmaschinen-Integration (OpenSearch), API-Scanner

This commit is contained in:
2026-07-24 02:21:58 +02:00
parent 458f58af6f
commit 0fc063b2a2
17 changed files with 507 additions and 4 deletions

View File

@@ -0,0 +1,97 @@
import http from "node:http";
import https from "node:https";
export interface DetectedApi {
path: string;
type: string;
status: number;
}
interface RawProbeResult {
status: number;
contentType: string | null;
bodySnippet: string;
}
const MAX_BODY_BYTES = 8_192;
function fetchRaw(url: string, timeoutMs = 2500): Promise<RawProbeResult | null> {
return new Promise((resolve) => {
const isHttps = url.startsWith("https://");
const client = isHttps ? https : http;
const req = client.get(
url,
{ timeout: timeoutMs, rejectUnauthorized: false, headers: { Accept: "application/json, */*" } },
(res) => {
let body = "";
let received = 0;
res.on("data", (chunk: Buffer) => {
received += chunk.length;
if (received <= MAX_BODY_BYTES) body += chunk.toString("utf-8");
});
res.on("end", () => {
const contentType = res.headers["content-type"] ?? null;
resolve({ status: res.statusCode ?? 0, contentType, bodySnippet: body });
});
res.on("error", () => resolve(null));
}
);
req.on("timeout", () => {
req.destroy();
resolve(null);
});
req.on("error", () => resolve(null));
});
}
/**
* Wohlbekannte Pfade, unter denen selbstgehostete Software üblicherweise
* ihre API bzw. deren Dokumentation/Schema anbietet. Bewusst eine
* kuratierte, kurze Liste statt eines vollständigen Wortlisten-Bruteforce -
* das hier ist ein Hinweis-Scanner, kein Sicherheits-/Pentesting-Werkzeug.
*/
const CANDIDATE_PATHS: { path: string; type: string }[] = [
{ path: "/openapi.json", type: "OpenAPI" },
{ path: "/swagger.json", type: "OpenAPI (Swagger)" },
{ path: "/api-docs", type: "OpenAPI (Swagger)" },
{ path: "/swagger/index.html", type: "Swagger-UI" },
{ path: "/docs", type: "API-Dokumentation" },
{ path: "/graphql", type: "GraphQL" },
{ path: "/api/v1", type: "REST-API" },
{ path: "/api", type: "REST-API" },
{ path: "/.well-known/openapi.json", type: "OpenAPI" },
];
function looksLikeJson(body: string): boolean {
const trimmed = body.trim();
return trimmed.startsWith("{") || trimmed.startsWith("[");
}
/**
* Prüft die kuratierten Kandidaten-Pfade unter einer Basis-URL parallel und
* liefert alle, die auf eine tatsächlich vorhandene API hindeuten: eine
* JSON-Antwort (egal ob 200 oder z. B. 401 "unauthorized" - eine
* JSON-Fehlermeldung zeigt trotzdem "hier läuft eine API"), oder ein
* Content-Type, der explizit auf JSON/GraphQL hindeutet. Reine HTML-Seiten
* (z. B. eine 404-Fehlerseite des Frontends) zählen nicht.
*/
export async function detectApis(baseUrl: string): Promise<DetectedApi[]> {
const checks = await Promise.all(
CANDIDATE_PATHS.map(async ({ path, type }) => {
const result = await fetchRaw(`${baseUrl}${path}`);
if (!result || result.status === 0 || result.status === 404) return null;
const contentTypeIsApi =
result.contentType?.includes("json") || result.contentType?.includes("graphql");
const bodyIsJson = looksLikeJson(result.bodySnippet);
if (!contentTypeIsApi && !bodyIsJson) return null;
const detected: DetectedApi = { path, type, status: result.status };
return detected;
})
);
return checks.filter((c): c is DetectedApi => c !== null);
}