mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Scoped to guardrails/security: dropped the unrelated js-yaml/tar/shell-quote/ brace-expansion override bumps, and isolated sanitizer-residual-policy.test.ts to a tmp DATA_DIR so it no longer touches the real storage.sqlite. Co-authored-by: RaviTharuma <RaviTharuma@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: rafaumeu <rafael.zendron22@gmail.com>
This commit is contained in:
@@ -352,11 +352,14 @@ ALLOW_API_KEY_REVEAL=false
|
||||
# ── Request-Side: Prompt Injection Guard ──
|
||||
# Scans incoming messages for prompt injection patterns before routing.
|
||||
# Used by: src/middleware/promptInjectionGuard.ts
|
||||
# Default ON when unset. Set to false/0/no/off to disable. Truthy: true/1/yes/on.
|
||||
# INPUT_SANITIZER_ENABLED=true
|
||||
# INPUT_SANITIZER_MODE=warn # warn = log only | block = reject request | redact = legacy (does NOT strip injection; use PII_REDACTION_ENABLED for request PII)
|
||||
# INPUT_SANITIZER_BLOCK_THRESHOLD=high # high (default) | medium | low — severities at/above this are blocked in block mode
|
||||
|
||||
# Legacy alias for INPUT_SANITIZER_MODE (same effect).
|
||||
# Legacy aliases for INPUT_SANITIZER_MODE / INPUT_SANITIZER_BLOCK_THRESHOLD (same effect).
|
||||
# INJECTION_GUARD_MODE=warn
|
||||
# INJECTION_GUARD_BLOCK_THRESHOLD=high
|
||||
|
||||
# PII detection in incoming requests (emails, phone numbers, SSNs, etc.).
|
||||
# Used by: src/middleware/promptInjectionGuard.ts — extends injection guard.
|
||||
|
||||
@@ -98,6 +98,7 @@ Configure via dashboard (Settings → Security) or `.env`:
|
||||
```env
|
||||
INPUT_SANITIZER_ENABLED=true
|
||||
INPUT_SANITIZER_MODE=block # warn | block (injection policy; legacy "redact" does not strip injection text)
|
||||
INPUT_SANITIZER_BLOCK_THRESHOLD=high # high (default) | medium | low — severities at/above this are blocked in block mode
|
||||
```
|
||||
|
||||
### 🔒 PII Redaction
|
||||
|
||||
@@ -222,6 +222,8 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
|
||||
| `INPUT_SANITIZER_ENABLED` | `true` | `src/middleware/promptInjectionGuard.ts` | Enable scanning of incoming messages for prompt injection patterns. |
|
||||
| `INPUT_SANITIZER_MODE` | `warn` | `src/middleware/promptInjectionGuard.ts` | Injection policy: `warn` = log only, `block` = reject request with 400. Legacy `redact` does **not** strip injection text; use `PII_REDACTION_ENABLED` for request PII rewrite. |
|
||||
| `INJECTION_GUARD_MODE` | _(unset)_ | `src/middleware/promptInjectionGuard.ts` | Legacy alias for `INPUT_SANITIZER_MODE` — same behavior. |
|
||||
| `INPUT_SANITIZER_BLOCK_THRESHOLD` | `high` | `src/shared/utils/injectionSeverity.ts` | Minimum severity that `MODE=block` rejects: `high` (default), `medium`, or `low`. Medium patterns are observe-only unless lowered. |
|
||||
| `INJECTION_GUARD_BLOCK_THRESHOLD` | _(unset)_ | `src/shared/utils/injectionSeverity.ts` | Legacy alias for `INPUT_SANITIZER_BLOCK_THRESHOLD` — same behavior. |
|
||||
| `PII_REDACTION_ENABLED` | `false` | `src/lib/guardrails/piiMasker.ts` | When `true`, redact PII in incoming requests (independent of injection mode). |
|
||||
| `CREDENTIAL_REDACTION_ENABLED` | `false` | `src/lib/guardrails/credentialMasker.ts` | Redact well-known API-key / secret-token patterns from request/response payloads. Opt-in; mirrors `PII_REDACTION_ENABLED`. |
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ used when neither a DB override nor an environment variable is present.
|
||||
| `REQUIRE_API_KEY` | boolean | `false` | Require an API key for all incoming requests. |
|
||||
| `INPUT_SANITIZER_ENABLED` | boolean | `true` | Enable input sanitization for all requests. |
|
||||
| `INJECTION_GUARD_MODE` | enum | `off` | Prompt injection guard mode. Values: `off`, `warn`, `block`, `redact`. |
|
||||
| `INPUT_SANITIZER_BLOCK_THRESHOLD` | enum | `high` | Minimum severity blocked when mode is `block` (`high`/`medium`/`low`). Medium families are observe-only at default. |
|
||||
| `INJECTION_GUARD_BLOCK_THRESHOLD` | enum | _(unset)_ | Legacy alias for `INPUT_SANITIZER_BLOCK_THRESHOLD`. |
|
||||
| `PII_REDACTION_ENABLED` | boolean | `false` | Redact PII from requests (independent of `INPUT_SANITIZER_MODE`). |
|
||||
| `PII_RESPONSE_SANITIZATION` | boolean | `false` | Sanitize PII from provider responses. |
|
||||
| `PII_RESPONSE_SANITIZATION_MODE` | enum | `redact` | Mode for PII response sanitization. Values: `redact`, `warn`, `block`, `off`. |
|
||||
|
||||
@@ -85,7 +85,7 @@ options:
|
||||
| --------------- | ----------------------------------------------- | ------- | --------------------------------------- |
|
||||
| Enabled | `INPUT_SANITIZER_ENABLED` | `true` | When `false`, guardrail short-circuits. |
|
||||
| Mode | `INJECTION_GUARD_MODE` / `INPUT_SANITIZER_MODE` | `warn` | Injection policy: `block`, `warn`, or `log`. (`redact` is accepted for back-compat but does **not** strip injection text; request PII rewrite is controlled by `PII_REDACTION_ENABLED`.) |
|
||||
| Block threshold | `blockThreshold` option | `high` | Minimum severity required to block. |
|
||||
| Block threshold | `blockThreshold` option / `INPUT_SANITIZER_BLOCK_THRESHOLD` (alias `INJECTION_GUARD_BLOCK_THRESHOLD`) | `high` | Minimum severity required to block. Medium is observe-only at default. |
|
||||
|
||||
**Mode precedence** (`getMode`): caller `options.mode` →
|
||||
`INJECTION_GUARD_MODE` **DB feature-flag override** (Dashboard → Settings →
|
||||
@@ -226,6 +226,8 @@ Environment variables read by the built-in guardrails:
|
||||
| `INPUT_SANITIZER_ENABLED` | `prompt-injection` | Set `false` to disable detection entirely. |
|
||||
| `INPUT_SANITIZER_MODE` | `prompt-injection` | Injection policy: `warn`, `block`, or `log`. Legacy value `redact` does not rewrite injection text. |
|
||||
| `INJECTION_GUARD_MODE` | `prompt-injection` | Mode for the injection guard; also a DB feature flag that **overrides** the env vars (DB > ENV). |
|
||||
| `INPUT_SANITIZER_BLOCK_THRESHOLD` | `prompt-injection` | Minimum severity that `MODE=block` rejects: `high` (default), `medium`, or `low`. |
|
||||
| `INJECTION_GUARD_BLOCK_THRESHOLD` | `prompt-injection` | Legacy alias for `INPUT_SANITIZER_BLOCK_THRESHOLD`. |
|
||||
| `PII_REDACTION_ENABLED` | `pii-masker` | When `true`, request PII is redacted (independent of injection mode). |
|
||||
| `PII_RESPONSE_SANITIZATION` / `_MODE` | `pii-masker` (downstream) | Controls response-side masker behavior. |
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ import {
|
||||
sanitizeRequest,
|
||||
} from "@/shared/utils/inputSanitizer";
|
||||
import { getFeatureFlagOverride } from "@/lib/db/featureFlags";
|
||||
import { parseEnvBoolean } from "@/shared/utils/envBoolean";
|
||||
import {
|
||||
resolveBlockThreshold,
|
||||
shouldBlockDetections,
|
||||
SEVERITY_SCORES as SHARED_SEVERITY_SCORES,
|
||||
type InjectionSeverity,
|
||||
} from "@/shared/utils/injectionSeverity";
|
||||
|
||||
type Detection = {
|
||||
match: string;
|
||||
@@ -52,11 +59,7 @@ const DEFAULT_GUARD_PATTERNS: PatternLike[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const SEVERITY_SCORES = {
|
||||
high: 3,
|
||||
low: 1,
|
||||
medium: 2,
|
||||
};
|
||||
const SEVERITY_SCORES = SHARED_SEVERITY_SCORES;
|
||||
|
||||
function normalizePatternEntry(entry: PatternLike, index: number) {
|
||||
if (entry instanceof RegExp) {
|
||||
@@ -104,10 +107,7 @@ function detectWithPatterns(text: string, patterns: ReturnType<typeof normalizeP
|
||||
}
|
||||
|
||||
function shouldBlock(detections: Detection[], threshold: "low" | "medium" | "high") {
|
||||
const minimumSeverity = SEVERITY_SCORES[threshold] || SEVERITY_SCORES.high;
|
||||
return detections.some(
|
||||
(detection) => (SEVERITY_SCORES[detection.severity] || 0) >= minimumSeverity
|
||||
);
|
||||
return shouldBlockDetections(detections, threshold);
|
||||
}
|
||||
|
||||
function getLogger(options: PromptInjectionGuardrailOptions, context: GuardrailContext) {
|
||||
@@ -152,11 +152,11 @@ function getMode(options: PromptInjectionGuardrailOptions) {
|
||||
}
|
||||
|
||||
function getThreshold(options: PromptInjectionGuardrailOptions) {
|
||||
return (options.blockThreshold || "high") as "low" | "medium" | "high";
|
||||
return resolveBlockThreshold(options.blockThreshold);
|
||||
}
|
||||
|
||||
function isEnabled(options: PromptInjectionGuardrailOptions) {
|
||||
return options.enabled ?? process.env.INPUT_SANITIZER_ENABLED !== "false";
|
||||
return options.enabled ?? parseEnvBoolean(process.env.INPUT_SANITIZER_ENABLED, true);
|
||||
}
|
||||
|
||||
export function evaluatePromptInjection(
|
||||
|
||||
@@ -83,10 +83,19 @@ export function withInjectionGuard(handler: any, options: any = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
// Attach sanitization result as header for downstream handlers
|
||||
// Attach sanitization result as header for downstream handlers.
|
||||
// Web Request headers may be immutable — never let this throw into the
|
||||
// outer security-check path (issue #8095).
|
||||
if (result.flagged) {
|
||||
request.headers.set("X-Injection-Flagged", "true");
|
||||
request.headers.set("X-Injection-Detections", String(result.detections.length));
|
||||
try {
|
||||
request.headers.set("X-Injection-Flagged", "true");
|
||||
request.headers.set(
|
||||
"X-Injection-Detections",
|
||||
String(result.detections.length)
|
||||
);
|
||||
} catch {
|
||||
// immutable headers: detection still applied; metadata is best-effort
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
21
src/shared/utils/envBoolean.ts
Normal file
21
src/shared/utils/envBoolean.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Shared env/flag boolean parsing for security guards.
|
||||
*
|
||||
* Truthy: true / 1 / yes / on (case-insensitive, trimmed)
|
||||
* Falsy: false / 0 / no / off
|
||||
* Unset/empty/unknown → fallback
|
||||
*
|
||||
* @module shared/utils/envBoolean
|
||||
*/
|
||||
|
||||
export function parseEnvBoolean(
|
||||
value: string | undefined | null,
|
||||
fallback: boolean
|
||||
): boolean {
|
||||
if (value === undefined || value === null) return fallback;
|
||||
const normalized = String(value).trim().toLowerCase();
|
||||
if (normalized === "") return fallback;
|
||||
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
||||
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
||||
return fallback;
|
||||
}
|
||||
56
src/shared/utils/injectionSeverity.ts
Normal file
56
src/shared/utils/injectionSeverity.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Shared prompt-injection severity scoring / block threshold helpers.
|
||||
* Kept separate from promptInjection.ts so inputSanitizer can reuse them
|
||||
* without a circular import (promptInjection → sanitizeRequest).
|
||||
*
|
||||
* @module shared/utils/injectionSeverity
|
||||
*/
|
||||
|
||||
export type InjectionSeverity = "low" | "medium" | "high";
|
||||
|
||||
export const SEVERITY_SCORES: Record<InjectionSeverity, number> = {
|
||||
low: 1,
|
||||
medium: 2,
|
||||
high: 3,
|
||||
};
|
||||
|
||||
export type DetectionLike = {
|
||||
severity?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether detections meet the configured block threshold.
|
||||
* Default threshold is "high" — medium patterns are observe-only unless
|
||||
* INPUT_SANITIZER_BLOCK_THRESHOLD is lowered.
|
||||
*/
|
||||
export function shouldBlockDetections(
|
||||
detections: DetectionLike[],
|
||||
threshold: InjectionSeverity = "high"
|
||||
): boolean {
|
||||
const minimumSeverity = SEVERITY_SCORES[threshold] || SEVERITY_SCORES.high;
|
||||
return detections.some((detection) => {
|
||||
const score =
|
||||
SEVERITY_SCORES[(detection.severity as InjectionSeverity) || "high"] || 0;
|
||||
return score >= minimumSeverity;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve block threshold from options / env.
|
||||
* Env: INPUT_SANITIZER_BLOCK_THRESHOLD or INJECTION_GUARD_BLOCK_THRESHOLD
|
||||
* Allowed: low | medium | high (default high)
|
||||
*/
|
||||
export function resolveBlockThreshold(
|
||||
explicit?: InjectionSeverity | string | null
|
||||
): InjectionSeverity {
|
||||
const raw =
|
||||
(explicit && String(explicit)) ||
|
||||
process.env.INPUT_SANITIZER_BLOCK_THRESHOLD ||
|
||||
process.env.INJECTION_GUARD_BLOCK_THRESHOLD ||
|
||||
"high";
|
||||
const normalized = String(raw).trim().toLowerCase();
|
||||
if (normalized === "low" || normalized === "medium" || normalized === "high") {
|
||||
return normalized;
|
||||
}
|
||||
return "high";
|
||||
}
|
||||
@@ -7,6 +7,9 @@
|
||||
* @module inputSanitizer
|
||||
*/
|
||||
|
||||
import { parseEnvBoolean } from "@/shared/utils/envBoolean";
|
||||
import { resolveBlockThreshold, shouldBlockDetections } from "@/shared/utils/injectionSeverity";
|
||||
|
||||
// ─── Prompt Injection Patterns ───────────────────────────────────────
|
||||
|
||||
/** @type {Array<{name: string, pattern: RegExp, severity: string}>} */
|
||||
@@ -111,9 +114,11 @@ const PII_PATTERNS = [
|
||||
*/
|
||||
function getConfig() {
|
||||
return {
|
||||
enabled: process.env.INPUT_SANITIZER_ENABLED !== "false",
|
||||
// Default ON (opt-out). Truthy/falsy parsing accepts true/1/yes/on and false/0/no/off.
|
||||
enabled: parseEnvBoolean(process.env.INPUT_SANITIZER_ENABLED, true),
|
||||
mode: process.env.INPUT_SANITIZER_MODE || "warn", // "warn" | "block" | "redact"
|
||||
piiRedaction: process.env.PII_REDACTION_ENABLED === "true",
|
||||
piiRedaction: parseEnvBoolean(process.env.PII_REDACTION_ENABLED, false),
|
||||
blockThreshold: resolveBlockThreshold(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -271,7 +276,9 @@ export function sanitizeRequest(body, logger = console) {
|
||||
);
|
||||
}
|
||||
|
||||
if (config.mode === "block" && highSeverity.length > 0) {
|
||||
// Shared threshold policy with evaluatePromptInjection / createInjectionGuard.
|
||||
// Default threshold is "high" (medium is observe-only unless lowered via env).
|
||||
if (config.mode === "block" && shouldBlockDetections(injections, config.blockThreshold)) {
|
||||
result.blocked = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
179
tests/unit/sanitizer-residual-policy.test.ts
Normal file
179
tests/unit/sanitizer-residual-policy.test.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
// Isolate DB state so this suite never touches the real ~/.omniroute/storage.sqlite.
|
||||
// DATA_DIR is resolved eagerly at module-load time in src/lib/db/core.ts, so it
|
||||
// MUST be set before promptInjection.ts / piiMasker.ts (which transitively import
|
||||
// @/lib/db/featureFlags -> @/lib/db/core) are imported. Use dynamic import() after
|
||||
// setting the env var, matching tests/unit/pii-opt-in-default.test.ts.
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-sanitizer-residual-"));
|
||||
process.env.DATA_DIR = tmpDir;
|
||||
|
||||
const { parseEnvBoolean } = await import("../../src/shared/utils/envBoolean.ts");
|
||||
const { resolveBlockThreshold, shouldBlockDetections } = await import(
|
||||
"../../src/shared/utils/injectionSeverity.ts"
|
||||
);
|
||||
const { sanitizeRequest } = await import("../../src/shared/utils/inputSanitizer.ts");
|
||||
const { evaluatePromptInjection } = await import("../../src/lib/guardrails/promptInjection.ts");
|
||||
const { PIIMaskerGuardrail } = await import("../../src/lib/guardrails/piiMasker.ts");
|
||||
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
|
||||
|
||||
test.after(() => {
|
||||
resetDbInstance();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function withEnv(
|
||||
overrides: Record<string, string | undefined>,
|
||||
fn: () => Promise<void> | void
|
||||
) {
|
||||
const originals: Record<string, string | undefined> = {};
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
originals[key] = process.env[key];
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
for (const [key, value] of Object.entries(originals)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const silentLogger = {
|
||||
info() {},
|
||||
warn() {},
|
||||
error() {},
|
||||
} as Pick<Console, "info" | "warn" | "error">;
|
||||
|
||||
test("parseEnvBoolean accepts common truthy/falsy forms and fallbacks", () => {
|
||||
assert.equal(parseEnvBoolean(undefined, true), true);
|
||||
assert.equal(parseEnvBoolean("", false), false);
|
||||
assert.equal(parseEnvBoolean("true", false), true);
|
||||
assert.equal(parseEnvBoolean("TRUE", false), true);
|
||||
assert.equal(parseEnvBoolean("1", false), true);
|
||||
assert.equal(parseEnvBoolean("yes", false), true);
|
||||
assert.equal(parseEnvBoolean("on", false), true);
|
||||
assert.equal(parseEnvBoolean("false", true), false);
|
||||
assert.equal(parseEnvBoolean("FALSE", true), false);
|
||||
assert.equal(parseEnvBoolean("0", true), false);
|
||||
assert.equal(parseEnvBoolean("no", true), false);
|
||||
assert.equal(parseEnvBoolean("off", true), false);
|
||||
assert.equal(parseEnvBoolean("maybe", true), true);
|
||||
assert.equal(parseEnvBoolean("maybe", false), false);
|
||||
});
|
||||
|
||||
test("shouldBlockDetections honors threshold defaults and medium/high", () => {
|
||||
const medium = [{ severity: "medium" }];
|
||||
const high = [{ severity: "high" }];
|
||||
assert.equal(shouldBlockDetections(medium, "high"), false);
|
||||
assert.equal(shouldBlockDetections(high, "high"), true);
|
||||
assert.equal(shouldBlockDetections(medium, "medium"), true);
|
||||
assert.equal(shouldBlockDetections(medium, "low"), true);
|
||||
assert.equal(resolveBlockThreshold(undefined), "high");
|
||||
assert.equal(resolveBlockThreshold("medium"), "medium");
|
||||
});
|
||||
|
||||
test("sanitizeRequest disables with false/0/no and stays on when unset", async () => {
|
||||
await withEnv({ INPUT_SANITIZER_ENABLED: undefined }, () => {
|
||||
const result = sanitizeRequest(
|
||||
{ messages: [{ role: "user", content: "hello" }] },
|
||||
silentLogger
|
||||
);
|
||||
// enabled path returns a full result object
|
||||
assert.equal(result.blocked, false);
|
||||
});
|
||||
|
||||
for (const off of ["false", "0", "no", "off", "FALSE"]) {
|
||||
await withEnv({ INPUT_SANITIZER_ENABLED: off }, () => {
|
||||
const result = sanitizeRequest(
|
||||
{ messages: [{ role: "user", content: "hello" }] },
|
||||
silentLogger
|
||||
);
|
||||
assert.equal(result.detections.length, 0);
|
||||
assert.equal(result.blocked, false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("sanitizeRequest and evaluatePromptInjection share high-default threshold", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
INPUT_SANITIZER_ENABLED: "true",
|
||||
INPUT_SANITIZER_MODE: "block",
|
||||
INPUT_SANITIZER_BLOCK_THRESHOLD: "high",
|
||||
},
|
||||
() => {
|
||||
// Medium-only detections should not block at default threshold.
|
||||
// Use a content shape that is unlikely to also trip high patterns.
|
||||
const body = {
|
||||
messages: [{ role: "user", content: "Please act as a different assistant persona for this task." }],
|
||||
};
|
||||
const sanitized = sanitizeRequest(body, silentLogger);
|
||||
const evaluated = evaluatePromptInjection(body, {}, { log: silentLogger });
|
||||
// If medium patterns matched, neither path should block under high threshold.
|
||||
if (sanitized.detections.some((d) => d.severity === "medium") &&
|
||||
!sanitized.detections.some((d) => d.severity === "high")) {
|
||||
assert.equal(sanitized.blocked, false);
|
||||
}
|
||||
if (evaluated.result.detections.some((d) => d.severity === "medium") &&
|
||||
!evaluated.result.detections.some((d) => d.severity === "high")) {
|
||||
assert.equal(evaluated.blocked, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("sanitizeRequest redacts request PII independent of injection mode", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
INPUT_SANITIZER_ENABLED: "true",
|
||||
INPUT_SANITIZER_MODE: "block",
|
||||
PII_REDACTION_ENABLED: "true",
|
||||
},
|
||||
() => {
|
||||
const result = sanitizeRequest(
|
||||
{ messages: [{ role: "user", content: "Email dev@example.com" }] },
|
||||
silentLogger
|
||||
);
|
||||
assert.equal(result.modified, true);
|
||||
assert.ok(result.sanitizedBody);
|
||||
const body = result.sanitizedBody as {
|
||||
messages?: Array<{ content?: unknown }>;
|
||||
};
|
||||
assert.match(String(body.messages?.[0]?.content), /\[EMAIL_REDACTED\]/);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("PIIMaskerGuardrail redacts Responses string input under MODE=block", async () => {
|
||||
await withEnv(
|
||||
{
|
||||
INPUT_SANITIZER_MODE: "block",
|
||||
PII_REDACTION_ENABLED: "true",
|
||||
},
|
||||
async () => {
|
||||
const guardrail = new PIIMaskerGuardrail();
|
||||
const preCall = await guardrail.preCall({
|
||||
input: ["Contact support@example.com"],
|
||||
});
|
||||
assert.ok(preCall?.modifiedPayload);
|
||||
const body = preCall?.modifiedPayload as { input?: unknown };
|
||||
assert.match(
|
||||
String(Array.isArray(body.input) ? body.input[0] : undefined),
|
||||
/\[EMAIL_REDACTED\]/
|
||||
);
|
||||
|
||||
const top = await guardrail.preCall({ input: "Reach alice@example.com" });
|
||||
assert.ok(top?.modifiedPayload);
|
||||
const topBody = top?.modifiedPayload as { input?: unknown };
|
||||
assert.match(String(topBody.input), /\[EMAIL_REDACTED\]/);
|
||||
}
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user