mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
import type { LogErrorInfo } from './log-types';
|
|
|
|
/**
|
|
* Sensitive log key matcher (single source of truth).
|
|
*
|
|
* Add new patterns conservatively. Numeric/boolean values are passed through
|
|
* even when their key matches (e.g., `expires_at` epoch numbers stay readable);
|
|
* only string and object values are redacted.
|
|
*/
|
|
const SENSITIVE_KEY_PATTERN =
|
|
/^(authorization|proxy[_-]?authorization|cookie|set-cookie|password|password_hash|secret|client[_-]?secret|token|auth[_-]?token|access[_-]?token|refresh[_-]?token|id[_-]?token|bearer|assertion|api[_-]?key|x[_-]?api[_-]?key|x[_-]?goog[_-]?api[_-]?key|management[_-]?key|copilot[_-]?token|cursor[_-]?session[_-]?key|oauth[_-]?code|auth[_-]?code)$/i;
|
|
|
|
/** CLI flags whose following argument should be redacted in argv arrays. */
|
|
const SENSITIVE_ARGV_FLAG_PATTERN =
|
|
/^--(token|api[_-]?key|auth|auth[_-]?token|secret|bearer|password|client[_-]?secret|refresh[_-]?token|access[_-]?token|id[_-]?token|prompt)$/i;
|
|
|
|
/** Short CLI flags whose following argument should be redacted in argv arrays. */
|
|
const SENSITIVE_SHORT_ARGV_FLAG_PATTERN = /^-p$/;
|
|
|
|
/** CLI flags whose inline `--flag=value` payload should be redacted in argv arrays. */
|
|
const SENSITIVE_ARGV_ASSIGNMENT_PATTERN =
|
|
/^--(token|api[_-]?key|auth|auth[_-]?token|secret|bearer|password|client[_-]?secret|refresh[_-]?token|access[_-]?token|id[_-]?token|prompt)=/i;
|
|
|
|
/** Bearer/Basic/Token auth-scheme prefix in raw string values. */
|
|
const AUTH_SCHEME_VALUE_PATTERN = /^(Bearer|Basic|Token)\s+\S+/;
|
|
|
|
const MAX_STRING_LENGTH = 2000;
|
|
const MAX_DEPTH = 5;
|
|
|
|
function truncateString(value: string): string {
|
|
if (value.length <= MAX_STRING_LENGTH) {
|
|
return value;
|
|
}
|
|
return `${value.slice(0, MAX_STRING_LENGTH)}...[truncated]`;
|
|
}
|
|
|
|
function maskAuthSchemeValue(value: string): string {
|
|
const match = AUTH_SCHEME_VALUE_PATTERN.exec(value);
|
|
if (!match) return value;
|
|
return `${match[1]} [redacted]`;
|
|
}
|
|
|
|
/**
|
|
* Known credential token shapes that may appear anywhere in a string value
|
|
* (error messages, URLs, free-text). Applied IN ADDITION to sensitive-key
|
|
* redaction so a token routed through a non-sensitive field (e.g. an Error
|
|
* captured under `err`) is still scrubbed. Each branch requires a distinctive
|
|
* prefix plus an ample body to minimise false positives on ordinary prose.
|
|
*/
|
|
const SECRET_TOKEN_PATTERN =
|
|
/(?:Bearer|Basic|Token)\s+\S{8,}|sk-ant-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9_-]{32,}|xox[bpoa]-[A-Za-z0-9-]{10,}|gh[opsu]_[A-Za-z0-9]{36,}|glpat-[A-Za-z0-9_-]{18,}|AIza[0-9A-Za-z_-]{35}|eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*|(?:api[_-]?key|access[_-]?token|refresh[_-]?token|secret)(?:=|%3D)[A-Za-z0-9._~+/=-]{8,}/g;
|
|
|
|
const URL_USERINFO_PATTERN = /([a-z][a-z\d+.-]*:\/\/)([^\s/@]+)@/gi;
|
|
|
|
/**
|
|
* Scrub known credential shapes from an arbitrary string. Preserves the
|
|
* Bearer/Basic/Token scheme prefix when present so the entry stays readable.
|
|
* Exported so the logger can also scrub the human-authored message string
|
|
* (defense-in-depth for the hotpath console.error sweep).
|
|
*/
|
|
export function maskSecretTokens(value: string): string {
|
|
return value
|
|
.replace(URL_USERINFO_PATTERN, '$1[redacted]@')
|
|
.replace(SECRET_TOKEN_PATTERN, (match) => {
|
|
const scheme = /^(Bearer|Basic|Token)\s+/.exec(match);
|
|
return scheme ? `${scheme[1]} [redacted]` : '[redacted]';
|
|
});
|
|
}
|
|
|
|
function sanitizeValue(value: unknown, depth: number): unknown {
|
|
if (value === null || value === undefined) {
|
|
return value;
|
|
}
|
|
|
|
if (depth >= MAX_DEPTH) {
|
|
return '[max-depth]';
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
return truncateString(maskSecretTokens(maskAuthSchemeValue(value)));
|
|
}
|
|
|
|
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
return value;
|
|
}
|
|
|
|
if (value instanceof Error) {
|
|
return {
|
|
name: value.name,
|
|
message: truncateString(maskSecretTokens(value.message)),
|
|
};
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => sanitizeValue(item, depth + 1));
|
|
}
|
|
|
|
if (typeof value === 'object') {
|
|
const sanitized: Record<string, unknown> = {};
|
|
for (const [key, nestedValue] of Object.entries(value as Record<string, unknown>)) {
|
|
sanitized[key] = SENSITIVE_KEY_PATTERN.test(key)
|
|
? '[redacted]'
|
|
: sanitizeValue(nestedValue, depth + 1);
|
|
}
|
|
return sanitized;
|
|
}
|
|
|
|
return String(value);
|
|
}
|
|
|
|
export function redactContext(
|
|
context: Record<string, unknown> | undefined
|
|
): Record<string, unknown> {
|
|
if (!context) {
|
|
return {};
|
|
}
|
|
|
|
return sanitizeValue(context, 0) as Record<string, unknown>;
|
|
}
|
|
|
|
export function redactErrorInfo(error: LogErrorInfo | undefined): LogErrorInfo | undefined {
|
|
if (!error) {
|
|
return undefined;
|
|
}
|
|
|
|
return sanitizeValue(error, 0) as LogErrorInfo;
|
|
}
|
|
|
|
/**
|
|
* Redact sensitive values from a CLI argv array (e.g. for spawn-arg logging).
|
|
*
|
|
* Pairs every sensitive flag (`--token`, `--api-key`, etc.) with its following
|
|
* argument and replaces that argument with `[redacted]`. Non-sensitive args
|
|
* pass through unchanged.
|
|
*/
|
|
export function redactArgv(argv: readonly string[]): string[] {
|
|
const out: string[] = [];
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const arg = argv[i];
|
|
if (SENSITIVE_ARGV_ASSIGNMENT_PATTERN.test(arg)) {
|
|
const separatorIndex = arg.indexOf('=');
|
|
out.push(`${arg.slice(0, separatorIndex + 1)}[redacted]`);
|
|
continue;
|
|
}
|
|
out.push(arg);
|
|
if (
|
|
(SENSITIVE_ARGV_FLAG_PATTERN.test(arg) || SENSITIVE_SHORT_ARGV_FLAG_PATTERN.test(arg)) &&
|
|
i + 1 < argv.length
|
|
) {
|
|
out.push('[redacted]');
|
|
i++;
|
|
}
|
|
}
|
|
return out;
|
|
}
|