| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 |
- import type { Cassette } from "./schema"
- export const REDACTED = "[REDACTED]"
- const DEFAULT_REDACT_HEADERS = [
- "authorization",
- "cookie",
- "proxy-authorization",
- "set-cookie",
- "x-api-key",
- "x-amz-security-token",
- "x-goog-api-key",
- ]
- const DEFAULT_REDACT_QUERY = [
- "access_token",
- "api-key",
- "api_key",
- "apikey",
- "code",
- "key",
- "signature",
- "sig",
- "token",
- "x-amz-credential",
- "x-amz-security-token",
- "x-amz-signature",
- ]
- const SECRET_PATTERNS: ReadonlyArray<{ readonly label: string; readonly pattern: RegExp }> = [
- { label: "bearer token", pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b/i },
- { label: "API key", pattern: /\bsk-[A-Za-z0-9][A-Za-z0-9_-]{20,}\b/ },
- { label: "Anthropic API key", pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/ },
- { label: "Google API key", pattern: /\bAIza[0-9A-Za-z_-]{20,}\b/ },
- { label: "AWS access key", pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
- { label: "GitHub token", pattern: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/ },
- { label: "private key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
- ]
- const ENV_SECRET_NAMES = /(?:API|AUTH|BEARER|CREDENTIAL|KEY|PASSWORD|SECRET|TOKEN)/i
- const SAFE_ENV_VALUES = new Set(["fixture", "test", "test-key"])
- const envSecrets = () =>
- Object.entries(process.env).flatMap(([name, value]) => {
- if (!value) return []
- if (!ENV_SECRET_NAMES.test(name)) return []
- if (value.length < 12) return []
- if (SAFE_ENV_VALUES.has(value.toLowerCase())) return []
- return [{ name, value }]
- })
- const pathFor = (base: string, key: string) => (base ? `${base}.${key}` : key)
- const stringEntries = (value: unknown, base = ""): ReadonlyArray<{ readonly path: string; readonly value: string }> => {
- if (typeof value === "string") return [{ path: base, value }]
- if (Array.isArray(value)) return value.flatMap((item, index) => stringEntries(item, `${base}[${index}]`))
- if (value && typeof value === "object") {
- return Object.entries(value).flatMap(([key, child]) => stringEntries(child, pathFor(base, key)))
- }
- return []
- }
- const redactionSet = (values: ReadonlyArray<string> | undefined, defaults: ReadonlyArray<string>) =>
- new Set([...defaults, ...(values ?? [])].map((value) => value.toLowerCase()))
- export type UrlRedactor = (url: string) => string
- export const redactUrl = (raw: string, query: ReadonlyArray<string> = DEFAULT_REDACT_QUERY, urlRedactor?: UrlRedactor) => {
- if (!URL.canParse(raw)) return urlRedactor?.(raw) ?? raw
- const url = new URL(raw)
- if (url.username) url.username = REDACTED
- if (url.password) url.password = REDACTED
- const redacted = redactionSet(query, DEFAULT_REDACT_QUERY)
- for (const key of [...url.searchParams.keys()]) {
- if (redacted.has(key.toLowerCase())) url.searchParams.set(key, REDACTED)
- }
- return urlRedactor?.(url.toString()) ?? url.toString()
- }
- export const redactHeaders = (
- headers: Record<string, string>,
- allow: ReadonlyArray<string>,
- redact: ReadonlyArray<string> = DEFAULT_REDACT_HEADERS,
- ) => {
- const allowed = new Set(allow.map((name) => name.toLowerCase()))
- const redacted = redactionSet(redact, DEFAULT_REDACT_HEADERS)
- return Object.fromEntries(
- Object.entries(headers)
- .map(([name, value]) => [name.toLowerCase(), value] as const)
- .filter(([name]) => allowed.has(name))
- .map(([name, value]) => [name, redacted.has(name) ? REDACTED : value] as const)
- .toSorted(([a], [b]) => a.localeCompare(b)),
- )
- }
- export type SecretFinding = {
- readonly path: string
- readonly reason: string
- }
- export const secretFindings = (value: unknown): ReadonlyArray<SecretFinding> =>
- stringEntries(value).flatMap((entry) => [
- ...SECRET_PATTERNS.filter((item) => item.pattern.test(entry.value)).map((item) => ({
- path: entry.path,
- reason: item.label,
- })),
- ...envSecrets()
- .filter((item) => entry.value.includes(item.value))
- .map((item) => ({ path: entry.path, reason: `environment secret ${item.name}` })),
- ])
- export const cassetteSecretFindings = (cassette: Cassette) => secretFindings(cassette)
|