167 lines
5.3 KiB
TypeScript
167 lines
5.3 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { HttpBody, HttpClientRequest } from "effect/unstable/http"
|
|
import { redactedErrorRequest } from "../src/http/recorder"
|
|
import { make, redactHeaders, redactUrl } from "../src/redaction/redactor"
|
|
import { secretFindings } from "../src/redaction/secrets"
|
|
|
|
describe("redaction", () => {
|
|
test("redacts sensitive URL query parameters", () => {
|
|
expect(
|
|
redactUrl(
|
|
"https://example.test/path?key=secret-google-key&api_key=secret-openai-key&safe=value&X-Amz-Signature=secret-signature",
|
|
),
|
|
).toBe(
|
|
"https://example.test/path?key=%5BREDACTED%5D&api_key=%5BREDACTED%5D&safe=value&X-Amz-Signature=%5BREDACTED%5D",
|
|
)
|
|
})
|
|
|
|
test("redacts URL credentials", () => {
|
|
expect(redactUrl("https://user:password@example.test/path?safe=value")).toBe(
|
|
"https://%5BREDACTED%5D:%5BREDACTED%5D@example.test/path?safe=value",
|
|
)
|
|
})
|
|
|
|
test("applies custom URL redaction after built-in redaction", () => {
|
|
expect(
|
|
redactUrl("https://example.test/accounts/real-account/path?key=secret-key", undefined, (url) =>
|
|
url.replace("/accounts/real-account/", "/accounts/{account}/"),
|
|
),
|
|
).toBe("https://example.test/accounts/{account}/path?key=%5BREDACTED%5D")
|
|
})
|
|
|
|
test("redacts sensitive headers when allow-listed", () => {
|
|
expect(
|
|
redactHeaders(
|
|
{
|
|
authorization: "Bearer secret-token",
|
|
"content-type": "application/json",
|
|
"x-custom-token": "custom-secret",
|
|
"x-api-key": "secret-key",
|
|
"x-goog-api-key": "secret-google-key",
|
|
},
|
|
["authorization", "content-type", "x-api-key", "x-goog-api-key", "x-custom-token"],
|
|
["x-custom-token"],
|
|
),
|
|
).toEqual({
|
|
authorization: "[REDACTED]",
|
|
"content-type": "application/json",
|
|
"x-api-key": "[REDACTED]",
|
|
"x-custom-token": "[REDACTED]",
|
|
"x-goog-api-key": "[REDACTED]",
|
|
})
|
|
})
|
|
|
|
test("redacts error requests without retaining headers, params, or body", () => {
|
|
const request = HttpClientRequest.post("https://example.test/path", {
|
|
headers: { authorization: "Bearer super-secret" },
|
|
body: HttpBody.text("super-secret-body", "text/plain"),
|
|
}).pipe(HttpClientRequest.setUrlParam("api_key", "super-secret-key"))
|
|
|
|
expect(redactedErrorRequest(request).toJSON()).toMatchObject({
|
|
url: "https://example.test/path",
|
|
urlParams: { params: [] },
|
|
headers: {},
|
|
body: { _tag: "Empty" },
|
|
})
|
|
})
|
|
|
|
test("detects secret-looking values without returning the secret", () => {
|
|
expect(
|
|
secretFindings({
|
|
version: 1,
|
|
interactions: [
|
|
{
|
|
transport: "http",
|
|
request: {
|
|
method: "POST",
|
|
url: "https://example.test/path?key=sk-123456789012345678901234",
|
|
headers: {},
|
|
body: JSON.stringify({
|
|
nested: "AIzaSyDHibiBRvJZLsFnPYPoiTwxY4ztQ55yqCE",
|
|
}),
|
|
},
|
|
response: {
|
|
status: 200,
|
|
headers: {},
|
|
body: "Bearer abcdefghijklmnopqrstuvwxyz",
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
).toEqual([
|
|
{ path: "interactions[0].request.url", reason: "API key" },
|
|
{ path: "interactions[0].request.body", reason: "Google API key" },
|
|
{ path: "interactions[0].response.body", reason: "bearer token" },
|
|
])
|
|
})
|
|
|
|
test("detects secret-looking values inside metadata", () => {
|
|
expect(
|
|
secretFindings({
|
|
version: 1,
|
|
metadata: { token: "sk-123456789012345678901234" },
|
|
interactions: [],
|
|
}),
|
|
).toEqual([{ path: "metadata.token", reason: "API key" }])
|
|
})
|
|
|
|
test("redacts configured and common sensitive JSON fields", () => {
|
|
const redactor = make({
|
|
jsonFields: ["account_id"],
|
|
})
|
|
const request = redactor.request({
|
|
method: "POST",
|
|
url: "https://example.test/path",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({
|
|
password: "secret-password",
|
|
accessToken: "access-token",
|
|
nested: { account_id: "account-123", safe: "visible" },
|
|
}),
|
|
})
|
|
|
|
expect(JSON.parse(request.body)).toEqual({
|
|
password: "[REDACTED]",
|
|
accessToken: "[REDACTED]",
|
|
nested: { account_id: "[REDACTED]", safe: "visible" },
|
|
})
|
|
})
|
|
|
|
test("preserves JSON text when no fields are redacted", () => {
|
|
const body = '{\n "id": 9007199254740993,\n "safe": true\n}'
|
|
|
|
expect(
|
|
make().request({
|
|
method: "POST",
|
|
url: "https://example.test/path",
|
|
headers: { "content-type": "application/json" },
|
|
body,
|
|
}).body,
|
|
).toBe(body)
|
|
})
|
|
|
|
test("extends default header redaction and allow lists", () => {
|
|
const redactor = make({
|
|
headers: ["x-custom-token"],
|
|
allowRequestHeaders: ["anthropic-version", "x-custom-token"],
|
|
})
|
|
|
|
expect(
|
|
redactor.request({
|
|
method: "GET",
|
|
url: "https://example.test/path",
|
|
headers: {
|
|
authorization: "Bearer secret",
|
|
"content-type": "application/json",
|
|
"anthropic-version": "2023-06-01",
|
|
"x-custom-token": "secret",
|
|
},
|
|
body: "",
|
|
}).headers,
|
|
).toEqual({
|
|
"anthropic-version": "2023-06-01",
|
|
"content-type": "application/json",
|
|
"x-custom-token": "[REDACTED]",
|
|
})
|
|
})
|
|
})
|