diff --git a/lib/attack-pipeline/utils/httpClient.ts b/lib/attack-pipeline/utils/httpClient.ts index aa1c548..b703fea 100644 --- a/lib/attack-pipeline/utils/httpClient.ts +++ b/lib/attack-pipeline/utils/httpClient.ts @@ -3,6 +3,8 @@ * Designed for stateful security testing workflows (e.g. login-then-probe). */ +import { executeSecurityRequest } from "../../utils/securityHttpClient"; + export interface RequestOptions { method?: string; headers?: Record; @@ -43,48 +45,17 @@ function serializeCookies(cookies: Record): string { * Stateless HTTP request — does not maintain session state. */ export async function httpRequest(url: string, opts?: RequestOptions): Promise { - const start = Date.now(); - try { - const res = await fetch(url, { - method: opts?.method ?? "GET", + return executeSecurityRequest( + url, + { + method: opts?.method, headers: opts?.headers, body: opts?.body, + timeoutMs: opts?.timeoutMs, redirect: opts?.followRedirects === false ? "manual" : "follow", - signal: AbortSignal.timeout(opts?.timeoutMs ?? 15_000), - }); - const body = await res.text().catch(() => ""); - const headers: Record = {}; - res.headers.forEach((v, k) => { headers[k] = v; }); - - // Collect all Set-Cookie headers - const setCookies: string[] = []; - if (typeof (res.headers as { getSetCookie?: () => string[] }).getSetCookie === "function") { - setCookies.push(...((res.headers as { getSetCookie: () => string[] }).getSetCookie())); - } else if (headers["set-cookie"]) { - setCookies.push(headers["set-cookie"]); - } - - return { - status: res.status, - headers, - setCookies, - body: body.slice(0, 20_000), - latency: Date.now() - start, - redirected: res.redirected, - finalUrl: res.url || url, - }; - } catch (err: unknown) { - return { - status: 0, - headers: {}, - setCookies: [], - body: "", - latency: Date.now() - start, - redirected: false, - finalUrl: url, - error: err instanceof Error ? err.message : String(err), - }; - } + }, + 20_000 + ); } /** diff --git a/lib/security-agent/utils/http-client.ts b/lib/security-agent/utils/http-client.ts index f0a9b22..71781b4 100644 --- a/lib/security-agent/utils/http-client.ts +++ b/lib/security-agent/utils/http-client.ts @@ -3,6 +3,8 @@ * response metadata capture, and safe error handling. */ +import { executeSecurityRequest } from "../../utils/securityHttpClient"; + export interface HttpResponse { status: number; headers: Record; @@ -24,26 +26,26 @@ export async function httpRequest( redirect?: RequestRedirect; } ): Promise { - const start = Date.now(); - try { - const res = await fetch(url, { - method: opts?.method ?? "GET", + const baseRes = await executeSecurityRequest( + url, + { + method: opts?.method, headers: opts?.headers, body: opts?.body, + timeoutMs: opts?.timeoutMs, redirect: opts?.redirect ?? "manual", - signal: AbortSignal.timeout(opts?.timeoutMs ?? 15_000), - }); - const body = await res.text().catch(() => ""); - const headers: Record = {}; - res.headers.forEach((v, k) => { headers[k] = v; }); - return { status: res.status, headers, body: body.slice(0, 10_000), latency: Date.now() - start }; - } catch (err: unknown) { - return { - status: 0, - headers: {}, - body: "", - latency: Date.now() - start, - error: err instanceof Error ? err.message : String(err), - }; + }, + 10_000 + ); + + const result: HttpResponse = { + status: baseRes.status, + headers: baseRes.headers, + body: baseRes.body, + latency: baseRes.latency, + }; + if (baseRes.error !== undefined) { + result.error = baseRes.error; } + return result; } diff --git a/lib/utils/securityHttpClient.ts b/lib/utils/securityHttpClient.ts new file mode 100644 index 0000000..07bacb1 --- /dev/null +++ b/lib/utils/securityHttpClient.ts @@ -0,0 +1,79 @@ +/** + * Shared internal implementation for security scanner HTTP execution. + * DO NOT import this file outside of the HTTP client wrappers. + */ + +export interface SharedRequestOptions { + method?: string; + headers?: Record; + body?: string; + timeoutMs?: number; + redirect?: RequestRedirect; +} + +export interface SharedResponse { + status: number; + headers: Record; + setCookies: string[]; + body: string; + latency: number; + redirected: boolean; + finalUrl: string; + error?: string; +} + +/** + * Executes a security test request with latency measurement, timeout support, + * body truncation, and custom error mapping. + */ +export async function executeSecurityRequest( + url: string, + opts?: SharedRequestOptions, + truncateLimit: number = 10_000 +): Promise { + const start = Date.now(); + try { + const res = await fetch(url, { + method: opts?.method ?? "GET", + headers: opts?.headers, + body: opts?.body, + redirect: opts?.redirect, + signal: AbortSignal.timeout(opts?.timeoutMs ?? 15_000), + }); + + const body = await res.text().catch(() => ""); + const headers: Record = {}; + res.headers.forEach((v, k) => { + headers[k] = v; + }); + + // Collect all Set-Cookie headers + const setCookies: string[] = []; + if (typeof (res.headers as { getSetCookie?: () => string[] }).getSetCookie === "function") { + setCookies.push(...((res.headers as { getSetCookie: () => string[] }).getSetCookie())); + } else if (headers["set-cookie"]) { + setCookies.push(...headers["set-cookie"].split(",").map(c => c.trim())); + } + + return { + status: res.status, + headers, + setCookies, + body: body.slice(0, truncateLimit), + latency: Date.now() - start, + redirected: res.redirected, + finalUrl: res.url || url, + }; + } catch (err: unknown) { + return { + status: 0, + headers: {}, + setCookies: [], + body: "", + latency: Date.now() - start, + redirected: false, + finalUrl: url, + error: err instanceof Error ? err.message : String(err), + }; + } +}