Parent directory

normalize.ts

3088 bytes
 1import { createHash } from "node:crypto";
 2import type { PolicyRequest } from "./types";
 3
 4// Bump to invalidate all cached decisions when rules change.
 5export const POLICY_VERSION = 40;
 6
 7const SENSITIVE_KEY_RE = /(?:api[-_]?key|auth(?:entication|orization)?|credential|cookie|password|secret|token)/i;
 8const SENSITIVE_ARGUMENT_RE =
 9  /((?:^|\s)(?:-u|-H)(?:=|\s*)|(?:^|\s)--(?:api[-_]?key|authorization|cookie|password|secret|token|user|proxy-user|header)(?:=|\s+))(?:(?:"(?:[^"\\]|\\.)*")|(?:'(?:[^'\\]|\\.)*')|\S+)/gi;
10const URL_USERINFO_RE = /([a-z][a-z\d+.-]*:\/\/)[^/\s@]+@/gi;
11const SENSITIVE_QUERY_RE = /([?&](?:api[-_]?key|authorization|credential|cookie|password|secret|token)=)[^&\s]*/gi;
12const SENSITIVE_ENVIRONMENT_RE =
13  /((?:^|[\s;|&])(?:export\s+)?(?=[A-Za-z_][A-Za-z0-9_]*\s*=\s*)(?=[A-Za-z0-9_]*(?:api[-_]?key|auth(?:entication|orization)?|credential|cookie|password|secret|token)[A-Za-z0-9_]*\s*=\s*)[A-Za-z_][A-Za-z0-9_]*\s*=\s*)(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\$\([^)]*\)|[^\s;&|]*)/gi;
14
15function stableJson(value: unknown): string {
16  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
17  if (value && typeof value === "object") {
18    const entries = Object.entries(value as Record<string, unknown>)
19      .sort(([left], [right]) => left.localeCompare(right))
20      .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`);
21    return `{${entries.join(",")}}`;
22  }
23  return JSON.stringify(value);
24}
25
26export function normalizeRequest(toolName: string, input: Record<string, unknown>): string {
27  switch (toolName) {
28    case "bash": {
29      const command = typeof input.command === "string" ? input.command : "";
30      return `Bash:${command}`;
31    }
32    case "webfetch":
33      return `WebFetch:${input.url ?? ""}`;
34    default:
35      return `${toolName}:${stableJson(input)}`;
36  }
37}
38
39export function cacheKey(normalized: string): string {
40  const payload = `${POLICY_VERSION}\n${normalized}`;
41  return createHash("sha256").update(payload).digest("hex").slice(0, 16);
42}
43
44function redactText(value: string): string {
45  return value
46    .replace(SENSITIVE_ENVIRONMENT_RE, "$1[REDACTED]")
47    .replace(SENSITIVE_ARGUMENT_RE, "$1[REDACTED]")
48    .replace(URL_USERINFO_RE, "$1[REDACTED]@")
49    .replace(SENSITIVE_QUERY_RE, "$1[REDACTED]");
50}
51
52function redactCommand(command: string): string {
53  return redactText(command);
54}
55
56function redactValue(value: unknown): unknown {
57  if (typeof value === "string") return redactText(value);
58  if (Array.isArray(value)) return value.map(redactValue);
59  if (!value || typeof value !== "object") return value;
60  return Object.fromEntries(
61    Object.entries(value as Record<string, unknown>).map(([key, entry]) => [
62      key,
63      SENSITIVE_KEY_RE.test(key) ? "[REDACTED]" : redactValue(entry),
64    ]),
65  );
66}
67
68export function auditInputSummary(request: PolicyRequest): string {
69  if (request.toolName === "bash" && typeof request.input.command === "string") {
70    return redactCommand(request.input.command).slice(0, 500);
71  }
72  return stableJson(redactValue(request.input)).slice(0, 500);
73}