mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-24 12:15:51 -04:00
118 lines
4.0 KiB
TypeScript
118 lines
4.0 KiB
TypeScript
import { Schema } from "effect"
|
|
|
|
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 const SecretFindingSchema = Schema.Struct({
|
|
path: Schema.String,
|
|
reason: Schema.String,
|
|
})
|
|
export type SecretFinding = Schema.Schema.Type<typeof SecretFindingSchema>
|
|
|
|
export const secretFindings = (value: unknown): ReadonlyArray<SecretFinding> => {
|
|
const environment = envSecrets()
|
|
return stringEntries(value).flatMap((entry) => [
|
|
...SECRET_PATTERNS.filter((item) => item.pattern.test(entry.value)).map((item) => ({
|
|
path: entry.path,
|
|
reason: item.label,
|
|
})),
|
|
...environment
|
|
.filter((item) => entry.value.includes(item.value))
|
|
.map((item) => ({ path: entry.path, reason: `environment secret ${item.name}` })),
|
|
])
|
|
}
|