@descryy/runtime-evidence-store 0.0.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/dist/evidence-store.d.ts +204 -0
- package/dist/evidence-store.d.ts.map +1 -0
- package/dist/evidence-store.js +511 -0
- package/dist/evidence-store.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/redaction.d.ts +120 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +337 -0
- package/dist/redaction.js.map +1 -0
- package/dist/replay.d.ts +118 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +104 -0
- package/dist/replay.js.map +1 -0
- package/package.json +29 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction (plan §28): "Raw runtime evidence must not automatically
|
|
3
|
+
* become unrestricted AI context." This runs at write time, unconditionally
|
|
4
|
+
* — `EvidenceStore.write` has no code path that skips it — rather than at
|
|
5
|
+
* read time, because a read-time pass is one forgotten call site away from
|
|
6
|
+
* failing (peer's framing, kept verbatim since it's exactly right).
|
|
7
|
+
*
|
|
8
|
+
* Extends the original key-name-only pass with the two capabilities §28
|
|
9
|
+
* names that a pure key-name walk structurally cannot reach:
|
|
10
|
+
*
|
|
11
|
+
* - **Body filtering.** `NETWORK_REQUEST`/`NETWORK_RESPONSE` evidence
|
|
12
|
+
* (`packages/browser`) carries `postData`/`body` as a JSON- or
|
|
13
|
+
* text-*encoded string*, not a parsed object -- the original redact()
|
|
14
|
+
* walks object structure, so a secret sitting inside that string (a
|
|
15
|
+
* `{"password": "hunter2"}` request body, say) was invisible to it
|
|
16
|
+
* regardless of how complete `SENSITIVE_KEY_PATTERN` was. Body-bearing
|
|
17
|
+
* keys now get parsed (if JSON) and recursed into with the same rules,
|
|
18
|
+
* or pattern-scanned as text otherwise.
|
|
19
|
+
* - **PII-aware field filtering.** A separate, independently toggleable
|
|
20
|
+
* pattern from credentials -- these are different concerns with
|
|
21
|
+
* different owners in a real system (security vs. privacy), and
|
|
22
|
+
* collapsing them into one pattern would make either impossible to
|
|
23
|
+
* reason about or configure on its own.
|
|
24
|
+
*
|
|
25
|
+
* **Configurable by whom, refused how (the two questions this row asked to
|
|
26
|
+
* have decided rather than defaulted):** by whoever constructs the
|
|
27
|
+
* `EvidenceStore` (`RedactionPolicy`, plumbed through
|
|
28
|
+
* `EvidenceStoreOptions` -- see `evidence-store.ts`), and every filter
|
|
29
|
+
* fails closed, disclosed, not silent: a value the body/PII filter cannot
|
|
30
|
+
* classify at all (not JSON, not text that looks safe to pattern-scan) is
|
|
31
|
+
* withheld wholesale rather than passed through on the assumption that
|
|
32
|
+
* "couldn't parse it" means "couldn't be sensitive." A marker replaces the
|
|
33
|
+
* value in place -- the key survives, so a reader can see *that* something
|
|
34
|
+
* was withheld, never just find it missing (same discipline the original
|
|
35
|
+
* key-based pass already established with `[REDACTED]`).
|
|
36
|
+
*
|
|
37
|
+
* **Not a claim of completeness.** Pattern-based text scanning is
|
|
38
|
+
* inherently partial -- a sensitive string shaped unlike every pattern
|
|
39
|
+
* here passes through unredacted, honestly, not silently promised as
|
|
40
|
+
* covered. What the "fail closed" guarantee actually claims: content this
|
|
41
|
+
* module cannot even classify never leaks by default; content it can
|
|
42
|
+
* classify gets the patterns below applied, not more.
|
|
43
|
+
*/
|
|
44
|
+
import type { RedactionStatus } from "@descryy/runtime-contracts";
|
|
45
|
+
export interface RedactionPolicy {
|
|
46
|
+
/** Parse and recurse into JSON-shaped body/postData strings, applying the same key rules inside them. Default true -- restricted by default, not opt-in. */
|
|
47
|
+
readonly bodyFiltering?: boolean;
|
|
48
|
+
/** Also treat PII_KEY_PATTERN field names as sensitive, not just credentials. Default true. */
|
|
49
|
+
readonly piiFiltering?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface RedactionResult {
|
|
52
|
+
readonly payload: unknown;
|
|
53
|
+
readonly redactionStatus: RedactionStatus;
|
|
54
|
+
/**
|
|
55
|
+
* The **key paths** this call redacted by name — `"sessionId"`,
|
|
56
|
+
* `"headers.authorization"`, `"items[0].email"`. Key names only; no value
|
|
57
|
+
* ever appears here, which is what makes it safe to log, assert on, and
|
|
58
|
+
* hand to an operator auditing a policy.
|
|
59
|
+
*
|
|
60
|
+
* **Added because `redactionStatus` alone made a real bug invisible for
|
|
61
|
+
* the life of a package (RT-063).** `SENSITIVE_KEY_PATTERN`'s
|
|
62
|
+
* `session(id)?` alternative matches the literal key `sessionId`, and all
|
|
63
|
+
* three browser collectors carried `sessionId: session.sessionId` — an
|
|
64
|
+
* internal `BrowserSession` handle, not a credential. Every
|
|
65
|
+
* `NAVIGATION` / `CLICK` / `INPUT` / `NETWORK_*` / `CONSOLE_MESSAGE` that
|
|
66
|
+
* package ever produced had it silently replaced with a marker.
|
|
67
|
+
*
|
|
68
|
+
* The status flag could not reveal it. `redacted` is what a real payload
|
|
69
|
+
* is *expected* to report, so the one signal that existed said exactly
|
|
70
|
+
* what it would have said if nothing were wrong — the silent-degradation
|
|
71
|
+
* shape this repo keeps meeting, in a module whose whole job is to be
|
|
72
|
+
* trustworthy about what it touched.
|
|
73
|
+
*
|
|
74
|
+
* A collector author can now assert `redactedKeys` is empty for a benign
|
|
75
|
+
* payload, which is a claim the boolean cannot express.
|
|
76
|
+
*
|
|
77
|
+
* **Text-scan hits are not listed here.** A secret found inside a body
|
|
78
|
+
* string by pattern has no key path — it was matched in free text — and
|
|
79
|
+
* inventing one (`"body[chars 40-64]"`) would put a fragment of the
|
|
80
|
+
* surrounding value into a field documented as name-only. Those still
|
|
81
|
+
* flip `redactionStatus`; `redactedKeys` answers "which field did the
|
|
82
|
+
* policy hit by name", not "was anything redacted at all".
|
|
83
|
+
*/
|
|
84
|
+
readonly redactedKeys: readonly string[];
|
|
85
|
+
}
|
|
86
|
+
export declare function redact(payload: unknown, policy?: RedactionPolicy): RedactionResult;
|
|
87
|
+
/**
|
|
88
|
+
* Which keys the default policy would redact, without redacting anything.
|
|
89
|
+
*
|
|
90
|
+
* The tool a collector author needs *before* shipping a payload shape: hand
|
|
91
|
+
* it the key names, get back the ones that collide and would be wiped. The
|
|
92
|
+
* `sessionId` collision (RT-063) was a name nobody thought to check against
|
|
93
|
+
* a pattern list nobody thought to read, and the check costs one call.
|
|
94
|
+
*/
|
|
95
|
+
export declare function collidingKeys(keys: readonly string[], policy?: RedactionPolicy): readonly string[];
|
|
96
|
+
/**
|
|
97
|
+
* Redact credential values out of a command line, keeping the shape.
|
|
98
|
+
*
|
|
99
|
+
* Two forms, because a command line has two:
|
|
100
|
+
*
|
|
101
|
+
* - **Attached** -- `--api-key=SEKRET`, `TOKEN:abc`, `DB_PASSWORD=hunter2`.
|
|
102
|
+
* The name and the separator survive; only the value is replaced.
|
|
103
|
+
* - **Detached** -- `["--api-key", "SEKRET"]` or `--api-key SEKRET`, where the
|
|
104
|
+
* secret is the *next* token. This is the form a naive per-token scanner
|
|
105
|
+
* misses entirely: `SEKRET` on its own carries nothing that marks it as a
|
|
106
|
+
* secret, and the only thing that does is the token before it.
|
|
107
|
+
*
|
|
108
|
+
* A detached flag at the very end of a command consumes nothing -- there is
|
|
109
|
+
* no value to redact, and inventing one would corrupt the command.
|
|
110
|
+
*
|
|
111
|
+
* The name and separator are kept on purpose. A command redacted to
|
|
112
|
+
* `[REDACTED]` wholesale is useless to the reader it was persisted for; a
|
|
113
|
+
* command redacted to `node server.mjs --api-key=[REDACTED]` still answers
|
|
114
|
+
* "what was run, and with what shape of argument".
|
|
115
|
+
*/
|
|
116
|
+
export declare function redactCommand(value: string | readonly string[]): {
|
|
117
|
+
value: string | string[];
|
|
118
|
+
redacted: boolean;
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=redaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AA2GlE,MAAM,WAAW,eAAe;IAC9B,4JAA4J;IAC5J,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,+FAA+F;IAC/F,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACjC;AAID,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAC1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED,wBAAgB,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,GAAE,eAAoB,GAAG,eAAe,CAStF;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,GAAE,eAAoB,GAAG,SAAS,MAAM,EAAE,CAQtG;AA2HD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG;IAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAahH"}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction (plan §28): "Raw runtime evidence must not automatically
|
|
3
|
+
* become unrestricted AI context." This runs at write time, unconditionally
|
|
4
|
+
* — `EvidenceStore.write` has no code path that skips it — rather than at
|
|
5
|
+
* read time, because a read-time pass is one forgotten call site away from
|
|
6
|
+
* failing (peer's framing, kept verbatim since it's exactly right).
|
|
7
|
+
*
|
|
8
|
+
* Extends the original key-name-only pass with the two capabilities §28
|
|
9
|
+
* names that a pure key-name walk structurally cannot reach:
|
|
10
|
+
*
|
|
11
|
+
* - **Body filtering.** `NETWORK_REQUEST`/`NETWORK_RESPONSE` evidence
|
|
12
|
+
* (`packages/browser`) carries `postData`/`body` as a JSON- or
|
|
13
|
+
* text-*encoded string*, not a parsed object -- the original redact()
|
|
14
|
+
* walks object structure, so a secret sitting inside that string (a
|
|
15
|
+
* `{"password": "hunter2"}` request body, say) was invisible to it
|
|
16
|
+
* regardless of how complete `SENSITIVE_KEY_PATTERN` was. Body-bearing
|
|
17
|
+
* keys now get parsed (if JSON) and recursed into with the same rules,
|
|
18
|
+
* or pattern-scanned as text otherwise.
|
|
19
|
+
* - **PII-aware field filtering.** A separate, independently toggleable
|
|
20
|
+
* pattern from credentials -- these are different concerns with
|
|
21
|
+
* different owners in a real system (security vs. privacy), and
|
|
22
|
+
* collapsing them into one pattern would make either impossible to
|
|
23
|
+
* reason about or configure on its own.
|
|
24
|
+
*
|
|
25
|
+
* **Configurable by whom, refused how (the two questions this row asked to
|
|
26
|
+
* have decided rather than defaulted):** by whoever constructs the
|
|
27
|
+
* `EvidenceStore` (`RedactionPolicy`, plumbed through
|
|
28
|
+
* `EvidenceStoreOptions` -- see `evidence-store.ts`), and every filter
|
|
29
|
+
* fails closed, disclosed, not silent: a value the body/PII filter cannot
|
|
30
|
+
* classify at all (not JSON, not text that looks safe to pattern-scan) is
|
|
31
|
+
* withheld wholesale rather than passed through on the assumption that
|
|
32
|
+
* "couldn't parse it" means "couldn't be sensitive." A marker replaces the
|
|
33
|
+
* value in place -- the key survives, so a reader can see *that* something
|
|
34
|
+
* was withheld, never just find it missing (same discipline the original
|
|
35
|
+
* key-based pass already established with `[REDACTED]`).
|
|
36
|
+
*
|
|
37
|
+
* **Not a claim of completeness.** Pattern-based text scanning is
|
|
38
|
+
* inherently partial -- a sensitive string shaped unlike every pattern
|
|
39
|
+
* here passes through unredacted, honestly, not silently promised as
|
|
40
|
+
* covered. What the "fail closed" guarantee actually claims: content this
|
|
41
|
+
* module cannot even classify never leaks by default; content it can
|
|
42
|
+
* classify gets the patterns below applied, not more.
|
|
43
|
+
*/
|
|
44
|
+
const SENSITIVE_KEY_PATTERN = /^(authorization|cookie|set-cookie|token|secret|password|passwd|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|private[-_]?key|session(id)?|bearer)$/i;
|
|
45
|
+
const PII_KEY_PATTERN = /^(email|e-?mail(?:[-_]?address)?|phone(?:[-_]?number)?|mobile|ssn|social[-_]?security(?:[-_]?number)?|dob|date[-_]?of[-_]?birth|address|street[-_]?address|first[-_]?name|last[-_]?name|full[-_]?name|credit[-_]?card(?:[-_]?number)?|card[-_]?number|cvv|national[-_]?id|passport(?:[-_]?number)?)$/i;
|
|
46
|
+
/**
|
|
47
|
+
* Free-text-bearing keys: request/response bodies AND §28's separate
|
|
48
|
+
* "sensitive log filtering" item -- `raw` is `LogCollector`'s payload key
|
|
49
|
+
* for a `BACKEND_LOG`/`EXCEPTION`/`STACK_TRACE`'s captured text
|
|
50
|
+
* (`backend-observation/collector.ts`), the same "an app can print a
|
|
51
|
+
* secret as plain text, not just carry one as a structured field" problem
|
|
52
|
+
* body filtering exists for, just from a different producer.
|
|
53
|
+
*/
|
|
54
|
+
const BODY_BEARING_KEY_PATTERN = /^(body|post[-_]?data|request[-_]?body|response[-_]?body|raw)$/i;
|
|
55
|
+
/**
|
|
56
|
+
* Keys whose value is a **command line** -- `command`, `args`, `argv`, `cmd`.
|
|
57
|
+
*
|
|
58
|
+
* **Added because a claim was written before it was true (RT-069).** The
|
|
59
|
+
* execution record persisted for replay carries
|
|
60
|
+
* `configuration.services[x].command`, and the note justifying it said a
|
|
61
|
+
* command line is one of the classic places a credential travels. It is --
|
|
62
|
+
* and nothing here caught one. Measured: `node server.mjs --api-key=SEKRET`
|
|
63
|
+
* went through `redact()` untouched, `redactedKeys: []`. `command` is not
|
|
64
|
+
* body-bearing, so it was never scanned; and even body scanning would have
|
|
65
|
+
* missed it, since the text patterns look for emails, card numbers and long
|
|
66
|
+
* tokens, and a short flag value is none of those.
|
|
67
|
+
*
|
|
68
|
+
* A command line needs its own scanner because its secrets are **positional
|
|
69
|
+
* rather than lexical**: what marks `SEKRET` as sensitive is not the value's
|
|
70
|
+
* shape but the *flag in front of it*.
|
|
71
|
+
*/
|
|
72
|
+
const COMMAND_BEARING_KEY_PATTERN = /^(command|args|argv|cmd)$/i;
|
|
73
|
+
/**
|
|
74
|
+
* A flag or environment assignment whose NAME says the value after it is a
|
|
75
|
+
* credential: `--api-key=x`, `-token x`, `DATABASE_PASSWORD=x`,
|
|
76
|
+
* `--client-secret=x`.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately name-driven and deliberately narrow. The alternative --
|
|
79
|
+
* redacting anything that looks high-entropy -- destroys ordinary arguments
|
|
80
|
+
* (a commit sha, a port, a base64 fixture path) and would make a redacted
|
|
81
|
+
* command unreadable, which costs the reader the thing a command line is
|
|
82
|
+
* kept for.
|
|
83
|
+
*/
|
|
84
|
+
const SENSITIVE_ARG_NAME = /(?:key|token|secret|password|passwd|auth|credential|bearer|cookie)/i;
|
|
85
|
+
/**
|
|
86
|
+
* Names that contain a sensitive word but whose value is a **location, not a
|
|
87
|
+
* credential**: `--keys-dir`, `--secret-file`, `--token-path`, `--key-store`.
|
|
88
|
+
*
|
|
89
|
+
* Measured while writing this: `--keys-dir keys/dev.json` was redacted, which
|
|
90
|
+
* is a false positive that costs a reader a real path and protects nothing --
|
|
91
|
+
* the secret is the file's *contents*, which never appear on the command
|
|
92
|
+
* line. Excluded so the scanner keeps the information it has no reason to
|
|
93
|
+
* destroy.
|
|
94
|
+
*/
|
|
95
|
+
const LOCATION_ARG_NAME = /(?:[-_](?:dir|directory|path|file|store|folder)|file|path)$/i;
|
|
96
|
+
const ARG_ASSIGNMENT = /^(-{0,2}[A-Za-z0-9_.-]*?)([=:])(.+)$/;
|
|
97
|
+
const REDACTED_MARKER = "[REDACTED]";
|
|
98
|
+
const REDACTED_BODY_MARKER = "[REDACTED-BODY]";
|
|
99
|
+
// Deliberately ordered narrowest-to-broadest so an email or card number
|
|
100
|
+
// inside what would also match the generic token pattern gets the more
|
|
101
|
+
// specific marker.
|
|
102
|
+
/**
|
|
103
|
+
* **Every quantifier here is bounded, and that is a fix rather than a style
|
|
104
|
+
* choice (RT-069).** The previous form was
|
|
105
|
+
* `/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g`, whose unbounded
|
|
106
|
+
* leading `+` backtracks catastrophically on text containing no `@`: the
|
|
107
|
+
* class consumes the whole input, fails to find `@`, gives back one
|
|
108
|
+
* character, and retries — O(n²).
|
|
109
|
+
*
|
|
110
|
+
* Measured, on a string of `x` with no `@` in it:
|
|
111
|
+
*
|
|
112
|
+
* | input | old | new |
|
|
113
|
+
* | --- | --- | --- |
|
|
114
|
+
* | 16 KB | 98 ms | 2.8 ms |
|
|
115
|
+
* | 64 KB | 2,073 ms | 10.7 ms |
|
|
116
|
+
* | 2 MB | ~35 min (extrapolated) | 218 ms |
|
|
117
|
+
*
|
|
118
|
+
* **This was a denial of service on our own collection path, reachable by
|
|
119
|
+
* ordinary application output.** Any backend log line of a few tens of
|
|
120
|
+
* kilobytes without an `@` — a serialised object, a stack dump, a base64
|
|
121
|
+
* blob — would wedge `redact()`, and `redact()` sits on the only write path
|
|
122
|
+
* into the store. Found by a §19 "large payload" test that hung instead of
|
|
123
|
+
* failing, which is the one symptom easy to mistake for a slow test.
|
|
124
|
+
*
|
|
125
|
+
* The bounds are RFC-shaped rather than invented: 64 characters for the local
|
|
126
|
+
* part (RFC 5321's limit), 63 per domain label, up to eight labels, 2–24 for
|
|
127
|
+
* the TLD. Verified behaviourally identical to the old pattern on every
|
|
128
|
+
* address form in this file's tests — the change costs no match and removes
|
|
129
|
+
* the quadratic.
|
|
130
|
+
*/
|
|
131
|
+
const EMAIL_PATTERN = /[a-zA-Z0-9._%+-]{1,64}@[a-zA-Z0-9-]{1,63}(?:\.[a-zA-Z0-9-]{1,63}){0,8}\.[a-zA-Z]{2,24}/g;
|
|
132
|
+
// Anchored to always end on a digit -- (?:\d[ -]?){13,16} over-consumes a
|
|
133
|
+
// trailing space/dash that follows the number but isn't part of it
|
|
134
|
+
// (caught by a real test: "card 4111 1111 1111 1111 successfully" matched
|
|
135
|
+
// through the space before "successfully").
|
|
136
|
+
const CREDIT_CARD_PATTERN = /\b\d(?:[ -]?\d){12,15}\b/g;
|
|
137
|
+
const GENERIC_TOKEN_PATTERN = /\b[A-Za-z0-9_-]{24,}\b/g;
|
|
138
|
+
const TEXT_SCAN_PATTERNS = [EMAIL_PATTERN, CREDIT_CARD_PATTERN, GENERIC_TOKEN_PATTERN];
|
|
139
|
+
const DEFAULT_POLICY = { bodyFiltering: true, piiFiltering: true };
|
|
140
|
+
export function redact(payload, policy = {}) {
|
|
141
|
+
const resolved = { ...DEFAULT_POLICY, ...policy };
|
|
142
|
+
const redactedKeys = [];
|
|
143
|
+
const { value, redacted } = redactValue(payload, resolved, "", redactedKeys);
|
|
144
|
+
return {
|
|
145
|
+
payload: value,
|
|
146
|
+
redactionStatus: redacted ? "redacted" : "not-required",
|
|
147
|
+
redactedKeys,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Which keys the default policy would redact, without redacting anything.
|
|
152
|
+
*
|
|
153
|
+
* The tool a collector author needs *before* shipping a payload shape: hand
|
|
154
|
+
* it the key names, get back the ones that collide and would be wiped. The
|
|
155
|
+
* `sessionId` collision (RT-063) was a name nobody thought to check against
|
|
156
|
+
* a pattern list nobody thought to read, and the check costs one call.
|
|
157
|
+
*/
|
|
158
|
+
export function collidingKeys(keys, policy = {}) {
|
|
159
|
+
const resolved = { ...DEFAULT_POLICY, ...policy };
|
|
160
|
+
return keys.filter((key) => SENSITIVE_KEY_PATTERN.test(key) ||
|
|
161
|
+
(resolved.piiFiltering && PII_KEY_PATTERN.test(key)) ||
|
|
162
|
+
(resolved.bodyFiltering && BODY_BEARING_KEY_PATTERN.test(key)));
|
|
163
|
+
}
|
|
164
|
+
function join(prefix, key) {
|
|
165
|
+
return prefix === "" ? key : `${prefix}.${key}`;
|
|
166
|
+
}
|
|
167
|
+
function redactValue(value, policy, path, redactedKeys) {
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
let redacted = false;
|
|
170
|
+
const result = value.map((item, index) => {
|
|
171
|
+
const inner = redactValue(item, policy, `${path}[${index}]`, redactedKeys);
|
|
172
|
+
redacted = redacted || inner.redacted;
|
|
173
|
+
return inner.value;
|
|
174
|
+
});
|
|
175
|
+
return { value: result, redacted };
|
|
176
|
+
}
|
|
177
|
+
if (value !== null && typeof value === "object") {
|
|
178
|
+
let redacted = false;
|
|
179
|
+
const result = {};
|
|
180
|
+
for (const [key, val] of Object.entries(value)) {
|
|
181
|
+
if (SENSITIVE_KEY_PATTERN.test(key) || (policy.piiFiltering && PII_KEY_PATTERN.test(key))) {
|
|
182
|
+
result[key] = REDACTED_MARKER;
|
|
183
|
+
redactedKeys.push(join(path, key));
|
|
184
|
+
redacted = true;
|
|
185
|
+
}
|
|
186
|
+
else if (COMMAND_BEARING_KEY_PATTERN.test(key) && (typeof val === "string" || Array.isArray(val))) {
|
|
187
|
+
const inner = redactCommand(val);
|
|
188
|
+
result[key] = inner.value;
|
|
189
|
+
if (inner.redacted)
|
|
190
|
+
redactedKeys.push(join(path, key));
|
|
191
|
+
redacted = redacted || inner.redacted;
|
|
192
|
+
}
|
|
193
|
+
else if (policy.bodyFiltering && typeof val === "string" && BODY_BEARING_KEY_PATTERN.test(key)) {
|
|
194
|
+
const inner = redactBodyString(val, policy, join(path, key), redactedKeys);
|
|
195
|
+
result[key] = inner.value;
|
|
196
|
+
redacted = redacted || inner.redacted;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const inner = redactValue(val, policy, join(path, key), redactedKeys);
|
|
200
|
+
result[key] = inner.value;
|
|
201
|
+
redacted = redacted || inner.redacted;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { value: result, redacted };
|
|
205
|
+
}
|
|
206
|
+
// A body-bearing key's value that isn't a string (null, or a non-string
|
|
207
|
+
// primitive some future producer emits) has nothing this module can
|
|
208
|
+
// scan -- left as-is, since a bare number or boolean can't itself carry
|
|
209
|
+
// a secret the way an encoded string can.
|
|
210
|
+
return { value, redacted: false };
|
|
211
|
+
}
|
|
212
|
+
/** JSON-shaped body content is parsed and recursed into; anything else is scanned as text, or withheld wholesale if it doesn't even look like text. */
|
|
213
|
+
function redactBodyString(text, policy, path, redactedKeys) {
|
|
214
|
+
const parsed = tryParseJson(text);
|
|
215
|
+
if (parsed !== undefined) {
|
|
216
|
+
// The body's own keys are reported under the body's path, so an operator
|
|
217
|
+
// can tell `headers.authorization` from `body.authorization` — different
|
|
218
|
+
// producers, different fixes.
|
|
219
|
+
const inner = redactValue(parsed, policy, path, redactedKeys);
|
|
220
|
+
return { value: JSON.stringify(inner.value), redacted: inner.redacted };
|
|
221
|
+
}
|
|
222
|
+
if (!looksLikeText(text)) {
|
|
223
|
+
// Fails closed: content this module cannot even classify as text is
|
|
224
|
+
// withheld wholesale rather than passed through on the assumption
|
|
225
|
+
// that "couldn't parse it" means "couldn't be sensitive."
|
|
226
|
+
redactedKeys.push(path);
|
|
227
|
+
return { value: REDACTED_BODY_MARKER, redacted: true };
|
|
228
|
+
}
|
|
229
|
+
return redactPlainText(text);
|
|
230
|
+
}
|
|
231
|
+
function tryParseJson(text) {
|
|
232
|
+
const trimmed = text.trim();
|
|
233
|
+
if (trimmed.length === 0 || (trimmed[0] !== "{" && trimmed[0] !== "[")) {
|
|
234
|
+
// A bare string/number/boolean JSON body ("true", "42") is technically
|
|
235
|
+
// valid JSON but never carries nested keys to redact -- treated as
|
|
236
|
+
// plain text instead, so the patterns below still get a chance at it.
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(trimmed);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** A crude but deliberate bar: mostly printable text, not a binary blob wearing a string's clothes. */
|
|
247
|
+
function looksLikeText(text) {
|
|
248
|
+
if (text.length === 0)
|
|
249
|
+
return true;
|
|
250
|
+
let controlChars = 0;
|
|
251
|
+
for (let i = 0; i < text.length; i++) {
|
|
252
|
+
const code = text.charCodeAt(i);
|
|
253
|
+
const isCommonWhitespace = code === 9 || code === 10 || code === 13;
|
|
254
|
+
if (!isCommonWhitespace && code < 0x20) {
|
|
255
|
+
controlChars++;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return controlChars / text.length < 0.05;
|
|
259
|
+
}
|
|
260
|
+
function redactPlainText(text) {
|
|
261
|
+
let redacted = false;
|
|
262
|
+
let result = text;
|
|
263
|
+
for (const pattern of TEXT_SCAN_PATTERNS) {
|
|
264
|
+
result = result.replace(pattern, () => {
|
|
265
|
+
redacted = true;
|
|
266
|
+
return REDACTED_MARKER;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return { value: result, redacted };
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Redact credential values out of a command line, keeping the shape.
|
|
273
|
+
*
|
|
274
|
+
* Two forms, because a command line has two:
|
|
275
|
+
*
|
|
276
|
+
* - **Attached** -- `--api-key=SEKRET`, `TOKEN:abc`, `DB_PASSWORD=hunter2`.
|
|
277
|
+
* The name and the separator survive; only the value is replaced.
|
|
278
|
+
* - **Detached** -- `["--api-key", "SEKRET"]` or `--api-key SEKRET`, where the
|
|
279
|
+
* secret is the *next* token. This is the form a naive per-token scanner
|
|
280
|
+
* misses entirely: `SEKRET` on its own carries nothing that marks it as a
|
|
281
|
+
* secret, and the only thing that does is the token before it.
|
|
282
|
+
*
|
|
283
|
+
* A detached flag at the very end of a command consumes nothing -- there is
|
|
284
|
+
* no value to redact, and inventing one would corrupt the command.
|
|
285
|
+
*
|
|
286
|
+
* The name and separator are kept on purpose. A command redacted to
|
|
287
|
+
* `[REDACTED]` wholesale is useless to the reader it was persisted for; a
|
|
288
|
+
* command redacted to `node server.mjs --api-key=[REDACTED]` still answers
|
|
289
|
+
* "what was run, and with what shape of argument".
|
|
290
|
+
*/
|
|
291
|
+
export function redactCommand(value) {
|
|
292
|
+
if (Array.isArray(value)) {
|
|
293
|
+
const out = redactTokens(value);
|
|
294
|
+
return { value: out.tokens, redacted: out.redacted };
|
|
295
|
+
}
|
|
296
|
+
// Split on whitespace, redact, rejoin. This does not attempt to parse
|
|
297
|
+
// shell quoting -- `tokenizeCommand` (controller) already refuses shell
|
|
298
|
+
// metacharacters, so a command reaching here is a plain argv-shaped
|
|
299
|
+
// string, and a quoting parser would be machinery for a case this repo
|
|
300
|
+
// deliberately does not accept.
|
|
301
|
+
const tokens = String(value).split(/\s+/);
|
|
302
|
+
const out = redactTokens(tokens);
|
|
303
|
+
return { value: out.tokens.join(" "), redacted: out.redacted };
|
|
304
|
+
}
|
|
305
|
+
function redactTokens(tokens) {
|
|
306
|
+
const result = [];
|
|
307
|
+
let redacted = false;
|
|
308
|
+
let consumeNext = false;
|
|
309
|
+
for (const token of tokens) {
|
|
310
|
+
if (consumeNext) {
|
|
311
|
+
result.push(REDACTED_MARKER);
|
|
312
|
+
redacted = true;
|
|
313
|
+
consumeNext = false;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const assignment = ARG_ASSIGNMENT.exec(token);
|
|
317
|
+
if (assignment !== null && isSensitiveArgName(assignment[1] ?? "")) {
|
|
318
|
+
result.push(`${assignment[1]}${assignment[2]}${REDACTED_MARKER}`);
|
|
319
|
+
redacted = true;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
// A bare sensitive-looking flag arms the next token. Only a flag --
|
|
323
|
+
// a bare word containing "key" (a path like `keys/dev.json`) must not
|
|
324
|
+
// swallow whatever follows it.
|
|
325
|
+
if (token.startsWith("-") && isSensitiveArgName(token)) {
|
|
326
|
+
result.push(token);
|
|
327
|
+
consumeNext = true;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
result.push(token);
|
|
331
|
+
}
|
|
332
|
+
return { tokens: result, redacted };
|
|
333
|
+
}
|
|
334
|
+
function isSensitiveArgName(name) {
|
|
335
|
+
return SENSITIVE_ARG_NAME.test(name) && !LOCATION_ARG_NAME.test(name);
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=redaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redaction.js","sourceRoot":"","sources":["../src/redaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAIH,MAAM,qBAAqB,GACzB,wKAAwK,CAAC;AAE3K,MAAM,eAAe,GACnB,uSAAuS,CAAC;AAE1S;;;;;;;GAOG;AACH,MAAM,wBAAwB,GAAG,gEAAgE,CAAC;AAElG;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,2BAA2B,GAAG,4BAA4B,CAAC;AAEjE;;;;;;;;;;GAUG;AACH,MAAM,kBAAkB,GAAG,qEAAqE,CAAC;AACjG;;;;;;;;;GASG;AACH,MAAM,iBAAiB,GAAG,8DAA8D,CAAC;AACzF,MAAM,cAAc,GAAG,sCAAsC,CAAC;AAE9D,MAAM,eAAe,GAAG,YAAY,CAAC;AACrC,MAAM,oBAAoB,GAAG,iBAAiB,CAAC;AAE/C,wEAAwE;AACxE,uEAAuE;AACvE,mBAAmB;AACnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,aAAa,GAAG,yFAAyF,CAAC;AAChH,0EAA0E;AAC1E,mEAAmE;AACnE,0EAA0E;AAC1E,4CAA4C;AAC5C,MAAM,mBAAmB,GAAG,2BAA2B,CAAC;AACxD,MAAM,qBAAqB,GAAG,yBAAyB,CAAC;AAExD,MAAM,kBAAkB,GAAsB,CAAC,aAAa,EAAE,mBAAmB,EAAE,qBAAqB,CAAC,CAAC;AAS1G,MAAM,cAAc,GAA8B,EAAE,aAAa,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;AAsC9F,MAAM,UAAU,MAAM,CAAC,OAAgB,EAAE,SAA0B,EAAE;IACnE,MAAM,QAAQ,GAA8B,EAAE,GAAG,cAAc,EAAE,GAAG,MAAM,EAAE,CAAC;IAC7E,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,WAAW,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,YAAY,CAAC,CAAC;IAC7E,OAAO;QACL,OAAO,EAAE,KAAK;QACd,eAAe,EAAE,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,cAAc;QACvD,YAAY;KACb,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,IAAuB,EAAE,SAA0B,EAAE;IACjF,MAAM,QAAQ,GAA8B,EAAE,GAAG,cAAc,EAAE,GAAG,MAAM,EAAE,CAAC;IAC7E,OAAO,IAAI,CAAC,MAAM,CAChB,CAAC,GAAG,EAAE,EAAE,CACN,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC;QAC/B,CAAC,QAAQ,CAAC,YAAY,IAAI,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACpD,CAAC,QAAQ,CAAC,aAAa,IAAI,wBAAwB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CACjE,CAAC;AACJ,CAAC;AAED,SAAS,IAAI,CAAC,MAAc,EAAE,GAAW;IACvC,OAAO,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC;AAClD,CAAC;AAED,SAAS,WAAW,CAClB,KAAc,EACd,MAAiC,EACjC,IAAY,EACZ,YAAsB;IAEtB,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;YACvC,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,YAAY,CAAC,CAAC;YAC3E,QAAQ,GAAG,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC;YACtC,OAAO,KAAK,CAAC,KAAK,CAAC;QACrB,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IACrC,CAAC;IAED,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;YAC1E,IAAI,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBAC1F,MAAM,CAAC,GAAG,CAAC,GAAG,eAAe,CAAC;gBAC9B,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;gBACnC,QAAQ,GAAG,IAAI,CAAC;YAClB,CAAC;iBAAM,IAAI,2BAA2B,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBACpG,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;gBACjC,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;gBAC1B,IAAI,KAAK,CAAC,QAAQ;oBAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;gBACvD,QAAQ,GAAG,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC;YACxC,CAAC;iBAAM,IAAI,MAAM,CAAC,aAAa,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,wBAAwB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACjG,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,YAAY,CAAC,CAAC;gBAC3E,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;gBAC1B,QAAQ,GAAG,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC;YACxC,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,YAAY,CAAC,CAAC;gBACtE,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;gBAC1B,QAAQ,GAAG,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC;YACxC,CAAC;QACH,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IACrC,CAAC;IAED,wEAAwE;IACxE,oEAAoE;IACpE,wEAAwE;IACxE,0CAA0C;IAC1C,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AACpC,CAAC;AAED,uJAAuJ;AACvJ,SAAS,gBAAgB,CACvB,IAAY,EACZ,MAAiC,EACjC,IAAY,EACZ,YAAsB;IAEtB,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,yEAAyE;QACzE,yEAAyE;QACzE,8BAA8B;QAC9B,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;QAC9D,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC1E,CAAC;IAED,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,oEAAoE;QACpE,kEAAkE;QAClE,0DAA0D;QAC1D,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,OAAO,EAAE,KAAK,EAAE,oBAAoB,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzD,CAAC;IAED,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,uEAAuE;QACvE,mEAAmE;QACnE,sEAAsE;QACtE,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,uGAAuG;AACvG,SAAS,aAAa,CAAC,IAAY;IACjC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,kBAAkB,GAAG,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,EAAE,CAAC;QACpE,IAAI,CAAC,kBAAkB,IAAI,IAAI,GAAG,IAAI,EAAE,CAAC;YACvC,YAAY,EAAE,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,YAAY,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;AAC3C,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,MAAM,OAAO,IAAI,kBAAkB,EAAE,CAAC;QACzC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE;YACpC,QAAQ,GAAG,IAAI,CAAC;YAChB,OAAO,eAAe,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAiC;IAC7D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,YAAY,CAAC,KAA0B,CAAC,CAAC;QACrD,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;IACvD,CAAC;IACD,sEAAsE;IACtE,wEAAwE;IACxE,oEAAoE;IACpE,uEAAuE;IACvE,gCAAgC;IAChC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC1C,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACjC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;AACjE,CAAC;AAED,SAAS,YAAY,CAAC,MAAyB;IAC7C,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,WAAW,GAAG,KAAK,CAAC;IAExB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,WAAW,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YAC7B,QAAQ,GAAG,IAAI,CAAC;YAChB,WAAW,GAAG,KAAK,CAAC;YACpB,SAAS;QACX,CAAC;QAED,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9C,IAAI,UAAU,KAAK,IAAI,IAAI,kBAAkB,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YACnE,MAAM,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,eAAe,EAAE,CAAC,CAAC;YAClE,QAAQ,GAAG,IAAI,CAAC;YAChB,SAAS;QACX,CAAC;QAED,oEAAoE;QACpE,sEAAsE;QACtE,+BAA+B;QAC/B,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC;YACvD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnB,WAAW,GAAG,IAAI,CAAC;YACnB,SAAS;QACX,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AACtC,CAAC;AAED,SAAS,kBAAkB,CAAC,IAAY;IACtC,OAAO,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxE,CAAC"}
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay (plan §24).
|
|
3
|
+
*
|
|
4
|
+
* ## What this replays, and what it does not
|
|
5
|
+
*
|
|
6
|
+
* **It replays evidence, not the application.** Given an `executionId`, it
|
|
7
|
+
* returns the recorded execution, the environment it was recorded in, and its
|
|
8
|
+
* evidence in recorded order — enough to re-derive correlation, findings and
|
|
9
|
+
* an evidence package without booting anything. It does not spawn a process,
|
|
10
|
+
* drive a browser, or reissue a request, and it never will from here: those
|
|
11
|
+
* are `descry-runtime`'s live path, and a "replay" that quietly re-executed
|
|
12
|
+
* would produce a *new* observation while claiming to reproduce an old one.
|
|
13
|
+
*
|
|
14
|
+
* That distinction is the whole reason this file names its own limits at the
|
|
15
|
+
* top. §24's row was `[ ]` with the note *"having the data is not having the
|
|
16
|
+
* capability"* — the opposite error is equally available, and it is shipping
|
|
17
|
+
* something called replay that reproduces less than the name promises.
|
|
18
|
+
*
|
|
19
|
+
* ## The gap this had to close first
|
|
20
|
+
*
|
|
21
|
+
* Evidence survived a process restart. **The execution that produced it did
|
|
22
|
+
* not.** `Execution` lived in `ExecutionController`'s memory and nowhere on
|
|
23
|
+
* disk, so after a restart the store held a pile of rows keyed by an
|
|
24
|
+
* `executionId` that no longer resolved to an application, a commit, or a
|
|
25
|
+
* configuration. §24 marked "execution configuration — persisted with the
|
|
26
|
+
* execution" as done, and it was true of the object and false of the disk.
|
|
27
|
+
*
|
|
28
|
+
* `recordExecution` is that missing half. It is a **new table**, never a new
|
|
29
|
+
* column on `evidence` — an added table appears under `CREATE TABLE IF NOT
|
|
30
|
+
* EXISTS` on an existing store file, where an added column would need a
|
|
31
|
+
* migration this package has no mechanism for.
|
|
32
|
+
*
|
|
33
|
+
* ## Redaction applies here too, and that is not obvious
|
|
34
|
+
*
|
|
35
|
+
* §20's rows are all about payloads, and every one of them is about evidence.
|
|
36
|
+
* But a service command is `configuration.services[x].command`, and a command
|
|
37
|
+
* line is one of the classic places a credential is passed —
|
|
38
|
+
* `node server.mjs --api-key=…`, `DATABASE_URL=… node app.js`. The execution
|
|
39
|
+
* record goes through the same `redact()` the evidence write path uses, for
|
|
40
|
+
* the same reason and on the same terms. `redactedKeys` (RT-063) reports what
|
|
41
|
+
* it caught.
|
|
42
|
+
*
|
|
43
|
+
* ## The environment fingerprint is deliberately narrow
|
|
44
|
+
*
|
|
45
|
+
* Four fields: node version, platform, arch, and the declared service
|
|
46
|
+
* commands. **`process.env` is not captured, and its absence is the design.**
|
|
47
|
+
* §20 records that the injected process environment is not captured or
|
|
48
|
+
* filtered — *"so there is nothing to leak yet, which is not the same as
|
|
49
|
+
* filtering."* Capturing it here to improve reproducibility would create
|
|
50
|
+
* exactly the leak that row says does not exist, inside the one module whose
|
|
51
|
+
* output is meant to be safe to hand to a model. A narrower fingerprint that
|
|
52
|
+
* can be trusted beats a fuller one that cannot.
|
|
53
|
+
*/
|
|
54
|
+
import type { Evidence } from "@descryy/runtime-contracts";
|
|
55
|
+
/** What was true of the machine when an execution was recorded. Narrow on purpose — see this file's header. */
|
|
56
|
+
export interface EnvironmentFingerprint {
|
|
57
|
+
/** `process.version`, e.g. `v22.23.1`. */
|
|
58
|
+
readonly nodeVersion: string;
|
|
59
|
+
readonly platform: string;
|
|
60
|
+
readonly arch: string;
|
|
61
|
+
/**
|
|
62
|
+
* `serviceName -> command`, from the execution's own configuration.
|
|
63
|
+
* Redacted on the way in: a command line is a classic credential carrier.
|
|
64
|
+
*/
|
|
65
|
+
readonly serviceCommands: Readonly<Record<string, string>>;
|
|
66
|
+
}
|
|
67
|
+
/** The recorded half of an execution — what `ExecutionController` held only in memory. */
|
|
68
|
+
export interface RecordedExecution {
|
|
69
|
+
readonly executionId: string;
|
|
70
|
+
readonly application: string;
|
|
71
|
+
readonly repository: string;
|
|
72
|
+
readonly commit: string;
|
|
73
|
+
readonly recordedAt: string;
|
|
74
|
+
readonly configuration: unknown;
|
|
75
|
+
readonly environment: EnvironmentFingerprint;
|
|
76
|
+
/** Key paths redaction removed from the configuration on the way in. Empty is a claim, not an absence (RT-063). */
|
|
77
|
+
readonly redactedKeys: readonly string[];
|
|
78
|
+
}
|
|
79
|
+
export interface ReplayWarning {
|
|
80
|
+
readonly kind: "environment-changed" | "no-evidence" | "unordered-timestamps";
|
|
81
|
+
readonly detail: string;
|
|
82
|
+
}
|
|
83
|
+
export interface Replay {
|
|
84
|
+
readonly execution: RecordedExecution;
|
|
85
|
+
/** In recorded order, oldest first. */
|
|
86
|
+
readonly evidence: readonly Evidence[];
|
|
87
|
+
/** The environment replay is happening *in*, for comparison against the recorded one. */
|
|
88
|
+
readonly currentEnvironment: EnvironmentFingerprint;
|
|
89
|
+
/**
|
|
90
|
+
* Never empty for a degraded replay, and each one names what differs
|
|
91
|
+
* rather than asserting the replay is invalid — whether a node-version
|
|
92
|
+
* change matters is the reader's call, not this module's.
|
|
93
|
+
*/
|
|
94
|
+
readonly warnings: readonly ReplayWarning[];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The current machine's fingerprint.
|
|
98
|
+
*
|
|
99
|
+
* `serviceCommands` comes from the caller's configuration rather than being
|
|
100
|
+
* discovered, because there is nothing on a machine to discover it from — an
|
|
101
|
+
* execution's services are a property of the run, not of the host.
|
|
102
|
+
*/
|
|
103
|
+
export declare function captureEnvironment(serviceCommands?: Readonly<Record<string, string>>, runtime?: {
|
|
104
|
+
version: string;
|
|
105
|
+
platform: string;
|
|
106
|
+
arch: string;
|
|
107
|
+
}): EnvironmentFingerprint;
|
|
108
|
+
/**
|
|
109
|
+
* Differences between the environment an execution was recorded in and the
|
|
110
|
+
* one it is being replayed in.
|
|
111
|
+
*
|
|
112
|
+
* **Reported, never enforced.** A replay in a different node version is not
|
|
113
|
+
* wrong; it is a fact a reader needs when a re-derived finding disagrees with
|
|
114
|
+
* the original. Refusing to replay would be this module deciding a question
|
|
115
|
+
* it has no information about.
|
|
116
|
+
*/
|
|
117
|
+
export declare function environmentDifferences(recorded: EnvironmentFingerprint, current: EnvironmentFingerprint): readonly string[];
|
|
118
|
+
//# sourceMappingURL=replay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAE3D,+GAA+G;AAC/G,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC5D;AAED,0FAA0F;AAC1F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,sBAAsB,CAAC;IAC7C,mHAAmH;IACnH,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,qBAAqB,GAAG,aAAa,GAAG,sBAAsB,CAAC;IAC9E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC;IACtC,uCAAuC;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,QAAQ,EAAE,CAAC;IACvC,yFAAyF;IACzF,QAAQ,CAAC,kBAAkB,EAAE,sBAAsB,CAAC;IACpD;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;CAC7C;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,eAAe,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,EACtD,OAAO,GAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAY,GACrE,sBAAsB,CAOxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,sBAAsB,EAChC,OAAO,EAAE,sBAAsB,GAC9B,SAAS,MAAM,EAAE,CAsBnB"}
|