Files
OmniRoute/tests/unit/credential-masker-guardrail.test.ts
Adrian Padurean 613464c24d feat(guardrails): add CredentialMaskerGuardrail for API key/secret redaction (#7683)
* 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>
2026-07-19 21:22:51 -03:00

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]");
});
});