@graphorin/observability 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/cost/cost-tracker.d.ts +28 -0
- package/dist/cost/cost-tracker.d.ts.map +1 -0
- package/dist/cost/cost-tracker.js +169 -0
- package/dist/cost/cost-tracker.js.map +1 -0
- package/dist/cost/index.d.ts +3 -0
- package/dist/cost/index.js +3 -0
- package/dist/cost/types.d.ts +104 -0
- package/dist/cost/types.d.ts.map +1 -0
- package/dist/eval/index.d.ts +3 -0
- package/dist/eval/index.js +3 -0
- package/dist/eval/runner.d.ts +11 -0
- package/dist/eval/runner.d.ts.map +1 -0
- package/dist/eval/runner.js +113 -0
- package/dist/eval/runner.js.map +1 -0
- package/dist/eval/types.d.ts +108 -0
- package/dist/eval/types.d.ts.map +1 -0
- package/dist/exporters/console.d.ts +27 -0
- package/dist/exporters/console.d.ts.map +1 -0
- package/dist/exporters/console.js +47 -0
- package/dist/exporters/console.js.map +1 -0
- package/dist/exporters/index.d.ts +6 -0
- package/dist/exporters/index.js +7 -0
- package/dist/exporters/jsonl.d.ts +57 -0
- package/dist/exporters/jsonl.d.ts.map +1 -0
- package/dist/exporters/jsonl.js +95 -0
- package/dist/exporters/jsonl.js.map +1 -0
- package/dist/exporters/otlp-http.d.ts +38 -0
- package/dist/exporters/otlp-http.d.ts.map +1 -0
- package/dist/exporters/otlp-http.js +115 -0
- package/dist/exporters/otlp-http.js.map +1 -0
- package/dist/exporters/types.d.ts +72 -0
- package/dist/exporters/types.d.ts.map +1 -0
- package/dist/exporters/types.js +13 -0
- package/dist/exporters/types.js.map +1 -0
- package/dist/exporters/with-validation.d.ts +45 -0
- package/dist/exporters/with-validation.d.ts.map +1 -0
- package/dist/exporters/with-validation.js +99 -0
- package/dist/exporters/with-validation.js.map +1 -0
- package/dist/gen-ai/emit.d.ts +27 -0
- package/dist/gen-ai/emit.d.ts.map +1 -0
- package/dist/gen-ai/emit.js +88 -0
- package/dist/gen-ai/emit.js.map +1 -0
- package/dist/gen-ai/index.d.ts +5 -0
- package/dist/gen-ai/index.js +5 -0
- package/dist/gen-ai/operation-mapping.d.ts +24 -0
- package/dist/gen-ai/operation-mapping.d.ts.map +1 -0
- package/dist/gen-ai/operation-mapping.js +67 -0
- package/dist/gen-ai/operation-mapping.js.map +1 -0
- package/dist/gen-ai/system-derivation.d.ts +41 -0
- package/dist/gen-ai/system-derivation.d.ts.map +1 -0
- package/dist/gen-ai/system-derivation.js +67 -0
- package/dist/gen-ai/system-derivation.js.map +1 -0
- package/dist/gen-ai/types.d.ts +87 -0
- package/dist/gen-ai/types.d.ts.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/logger/index.d.ts +2 -0
- package/dist/logger/index.js +3 -0
- package/dist/logger/logger.d.ts +53 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +145 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/openinference/index.d.ts +45 -0
- package/dist/openinference/index.d.ts.map +1 -0
- package/dist/openinference/index.js +92 -0
- package/dist/openinference/index.js.map +1 -0
- package/dist/redaction/config.d.ts +39 -0
- package/dist/redaction/config.d.ts.map +1 -0
- package/dist/redaction/config.js +15 -0
- package/dist/redaction/config.js.map +1 -0
- package/dist/redaction/errors.d.ts +36 -0
- package/dist/redaction/errors.d.ts.map +1 -0
- package/dist/redaction/errors.js +42 -0
- package/dist/redaction/errors.js.map +1 -0
- package/dist/redaction/imperative-patterns.d.ts +108 -0
- package/dist/redaction/imperative-patterns.d.ts.map +1 -0
- package/dist/redaction/imperative-patterns.js +187 -0
- package/dist/redaction/imperative-patterns.js.map +1 -0
- package/dist/redaction/index.d.ts +7 -0
- package/dist/redaction/index.js +7 -0
- package/dist/redaction/patterns.d.ts +85 -0
- package/dist/redaction/patterns.d.ts.map +1 -0
- package/dist/redaction/patterns.js +172 -0
- package/dist/redaction/patterns.js.map +1 -0
- package/dist/redaction/types.d.ts +103 -0
- package/dist/redaction/types.d.ts.map +1 -0
- package/dist/redaction/validator.d.ts +26 -0
- package/dist/redaction/validator.d.ts.map +1 -0
- package/dist/redaction/validator.js +195 -0
- package/dist/redaction/validator.js.map +1 -0
- package/dist/replay/config.d.ts +56 -0
- package/dist/replay/config.d.ts.map +1 -0
- package/dist/replay/config.js +18 -0
- package/dist/replay/config.js.map +1 -0
- package/dist/replay/index.d.ts +5 -0
- package/dist/replay/index.js +5 -0
- package/dist/replay/log.d.ts +39 -0
- package/dist/replay/log.d.ts.map +1 -0
- package/dist/replay/log.js +59 -0
- package/dist/replay/log.js.map +1 -0
- package/dist/replay/replay.d.ts +25 -0
- package/dist/replay/replay.d.ts.map +1 -0
- package/dist/replay/replay.js +173 -0
- package/dist/replay/replay.js.map +1 -0
- package/dist/replay/types.d.ts +110 -0
- package/dist/replay/types.d.ts.map +1 -0
- package/dist/telemetry/index.d.ts +63 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +49 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/tracer/ids.d.ts +23 -0
- package/dist/tracer/ids.d.ts.map +1 -0
- package/dist/tracer/ids.js +30 -0
- package/dist/tracer/ids.js.map +1 -0
- package/dist/tracer/index.d.ts +6 -0
- package/dist/tracer/index.js +6 -0
- package/dist/tracer/sampling.d.ts +77 -0
- package/dist/tracer/sampling.d.ts.map +1 -0
- package/dist/tracer/sampling.js +67 -0
- package/dist/tracer/sampling.js.map +1 -0
- package/dist/tracer/span-names.d.ts +11 -0
- package/dist/tracer/span-names.d.ts.map +1 -0
- package/dist/tracer/span-names.js +11 -0
- package/dist/tracer/span-names.js.map +1 -0
- package/dist/tracer/span.d.ts +27 -0
- package/dist/tracer/span.d.ts.map +1 -0
- package/dist/tracer/span.js +80 -0
- package/dist/tracer/span.js.map +1 -0
- package/dist/tracer/tracer.d.ts +101 -0
- package/dist/tracer/tracer.d.ts.map +1 -0
- package/dist/tracer/tracer.js +150 -0
- package/dist/tracer/tracer.js.map +1 -0
- package/package.json +138 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region src/redaction/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by the validator when `failOnUnredactedSensitive: true` and
|
|
4
|
+
* a value would otherwise be dropped or masked. The error never
|
|
5
|
+
* carries the secret value itself; only the sanitized
|
|
6
|
+
* {@link RedactionViolation} metadata.
|
|
7
|
+
*
|
|
8
|
+
* @stable
|
|
9
|
+
*/
|
|
10
|
+
var RedactionValidationError = class extends Error {
|
|
11
|
+
/** @stable */
|
|
12
|
+
kind = "redaction-validation-failed";
|
|
13
|
+
/** @stable */
|
|
14
|
+
violation;
|
|
15
|
+
constructor(message, violation) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "RedactionValidationError";
|
|
18
|
+
this.violation = violation;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Thrown at startup when an exporter is registered without going
|
|
23
|
+
* through `withValidation(...)`. Enforces ADR-035: every exporter
|
|
24
|
+
* must validate before forwarding.
|
|
25
|
+
*
|
|
26
|
+
* @stable
|
|
27
|
+
*/
|
|
28
|
+
var UnvalidatedExporterError = class extends Error {
|
|
29
|
+
/** @stable */
|
|
30
|
+
kind = "unvalidated-exporter";
|
|
31
|
+
/** Exporter identifier (typically `exporter.constructor.name`). */
|
|
32
|
+
exporterId;
|
|
33
|
+
constructor(exporterId) {
|
|
34
|
+
super(`Exporter "${exporterId}" was registered without withValidation(). All exporters must be wrapped — see RedactionValidator policy. Wrap with \`withValidation(exporter, opts)\` or set \`validation: 'off'\` (NOT recommended) on the tracer to opt out explicitly (logs a startup WARN).`);
|
|
35
|
+
this.name = "UnvalidatedExporterError";
|
|
36
|
+
this.exporterId = exporterId;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { RedactionValidationError, UnvalidatedExporterError };
|
|
42
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../../src/redaction/errors.ts"],"sourcesContent":["/**\n * Errors thrown by the redaction layer.\n *\n * @packageDocumentation\n */\n\nimport type { RedactionViolation } from './types.js';\n\n/**\n * Thrown by the validator when `failOnUnredactedSensitive: true` and\n * a value would otherwise be dropped or masked. The error never\n * carries the secret value itself; only the sanitized\n * {@link RedactionViolation} metadata.\n *\n * @stable\n */\nexport class RedactionValidationError extends Error {\n /** @stable */\n readonly kind = 'redaction-validation-failed' as const;\n /** @stable */\n readonly violation: RedactionViolation;\n\n constructor(message: string, violation: RedactionViolation) {\n super(message);\n this.name = 'RedactionValidationError';\n this.violation = violation;\n }\n}\n\n/**\n * Thrown at startup when an exporter is registered without going\n * through `withValidation(...)`. Enforces ADR-035: every exporter\n * must validate before forwarding.\n *\n * @stable\n */\nexport class UnvalidatedExporterError extends Error {\n /** @stable */\n readonly kind = 'unvalidated-exporter' as const;\n /** Exporter identifier (typically `exporter.constructor.name`). */\n readonly exporterId: string;\n\n constructor(exporterId: string) {\n super(\n `Exporter \"${exporterId}\" was registered without withValidation(). All ` +\n 'exporters must be wrapped — see RedactionValidator policy. Wrap with ' +\n \"`withValidation(exporter, opts)` or set `validation: 'off'` (NOT recommended) \" +\n 'on the tracer to opt out explicitly (logs a startup WARN).',\n );\n this.name = 'UnvalidatedExporterError';\n this.exporterId = exporterId;\n }\n}\n"],"mappings":";;;;;;;;;AAgBA,IAAa,2BAAb,cAA8C,MAAM;;CAElD,AAAS,OAAO;;CAEhB,AAAS;CAET,YAAY,SAAiB,WAA+B;AAC1D,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,YAAY;;;;;;;;;;AAWrB,IAAa,2BAAb,cAA8C,MAAM;;CAElD,AAAS,OAAO;;CAEhB,AAAS;CAET,YAAY,YAAoB;AAC9B,QACE,aAAa,WAAW,kQAIzB;AACD,OAAK,OAAO;AACZ,OAAK,aAAa"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//#region src/redaction/imperative-patterns.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Imperative-pattern catalogue for inbound prompt-injection defence.
|
|
4
|
+
*
|
|
5
|
+
* Sibling to {@link BUILT_IN_PATTERNS} (PII / secrets) — the two
|
|
6
|
+
* catalogues are disjoint by construction. The imperative catalogue
|
|
7
|
+
* is consumed by the inbound sanitization layer in `@graphorin/tools`
|
|
8
|
+
* to scan tool / MCP results before they reach the agent's message
|
|
9
|
+
* store; the PII / secrets catalogue is consumed by the outbound
|
|
10
|
+
* exporter validators to scan span attributes before they reach an
|
|
11
|
+
* exporter.
|
|
12
|
+
*
|
|
13
|
+
* The patterns target the canonical English "ignore previous
|
|
14
|
+
* instructions" / "system override" injection family — the concrete
|
|
15
|
+
* surface that an untrusted-skill or MCP server result might use to
|
|
16
|
+
* smuggle imperative content into the next provider call. The
|
|
17
|
+
* catalogue is intentionally conservative: every entry has a fixed-
|
|
18
|
+
* substring prefilter so the per-byte scan budget stays sub-millisecond
|
|
19
|
+
* on typical 16 KB tool results.
|
|
20
|
+
*
|
|
21
|
+
* @packageDocumentation
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Stable name of an imperative pattern. The catalogue is curated;
|
|
25
|
+
* user-supplied patterns can use any identifier they want and will be
|
|
26
|
+
* passed through the sanitization layer alongside the built-ins.
|
|
27
|
+
*
|
|
28
|
+
* @stable
|
|
29
|
+
*/
|
|
30
|
+
type ImperativePatternName = 'ignore-previous-instructions' | 'forget-instructions' | 'override-instructions' | 'system-prompt-leak' | 'role-reassignment' | 'developer-mode' | 'jailbreak-marker' | 'tool-call-injection' | 'role-tag-injection';
|
|
31
|
+
/**
|
|
32
|
+
* One entry in the imperative-pattern catalogue. The shape mirrors
|
|
33
|
+
* {@link BUILT_IN_PATTERNS} so consumers can share scan / replace
|
|
34
|
+
* machinery, but the fields are typed as imperative-only so the two
|
|
35
|
+
* catalogues do not accidentally merge.
|
|
36
|
+
*
|
|
37
|
+
* @stable
|
|
38
|
+
*/
|
|
39
|
+
interface ImperativePattern {
|
|
40
|
+
readonly name: ImperativePatternName | (string & {});
|
|
41
|
+
readonly description: string;
|
|
42
|
+
/**
|
|
43
|
+
* Cheap substring prefilter applied before the regex; if the body
|
|
44
|
+
* does not contain any of the prefilter substrings the regex is
|
|
45
|
+
* skipped entirely. The prefilter is case-insensitive.
|
|
46
|
+
*/
|
|
47
|
+
readonly prefilter: ReadonlyArray<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Full regex applied when the prefilter matches. Always carries the
|
|
50
|
+
* `g` and `i` flags; the catalogue construction validates this.
|
|
51
|
+
*/
|
|
52
|
+
readonly regex: RegExp;
|
|
53
|
+
/** Replacement string applied by the `'detect-and-strip*'` policies. */
|
|
54
|
+
readonly mask: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The default-on imperative-pattern catalogue. Stable across patches;
|
|
58
|
+
* additions during the pre-1.0 window are minor-bumps because new
|
|
59
|
+
* patterns may produce additional `tool.inbound.sanitization.hit{...}`
|
|
60
|
+
* counter increments on existing deployments.
|
|
61
|
+
*
|
|
62
|
+
* @stable
|
|
63
|
+
*/
|
|
64
|
+
declare const BUILT_IN_IMPERATIVE_PATTERNS: readonly ImperativePattern[];
|
|
65
|
+
/**
|
|
66
|
+
* Combined Aho-Corasick-style prefilter set across every pattern.
|
|
67
|
+
* Lower-cased substrings; consumers test the body once with the
|
|
68
|
+
* combined filter before iterating regexes.
|
|
69
|
+
*
|
|
70
|
+
* @stable
|
|
71
|
+
*/
|
|
72
|
+
declare const IMPERATIVE_PREFILTER_SUBSTRINGS: ReadonlyArray<string>;
|
|
73
|
+
/**
|
|
74
|
+
* Compiled scan helper. Returns the list of pattern names that fired
|
|
75
|
+
* AND the number of bytes the strip would remove if applied. Bounded
|
|
76
|
+
* by the budget hint — when exceeded, returns `null` to let the caller
|
|
77
|
+
* apply the best-effort `'detect-failed'` annotation.
|
|
78
|
+
*
|
|
79
|
+
* @stable
|
|
80
|
+
*/
|
|
81
|
+
interface ScanResult {
|
|
82
|
+
readonly hits: ReadonlyArray<{
|
|
83
|
+
readonly pattern: string;
|
|
84
|
+
readonly matchCount: number;
|
|
85
|
+
}>;
|
|
86
|
+
readonly bytesMatched: number;
|
|
87
|
+
readonly scanDurationUs: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Run the imperative-pattern scan against `body`. Patterns are
|
|
91
|
+
* iterated in catalogue order; the prefilter shortcut returns early
|
|
92
|
+
* for bodies that do not contain any imperative-family substring.
|
|
93
|
+
*
|
|
94
|
+
* @stable
|
|
95
|
+
*/
|
|
96
|
+
declare function scanImperativePatterns(body: string, patterns?: ReadonlyArray<ImperativePattern>, budgetMs?: number): ScanResult | null;
|
|
97
|
+
/**
|
|
98
|
+
* Apply `pattern.mask` to every match of every pattern in `body`. Used
|
|
99
|
+
* by the `'detect-and-strip*'` policies. The mask is calibrated to NOT
|
|
100
|
+
* match any imperative pattern itself, so post-strip bodies do not
|
|
101
|
+
* trigger another scan hit on round trips.
|
|
102
|
+
*
|
|
103
|
+
* @stable
|
|
104
|
+
*/
|
|
105
|
+
declare function stripImperativePatterns(body: string, patterns?: ReadonlyArray<ImperativePattern>): string;
|
|
106
|
+
//#endregion
|
|
107
|
+
export { BUILT_IN_IMPERATIVE_PATTERNS, IMPERATIVE_PREFILTER_SUBSTRINGS, ImperativePattern, ImperativePatternName, ScanResult, scanImperativePatterns, stripImperativePatterns };
|
|
108
|
+
//# sourceMappingURL=imperative-patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imperative-patterns.d.ts","names":[],"sources":["../../src/redaction/imperative-patterns.ts"],"sourcesContent":[],"mappings":";;AA6BA;AAmBA;;;;;AAuGA;AASA;AAYA;AAaA;;;;;AA4DA;;;;;;;;;;;;;KAxNY,qBAAA;;;;;;;;;UAmBK,iBAAA;iBACA;;;;;;;sBAOK;;;;;kBAKJ;;;;;;;;;;;;cA0FL,uCAAuC;;;;;;;;cASvC,iCAAiC;;;;;;;;;UAY7B,UAAA;iBACA;;;;;;;;;;;;;;iBAYD,sBAAA,0BAEJ,cAAc,wCAEvB;;;;;;;;;iBAwDa,uBAAA,0BAEJ,cAAc"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
//#region src/redaction/imperative-patterns.ts
|
|
2
|
+
const PATTERNS = [
|
|
3
|
+
{
|
|
4
|
+
name: "ignore-previous-instructions",
|
|
5
|
+
description: "The canonical \"ignore previous instructions\" injection — the most common form of prompt injection seen across MCP results.",
|
|
6
|
+
prefilter: [
|
|
7
|
+
"ignore",
|
|
8
|
+
"disregard",
|
|
9
|
+
"forget"
|
|
10
|
+
],
|
|
11
|
+
regex: /\b(?:ignore|disregard|forget)\s+(?:all\s+)?(?:the\s+)?(?:previous|prior|above|earlier|preceding|original)\s+(?:instructions?|prompts?|messages?|directives?|rules?)\b/gi,
|
|
12
|
+
mask: "[REDACTED:imperative-pattern]"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "forget-instructions",
|
|
16
|
+
description: "Imperative \"forget what I told you\" / memory-wipe family.",
|
|
17
|
+
prefilter: [
|
|
18
|
+
"forget",
|
|
19
|
+
"erase",
|
|
20
|
+
"clear"
|
|
21
|
+
],
|
|
22
|
+
regex: /\b(?:forget|erase|clear|wipe)\s+(?:everything|all)\s+(?:above|before|previous|i\s+(?:told|said))\b/gi,
|
|
23
|
+
mask: "[REDACTED:imperative-pattern]"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "override-instructions",
|
|
27
|
+
description: "\"Override the system prompt\" / \"new instructions\" injection family.",
|
|
28
|
+
prefilter: [
|
|
29
|
+
"override",
|
|
30
|
+
"new instructions",
|
|
31
|
+
"updated instructions",
|
|
32
|
+
"replace"
|
|
33
|
+
],
|
|
34
|
+
regex: /\b(?:override|replace)\s+(?:the\s+)?(?:system\s+)?(?:prompt|instructions?|directives?)\b|\bnew\s+(?:instructions?|prompts?|directives?)\s*(?::|follow|are)\b|\bupdated\s+(?:instructions?|prompts?|directives?)\b/gi,
|
|
35
|
+
mask: "[REDACTED:imperative-pattern]"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "system-prompt-leak",
|
|
39
|
+
description: "Requests asking the model to reveal its system prompt / hidden instructions / configuration.",
|
|
40
|
+
prefilter: [
|
|
41
|
+
"system prompt",
|
|
42
|
+
"system message",
|
|
43
|
+
"reveal",
|
|
44
|
+
"print",
|
|
45
|
+
"show"
|
|
46
|
+
],
|
|
47
|
+
regex: /\b(?:reveal|print|show|output|repeat|expose|disclose)\s+(?:your|the)\s+(?:system\s+(?:prompt|message|instructions?)|hidden\s+instructions?|initial\s+(?:prompt|instructions?)|configuration)\b/gi,
|
|
48
|
+
mask: "[REDACTED:imperative-pattern]"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "role-reassignment",
|
|
52
|
+
description: "\"You are now ...\" / role-reassignment imperative family.",
|
|
53
|
+
prefilter: [
|
|
54
|
+
"you are now",
|
|
55
|
+
"you are no longer",
|
|
56
|
+
"pretend",
|
|
57
|
+
"act as"
|
|
58
|
+
],
|
|
59
|
+
regex: /\byou\s+are\s+(?:now|no\s+longer)\s+(?:a\b|an\b|the\b)|\bpretend\s+(?:to\s+be|you\s+are)\b|\bact\s+as\s+(?:a|an|the|if\s+you)\b/gi,
|
|
60
|
+
mask: "[REDACTED:imperative-pattern]"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "developer-mode",
|
|
64
|
+
description: "Requests to enter \"developer mode\" / \"DAN\" / unrestricted modes — a long-running jailbreak family.",
|
|
65
|
+
prefilter: [
|
|
66
|
+
"developer mode",
|
|
67
|
+
"admin mode",
|
|
68
|
+
"unrestricted"
|
|
69
|
+
],
|
|
70
|
+
regex: /\b(?:enter|enable|activate)\s+(?:developer|admin|debug|unrestricted|god|root)\s+mode\b/gi,
|
|
71
|
+
mask: "[REDACTED:imperative-pattern]"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "jailbreak-marker",
|
|
75
|
+
description: "Direct \"jailbreak\" markers in tool result content.",
|
|
76
|
+
prefilter: ["jailbreak", "jailbroken"],
|
|
77
|
+
regex: /\bjailbroken?\s+(?:mode|response|model|assistant)\b/gi,
|
|
78
|
+
mask: "[REDACTED:imperative-pattern]"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "tool-call-injection",
|
|
82
|
+
description: "Tool-call markers that try to coerce the model into invoking a tool from the result content.",
|
|
83
|
+
prefilter: [
|
|
84
|
+
"<tool_use",
|
|
85
|
+
"<function_call",
|
|
86
|
+
"<invoke"
|
|
87
|
+
],
|
|
88
|
+
regex: /<(?:tool_use|function_call|invoke|tool_call)\b[^>]*>/gi,
|
|
89
|
+
mask: "[REDACTED:imperative-pattern]"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "role-tag-injection",
|
|
93
|
+
description: "Chat-role tags injected into tool results to spoof a system / assistant message in the conversation.",
|
|
94
|
+
prefilter: [
|
|
95
|
+
"<|im_start|",
|
|
96
|
+
"<|system|",
|
|
97
|
+
"<|assistant|",
|
|
98
|
+
"<|user|"
|
|
99
|
+
],
|
|
100
|
+
regex: /<\|(?:im_start|system|assistant|user|im_end)\|>/gi,
|
|
101
|
+
mask: "[REDACTED:imperative-pattern]"
|
|
102
|
+
}
|
|
103
|
+
];
|
|
104
|
+
/**
|
|
105
|
+
* The default-on imperative-pattern catalogue. Stable across patches;
|
|
106
|
+
* additions during the pre-1.0 window are minor-bumps because new
|
|
107
|
+
* patterns may produce additional `tool.inbound.sanitization.hit{...}`
|
|
108
|
+
* counter increments on existing deployments.
|
|
109
|
+
*
|
|
110
|
+
* @stable
|
|
111
|
+
*/
|
|
112
|
+
const BUILT_IN_IMPERATIVE_PATTERNS = PATTERNS;
|
|
113
|
+
/**
|
|
114
|
+
* Combined Aho-Corasick-style prefilter set across every pattern.
|
|
115
|
+
* Lower-cased substrings; consumers test the body once with the
|
|
116
|
+
* combined filter before iterating regexes.
|
|
117
|
+
*
|
|
118
|
+
* @stable
|
|
119
|
+
*/
|
|
120
|
+
const IMPERATIVE_PREFILTER_SUBSTRINGS = Object.freeze([...new Set(BUILT_IN_IMPERATIVE_PATTERNS.flatMap((p) => p.prefilter.map((s) => s.toLowerCase())))]);
|
|
121
|
+
/**
|
|
122
|
+
* Run the imperative-pattern scan against `body`. Patterns are
|
|
123
|
+
* iterated in catalogue order; the prefilter shortcut returns early
|
|
124
|
+
* for bodies that do not contain any imperative-family substring.
|
|
125
|
+
*
|
|
126
|
+
* @stable
|
|
127
|
+
*/
|
|
128
|
+
function scanImperativePatterns(body, patterns = BUILT_IN_IMPERATIVE_PATTERNS, budgetMs = 5) {
|
|
129
|
+
const start = performance.now();
|
|
130
|
+
if (body.length === 0) return {
|
|
131
|
+
hits: [],
|
|
132
|
+
bytesMatched: 0,
|
|
133
|
+
scanDurationUs: 0
|
|
134
|
+
};
|
|
135
|
+
const lower = body.toLowerCase();
|
|
136
|
+
let prefilterHit = false;
|
|
137
|
+
for (const sub of IMPERATIVE_PREFILTER_SUBSTRINGS) if (lower.includes(sub)) {
|
|
138
|
+
prefilterHit = true;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
if (!prefilterHit) return {
|
|
142
|
+
hits: [],
|
|
143
|
+
bytesMatched: 0,
|
|
144
|
+
scanDurationUs: Math.round((performance.now() - start) * 1e3)
|
|
145
|
+
};
|
|
146
|
+
const hits = [];
|
|
147
|
+
let bytesMatched = 0;
|
|
148
|
+
for (const pattern of patterns) {
|
|
149
|
+
if (performance.now() - start > budgetMs) return null;
|
|
150
|
+
if (!pattern.prefilter.some((sub) => lower.includes(sub.toLowerCase()))) continue;
|
|
151
|
+
let matchCount = 0;
|
|
152
|
+
pattern.regex.lastIndex = 0;
|
|
153
|
+
let match = pattern.regex.exec(body);
|
|
154
|
+
while (match !== null) {
|
|
155
|
+
matchCount++;
|
|
156
|
+
bytesMatched += match[0].length;
|
|
157
|
+
if (pattern.regex.lastIndex === match.index) pattern.regex.lastIndex = match.index + 1;
|
|
158
|
+
match = pattern.regex.exec(body);
|
|
159
|
+
}
|
|
160
|
+
if (matchCount > 0) hits.push({
|
|
161
|
+
pattern: pattern.name,
|
|
162
|
+
matchCount
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
hits,
|
|
167
|
+
bytesMatched,
|
|
168
|
+
scanDurationUs: Math.round((performance.now() - start) * 1e3)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Apply `pattern.mask` to every match of every pattern in `body`. Used
|
|
173
|
+
* by the `'detect-and-strip*'` policies. The mask is calibrated to NOT
|
|
174
|
+
* match any imperative pattern itself, so post-strip bodies do not
|
|
175
|
+
* trigger another scan hit on round trips.
|
|
176
|
+
*
|
|
177
|
+
* @stable
|
|
178
|
+
*/
|
|
179
|
+
function stripImperativePatterns(body, patterns = BUILT_IN_IMPERATIVE_PATTERNS) {
|
|
180
|
+
let out = body;
|
|
181
|
+
for (const pattern of patterns) out = out.replace(pattern.regex, pattern.mask);
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
//#endregion
|
|
186
|
+
export { BUILT_IN_IMPERATIVE_PATTERNS, IMPERATIVE_PREFILTER_SUBSTRINGS, scanImperativePatterns, stripImperativePatterns };
|
|
187
|
+
//# sourceMappingURL=imperative-patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imperative-patterns.js","names":["PATTERNS: readonly ImperativePattern[]","BUILT_IN_IMPERATIVE_PATTERNS: readonly ImperativePattern[]","IMPERATIVE_PREFILTER_SUBSTRINGS: ReadonlyArray<string>","hits: { pattern: string; matchCount: number }[]","match: RegExpExecArray | null"],"sources":["../../src/redaction/imperative-patterns.ts"],"sourcesContent":["/**\n * Imperative-pattern catalogue for inbound prompt-injection defence.\n *\n * Sibling to {@link BUILT_IN_PATTERNS} (PII / secrets) — the two\n * catalogues are disjoint by construction. The imperative catalogue\n * is consumed by the inbound sanitization layer in `@graphorin/tools`\n * to scan tool / MCP results before they reach the agent's message\n * store; the PII / secrets catalogue is consumed by the outbound\n * exporter validators to scan span attributes before they reach an\n * exporter.\n *\n * The patterns target the canonical English \"ignore previous\n * instructions\" / \"system override\" injection family — the concrete\n * surface that an untrusted-skill or MCP server result might use to\n * smuggle imperative content into the next provider call. The\n * catalogue is intentionally conservative: every entry has a fixed-\n * substring prefilter so the per-byte scan budget stays sub-millisecond\n * on typical 16 KB tool results.\n *\n * @packageDocumentation\n */\n\n/**\n * Stable name of an imperative pattern. The catalogue is curated;\n * user-supplied patterns can use any identifier they want and will be\n * passed through the sanitization layer alongside the built-ins.\n *\n * @stable\n */\nexport type ImperativePatternName =\n | 'ignore-previous-instructions'\n | 'forget-instructions'\n | 'override-instructions'\n | 'system-prompt-leak'\n | 'role-reassignment'\n | 'developer-mode'\n | 'jailbreak-marker'\n | 'tool-call-injection'\n | 'role-tag-injection';\n\n/**\n * One entry in the imperative-pattern catalogue. The shape mirrors\n * {@link BUILT_IN_PATTERNS} so consumers can share scan / replace\n * machinery, but the fields are typed as imperative-only so the two\n * catalogues do not accidentally merge.\n *\n * @stable\n */\nexport interface ImperativePattern {\n readonly name: ImperativePatternName | (string & {});\n readonly description: string;\n /**\n * Cheap substring prefilter applied before the regex; if the body\n * does not contain any of the prefilter substrings the regex is\n * skipped entirely. The prefilter is case-insensitive.\n */\n readonly prefilter: ReadonlyArray<string>;\n /**\n * Full regex applied when the prefilter matches. Always carries the\n * `g` and `i` flags; the catalogue construction validates this.\n */\n readonly regex: RegExp;\n /** Replacement string applied by the `'detect-and-strip*'` policies. */\n readonly mask: string;\n}\n\nconst PATTERNS: readonly ImperativePattern[] = [\n {\n name: 'ignore-previous-instructions',\n description:\n 'The canonical \"ignore previous instructions\" injection — the most common form of prompt injection seen across MCP results.',\n prefilter: ['ignore', 'disregard', 'forget'],\n regex:\n /\\b(?:ignore|disregard|forget)\\s+(?:all\\s+)?(?:the\\s+)?(?:previous|prior|above|earlier|preceding|original)\\s+(?:instructions?|prompts?|messages?|directives?|rules?)\\b/gi,\n mask: '[REDACTED:imperative-pattern]',\n },\n {\n name: 'forget-instructions',\n description: 'Imperative \"forget what I told you\" / memory-wipe family.',\n prefilter: ['forget', 'erase', 'clear'],\n regex:\n /\\b(?:forget|erase|clear|wipe)\\s+(?:everything|all)\\s+(?:above|before|previous|i\\s+(?:told|said))\\b/gi,\n mask: '[REDACTED:imperative-pattern]',\n },\n {\n name: 'override-instructions',\n description: '\"Override the system prompt\" / \"new instructions\" injection family.',\n prefilter: ['override', 'new instructions', 'updated instructions', 'replace'],\n regex:\n /\\b(?:override|replace)\\s+(?:the\\s+)?(?:system\\s+)?(?:prompt|instructions?|directives?)\\b|\\bnew\\s+(?:instructions?|prompts?|directives?)\\s*(?::|follow|are)\\b|\\bupdated\\s+(?:instructions?|prompts?|directives?)\\b/gi,\n mask: '[REDACTED:imperative-pattern]',\n },\n {\n name: 'system-prompt-leak',\n description:\n 'Requests asking the model to reveal its system prompt / hidden instructions / configuration.',\n prefilter: ['system prompt', 'system message', 'reveal', 'print', 'show'],\n regex:\n /\\b(?:reveal|print|show|output|repeat|expose|disclose)\\s+(?:your|the)\\s+(?:system\\s+(?:prompt|message|instructions?)|hidden\\s+instructions?|initial\\s+(?:prompt|instructions?)|configuration)\\b/gi,\n mask: '[REDACTED:imperative-pattern]',\n },\n {\n name: 'role-reassignment',\n description: '\"You are now ...\" / role-reassignment imperative family.',\n prefilter: ['you are now', 'you are no longer', 'pretend', 'act as'],\n regex:\n /\\byou\\s+are\\s+(?:now|no\\s+longer)\\s+(?:a\\b|an\\b|the\\b)|\\bpretend\\s+(?:to\\s+be|you\\s+are)\\b|\\bact\\s+as\\s+(?:a|an|the|if\\s+you)\\b/gi,\n mask: '[REDACTED:imperative-pattern]',\n },\n {\n name: 'developer-mode',\n description:\n 'Requests to enter \"developer mode\" / \"DAN\" / unrestricted modes — a long-running jailbreak family.',\n prefilter: ['developer mode', 'admin mode', 'unrestricted'],\n regex:\n /\\b(?:enter|enable|activate)\\s+(?:developer|admin|debug|unrestricted|god|root)\\s+mode\\b/gi,\n mask: '[REDACTED:imperative-pattern]',\n },\n {\n name: 'jailbreak-marker',\n description: 'Direct \"jailbreak\" markers in tool result content.',\n prefilter: ['jailbreak', 'jailbroken'],\n regex: /\\bjailbroken?\\s+(?:mode|response|model|assistant)\\b/gi,\n mask: '[REDACTED:imperative-pattern]',\n },\n {\n name: 'tool-call-injection',\n description:\n 'Tool-call markers that try to coerce the model into invoking a tool from the result content.',\n prefilter: ['<tool_use', '<function_call', '<invoke'],\n regex: /<(?:tool_use|function_call|invoke|tool_call)\\b[^>]*>/gi,\n mask: '[REDACTED:imperative-pattern]',\n },\n {\n name: 'role-tag-injection',\n description:\n 'Chat-role tags injected into tool results to spoof a system / assistant message in the conversation.',\n prefilter: ['<|im_start|', '<|system|', '<|assistant|', '<|user|'],\n regex: /<\\|(?:im_start|system|assistant|user|im_end)\\|>/gi,\n mask: '[REDACTED:imperative-pattern]',\n },\n];\n\n/**\n * The default-on imperative-pattern catalogue. Stable across patches;\n * additions during the pre-1.0 window are minor-bumps because new\n * patterns may produce additional `tool.inbound.sanitization.hit{...}`\n * counter increments on existing deployments.\n *\n * @stable\n */\nexport const BUILT_IN_IMPERATIVE_PATTERNS: readonly ImperativePattern[] = PATTERNS;\n\n/**\n * Combined Aho-Corasick-style prefilter set across every pattern.\n * Lower-cased substrings; consumers test the body once with the\n * combined filter before iterating regexes.\n *\n * @stable\n */\nexport const IMPERATIVE_PREFILTER_SUBSTRINGS: ReadonlyArray<string> = Object.freeze([\n ...new Set(BUILT_IN_IMPERATIVE_PATTERNS.flatMap((p) => p.prefilter.map((s) => s.toLowerCase()))),\n]);\n\n/**\n * Compiled scan helper. Returns the list of pattern names that fired\n * AND the number of bytes the strip would remove if applied. Bounded\n * by the budget hint — when exceeded, returns `null` to let the caller\n * apply the best-effort `'detect-failed'` annotation.\n *\n * @stable\n */\nexport interface ScanResult {\n readonly hits: ReadonlyArray<{ readonly pattern: string; readonly matchCount: number }>;\n readonly bytesMatched: number;\n readonly scanDurationUs: number;\n}\n\n/**\n * Run the imperative-pattern scan against `body`. Patterns are\n * iterated in catalogue order; the prefilter shortcut returns early\n * for bodies that do not contain any imperative-family substring.\n *\n * @stable\n */\nexport function scanImperativePatterns(\n body: string,\n patterns: ReadonlyArray<ImperativePattern> = BUILT_IN_IMPERATIVE_PATTERNS,\n budgetMs = 5,\n): ScanResult | null {\n const start = performance.now();\n if (body.length === 0) {\n return { hits: [], bytesMatched: 0, scanDurationUs: 0 };\n }\n const lower = body.toLowerCase();\n let prefilterHit = false;\n for (const sub of IMPERATIVE_PREFILTER_SUBSTRINGS) {\n if (lower.includes(sub)) {\n prefilterHit = true;\n break;\n }\n }\n if (!prefilterHit) {\n return {\n hits: [],\n bytesMatched: 0,\n scanDurationUs: Math.round((performance.now() - start) * 1000),\n };\n }\n const hits: { pattern: string; matchCount: number }[] = [];\n let bytesMatched = 0;\n for (const pattern of patterns) {\n if (performance.now() - start > budgetMs) return null;\n const localPrefilterMatch = pattern.prefilter.some((sub) => lower.includes(sub.toLowerCase()));\n if (!localPrefilterMatch) continue;\n let matchCount = 0;\n pattern.regex.lastIndex = 0;\n let match: RegExpExecArray | null = pattern.regex.exec(body);\n while (match !== null) {\n matchCount++;\n bytesMatched += match[0].length;\n if (pattern.regex.lastIndex === match.index) {\n pattern.regex.lastIndex = match.index + 1;\n }\n match = pattern.regex.exec(body);\n }\n if (matchCount > 0) {\n hits.push({ pattern: pattern.name as string, matchCount });\n }\n }\n return {\n hits,\n bytesMatched,\n scanDurationUs: Math.round((performance.now() - start) * 1000),\n };\n}\n\n/**\n * Apply `pattern.mask` to every match of every pattern in `body`. Used\n * by the `'detect-and-strip*'` policies. The mask is calibrated to NOT\n * match any imperative pattern itself, so post-strip bodies do not\n * trigger another scan hit on round trips.\n *\n * @stable\n */\nexport function stripImperativePatterns(\n body: string,\n patterns: ReadonlyArray<ImperativePattern> = BUILT_IN_IMPERATIVE_PATTERNS,\n): string {\n let out = body;\n for (const pattern of patterns) {\n out = out.replace(pattern.regex, pattern.mask);\n }\n return out;\n}\n"],"mappings":";AAkEA,MAAMA,WAAyC;CAC7C;EACE,MAAM;EACN,aACE;EACF,WAAW;GAAC;GAAU;GAAa;GAAS;EAC5C,OACE;EACF,MAAM;EACP;CACD;EACE,MAAM;EACN,aAAa;EACb,WAAW;GAAC;GAAU;GAAS;GAAQ;EACvC,OACE;EACF,MAAM;EACP;CACD;EACE,MAAM;EACN,aAAa;EACb,WAAW;GAAC;GAAY;GAAoB;GAAwB;GAAU;EAC9E,OACE;EACF,MAAM;EACP;CACD;EACE,MAAM;EACN,aACE;EACF,WAAW;GAAC;GAAiB;GAAkB;GAAU;GAAS;GAAO;EACzE,OACE;EACF,MAAM;EACP;CACD;EACE,MAAM;EACN,aAAa;EACb,WAAW;GAAC;GAAe;GAAqB;GAAW;GAAS;EACpE,OACE;EACF,MAAM;EACP;CACD;EACE,MAAM;EACN,aACE;EACF,WAAW;GAAC;GAAkB;GAAc;GAAe;EAC3D,OACE;EACF,MAAM;EACP;CACD;EACE,MAAM;EACN,aAAa;EACb,WAAW,CAAC,aAAa,aAAa;EACtC,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,aACE;EACF,WAAW;GAAC;GAAa;GAAkB;GAAU;EACrD,OAAO;EACP,MAAM;EACP;CACD;EACE,MAAM;EACN,aACE;EACF,WAAW;GAAC;GAAe;GAAa;GAAgB;GAAU;EAClE,OAAO;EACP,MAAM;EACP;CACF;;;;;;;;;AAUD,MAAaC,+BAA6D;;;;;;;;AAS1E,MAAaC,kCAAyD,OAAO,OAAO,CAClF,GAAG,IAAI,IAAI,6BAA6B,SAAS,MAAM,EAAE,UAAU,KAAK,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,CACjG,CAAC;;;;;;;;AAuBF,SAAgB,uBACd,MACA,WAA6C,8BAC7C,WAAW,GACQ;CACnB,MAAM,QAAQ,YAAY,KAAK;AAC/B,KAAI,KAAK,WAAW,EAClB,QAAO;EAAE,MAAM,EAAE;EAAE,cAAc;EAAG,gBAAgB;EAAG;CAEzD,MAAM,QAAQ,KAAK,aAAa;CAChC,IAAI,eAAe;AACnB,MAAK,MAAM,OAAO,gCAChB,KAAI,MAAM,SAAS,IAAI,EAAE;AACvB,iBAAe;AACf;;AAGJ,KAAI,CAAC,aACH,QAAO;EACL,MAAM,EAAE;EACR,cAAc;EACd,gBAAgB,KAAK,OAAO,YAAY,KAAK,GAAG,SAAS,IAAK;EAC/D;CAEH,MAAMC,OAAkD,EAAE;CAC1D,IAAI,eAAe;AACnB,MAAK,MAAM,WAAW,UAAU;AAC9B,MAAI,YAAY,KAAK,GAAG,QAAQ,SAAU,QAAO;AAEjD,MAAI,CADwB,QAAQ,UAAU,MAAM,QAAQ,MAAM,SAAS,IAAI,aAAa,CAAC,CAAC,CACpE;EAC1B,IAAI,aAAa;AACjB,UAAQ,MAAM,YAAY;EAC1B,IAAIC,QAAgC,QAAQ,MAAM,KAAK,KAAK;AAC5D,SAAO,UAAU,MAAM;AACrB;AACA,mBAAgB,MAAM,GAAG;AACzB,OAAI,QAAQ,MAAM,cAAc,MAAM,MACpC,SAAQ,MAAM,YAAY,MAAM,QAAQ;AAE1C,WAAQ,QAAQ,MAAM,KAAK,KAAK;;AAElC,MAAI,aAAa,EACf,MAAK,KAAK;GAAE,SAAS,QAAQ;GAAgB;GAAY,CAAC;;AAG9D,QAAO;EACL;EACA;EACA,gBAAgB,KAAK,OAAO,YAAY,KAAK,GAAG,SAAS,IAAK;EAC/D;;;;;;;;;;AAWH,SAAgB,wBACd,MACA,WAA6C,8BACrC;CACR,IAAI,MAAM;AACV,MAAK,MAAM,WAAW,SACpB,OAAM,IAAI,QAAQ,QAAQ,OAAO,QAAQ,KAAK;AAEhD,QAAO"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ALL_BUILT_IN_PATTERNS, BUILT_IN_PATTERNS, BuiltInPatternName, OPT_IN_PATTERNS, PatternCategory, RedactionPattern } from "./patterns.js";
|
|
2
|
+
import { RedactionCounters, RedactionInput, RedactionOutput, RedactionValidator, RedactionValidatorInstance, RedactionValidatorOptions, RedactionViolation, RedactionViolationCallback } from "./types.js";
|
|
3
|
+
import { DEFAULT_VALIDATION_CONFIG, ValidationConfig } from "./config.js";
|
|
4
|
+
import { RedactionValidationError, UnvalidatedExporterError } from "./errors.js";
|
|
5
|
+
import { BUILT_IN_IMPERATIVE_PATTERNS, IMPERATIVE_PREFILTER_SUBSTRINGS, ImperativePattern, ImperativePatternName, ScanResult, scanImperativePatterns, stripImperativePatterns } from "./imperative-patterns.js";
|
|
6
|
+
import { compareSensitivityTiers, createRedactionValidator } from "./validator.js";
|
|
7
|
+
export { ALL_BUILT_IN_PATTERNS, BUILT_IN_IMPERATIVE_PATTERNS, BUILT_IN_PATTERNS, type BuiltInPatternName, DEFAULT_VALIDATION_CONFIG, IMPERATIVE_PREFILTER_SUBSTRINGS, type ImperativePattern, type ImperativePatternName, type ScanResult as ImperativeScanResult, OPT_IN_PATTERNS, type PatternCategory, type RedactionCounters, type RedactionInput, type RedactionOutput, type RedactionPattern, RedactionValidationError, type RedactionValidator, type RedactionValidatorInstance, type RedactionValidatorOptions, type RedactionViolation, type RedactionViolationCallback, UnvalidatedExporterError, type ValidationConfig, compareSensitivityTiers, createRedactionValidator, scanImperativePatterns, stripImperativePatterns };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { RedactionValidationError, UnvalidatedExporterError } from "./errors.js";
|
|
2
|
+
import { ALL_BUILT_IN_PATTERNS, BUILT_IN_PATTERNS, OPT_IN_PATTERNS } from "./patterns.js";
|
|
3
|
+
import { compareSensitivityTiers, createRedactionValidator } from "./validator.js";
|
|
4
|
+
import { DEFAULT_VALIDATION_CONFIG } from "./config.js";
|
|
5
|
+
import { BUILT_IN_IMPERATIVE_PATTERNS, IMPERATIVE_PREFILTER_SUBSTRINGS, scanImperativePatterns, stripImperativePatterns } from "./imperative-patterns.js";
|
|
6
|
+
|
|
7
|
+
export { ALL_BUILT_IN_PATTERNS, BUILT_IN_IMPERATIVE_PATTERNS, BUILT_IN_PATTERNS, DEFAULT_VALIDATION_CONFIG, IMPERATIVE_PREFILTER_SUBSTRINGS, OPT_IN_PATTERNS, RedactionValidationError, UnvalidatedExporterError, compareSensitivityTiers, createRedactionValidator, scanImperativePatterns, stripImperativePatterns };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//#region src/redaction/patterns.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Built-in PII / secret detection patterns. The catalogue is intentionally
|
|
4
|
+
* conservative — every pattern has both positive and negative test
|
|
5
|
+
* fixtures and is documented so operators understand exactly what is
|
|
6
|
+
* matched.
|
|
7
|
+
*
|
|
8
|
+
* The catalogue is split into two groups:
|
|
9
|
+
*
|
|
10
|
+
* - **secret** — credentials, API tokens, JWTs, private keys. Matches
|
|
11
|
+
* are always dropped + counted, regardless of the configured tier
|
|
12
|
+
* floor.
|
|
13
|
+
* - **pii** — email / phone / IBAN / credit card / SSN / IP address.
|
|
14
|
+
* Subject to the configured tier floor + per-pattern enable / disable
|
|
15
|
+
* knobs.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Stable pattern identifier. The catalogue is curated; user-supplied
|
|
21
|
+
* patterns can use any identifier they want and will be passed through
|
|
22
|
+
* the validator in addition to the built-ins.
|
|
23
|
+
*
|
|
24
|
+
* @stable
|
|
25
|
+
*/
|
|
26
|
+
type BuiltInPatternName = 'graphorin-token' | 'openai-key' | 'anthropic-key' | 'aws-access-key' | 'gcp-service-account' | 'github-pat' | 'jwt' | 'bearer-header' | 'basic-auth' | 'private-key-pem' | 'email' | 'creditcard' | 'us-ssn' | 'phone-e164' | 'iban' | 'ipv4' | 'ipv6';
|
|
27
|
+
/**
|
|
28
|
+
* Pattern category — `secret` matches always force a drop; `pii`
|
|
29
|
+
* matches respect the configured `enabledPatterns` allow-list.
|
|
30
|
+
*
|
|
31
|
+
* @stable
|
|
32
|
+
*/
|
|
33
|
+
type PatternCategory = 'secret' | 'pii';
|
|
34
|
+
/**
|
|
35
|
+
* One entry in the redaction catalogue.
|
|
36
|
+
*
|
|
37
|
+
* @stable
|
|
38
|
+
*/
|
|
39
|
+
interface RedactionPattern {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly category: PatternCategory;
|
|
42
|
+
readonly description: string;
|
|
43
|
+
readonly regex: RegExp;
|
|
44
|
+
/** Replacement string used when `mode === 'mask'`. */
|
|
45
|
+
readonly mask?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Optional per-match predicate (RP-21). When present, a regex hit is only
|
|
48
|
+
* treated as a real match — and masked — when this returns `true` for the
|
|
49
|
+
* matched substring. Used by the `creditcard` pattern to require a valid
|
|
50
|
+
* Luhn checksum so look-alike digit runs (epoch-ms timestamps, order ids)
|
|
51
|
+
* are not corrupted.
|
|
52
|
+
*/
|
|
53
|
+
readonly verify?: (match: string) => boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Optional opt-in flag. When `true` the pattern is **not** active by
|
|
56
|
+
* default; operators must add it to `enabledPatterns` explicitly. Used
|
|
57
|
+
* by the IPv4 / IPv6 patterns because raw IPs frequently appear in
|
|
58
|
+
* non-PII log lines (host headers, debug traces, …).
|
|
59
|
+
*/
|
|
60
|
+
readonly optIn?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The 14 default-on built-in patterns (the IPv4 and IPv6 detectors are
|
|
64
|
+
* opt-in and live in {@link OPT_IN_PATTERNS}).
|
|
65
|
+
*
|
|
66
|
+
* @stable
|
|
67
|
+
*/
|
|
68
|
+
declare const BUILT_IN_PATTERNS: readonly RedactionPattern[];
|
|
69
|
+
/**
|
|
70
|
+
* Patterns that are recognised by the validator but are NOT enabled by
|
|
71
|
+
* default. Use them via `patterns: [...BUILT_IN_PATTERNS, ...OPT_IN_PATTERNS]`.
|
|
72
|
+
*
|
|
73
|
+
* @stable
|
|
74
|
+
*/
|
|
75
|
+
declare const OPT_IN_PATTERNS: readonly RedactionPattern[];
|
|
76
|
+
/**
|
|
77
|
+
* Full registry — for tooling that wants to introspect every pattern
|
|
78
|
+
* the framework knows about (e.g. CLI `graphorin redaction list`).
|
|
79
|
+
*
|
|
80
|
+
* @stable
|
|
81
|
+
*/
|
|
82
|
+
declare const ALL_BUILT_IN_PATTERNS: readonly RedactionPattern[];
|
|
83
|
+
//#endregion
|
|
84
|
+
export { ALL_BUILT_IN_PATTERNS, BUILT_IN_PATTERNS, BuiltInPatternName, OPT_IN_PATTERNS, PatternCategory, RedactionPattern };
|
|
85
|
+
//# sourceMappingURL=patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.d.ts","names":[],"sources":["../../src/redaction/patterns.ts"],"sourcesContent":[],"mappings":";;AAyBA;AAyBA;AAOA;AA+JA;AAUA;AAUA;;;;;;;;;;;;;;;;;;KAnNY,kBAAA;;;;;;;KAyBA,eAAA;;;;;;UAOK,gBAAA;;qBAEI;;kBAEH;;;;;;;;;;;;;;;;;;;;;;;;;cA2JL,4BAA4B;;;;;;;cAU5B,0BAA0B;;;;;;;cAU1B,gCAAgC"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
//#region src/redaction/patterns.ts
|
|
2
|
+
const PATTERNS = [
|
|
3
|
+
{
|
|
4
|
+
name: "graphorin-token",
|
|
5
|
+
category: "secret",
|
|
6
|
+
description: "Graphorin server token (`kru_<env>_v1_<entropy>_<crc32>`).",
|
|
7
|
+
regex: /kru_(?:dev|test|prod)_v1_[A-Za-z0-9]{20,80}_[A-Za-z0-9]{6}/g,
|
|
8
|
+
mask: "[REDACTED graphorin-token]"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "openai-key",
|
|
12
|
+
category: "secret",
|
|
13
|
+
description: "OpenAI API key (`sk-...`).",
|
|
14
|
+
regex: /\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
15
|
+
mask: "[REDACTED openai-key]"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "anthropic-key",
|
|
19
|
+
category: "secret",
|
|
20
|
+
description: "Anthropic API key (`sk-ant-...`).",
|
|
21
|
+
regex: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g,
|
|
22
|
+
mask: "[REDACTED anthropic-key]"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "aws-access-key",
|
|
26
|
+
category: "secret",
|
|
27
|
+
description: "AWS access-key id (`AKIA...`).",
|
|
28
|
+
regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g,
|
|
29
|
+
mask: "[REDACTED aws-access-key]"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "gcp-service-account",
|
|
33
|
+
category: "secret",
|
|
34
|
+
description: "GCP service-account email (`...@...iam.gserviceaccount.com`). Opt-in; the `email` pattern already redacts the address shape; this entry exists for operators wanting the explicit `gcp-service-account` counter label.",
|
|
35
|
+
regex: /[A-Za-z0-9._-]+@[A-Za-z0-9-]+\.iam\.gserviceaccount\.com\b/g,
|
|
36
|
+
mask: "[REDACTED gcp-service-account]",
|
|
37
|
+
optIn: true
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "github-pat",
|
|
41
|
+
category: "secret",
|
|
42
|
+
description: "GitHub personal-access token / OAuth token / app token.",
|
|
43
|
+
regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,255}\b/g,
|
|
44
|
+
mask: "[REDACTED github-pat]"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "jwt",
|
|
48
|
+
category: "secret",
|
|
49
|
+
description: "JSON Web Token (`eyJ...`).",
|
|
50
|
+
regex: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
|
|
51
|
+
mask: "[REDACTED jwt]"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "bearer-header",
|
|
55
|
+
category: "secret",
|
|
56
|
+
description: "`Authorization: Bearer ...` header values.",
|
|
57
|
+
regex: /\bBearer\s+[A-Za-z0-9._-]{16,}\b/gi,
|
|
58
|
+
mask: "Bearer [REDACTED]"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "basic-auth",
|
|
62
|
+
category: "secret",
|
|
63
|
+
description: "`Authorization: Basic ...` header values.",
|
|
64
|
+
regex: /\bBasic\s+[A-Za-z0-9+/=]{16,}\b/gi,
|
|
65
|
+
mask: "Basic [REDACTED]"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "private-key-pem",
|
|
69
|
+
category: "secret",
|
|
70
|
+
description: "PEM private-key block (`-----BEGIN ... PRIVATE KEY-----`).",
|
|
71
|
+
regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
72
|
+
mask: "[REDACTED private-key-pem]"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "email",
|
|
76
|
+
category: "pii",
|
|
77
|
+
description: "RFC-5322-ish email address.",
|
|
78
|
+
regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
79
|
+
mask: "[REDACTED email]"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "creditcard",
|
|
83
|
+
category: "pii",
|
|
84
|
+
description: "Credit card number (13–19 digits, optional spaces / dashes; Luhn-checked).",
|
|
85
|
+
regex: /\b(?:\d[\s-]*?){13,19}\b/g,
|
|
86
|
+
mask: "[REDACTED creditcard]",
|
|
87
|
+
verify: isLuhnValid
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "us-ssn",
|
|
91
|
+
category: "pii",
|
|
92
|
+
description: "US Social Security Number (`123-45-6789`).",
|
|
93
|
+
regex: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
94
|
+
mask: "[REDACTED us-ssn]"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "phone-e164",
|
|
98
|
+
category: "pii",
|
|
99
|
+
description: "E.164 phone number (`+11234567890`).",
|
|
100
|
+
regex: /\+\d{7,15}\b/g,
|
|
101
|
+
mask: "[REDACTED phone-e164]"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "iban",
|
|
105
|
+
category: "pii",
|
|
106
|
+
description: "IBAN (2-letter country + 2 check digits + up to 30 alphanumerics).",
|
|
107
|
+
regex: /\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/g,
|
|
108
|
+
mask: "[REDACTED iban]"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "ipv4",
|
|
112
|
+
category: "pii",
|
|
113
|
+
description: "IPv4 dotted-quad address. Opt-in (often appears legitimately in logs).",
|
|
114
|
+
regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
|
|
115
|
+
mask: "[REDACTED ipv4]",
|
|
116
|
+
optIn: true
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "ipv6",
|
|
120
|
+
category: "pii",
|
|
121
|
+
description: "IPv6 address (full / compressed). Opt-in.",
|
|
122
|
+
regex: /\b(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\b/g,
|
|
123
|
+
mask: "[REDACTED ipv6]",
|
|
124
|
+
optIn: true
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
/**
|
|
128
|
+
* The 14 default-on built-in patterns (the IPv4 and IPv6 detectors are
|
|
129
|
+
* opt-in and live in {@link OPT_IN_PATTERNS}).
|
|
130
|
+
*
|
|
131
|
+
* @stable
|
|
132
|
+
*/
|
|
133
|
+
const BUILT_IN_PATTERNS = PATTERNS.filter((p) => p.optIn !== true);
|
|
134
|
+
/**
|
|
135
|
+
* Patterns that are recognised by the validator but are NOT enabled by
|
|
136
|
+
* default. Use them via `patterns: [...BUILT_IN_PATTERNS, ...OPT_IN_PATTERNS]`.
|
|
137
|
+
*
|
|
138
|
+
* @stable
|
|
139
|
+
*/
|
|
140
|
+
const OPT_IN_PATTERNS = PATTERNS.filter((p) => p.optIn === true);
|
|
141
|
+
/**
|
|
142
|
+
* Full registry — for tooling that wants to introspect every pattern
|
|
143
|
+
* the framework knows about (e.g. CLI `graphorin redaction list`).
|
|
144
|
+
*
|
|
145
|
+
* @stable
|
|
146
|
+
*/
|
|
147
|
+
const ALL_BUILT_IN_PATTERNS = PATTERNS;
|
|
148
|
+
/**
|
|
149
|
+
* Luhn (mod-10) checksum validator used by the `creditcard` pattern (RP-21).
|
|
150
|
+
* Strips spaces / dashes, bounds the length to 13–19 digits, and verifies the
|
|
151
|
+
* checksum so a digit run that merely *looks* like a PAN is not redacted.
|
|
152
|
+
*/
|
|
153
|
+
function isLuhnValid(value) {
|
|
154
|
+
const digits = value.replace(/\D/g, "");
|
|
155
|
+
if (digits.length < 13 || digits.length > 19) return false;
|
|
156
|
+
let sum = 0;
|
|
157
|
+
let double = false;
|
|
158
|
+
for (let i = digits.length - 1; i >= 0; i -= 1) {
|
|
159
|
+
let d = digits.charCodeAt(i) - 48;
|
|
160
|
+
if (double) {
|
|
161
|
+
d *= 2;
|
|
162
|
+
if (d > 9) d -= 9;
|
|
163
|
+
}
|
|
164
|
+
sum += d;
|
|
165
|
+
double = !double;
|
|
166
|
+
}
|
|
167
|
+
return sum % 10 === 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
export { ALL_BUILT_IN_PATTERNS, BUILT_IN_PATTERNS, OPT_IN_PATTERNS };
|
|
172
|
+
//# sourceMappingURL=patterns.js.map
|