mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
* feat(guardrails): add CredentialMaskerGuardrail for API key/secret redaction - New CredentialMaskerGuardrail (src/lib/guardrails/credentialMasker.ts) extends BaseGuardrail - preCall: redacts well-known credential patterns from upstream payload (message content, tool_call function.arguments, tool results) before sending to the provider - postCall: redacts credentials from the provider response - Patterns (13+ types, conservative/low-false-positive): OpenAI (sk-/sk-proj-), Anthropic (sk-ant-), GitHub (gh[pousr]_), Slack (xox[bpoa]-), Google (AIza), HuggingFace (hf_), Replicate (r8_), Stripe (sk_live_/rk_live_), Square, AWS (AKIA), Twilio, SendGrid, Mailgun, Discord, Notion, Linear, npm, Postman, private keys (PEM), JWTs, connection strings (mongodb/postgres/mysql/redis), auth headers (Authorization: Bearer / x-api-key) - Registered in registerDefaultGuardrails + exported from index - Opt-in via CREDENTIAL_REDACTION_ENABLED=true (mirrors PII_REDACTION_ENABLED) - Tested: all 13 pattern types redacted, benign text unchanged, tool_call args + tool results scrubbed, tsc + eslint clean * feat(guardrails): make credential-masker settings-driven + Security tab toggle - Add credentialRedactionEnabled setting (default false) to getSettings + updateSettingsSchema - Guardrail preCall/postCall now read getSettings().credentialRedactionEnabled (with CREDENTIAL_REDACTION_ENABLED env fallback) instead of constructor-only enable - Add Credential Redaction toggle Card to SecurityTab (PATCHes /api/settings) - Toggleable from the dashboard Security settings, no restart needed * fix: harden credential redaction guardrail * fix: cover auth header redaction edge cases * fix(i18n): propagate CredentialMaskerGuardrail keys to all locales (#6695 drift) en.json gained 4 new settings.* keys (credentialRedaction, credentialRedactionDesc, enableCredentialRedaction, enableCredentialRedactionDesc) that were never mirrored into the other locale catalogs, tripping the "no drift" regression guard added for #6695. Fill them with the __MISSING__ sentinel (same convention as scripts/i18n/sync-ui-keys.mjs) across all 42 non-English locales. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
94 lines
3.9 KiB
TypeScript
94 lines
3.9 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
CredentialMaskerGuardrail,
|
|
redactCredentials,
|
|
} from "../../src/lib/guardrails/credentialMasker.ts";
|
|
|
|
async function withCredentialRedactionEnabled(fn: () => Promise<void>) {
|
|
const original = process.env.CREDENTIAL_REDACTION_ENABLED;
|
|
process.env.CREDENTIAL_REDACTION_ENABLED = "true";
|
|
try {
|
|
await fn();
|
|
} finally {
|
|
if (original === undefined) delete process.env.CREDENTIAL_REDACTION_ENABLED;
|
|
else process.env.CREDENTIAL_REDACTION_ENABLED = original;
|
|
}
|
|
}
|
|
|
|
test("redacts JWTs without requiring an eyJ payload prefix", () => {
|
|
const token = "eyJ" + "a".repeat(12) + "." + "b".repeat(12) + "." + "c".repeat(12);
|
|
const result = redactCredentials("token=" + token);
|
|
assert.equal(result.modified, true);
|
|
assert.match(result.text, /\[REDACTED:jwt\]/);
|
|
});
|
|
|
|
test("redacts JSON-style authorization text while preserving its structure", () => {
|
|
const result = redactCredentials('{"Authorization": "Bearer ' + "a".repeat(24) + '"}');
|
|
assert.equal(result.modified, true);
|
|
assert.match(result.text, /"Authorization": "Bearer \[REDACTED:auth_header\]"/);
|
|
});
|
|
|
|
test("redacts Basic and Token header schemes with base64 token characters", () => {
|
|
const basic = redactCredentials("Authorization=Basic " + "ab+/=".repeat(4));
|
|
const token = redactCredentials("x-api-key: Token " + "ab+/=".repeat(4));
|
|
|
|
assert.match(basic.text, /Authorization=Basic \[REDACTED:auth_header\]/);
|
|
assert.match(token.text, /x-api-key: Token \[REDACTED:auth_header\]/);
|
|
});
|
|
|
|
test("redacts structured authorization values in nested request payloads", async () => {
|
|
await withCredentialRedactionEnabled(async () => {
|
|
const guardrail = new CredentialMaskerGuardrail();
|
|
const result = await guardrail.preCall({
|
|
messages: [{ role: "user", content: { headers: { Authorization: "Bearer short-token" } } }],
|
|
});
|
|
const payload = result?.modifiedPayload as {
|
|
messages: Array<{ content: { headers: { Authorization: string } } }>;
|
|
};
|
|
const content = payload.messages[0].content;
|
|
assert.equal(content.headers.Authorization, "Bearer [REDACTED:auth_header]");
|
|
assert.equal((result?.meta as { count: number }).count, 1);
|
|
});
|
|
});
|
|
|
|
test("redacts every shared reference without mutating the original", async () => {
|
|
await withCredentialRedactionEnabled(async () => {
|
|
const guardrail = new CredentialMaskerGuardrail();
|
|
const shared = { Authorization: "Bearer shared-token" };
|
|
const result = await guardrail.postCall({ first: shared, second: shared });
|
|
const response = result?.modifiedResponse as {
|
|
first: { Authorization: string };
|
|
second: { Authorization: string };
|
|
};
|
|
|
|
assert.equal(response.first.Authorization, "Bearer [REDACTED:auth_header]");
|
|
assert.equal(response.second.Authorization, "Bearer [REDACTED:auth_header]");
|
|
assert.equal(shared.Authorization, "Bearer shared-token");
|
|
});
|
|
});
|
|
|
|
test("preserves unchanged cyclic provider responses without JSON serialization", async () => {
|
|
await withCredentialRedactionEnabled(async () => {
|
|
const guardrail = new CredentialMaskerGuardrail();
|
|
const response: Record<string, unknown> = { value: undefined, nested: { safe: true } };
|
|
response.self = response;
|
|
const result = await guardrail.postCall(response);
|
|
assert.equal(result?.modifiedResponse, undefined);
|
|
assert.equal(response.value, undefined);
|
|
assert.equal(response.self, response);
|
|
});
|
|
});
|
|
|
|
test("does not re-redact an already-redacted structured header", async () => {
|
|
await withCredentialRedactionEnabled(async () => {
|
|
const guardrail = new CredentialMaskerGuardrail();
|
|
const response = { headers: { Authorization: "Bearer [REDACTED:auth_header]" } };
|
|
const result = await guardrail.postCall(response);
|
|
|
|
assert.equal(result?.modifiedResponse, undefined);
|
|
assert.equal(response.headers.Authorization, "Bearer [REDACTED:auth_header]");
|
|
});
|
|
});
|