From 99608d7d573492ede33b9bf2a54ec9f0078954d5 Mon Sep 17 00:00:00 2001 From: Dan Levy Date: Thu, 27 Aug 2026 02:16:58 -0400 Subject: [PATCH 1/3] Redact credentials in passive discovery URLs --- src/server/evidence/index.ts | 1 + src/server/evidence/ingestion.ts | 8 +- src/server/evidence/redaction.ts | 99 +++++++++++++++++++ .../recon/discovery-artifact-normalizer.ts | 8 +- .../discovery-artifact-normalizer.test.ts | 12 ++- tests/integration/evidence-ingestion.test.ts | 12 +++ .../stored-passive-auth-surface-api.test.ts | 74 ++++++++++---- 7 files changed, 186 insertions(+), 28 deletions(-) create mode 100644 src/server/evidence/redaction.ts diff --git a/src/server/evidence/index.ts b/src/server/evidence/index.ts index e3c526c98..36937a827 100644 --- a/src/server/evidence/index.ts +++ b/src/server/evidence/index.ts @@ -54,6 +54,7 @@ export { type EvidenceSource, isEvidenceRedactionEnabled, redactEvidenceSecrets, + redactEvidenceUrlSecrets, runWithEvidenceRedaction, } from "./ingestion"; export { diff --git a/src/server/evidence/ingestion.ts b/src/server/evidence/ingestion.ts index 191e2a55c..7dd2a60a3 100644 --- a/src/server/evidence/ingestion.ts +++ b/src/server/evidence/ingestion.ts @@ -8,6 +8,9 @@ import { ensureThreadRagIndex, THREAD_RAG_INDEX_NAME } from "../../mastra/tools/ import { type Queryable, withDatabase } from "../db/client"; import { sanitizePostgresText } from "../db/postgres-unicode"; import { replaceEvidenceLexicalChunks } from "./hybrid-search"; +import { redactEvidenceUrlSecrets } from "./redaction"; + +export { redactEvidenceUrlSecrets } from "./redaction"; export const EVIDENCE_SOURCES = [ "upload", @@ -409,11 +412,12 @@ export function runWithEvidenceRedaction(enabled: boolean, callback: () => T) } export function redactEvidenceSecrets(text: string) { + const urlRedactedText = redactEvidenceUrlSecrets(text); if (!isEvidenceRedactionEnabled()) { - return text; + return urlRedactedText; } - return text + return urlRedactedText .replace( /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[redacted-private-key]", diff --git a/src/server/evidence/redaction.ts b/src/server/evidence/redaction.ts new file mode 100644 index 000000000..d838768db --- /dev/null +++ b/src/server/evidence/redaction.ts @@ -0,0 +1,99 @@ +const HTTP_URL_PATTERN = /https?:\/\/[^\s<>"'`]+/giu; + +const SENSITIVE_URL_PARAMETER_NAMES = new Set([ + "accesstoken", + "apikey", + "authorization", + "authtoken", + "clientsecret", + "code", + "credential", + "credentials", + "idtoken", + "jwt", + "password", + "passwd", + "privatekey", + "pwd", + "refreshtoken", + "secret", + "session", + "sessionid", + "sig", + "signature", + "token", +]); + +/** + * Removes credentials carried by HTTP(S) URLs even when general evidence + * redaction is disabled. URLs cross report, Artifact, RAG, and response + * boundaries, so their credential fields are never an operator-visible raw + * evidence exception. + */ +export function redactEvidenceUrlSecrets(text: string): string { + return text.replace(HTTP_URL_PATTERN, (rawUrl) => redactHttpUrl(rawUrl)); +} + +function redactHttpUrl(rawUrl: string): string { + let parsed: URL; + try { + parsed = new URL(rawUrl); + } catch { + return rawUrl; + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return rawUrl; + } + + let redacted = rawUrl; + if (parsed.username || parsed.password) { + redacted = removeUrlUserinfo(redacted); + } + return redactSensitiveUrlParameters(redacted); +} + +function removeUrlUserinfo(url: string): string { + const schemeEnd = url.indexOf("://") + 3; + const authorityEnd = firstIndexOf(url, ["/", "?", "#"], schemeEnd); + const at = url.lastIndexOf("@", authorityEnd); + if (schemeEnd < 3 || at < schemeEnd) return url; + return `${url.slice(0, schemeEnd)}${url.slice(at + 1)}`; +} + +function redactSensitiveUrlParameters(url: string): string { + return url.replace( + /([?&#;])([^=&#;]+)=([^&#;]*)/gu, + (match, separator: string, encodedName: string) => { + if (!isSensitiveUrlParameterName(encodedName)) return match; + return `${separator}${encodedName}=[redacted]`; + }, + ); +} + +function isSensitiveUrlParameterName(encodedName: string): boolean { + let decodedName = encodedName; + try { + decodedName = decodeURIComponent(encodedName.replaceAll("+", " ")); + } catch { + // An invalid escape is still compared in its original form. + } + const normalized = decodedName.toLowerCase().replace(/[^a-z0-9]/gu, ""); + return ( + SENSITIVE_URL_PARAMETER_NAMES.has(normalized) || + normalized.endsWith("password") || + normalized.endsWith("secret") || + normalized.endsWith("token") || + normalized.endsWith("credential") + ); +} + +function firstIndexOf( + value: string, + needles: readonly string[], + from: number, +): number { + const indexes = needles + .map((needle) => value.indexOf(needle, from)) + .filter((index) => index >= 0); + return indexes.length > 0 ? Math.min(...indexes) : value.length; +} diff --git a/src/server/recon/discovery-artifact-normalizer.ts b/src/server/recon/discovery-artifact-normalizer.ts index 7fd67c42d..9b4d1ccbc 100644 --- a/src/server/recon/discovery-artifact-normalizer.ts +++ b/src/server/recon/discovery-artifact-normalizer.ts @@ -1,3 +1,5 @@ +import { redactEvidenceUrlSecrets } from "../evidence/redaction"; + export type DiscoveryArtifactSource = | "upload" | "terminal-note" @@ -224,7 +226,7 @@ export function normalizeDiscoveryArtifacts( } for (const line of artifact.content.split(/\r?\n/u)) { - const excerpt = line.trim(); + const excerpt = redactEvidenceUrlSecrets(line.trim()); if (!excerpt) continue; if (SESSION_OBSERVATION.test(excerpt)) sessionObservations.add(excerpt); @@ -320,9 +322,11 @@ export function normalizeDiscoveryArtifacts( function normalizeUrl(raw: string): string | undefined { const cleaned = raw.replace(/[),.;\]}]+$/u, ""); try { - const parsed = new URL(cleaned); + const parsed = new URL(redactEvidenceUrlSecrets(cleaned)); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return undefined; + parsed.username = ""; + parsed.password = ""; parsed.hash = ""; return parsed.toString(); } catch { diff --git a/tests/integration/discovery-artifact-normalizer.test.ts b/tests/integration/discovery-artifact-normalizer.test.ts index 11cd5e9f6..0546429ce 100644 --- a/tests/integration/discovery-artifact-normalizer.test.ts +++ b/tests/integration/discovery-artifact-normalizer.test.ts @@ -13,7 +13,7 @@ describe("discovery artifact normalization", () => { source: "lab-command", content: [ "https://app.example.test/login", - "https://app.example.test/oauth/callback?code=example#fragment", + "https://user%40tenant:password@app.example.test/oauth/callback?code=oauth-secret&view=compact#access_token=fragment-secret", "https://app.example.test/admin/", "https://app.example.test/login", ].join("\n"), @@ -37,8 +37,11 @@ describe("discovery artifact normalization", () => { "https://app.example.test/admin/", "https://app.example.test/api/v2/session", "https://app.example.test/login", - "https://app.example.test/oauth/callback?code=example", + "https://app.example.test/oauth/callback?code=[redacted]&view=compact", ]); + expect(JSON.stringify(result)).not.toMatch( + /user%40tenant|password|oauth-secret|fragment-secret/u, + ); expect( result.urls.find((entry) => entry.path === "/login")?.sourceArtifactIds, ).toEqual(["artifact-gau"]); @@ -78,7 +81,10 @@ describe("discovery artifact normalization", () => { "https://app.example.test/api/v2/session", "set-cookie: sessionid=[REDACTED]; HttpOnly; SameSite=Lax", ]); - expect(result.observations.token).toEqual(["www-authenticate: Bearer"]); + expect(result.observations.token).toEqual([ + "https://app.example.test/oauth/callback?code=[redacted]&view=compact#access_token=[redacted]", + "www-authenticate: Bearer", + ]); }); it("persists a redaction-ready, RAG-indexed summary linked to its raw evidence", async () => { diff --git a/tests/integration/evidence-ingestion.test.ts b/tests/integration/evidence-ingestion.test.ts index ae80497fa..2ff5f0722 100644 --- a/tests/integration/evidence-ingestion.test.ts +++ b/tests/integration/evidence-ingestion.test.ts @@ -10,6 +10,7 @@ import { EvidenceIndexingTemporarilyUnavailableError, ingestEvidenceBestEffort, redactEvidenceSecrets, + runWithEvidenceRedaction, setEvidenceIngestor, } from "../../src/server/evidence"; @@ -313,6 +314,17 @@ describe("shared evidence ingestion", () => { expect(index.mock.calls[0]?.[0].chunks[0]?.metadata.redacted).toBe(true); }); + it("always removes HTTP URL credentials when general evidence redaction is disabled", () => { + const raw = + "GET https://user%40tenant:p%40ss@app.example.test/login?password=query-secret&view=compact#access_token=fragment-secret"; + const redacted = runWithEvidenceRedaction(false, () => redactEvidenceSecrets(raw)); + + expect(redacted).toBe( + "GET https://app.example.test/login?password=[redacted]&view=compact#access_token=[redacted]", + ); + expect(redacted).not.toMatch(/user%40tenant|p%40ss|query-secret|fragment-secret/u); + }); + it("chunks awkward log output by line windows with searchable line metadata", async () => { const index = vi.fn(async (input: EvidenceIndexInput) => input.chunks.length); const ingestor = createEvidenceIngestor({ diff --git a/tests/integration/stored-passive-auth-surface-api.test.ts b/tests/integration/stored-passive-auth-surface-api.test.ts index 015927e0b..61ec2eaf4 100644 --- a/tests/integration/stored-passive-auth-surface-api.test.ts +++ b/tests/integration/stored-passive-auth-surface-api.test.ts @@ -9,6 +9,7 @@ import { getProjectStore } from "../../src/server/chat/projectAdapter"; import { createArtifactService, createEvidenceIngestor, + runWithEvidenceRedaction, setArtifactService, setEvidenceIngestor, } from "../../src/server/evidence"; @@ -62,18 +63,22 @@ describe("stored passive auth-surface API", () => { const artifactService = createArtifactService({ storage: null }); setArtifactService(artifactService); - const source = await artifactService.createArtifact({ - projectId: project.id, - threadId: thread.id, - targetId: "target-app", - name: "passive-urls.txt", - kind: "log", - contentType: "text/plain", - content: - "https://app.example.test/login\nhttps://app.example.test/api/session", - source: "upload", - indexForRag: false, - }); + const source = await runWithEvidenceRedaction(false, () => + artifactService.createArtifact({ + projectId: project.id, + threadId: thread.id, + targetId: "target-app", + name: "passive-urls.txt", + kind: "log", + contentType: "text/plain", + content: [ + "https://url-user:p%40ssword@app.example.test/login/?password=query-secret&view=compact#access_token=fragment-secret", + "https://app.example.test/api/session", + ].join("\n"), + source: "upload", + indexForRag: false, + }), + ); const foreignSource = await artifactService.createArtifact({ projectId: otherProject.id, projectScoped: true, @@ -155,17 +160,25 @@ describe("stored passive auth-surface API", () => { }); expect(relabeled.status).toBe(400); - const response = await invoke(project.id, { - targetId: "target-app", - threadId: thread.id, - taskId: "task-passive-map", - artifacts: [ - { artifactId: source.id }, - { artifactId: projectReference.id }, - ], - }); + const response = await runWithEvidenceRedaction(false, () => + invoke(project.id, { + targetId: "target-app", + threadId: thread.id, + taskId: "task-passive-map", + artifacts: [ + { artifactId: source.id }, + { artifactId: projectReference.id }, + ], + }), + ); expect(response.status).toBe(200); const body = (await response.json()) as Record; + const serializedBody = JSON.stringify(body); + expect(serializedBody).not.toMatch( + /url-user|p%40ssword|query-secret|fragment-secret/u, + ); + expect(serializedBody).toContain("app.example.test/login/"); + expect(serializedBody).toContain("view=compact"); expect(body).toMatchObject({ authorizationId: authorization.id, summary: { @@ -208,6 +221,25 @@ describe("stored passive auth-surface API", () => { }), }), ); + const indexedPayload = JSON.stringify(indexed.mock.calls); + expect(indexedPayload).not.toMatch( + /url-user|p%40ssword|query-secret|fragment-secret/u, + ); + expect(indexedPayload).toContain("app.example.test/login/"); + expect(indexedPayload).toContain("view=compact"); + + const reportArtifactId = (body.artifact as { id?: string }).id; + expect(reportArtifactId).toBeTruthy(); + if (!reportArtifactId) throw new Error("Passive report artifact id was missing."); + const storedReport = await artifactService.readArtifactText?.({ + projectId: project.id, + artifactId: reportArtifactId, + }); + expect(storedReport?.text).not.toMatch( + /url-user|p%40ssword|query-secret|fragment-secret/u, + ); + expect(storedReport?.text).toContain("app.example.test/login/"); + expect(storedReport?.text).toContain("view=compact"); }); }); From d99357c6dbf7b735f9865db262177c3aa223a1dc Mon Sep 17 00:00:00 2001 From: Dan Levy Date: Thu, 27 Aug 2026 02:30:31 -0400 Subject: [PATCH 2/3] Harden passive URL secret redaction --- src/server/evidence/redaction.ts | 9 +- tests/integration/evidence-ingestion.test.ts | 15 +++- .../stored-passive-auth-surface-api.test.ts | 82 ++++++++++++++----- 3 files changed, 80 insertions(+), 26 deletions(-) diff --git a/src/server/evidence/redaction.ts b/src/server/evidence/redaction.ts index d838768db..01b353ce3 100644 --- a/src/server/evidence/redaction.ts +++ b/src/server/evidence/redaction.ts @@ -5,8 +5,10 @@ const SENSITIVE_URL_PARAMETER_NAMES = new Set([ "apikey", "authorization", "authtoken", + "clientassertion", "clientsecret", "code", + "codeverifier", "credential", "credentials", "idtoken", @@ -62,7 +64,7 @@ function removeUrlUserinfo(url: string): string { function redactSensitiveUrlParameters(url: string): string { return url.replace( - /([?&#;])([^=&#;]+)=([^&#;]*)/gu, + /([?&#;])([^=?&#;]+)=([^?&#;]*)/gu, (match, separator: string, encodedName: string) => { if (!isSensitiveUrlParameterName(encodedName)) return match; return `${separator}${encodedName}=[redacted]`; @@ -83,7 +85,10 @@ function isSensitiveUrlParameterName(encodedName: string): boolean { normalized.endsWith("password") || normalized.endsWith("secret") || normalized.endsWith("token") || - normalized.endsWith("credential") + normalized.endsWith("credential") || + normalized.endsWith("signature") || + normalized.endsWith("sessionid") || + normalized.endsWith("sig") ); } diff --git a/tests/integration/evidence-ingestion.test.ts b/tests/integration/evidence-ingestion.test.ts index 2ff5f0722..e2d171b2c 100644 --- a/tests/integration/evidence-ingestion.test.ts +++ b/tests/integration/evidence-ingestion.test.ts @@ -315,14 +315,21 @@ describe("shared evidence ingestion", () => { }); it("always removes HTTP URL credentials when general evidence redaction is disabled", () => { - const raw = - "GET https://user%40tenant:p%40ss@app.example.test/login?password=query-secret&view=compact#access_token=fragment-secret"; + const raw = [ + "GET https://user%40tenant:p%40ss@app.example.test/login?password=query-secret&view=compact#access_token=fragment-secret", + "GET https://app.example.test/login;JSESSIONID=java-session?X-Amz-Signature=aws-signature&X-Goog-Signature=google-signature&ASP.NET_SessionId=asp-session&code_verifier=oauth-verifier&client_assertion=oauth-assertion&view=compact", + ].join("\n"); const redacted = runWithEvidenceRedaction(false, () => redactEvidenceSecrets(raw)); expect(redacted).toBe( - "GET https://app.example.test/login?password=[redacted]&view=compact#access_token=[redacted]", + [ + "GET https://app.example.test/login?password=[redacted]&view=compact#access_token=[redacted]", + "GET https://app.example.test/login;JSESSIONID=[redacted]?X-Amz-Signature=[redacted]&X-Goog-Signature=[redacted]&ASP.NET_SessionId=[redacted]&code_verifier=[redacted]&client_assertion=[redacted]&view=compact", + ].join("\n"), + ); + expect(redacted).not.toMatch( + /user%40tenant|p%40ss|query-secret|fragment-secret|java-session|aws-signature|google-signature|asp-session|oauth-verifier|oauth-assertion/u, ); - expect(redacted).not.toMatch(/user%40tenant|p%40ss|query-secret|fragment-secret/u); }); it("chunks awkward log output by line windows with searchable line metadata", async () => { diff --git a/tests/integration/stored-passive-auth-surface-api.test.ts b/tests/integration/stored-passive-auth-surface-api.test.ts index 61ec2eaf4..7fafea2fa 100644 --- a/tests/integration/stored-passive-auth-surface-api.test.ts +++ b/tests/integration/stored-passive-auth-surface-api.test.ts @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { POST } from "../../src/app/api/projects/[projectId]/recon/passive-auth-surface/route"; import { getProjectStore } from "../../src/server/chat/projectAdapter"; +import { withDatabase } from "../../src/server/db/client"; import { createArtifactService, createEvidenceIngestor, @@ -62,23 +63,63 @@ describe("stored passive auth-surface API", () => { }); const artifactService = createArtifactService({ storage: null }); - setArtifactService(artifactService); - const source = await runWithEvidenceRedaction(false, () => - artifactService.createArtifact({ - projectId: project.id, - threadId: thread.id, - targetId: "target-app", - name: "passive-urls.txt", - kind: "log", - contentType: "text/plain", - content: [ - "https://url-user:p%40ssword@app.example.test/login/?password=query-secret&view=compact#access_token=fragment-secret", - "https://app.example.test/api/session", - ].join("\n"), - source: "upload", - indexForRag: false, - }), + const source = { id: "artifact-legacy-passive-urls" }; + const legacySourceContent = [ + "https://url-user:p%40ssword@app.example.test/login/?password=query-secret&view=compact#access_token=fragment-secret", + "https://app.example.test/login;JSESSIONID=java-session?X-Amz-Signature=aws-signature&X-Goog-Signature=google-signature&ASP.NET_SessionId=asp-session&code_verifier=oauth-verifier&client_assertion=oauth-assertion&view=compact", + "https://app.example.test/api/session", + ].join("\n"); + await withDatabase((db) => + db.query( + `INSERT INTO artifacts ( + id, project_id, thread_id, kind, name, content_type, inline_content, metadata + ) + VALUES ($1, $2, $3, 'log', 'passive-urls.txt', 'text/plain', $4, $5::jsonb)`, + [ + source.id, + project.id, + thread.id, + legacySourceContent, + JSON.stringify({ + source: "upload", + targetId: "target-app", + targetIds: ["target-app"], + targetScope: "targets", + }), + ], + ), ); + const readArtifactText = artifactService.readArtifactText; + if (!readArtifactText) + throw new Error("Artifact text reads are unavailable."); + setArtifactService({ + ...artifactService, + readArtifactText: async (input) => { + if (input.artifactId !== source.id) return readArtifactText(input); + const result = await withDatabase((db) => + db.query<{ inline_content: string }>( + "SELECT inline_content FROM artifacts WHERE id = $1 AND project_id = $2", + [input.artifactId, input.projectId], + ), + ); + const row = result.rows[0]; + if (!row) throw new Error("Legacy source artifact was not found."); + return { + artifactId: source.id, + projectId: project.id, + threadId: thread.id, + name: "passive-urls.txt", + contentType: "text/plain", + sizeBytes: new TextEncoder().encode(row.inline_content).byteLength, + sha256: null, + source: "upload" as const, + targetIds: ["target-app"], + targetScope: "targets" as const, + text: row.inline_content, + truncated: false, + }; + }, + }); const foreignSource = await artifactService.createArtifact({ projectId: otherProject.id, projectScoped: true, @@ -175,7 +216,7 @@ describe("stored passive auth-surface API", () => { const body = (await response.json()) as Record; const serializedBody = JSON.stringify(body); expect(serializedBody).not.toMatch( - /url-user|p%40ssword|query-secret|fragment-secret/u, + /url-user|p%40ssword|query-secret|fragment-secret|java-session|aws-signature|google-signature|asp-session|oauth-verifier|oauth-assertion/u, ); expect(serializedBody).toContain("app.example.test/login/"); expect(serializedBody).toContain("view=compact"); @@ -223,20 +264,21 @@ describe("stored passive auth-surface API", () => { ); const indexedPayload = JSON.stringify(indexed.mock.calls); expect(indexedPayload).not.toMatch( - /url-user|p%40ssword|query-secret|fragment-secret/u, + /url-user|p%40ssword|query-secret|fragment-secret|java-session|aws-signature|google-signature|asp-session|oauth-verifier|oauth-assertion/u, ); expect(indexedPayload).toContain("app.example.test/login/"); expect(indexedPayload).toContain("view=compact"); const reportArtifactId = (body.artifact as { id?: string }).id; expect(reportArtifactId).toBeTruthy(); - if (!reportArtifactId) throw new Error("Passive report artifact id was missing."); + if (!reportArtifactId) + throw new Error("Passive report artifact id was missing."); const storedReport = await artifactService.readArtifactText?.({ projectId: project.id, artifactId: reportArtifactId, }); expect(storedReport?.text).not.toMatch( - /url-user|p%40ssword|query-secret|fragment-secret/u, + /url-user|p%40ssword|query-secret|fragment-secret|java-session|aws-signature|google-signature|asp-session|oauth-verifier|oauth-assertion/u, ); expect(storedReport?.text).toContain("app.example.test/login/"); expect(storedReport?.text).toContain("view=compact"); From 4eb5456fcf13536fa3beb9fcd374fb5f2e0ad773 Mon Sep 17 00:00:00 2001 From: Dan Levy Date: Thu, 27 Aug 2026 02:34:45 -0400 Subject: [PATCH 3/3] Parse URL secret components independently --- src/server/evidence/redaction.ts | 41 ++++++++++++++++++- tests/integration/evidence-ingestion.test.ts | 4 +- .../stored-passive-auth-surface-api.test.ts | 8 ++-- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/server/evidence/redaction.ts b/src/server/evidence/redaction.ts index 01b353ce3..eac38c2da 100644 --- a/src/server/evidence/redaction.ts +++ b/src/server/evidence/redaction.ts @@ -63,8 +63,45 @@ function removeUrlUserinfo(url: string): string { } function redactSensitiveUrlParameters(url: string): string { - return url.replace( - /([?&#;])([^=?&#;]+)=([^?&#;]*)/gu, + const fragmentStart = url.indexOf("#"); + const possibleQueryStart = url.indexOf("?"); + const queryStart = + possibleQueryStart >= 0 && + (fragmentStart < 0 || possibleQueryStart < fragmentStart) + ? possibleQueryStart + : -1; + const pathEnd = + queryStart >= 0 + ? queryStart + : fragmentStart >= 0 + ? fragmentStart + : url.length; + const queryEnd = fragmentStart >= 0 ? fragmentStart : url.length; + + const path = redactUrlParameterSegment( + url.slice(0, pathEnd), + /([;])([^=?&#;]+)=([^?&#;]*)/gu, + ); + const query = + queryStart >= 0 + ? redactUrlParameterSegment( + url.slice(queryStart, queryEnd), + /([?&;])([^=&#;]+)=([^&#;]*)/gu, + ) + : ""; + const fragment = + fragmentStart >= 0 + ? redactUrlParameterSegment( + url.slice(fragmentStart), + /([#&;])([^=&#;]+)=([^&#;]*)/gu, + ) + : ""; + return `${path}${query}${fragment}`; +} + +function redactUrlParameterSegment(segment: string, pattern: RegExp): string { + return segment.replace( + pattern, (match, separator: string, encodedName: string) => { if (!isSensitiveUrlParameterName(encodedName)) return match; return `${separator}${encodedName}=[redacted]`; diff --git a/tests/integration/evidence-ingestion.test.ts b/tests/integration/evidence-ingestion.test.ts index e2d171b2c..f2d17902a 100644 --- a/tests/integration/evidence-ingestion.test.ts +++ b/tests/integration/evidence-ingestion.test.ts @@ -318,6 +318,7 @@ describe("shared evidence ingestion", () => { const raw = [ "GET https://user%40tenant:p%40ss@app.example.test/login?password=query-secret&view=compact#access_token=fragment-secret", "GET https://app.example.test/login;JSESSIONID=java-session?X-Amz-Signature=aws-signature&X-Goog-Signature=google-signature&ASP.NET_SessionId=asp-session&code_verifier=oauth-verifier&client_assertion=oauth-assertion&view=compact", + "GET https://app.example.test/callback?token=query-secret?tail-secret&view=compact", ].join("\n"); const redacted = runWithEvidenceRedaction(false, () => redactEvidenceSecrets(raw)); @@ -325,10 +326,11 @@ describe("shared evidence ingestion", () => { [ "GET https://app.example.test/login?password=[redacted]&view=compact#access_token=[redacted]", "GET https://app.example.test/login;JSESSIONID=[redacted]?X-Amz-Signature=[redacted]&X-Goog-Signature=[redacted]&ASP.NET_SessionId=[redacted]&code_verifier=[redacted]&client_assertion=[redacted]&view=compact", + "GET https://app.example.test/callback?token=[redacted]&view=compact", ].join("\n"), ); expect(redacted).not.toMatch( - /user%40tenant|p%40ss|query-secret|fragment-secret|java-session|aws-signature|google-signature|asp-session|oauth-verifier|oauth-assertion/u, + /user%40tenant|p%40ss|query-secret|tail-secret|fragment-secret|java-session|aws-signature|google-signature|asp-session|oauth-verifier|oauth-assertion/u, ); }); diff --git a/tests/integration/stored-passive-auth-surface-api.test.ts b/tests/integration/stored-passive-auth-surface-api.test.ts index 7fafea2fa..7a0a3bf6a 100644 --- a/tests/integration/stored-passive-auth-surface-api.test.ts +++ b/tests/integration/stored-passive-auth-surface-api.test.ts @@ -65,7 +65,7 @@ describe("stored passive auth-surface API", () => { const artifactService = createArtifactService({ storage: null }); const source = { id: "artifact-legacy-passive-urls" }; const legacySourceContent = [ - "https://url-user:p%40ssword@app.example.test/login/?password=query-secret&view=compact#access_token=fragment-secret", + "https://url-user:p%40ssword@app.example.test/login/?password=query-secret?tail-secret&view=compact#access_token=fragment-secret", "https://app.example.test/login;JSESSIONID=java-session?X-Amz-Signature=aws-signature&X-Goog-Signature=google-signature&ASP.NET_SessionId=asp-session&code_verifier=oauth-verifier&client_assertion=oauth-assertion&view=compact", "https://app.example.test/api/session", ].join("\n"); @@ -216,7 +216,7 @@ describe("stored passive auth-surface API", () => { const body = (await response.json()) as Record; const serializedBody = JSON.stringify(body); expect(serializedBody).not.toMatch( - /url-user|p%40ssword|query-secret|fragment-secret|java-session|aws-signature|google-signature|asp-session|oauth-verifier|oauth-assertion/u, + /url-user|p%40ssword|query-secret|tail-secret|fragment-secret|java-session|aws-signature|google-signature|asp-session|oauth-verifier|oauth-assertion/u, ); expect(serializedBody).toContain("app.example.test/login/"); expect(serializedBody).toContain("view=compact"); @@ -264,7 +264,7 @@ describe("stored passive auth-surface API", () => { ); const indexedPayload = JSON.stringify(indexed.mock.calls); expect(indexedPayload).not.toMatch( - /url-user|p%40ssword|query-secret|fragment-secret|java-session|aws-signature|google-signature|asp-session|oauth-verifier|oauth-assertion/u, + /url-user|p%40ssword|query-secret|tail-secret|fragment-secret|java-session|aws-signature|google-signature|asp-session|oauth-verifier|oauth-assertion/u, ); expect(indexedPayload).toContain("app.example.test/login/"); expect(indexedPayload).toContain("view=compact"); @@ -278,7 +278,7 @@ describe("stored passive auth-surface API", () => { artifactId: reportArtifactId, }); expect(storedReport?.text).not.toMatch( - /url-user|p%40ssword|query-secret|fragment-secret|java-session|aws-signature|google-signature|asp-session|oauth-verifier|oauth-assertion/u, + /url-user|p%40ssword|query-secret|tail-secret|fragment-secret|java-session|aws-signature|google-signature|asp-session|oauth-verifier|oauth-assertion/u, ); expect(storedReport?.text).toContain("app.example.test/login/"); expect(storedReport?.text).toContain("view=compact");