@sema-agent/core 7.50.2 → 7.51.1
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 +313 -0
- package/dist/agents/child-failure.d.ts +5 -16
- package/dist/agents/child-failure.js +4 -24
- package/dist/agents/subagent-steps.js +2 -2
- package/dist/agents/subagent.d.ts +4 -13
- package/dist/agents/subagent.js +94 -204
- package/dist/agents/tool-filter.d.ts +2 -2
- package/dist/brain/anthropic.d.ts +1 -1
- package/dist/brain/anthropic.js +7 -5
- package/dist/brain/open-responses.d.ts +1 -1
- package/dist/brain/open-responses.js +3 -2
- package/dist/brain/openai.d.ts +1 -1
- package/dist/brain/openai.js +3 -2
- package/dist/brain/request-route.d.ts +2 -2
- package/dist/brain/route-adjudicator.d.ts +16 -7
- package/dist/brain/route-adjudicator.js +23 -7
- package/dist/brain/route-conformance.js +23 -5
- package/dist/brain/stream-engine.js +9 -3
- package/dist/core/a2a.js +6 -8
- package/dist/core/allow-entry.d.ts +45 -26
- package/dist/core/allow-entry.js +48 -24
- package/dist/core/arg-summary.d.ts +8 -85
- package/dist/core/arg-summary.js +4 -386
- package/dist/core/ask-origin.d.ts +35 -33
- package/dist/core/ask-origin.js +9 -11
- package/dist/core/checkpoint-requirements.d.ts +6 -0
- package/dist/core/checkpoint-requirements.js +1 -0
- package/dist/core/checkpoint-store.d.ts +15 -34
- package/dist/core/checkpoint-store.js +5 -4
- package/dist/core/classifier-unavailable-arm.d.ts +12 -8
- package/dist/core/classifier-unavailable-arm.js +7 -2
- package/dist/core/credential-vocabulary.d.ts +3 -2
- package/dist/core/credential-vocabulary.js +27 -18
- package/dist/core/delegation-frames.d.ts +40 -15
- package/dist/core/delegation-frames.js +34 -0
- package/dist/core/engine-capabilities.d.ts +15 -0
- package/dist/core/engine-capabilities.js +1 -0
- package/dist/core/engine-notice.d.ts +41 -14
- package/dist/core/gate-fold.js +9 -7
- package/dist/core/gate-lanes.js +6 -4
- package/dist/core/gate-outcome.d.ts +81 -15
- package/dist/core/gate-outcome.js +168 -67
- package/dist/core/governance-codes.d.ts +7 -1
- package/dist/core/governance-codes.js +9 -0
- package/dist/core/hooks.d.ts +4 -4
- package/dist/core/hooks.js +4 -3
- package/dist/core/mcp-connection-pool.d.ts +2 -0
- package/dist/core/mcp-connection-pool.js +2 -1
- package/dist/core/mcp-failure.js +2 -1
- package/dist/core/mcp-server-spec.d.ts +27 -5
- package/dist/core/mcp.d.ts +13 -38
- package/dist/core/mcp.js +28 -23
- package/dist/core/media-byte-cap.d.ts +25 -0
- package/dist/core/media-byte-cap.js +18 -2
- package/dist/core/memory-engine/consolidation-driver.js +1 -1
- package/dist/core/mode-policy.d.ts +25 -1
- package/dist/core/mode-policy.js +28 -13
- package/dist/core/own-property-receipt.d.ts +18 -0
- package/dist/core/own-property-receipt.js +39 -0
- package/dist/core/permission-rule-model.d.ts +32 -11
- package/dist/core/permission-rule-model.js +12 -7
- package/dist/core/persisted-rule-arms.d.ts +3 -3
- package/dist/core/prompt-section-seat.d.ts +54 -0
- package/dist/core/prompt-section-seat.js +49 -0
- package/dist/core/protocol-http-auth.d.ts +44 -0
- package/dist/core/protocol-http-auth.js +45 -0
- package/dist/core/read-face-seats.d.ts +30 -0
- package/dist/core/read-face-seats.js +38 -0
- package/dist/core/redaction-passes.d.ts +82 -0
- package/dist/core/redaction-passes.js +378 -0
- package/dist/core/remote-env.d.ts +4 -3
- package/dist/core/resume-startup-cause.d.ts +56 -0
- package/dist/core/resume-startup-cause.js +43 -0
- package/dist/core/rule-denial-sentence.d.ts +2 -2
- package/dist/core/runner/announce-once-ledger.js +2 -2
- package/dist/core/runner/classifier-ladder.d.ts +17 -13
- package/dist/core/runner/classifier-ladder.js +12 -10
- package/dist/core/runner/construction-doors.js +9 -20
- package/dist/core/runner/contracts.d.ts +4 -4
- package/dist/core/runner/gate-exit.js +2 -2
- package/dist/core/runner/memory-consolidation-write-judge.d.ts +53 -9
- package/dist/core/runner/memory-consolidation-write-judge.js +39 -1
- package/dist/core/runner/permission-rule-lanes.d.ts +8 -13
- package/dist/core/runner/person-path-rows.d.ts +4 -3
- package/dist/core/runner/policy-throw.js +4 -4
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +8 -6
- package/dist/core/runner/prepare-gate-stations.js +4 -4
- package/dist/core/runner/prepare-hands-readface.js +1 -19
- package/dist/core/runner/prepare-inherited-gate.js +10 -9
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -5
- package/dist/core/runner/prepare-policy-chain.js +10 -13
- package/dist/core/runner/prepare-project-context.d.ts +4 -4
- package/dist/core/runner/prepare-project-context.js +2 -1
- package/dist/core/runner/prepare-prompt-assembly.js +1 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -1
- package/dist/core/runner/prepare-prompt-inputs.js +4 -4
- package/dist/core/runner/prepare-protocol-tools.d.ts +4 -3
- package/dist/core/runner/prepare-protocol-tools.js +3 -2
- package/dist/core/runner/prepare-task.js +6 -4
- package/dist/core/runner/prepare-turn-wiring.d.ts +16 -13
- package/dist/core/runner/prepare-turn-wiring.js +8 -7
- package/dist/core/runner/run-harness-handlers.js +2 -2
- package/dist/core/runner/run-leg.js +10 -9
- package/dist/core/runner/run-settle-and-teardown.js +5 -5
- package/dist/core/runner/run-turn-boundary.js +3 -2
- package/dist/core/runner/runtask.d.ts +1 -2
- package/dist/core/runner/runtask.js +8 -12
- package/dist/core/runner/session-rule-policy.d.ts +15 -11
- package/dist/core/runner/session-rule-policy.js +43 -33
- package/dist/core/runner/stream-lifecycle-verbs.js +2 -2
- package/dist/core/runner/stream-settle-backstop.js +2 -1
- package/dist/core/runner/sub-agent-deny-lists.d.ts +12 -8
- package/dist/core/runner/sub-agent-deny-lists.js +23 -12
- package/dist/core/runner/terminal-account.js +3 -3
- package/dist/core/runner/tool-output-projection.d.ts +5 -0
- package/dist/core/runner/tool-output-projection.js +6 -11
- package/dist/core/runner/tool-result-media-seat.d.ts +19 -0
- package/dist/core/runner/tool-result-media-seat.js +13 -0
- package/dist/core/runner/turn-attachments.d.ts +13 -12
- package/dist/core/runner/turn-attachments.js +11 -7
- package/dist/core/runner-deps.d.ts +4 -3
- package/dist/core/safety-merge-corpus.js +21 -21
- package/dist/core/secret-env.d.ts +41 -15
- package/dist/core/secret-env.js +85 -19
- package/dist/core/session-policy-store.d.ts +89 -72
- package/dist/core/session-policy-store.js +100 -93
- package/dist/core/shell-read-silence.d.ts +38 -4
- package/dist/core/shell-read-silence.js +102 -0
- package/dist/core/side-query.d.ts +7 -8
- package/dist/core/skill-grant-binding.d.ts +32 -11
- package/dist/core/skill-grant-binding.js +11 -3
- package/dist/core/skill-grant.d.ts +4 -3
- package/dist/core/skill-grant.js +5 -4
- package/dist/core/skills-directory.d.ts +12 -1
- package/dist/core/skills-directory.js +17 -8
- package/dist/core/spec-contract.d.ts +5 -3
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +3 -0
- package/dist/core/swappable-deps.d.ts +15 -1
- package/dist/core/swappable-deps.js +25 -1
- package/dist/core/synthetic-read-verdict.d.ts +16 -14
- package/dist/core/synthetic-read-verdict.js +28 -8
- package/dist/core/task-notification.d.ts +13 -3
- package/dist/core/task-notification.js +12 -1
- package/dist/core/task-spec.d.ts +14 -15
- package/dist/core/tool-name-readings.d.ts +0 -4
- package/dist/core/tool-name-readings.js +0 -1
- package/dist/core/tool-policy.d.ts +40 -9
- package/dist/core/tool-policy.js +25 -7
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/core/untrusted-egress.d.ts +60 -11
- package/dist/core/untrusted-egress.js +120 -9
- package/dist/core/untrusted-text.js +16 -0
- package/dist/core/wiring-manifest.d.ts +22 -8
- package/dist/core/wiring-manifest.js +12 -4
- package/dist/engine/execution-env/kill-tree.js +4 -4
- package/dist/engine/execution-env/node-execution-env.d.ts +11 -4
- package/dist/engine/execution-env/node-execution-env.js +9 -4
- package/dist/engine/harness/agent-harness.d.ts +2 -1
- package/dist/engine/harness/agent-harness.js +4 -1
- package/dist/engine/harness/types.d.ts +3 -0
- package/dist/engine/llm/types.d.ts +7 -9
- package/dist/engine/loop/agent-loop.d.ts +8 -3
- package/dist/engine/loop/agent-loop.js +26 -9
- package/dist/engine/loop/types.d.ts +10 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -1
- package/dist/engine/lsp/node-lsp-manager.js +2 -2
- package/dist/index.d.ts +22 -5
- package/dist/index.js +18 -5
- package/dist/orchestration/run-workflow-tool.js +8 -8
- package/dist/orchestration/workflow.js +2 -2
- package/dist/prompt-assembly/assemble.d.ts +3 -1
- package/dist/prompt-assembly/assemble.js +6 -0
- package/dist/prompts/cowork.d.ts +42 -0
- package/dist/prompts/cowork.js +529 -0
- package/dist/prompts/default.d.ts +14 -1
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/fs-atomic.js +2 -2
- package/dist/stores/file/session-policy-store.d.ts +6 -3
- package/dist/stores/file/session-policy-store.js +5 -16
- package/dist/tools/artifact/artifact-tool.js +2 -0
- package/dist/tools/fs/image-admission.d.ts +57 -4
- package/dist/tools/fs/image-admission.js +85 -0
- package/dist/tools/fs/path-containment.d.ts +2 -0
- package/dist/tools/fs/path-containment.js +5 -0
- package/dist/tools/fs/shell-read-check.d.ts +31 -6
- package/dist/tools/fs/shell-read-check.js +2 -4
- package/dist/tools/fs/shell-read-environment.d.ts +10 -25
- package/dist/tools/fs/shell-read-environment.js +6 -18
- package/dist/tools/fs/shell-read-establish.d.ts +3 -2
- package/dist/tools/fs/shell-read-establish.js +5 -5
- package/dist/tools/fs/shell-read-judge.d.ts +38 -8
- package/dist/tools/fs/shell-read-judge.js +12 -10
- package/dist/tools/web.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +112 -10
- package/wire-changes/core@7.51.0.json +2421 -0
- package/wire-changes/core@7.51.1.json +259 -0
package/dist/core/allow-entry.js
CHANGED
|
@@ -3,7 +3,7 @@ import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermission
|
|
|
3
3
|
import { judgeRuleToolName, splitNamespacedName } from "./tool-name-aliases.js";
|
|
4
4
|
import { catalogToolNames, isShellFaceToolName } from "./tool-registry.js";
|
|
5
5
|
import { ALLOW_RULE_MEMBERS } from "./allow-rule-shadow.js";
|
|
6
|
-
import { COMMAND_RULE_TOOL, adjudicatePersistedDomainRules, adjudicatePersistedNameRules,
|
|
6
|
+
import { COMMAND_RULE_TOOL, adjudicatePersistedDomainRules, adjudicatePersistedNameRules, commandCoverageOf, domainRuleSubjectOf, laneToolNamesOf, nameRuleSubjectOf, parseRuleText, programRunReachOf, ruleToolGrammarOf } from "./permission-rule-model.js";
|
|
7
7
|
import { pathRuleLaneAnswer } from "./runner/permission-rule-lanes.js";
|
|
8
8
|
import { callTargetReadingOf } from "./effective-path-target.js";
|
|
9
9
|
import { splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
@@ -31,20 +31,21 @@ function mountedSpellingOf(name, roster) {
|
|
|
31
31
|
}
|
|
32
32
|
function toolNameReading(name, roster) {
|
|
33
33
|
const verdict = judgeRuleToolName(name, roster);
|
|
34
|
+
if (verdict.kind === "unprefixed_namespace")
|
|
35
|
+
return { kind: "inert", reason: "tool_name", verdict: verdict.kind };
|
|
34
36
|
if (verdict.kind !== "current") {
|
|
35
|
-
|
|
36
|
-
return { inert: "tool_name", detail: { verdict: verdict.kind, ...(currentName !== undefined ? { currentName } : {}) } };
|
|
37
|
+
return { kind: "inert", reason: "tool_name", verdict: verdict.kind, guidance: verdict.guidance, ...(verdict.currentName !== undefined ? { currentName: verdict.currentName } : {}) };
|
|
37
38
|
}
|
|
38
39
|
if (roster.has(name) || catalogToolNames().has(name) || splitNamespacedName(name) !== undefined)
|
|
39
40
|
return undefined;
|
|
40
41
|
const mounted = mountedSpellingOf(name, roster);
|
|
41
|
-
return mounted === undefined ? undefined : {
|
|
42
|
+
return mounted === undefined ? undefined : { kind: "inert", reason: "near_miss", currentName: mounted };
|
|
42
43
|
}
|
|
43
|
-
function
|
|
44
|
-
const inert = (reason) => ({
|
|
45
|
-
const row = (compiled) => ({ compiled });
|
|
44
|
+
export function readRuleEntry(raw, behavior, roster) {
|
|
45
|
+
const inert = (reason) => ({ kind: "inert", reason });
|
|
46
|
+
const row = (compiled) => ({ kind: "row", compiled });
|
|
46
47
|
if (typeof raw !== "string")
|
|
47
|
-
return {
|
|
48
|
+
return { kind: "inert", reason: "grammar", valueType: valueTypeLabel(raw) };
|
|
48
49
|
if (raw !== raw.trim())
|
|
49
50
|
return inert("grammar");
|
|
50
51
|
const parsed = parsePermissionRule(raw);
|
|
@@ -82,26 +83,39 @@ function readAllowEntry(raw, behavior, roster) {
|
|
|
82
83
|
}
|
|
83
84
|
return inert(inertReasonOf(read.reject.code));
|
|
84
85
|
}
|
|
86
|
+
export function compiledEntryName(compiled) {
|
|
87
|
+
switch (compiled.kind) {
|
|
88
|
+
case "tool":
|
|
89
|
+
case "shell":
|
|
90
|
+
return compiled.name;
|
|
91
|
+
case "namespace":
|
|
92
|
+
return compiled.pattern;
|
|
93
|
+
default:
|
|
94
|
+
return compiled.rule.tool;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
85
97
|
function inertReasonOf(code) {
|
|
86
98
|
return code === "unsupported.tool" ? "content_unsupported" : "grammar";
|
|
87
99
|
}
|
|
88
|
-
export function compileAllowEntry(raw, behavior, roster) {
|
|
89
|
-
const read = readAllowEntry(raw, behavior, roster);
|
|
90
|
-
return "compiled" in read ? read.compiled : read.inert;
|
|
91
|
-
}
|
|
92
100
|
export function compileAllowList(entries, behavior, roster) {
|
|
93
|
-
const voided = behavior === "allow" && entries.some((e) =>
|
|
101
|
+
const voided = behavior === "allow" && entries.some((e) => isWildcardAll(readRuleEntry(e, behavior)));
|
|
94
102
|
const rows = [];
|
|
95
103
|
const inert = [];
|
|
96
104
|
for (const entry of entries) {
|
|
97
|
-
const read = typeof entry !== "string" || !voided ?
|
|
98
|
-
if ("
|
|
105
|
+
const read = typeof entry !== "string" || !voided ? readRuleEntry(entry, behavior, roster) : { kind: "inert", reason: "wildcard_all" };
|
|
106
|
+
if (read.kind === "row")
|
|
99
107
|
rows.push({ entry, compiled: read.compiled });
|
|
100
|
-
else
|
|
101
|
-
|
|
108
|
+
else {
|
|
109
|
+
const { kind: _kind, ...why } = read;
|
|
110
|
+
void _kind;
|
|
111
|
+
inert.push({ entry: typeof entry === "string" ? entry : nonStringMemberLabel(entry), ...why });
|
|
112
|
+
}
|
|
102
113
|
}
|
|
103
114
|
return { rows, inert };
|
|
104
115
|
}
|
|
116
|
+
function isWildcardAll(read) {
|
|
117
|
+
return read.kind === "inert" && read.reason === "wildcard_all";
|
|
118
|
+
}
|
|
105
119
|
function answersTo(req, has) {
|
|
106
120
|
return heldBy("allowEntry.names", { has }, req);
|
|
107
121
|
}
|
|
@@ -164,7 +178,18 @@ function commandOf(req) {
|
|
|
164
178
|
const command = req.args?.command;
|
|
165
179
|
return tool !== undefined && typeof command === "string" ? { tool, command } : undefined;
|
|
166
180
|
}
|
|
167
|
-
|
|
181
|
+
function admissionCallOf(call, ctx, liveCwd, posture) {
|
|
182
|
+
return { tool: call.tool, command: call.command, cwd: ctx.root, ...(ctx.sessionId !== undefined ? { sessionId: ctx.sessionId } : {}), ...(liveCwd !== undefined ? { execCwd: liveCwd } : {}), posture };
|
|
183
|
+
}
|
|
184
|
+
export function uncoveredCommandSegment(rows, req, ctx, posture) {
|
|
185
|
+
const call = commandOf(req);
|
|
186
|
+
if (call === undefined)
|
|
187
|
+
return undefined;
|
|
188
|
+
const rules = rows.flatMap((r) => (r.behavior === "allow" && r.compiled.kind === "command" ? [r.compiled.rule] : []));
|
|
189
|
+
const coverage = commandCoverageOf(rules, admissionCallOf(call, ctx, ctx.liveCwd(), posture));
|
|
190
|
+
return "uncovered" in coverage ? coverage.uncovered : undefined;
|
|
191
|
+
}
|
|
192
|
+
export function judgeAllowEntries(rows, req, behavior, ctx, posture = "grant") {
|
|
168
193
|
const live = rows.filter((r) => r.behavior === behavior);
|
|
169
194
|
if (live.length === 0)
|
|
170
195
|
return undefined;
|
|
@@ -179,8 +204,8 @@ export function judgeAllowEntries(rows, req, behavior, ctx) {
|
|
|
179
204
|
if (r.compiled.kind === "command")
|
|
180
205
|
byRule.set(r.compiled.rule, r);
|
|
181
206
|
if (behavior === "allow") {
|
|
182
|
-
const
|
|
183
|
-
const hits = (covering
|
|
207
|
+
const coverage = commandCoverageOf([...byRule.keys()], admissionCallOf(call, ctx, liveCwd, posture));
|
|
208
|
+
const hits = ("covering" in coverage ? coverage.covering : []).flatMap((rule) => byRule.get(rule) ?? []);
|
|
184
209
|
if (hits.length > 0)
|
|
185
210
|
return hits;
|
|
186
211
|
}
|
|
@@ -319,7 +344,7 @@ export function readAllowRuleShadows(value) {
|
|
|
319
344
|
return undefined;
|
|
320
345
|
}
|
|
321
346
|
}
|
|
322
|
-
const SHADOW_REASON = {
|
|
347
|
+
export const SHADOW_REASON = Object.freeze({
|
|
323
348
|
content_question: "the call is the engine's own question to a person",
|
|
324
349
|
hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person",
|
|
325
350
|
ancestor: "the task that delegated this one owns this question",
|
|
@@ -328,8 +353,7 @@ const SHADOW_REASON = {
|
|
|
328
353
|
unverifiable_shell_text: "the text this shell will run cannot be read by the engine — it is cleared by confirming this call, never by an allow rule",
|
|
329
354
|
transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule",
|
|
330
355
|
org_rule: "an organization policy rule requires approval for this call",
|
|
331
|
-
|
|
332
|
-
rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown",
|
|
356
|
+
rule_unreadable: "a rule that may govern this call cannot currently be read (this deployment's organization policy, a partition of the person's standing rules, or this call against a deny or ask rule), so whether one forbids it is unknown",
|
|
333
357
|
ask_rule: mandateDisclosureOf(undefined),
|
|
334
358
|
denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides",
|
|
335
359
|
classifier_no_ruling: "the auto-mode classifier could not judge this call, so a person decides",
|
|
@@ -346,7 +370,7 @@ const SHADOW_REASON = {
|
|
|
346
370
|
mode: "the permission mode puts this question to a person",
|
|
347
371
|
unstated: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule",
|
|
348
372
|
ordinary: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule",
|
|
349
|
-
};
|
|
373
|
+
});
|
|
350
374
|
function memberPhrase(s) {
|
|
351
375
|
const listed = `${s.entries.join(", ")}${s.omitted !== undefined ? ` and ${s.omitted} more` : ""}`;
|
|
352
376
|
switch (s.member) {
|
|
@@ -1,88 +1,11 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* value family (the row's stable id; a host-leak hit such as a URL or a path names none). */
|
|
6
|
-
export interface RedactionFinding {
|
|
7
|
-
kind: string;
|
|
8
|
-
span: readonly [number, number];
|
|
9
|
-
confidence: RedactionConfidence;
|
|
10
|
-
marker: string;
|
|
11
|
-
source: RedactionSource;
|
|
12
|
-
family?: string;
|
|
13
|
-
}
|
|
14
|
-
/** The minimum a finding must declare to be summarizable. Both text findings ({@link RedactionFinding})
|
|
15
|
-
* and the env arm's entry findings satisfy it, so {@link summarizeRedactions} is ONE helper rather than
|
|
16
|
-
* one per material kind. */
|
|
17
|
-
export interface SummarizableFinding {
|
|
18
|
-
kind: string;
|
|
19
|
-
confidence: RedactionConfidence;
|
|
20
|
-
}
|
|
21
|
-
/** ANG-3 (envelope declaration) — render findings as ONE human-readable line, e.g.
|
|
22
|
-
* `"3 redactions: prefixed-token (high) x1, url (medium) x1, fs-path (low) x1"`, so an upper layer can
|
|
23
|
-
* declare to its consumer how much was removed and how confidently.
|
|
24
|
-
*
|
|
25
|
-
* PURE, and deliberately only that: it reads the array, returns a string, and injects nothing anywhere.
|
|
26
|
-
* Attaching this line to an output is the caller's decision — a redaction pipeline that silently
|
|
27
|
-
* appended prose to the very text it was asked to bound would break both the size contract
|
|
28
|
-
* (`boundedString`) and the byte-invariance the whole channel is built on.
|
|
29
|
-
*
|
|
30
|
-
* Grouping is by kind AND confidence together: collapsing on kind alone would report whichever
|
|
31
|
-
* confidence happened to arrive first for the rest of that group, which is exactly the silent
|
|
32
|
-
* mis-statement ANG-4 exists to prevent. */
|
|
33
|
-
export declare function summarizeRedactions(findings: readonly SummarizableFinding[]): string;
|
|
34
|
-
/** The optional collector every redaction entry point accepts. `preexistingMarkers` (ANG-11) counts
|
|
35
|
-
* marker-shaped literals ALREADY present in the input before any pass ran — a nonzero value means the
|
|
36
|
-
* text arrived pre-redacted or carries caller-declared markers, either way "marker present" is not proof this
|
|
37
|
-
* pipeline ran. Caller-owned: pass `{ findings: [] }` and read it back after the call. */
|
|
38
|
-
export interface RedactionReport {
|
|
39
|
-
findings: RedactionFinding[];
|
|
40
|
-
preexistingMarkers?: number;
|
|
41
|
-
}
|
|
42
|
-
/** A declared redaction pass — the unit {@link runRedactionPasses} executes and reports on. */
|
|
43
|
-
export interface RedactionPass {
|
|
44
|
-
kind: string;
|
|
45
|
-
confidence: RedactionConfidence;
|
|
46
|
-
marker: string;
|
|
47
|
-
/** The rule table that owns this pass's rule — copied verbatim onto every finding it mints (ANG-11), whoever
|
|
48
|
-
* runs the pass. */
|
|
49
|
-
source: RedactionSource;
|
|
50
|
-
re: RegExp;
|
|
51
|
-
/** A literal replacement (only `$1` substitution is used by these passes) or a replacer fn. */
|
|
52
|
-
replace: string | ((match: string, ...groups: string[]) => string);
|
|
53
|
-
/** The value family a hit belongs to: one id, or — for a pass joining several rows — the id of the row that
|
|
54
|
-
* matches at the hit's offset in the pass input. Absent = the pass is no vocabulary row. */
|
|
55
|
-
family?: string | ((input: string, at: number) => string);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Run the declared passes in order, collecting findings (original-input spans) when a report is given.
|
|
59
|
-
*
|
|
60
|
-
* FORMAT CHARACTERS (#532): the passes scan a VIEW of the input with every `\p{Cf}` removed, so a
|
|
61
|
-
* credential split by a zero-width character is one token to the scanner, not two fragments of which
|
|
62
|
-
* only the first (or neither) matches. The OUTPUT keeps every format character that sits OUTSIDE a
|
|
63
|
-
* replaced span — a joiner in an emoji sequence, a non-joiner in Persian text, a soft hyphen in a word
|
|
64
|
-
* — and drops only those INSIDE the span the marker replaces (they were part of the secret). A
|
|
65
|
-
* format-free input takes the exact historical path (byte-identical output). The view's replacements and
|
|
66
|
-
* every boundary scan's are taken as disjoint spans of their input (a later pass that matched across an
|
|
67
|
-
* earlier pass's inserted text is rendered from the removals instead), so the spans always map back
|
|
68
|
-
* exactly and no scan is skipped.
|
|
69
|
-
*/
|
|
70
|
-
export declare function runRedactionPasses(input: string, passes: readonly RedactionPass[], report?: RedactionReport): string;
|
|
71
|
-
/**
|
|
72
|
-
* The egress view rendered as passes, in the vocabulary's order: one pass per group (a group of several rows is ONE
|
|
73
|
-
* leftmost alternation, its hits attributed to the row that matches at the hit's offset). A row whose wide form
|
|
74
|
-
* captures a label keeps it ahead of the marker; `replacers` renders a kind whose marker placement is a policy (the
|
|
75
|
-
* URL userinfo).
|
|
76
|
-
*/
|
|
77
|
-
export declare function egressPasses(groups: ReadonlyArray<readonly ValueFamilyRow[]>, replacers?: Readonly<Record<string, (match: string, ...groups: string[]) => string>>): RedactionPass[];
|
|
78
|
-
export declare function scrubSecrets(s: string, report?: RedactionReport): string;
|
|
79
|
-
/** A SHORT, SECRET-SCRUBBED primary-arg summary. Abbreviation REDUCES the surface; a final {@link scrubSecrets}
|
|
80
|
-
* pass is the leak boundary (the value is PERSISTED + SSE-forwarded and may be read by a platform operator in a
|
|
81
|
-
* multi-tenant deployment). Rules:
|
|
1
|
+
import { type Egress } from "./untrusted-egress.js";
|
|
2
|
+
/** A SHORT, SECRET-SCRUBBED primary-arg summary. Abbreviation REDUCES the surface; the `same-principal` tier's pass
|
|
3
|
+
* is the leak boundary, on whichever field the summary came from (the value is PERSISTED + SSE-forwarded and may be
|
|
4
|
+
* read by a platform operator in a multi-tenant deployment). Rules:
|
|
82
5
|
* • command → the command NAME (skip leading `VAR=val` env-prefixes; bail to `undefined` on a quoted assignment
|
|
83
6
|
* or a lone assignment; basename a path-form command; `+ …` if it had args).
|
|
84
7
|
* • file path → BASENAME only (no host directory layout).
|
|
85
|
-
* • url → `new URL` origin+pathname (
|
|
86
|
-
* • else → pattern/query/description, else the first
|
|
87
|
-
* Then
|
|
88
|
-
export declare function primaryActivityArg(args: unknown):
|
|
8
|
+
* • url → `new URL` origin+pathname (drops `user:pass@` userinfo + query/fragment).
|
|
9
|
+
* • else → pattern/query/description, else the first string field whose name is no credential name.
|
|
10
|
+
* Then the tier's pass, then a code-point cut. */
|
|
11
|
+
export declare function primaryActivityArg(args: unknown): Egress<"same-principal"> | undefined;
|
package/dist/core/arg-summary.js
CHANGED
|
@@ -1,389 +1,7 @@
|
|
|
1
1
|
import { writtenAssignmentPrefix } from "../tools/fs/bash-program-position.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isSensitiveRecordKey } from "./credential-vocabulary.js";
|
|
3
|
+
import { egressText } from "./untrusted-egress.js";
|
|
3
4
|
const ACTIVITY_ARG_MAX = 80;
|
|
4
|
-
const SENSITIVE_KEY = /token|secret|key|password|passwd|credential|auth/i;
|
|
5
|
-
export function summarizeRedactions(findings) {
|
|
6
|
-
if (findings.length === 0)
|
|
7
|
-
return "no redactions";
|
|
8
|
-
const counts = new Map();
|
|
9
|
-
for (const f of findings) {
|
|
10
|
-
const label = `${f.kind} (${f.confidence})`;
|
|
11
|
-
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
12
|
-
}
|
|
13
|
-
const parts = [...counts].map(([label, n]) => `${label} x${n}`);
|
|
14
|
-
return `${findings.length} redaction${findings.length === 1 ? "" : "s"}: ${parts.join(", ")}`;
|
|
15
|
-
}
|
|
16
|
-
function passZeroMapper(batches) {
|
|
17
|
-
const cursors = batches.map(() => ({ i: 0, delta: 0 }));
|
|
18
|
-
return (pos) => {
|
|
19
|
-
let p = pos;
|
|
20
|
-
for (let b = batches.length - 1; b >= 0; b--) {
|
|
21
|
-
const edits = batches[b];
|
|
22
|
-
const c = cursors[b];
|
|
23
|
-
let mapped;
|
|
24
|
-
while (mapped === undefined && c.i < edits.length) {
|
|
25
|
-
const e = edits[c.i];
|
|
26
|
-
const outStart = e.at + c.delta;
|
|
27
|
-
if (p < outStart)
|
|
28
|
-
mapped = p - c.delta;
|
|
29
|
-
else if (p < outStart + e.insertedLen)
|
|
30
|
-
mapped = e.at;
|
|
31
|
-
else {
|
|
32
|
-
c.delta += e.insertedLen - e.removedLen;
|
|
33
|
-
c.i++;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
p = mapped ?? p - c.delta;
|
|
37
|
-
}
|
|
38
|
-
return p;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
const PREEXISTING_MARKER_RE = /\[redacted(?:-[a-z]+)?\]/g;
|
|
42
|
-
const FORMAT_CHAR_RE = /\p{Cf}/u;
|
|
43
|
-
const MAX_FORMAT_SLOT_SCANS = 64;
|
|
44
|
-
const REDACTED_WHOLE = "[redacted]";
|
|
45
|
-
function finding(pass, span, family) {
|
|
46
|
-
const f = { kind: pass.kind, confidence: pass.confidence, span, marker: pass.marker, source: pass.source };
|
|
47
|
-
if (family !== undefined)
|
|
48
|
-
f.family = family;
|
|
49
|
-
return f;
|
|
50
|
-
}
|
|
51
|
-
function insertedFor(pass, match, groups) {
|
|
52
|
-
return typeof pass.replace === "string"
|
|
53
|
-
? pass.replace.replace(/\$(\d)/g, (_m, d) => groups[Number(d) - 1] ?? "")
|
|
54
|
-
: pass.replace(match, ...groups);
|
|
55
|
-
}
|
|
56
|
-
function removedRegion(pass, match, groups, inserted) {
|
|
57
|
-
if (typeof pass.replace === "string") {
|
|
58
|
-
const parts = pass.replace.split(/(\$\d)/).filter((p) => p !== "");
|
|
59
|
-
const isRef = (p) => /^\$\d$/.test(p);
|
|
60
|
-
const refText = (p) => groups[Number(p[1]) - 1] ?? "";
|
|
61
|
-
let head = "";
|
|
62
|
-
for (const p of parts) {
|
|
63
|
-
if (!isRef(p))
|
|
64
|
-
break;
|
|
65
|
-
head += refText(p);
|
|
66
|
-
}
|
|
67
|
-
let tail = "";
|
|
68
|
-
for (let i = parts.length - 1; i >= 0 && isRef(parts[i]); i--)
|
|
69
|
-
tail = refText(parts[i]) + tail;
|
|
70
|
-
if (parts.every(isRef))
|
|
71
|
-
return [0, match.length];
|
|
72
|
-
if (!match.startsWith(head))
|
|
73
|
-
head = "";
|
|
74
|
-
if (!match.endsWith(tail) || head.length + tail.length > match.length)
|
|
75
|
-
tail = "";
|
|
76
|
-
return [head.length, match.length - tail.length];
|
|
77
|
-
}
|
|
78
|
-
const markerAt = pass.marker === "" ? -1 : inserted.lastIndexOf(pass.marker);
|
|
79
|
-
const headStop = markerAt < 0 ? inserted.length : markerAt;
|
|
80
|
-
const tailStop = markerAt < 0 ? inserted.length : inserted.length - markerAt - pass.marker.length;
|
|
81
|
-
let head = 0;
|
|
82
|
-
while (head < match.length && head < headStop && match[head] === inserted[head])
|
|
83
|
-
head++;
|
|
84
|
-
let tail = 0;
|
|
85
|
-
while (tail < match.length - head && tail < inserted.length - head && tail < tailStop && match[match.length - 1 - tail] === inserted[inserted.length - 1 - tail])
|
|
86
|
-
tail++;
|
|
87
|
-
return [head, match.length - tail];
|
|
88
|
-
}
|
|
89
|
-
const SOLO_SCAN_WORK_PER_CHAR = 8;
|
|
90
|
-
const SOLO_SCAN_WORK_FLOOR = 1 << 17;
|
|
91
|
-
function soloHits(input, passes) {
|
|
92
|
-
const hits = [];
|
|
93
|
-
passes.forEach((pass, i) => {
|
|
94
|
-
const re = new RegExp(pass.re.source, pass.re.flags.includes("g") ? pass.re.flags : `${pass.re.flags}g`);
|
|
95
|
-
const rescans = pass.family !== undefined;
|
|
96
|
-
let work = Math.max(SOLO_SCAN_WORK_PER_CHAR * input.length, SOLO_SCAN_WORK_FLOOR);
|
|
97
|
-
for (let m = re.exec(input); m !== null; m = re.exec(input)) {
|
|
98
|
-
const at = m.index;
|
|
99
|
-
const familyAt = () => (typeof pass.family === "function" ? pass.family(input, at) : pass.family);
|
|
100
|
-
if (rescans) {
|
|
101
|
-
work -= m[0].length;
|
|
102
|
-
if (work < 0) {
|
|
103
|
-
hits.push({ start: at, end: input.length, marker: pass.marker === "" ? REDACTED_WHOLE : pass.marker, pass: i, family: familyAt(), matchStart: at, matchEnd: input.length });
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
re.lastIndex = at + 1;
|
|
107
|
-
}
|
|
108
|
-
else
|
|
109
|
-
re.lastIndex = at + Math.max(m[0].length, 1);
|
|
110
|
-
const inserted = insertedFor(pass, m[0], m.slice(1));
|
|
111
|
-
if (inserted === m[0])
|
|
112
|
-
continue;
|
|
113
|
-
const [a, b] = removedRegion(pass, m[0], m.slice(1), inserted);
|
|
114
|
-
if (b <= a)
|
|
115
|
-
continue;
|
|
116
|
-
const family = familyAt();
|
|
117
|
-
hits.push({ start: at + a, end: at + b, marker: inserted.slice(a, inserted.length - (m[0].length - b)), pass: i, family, matchStart: at, matchEnd: at + m[0].length });
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
return hits;
|
|
121
|
-
}
|
|
122
|
-
function unremovedHits(hits, removals) {
|
|
123
|
-
const gone = [];
|
|
124
|
-
for (const r of [...removals].filter((x) => x.end > x.start).sort((x, y) => x.start - y.start || y.end - x.end)) {
|
|
125
|
-
const last = gone[gone.length - 1];
|
|
126
|
-
if (last !== undefined && r.start <= last[1])
|
|
127
|
-
last[1] = Math.max(last[1], r.end);
|
|
128
|
-
else
|
|
129
|
-
gone.push([r.start, r.end]);
|
|
130
|
-
}
|
|
131
|
-
return hits.filter((h) => {
|
|
132
|
-
let lo = 0;
|
|
133
|
-
let hi = gone.length - 1;
|
|
134
|
-
let at = -1;
|
|
135
|
-
while (lo <= hi) {
|
|
136
|
-
const mid = (lo + hi) >> 1;
|
|
137
|
-
if (gone[mid][0] <= h.start) {
|
|
138
|
-
at = mid;
|
|
139
|
-
lo = mid + 1;
|
|
140
|
-
}
|
|
141
|
-
else
|
|
142
|
-
hi = mid - 1;
|
|
143
|
-
}
|
|
144
|
-
return at === -1 || gone[at][1] < h.end;
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
function sequentialRemovals(batches) {
|
|
148
|
-
const removals = [];
|
|
149
|
-
for (let j = 0; j < batches.length; j++) {
|
|
150
|
-
const toPassZero = passZeroMapper(batches.slice(0, j));
|
|
151
|
-
for (const e of batches[j]) {
|
|
152
|
-
const start = toPassZero(e.at + e.head);
|
|
153
|
-
const end = toPassZero(e.at + e.removedLen - e.tail);
|
|
154
|
-
removals.push({ start, end: Math.max(start, end), marker: e.inserted.slice(e.head, e.insertedLen - e.tail), pass: e.pass, family: e.family });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return removals;
|
|
158
|
-
}
|
|
159
|
-
function renderRemovals(input, removals) {
|
|
160
|
-
const sorted = removals.filter((r) => r.end > r.start || r.marker !== "").sort((x, y) => x.start - y.start || y.end - x.end);
|
|
161
|
-
const edits = [];
|
|
162
|
-
let out = "";
|
|
163
|
-
let cursor = 0;
|
|
164
|
-
for (let i = 0; i < sorted.length;) {
|
|
165
|
-
const start = sorted[i].start;
|
|
166
|
-
let end = sorted[i].end;
|
|
167
|
-
let widest;
|
|
168
|
-
for (; i < sorted.length && (sorted[i].start < end || sorted[i].start === start || (sorted[i].marker === "" && sorted[i].start === end)); i++) {
|
|
169
|
-
const r = sorted[i];
|
|
170
|
-
if (r.end > end)
|
|
171
|
-
end = r.end;
|
|
172
|
-
if (r.marker !== "" && (widest === undefined || r.end - r.start > widest.end - widest.start))
|
|
173
|
-
widest = r;
|
|
174
|
-
}
|
|
175
|
-
const marker = widest?.marker ?? REDACTED_WHOLE;
|
|
176
|
-
out += input.slice(cursor, start) + marker;
|
|
177
|
-
cursor = end;
|
|
178
|
-
edits.push({ at: start, removedLen: end - start, insertedLen: marker.length, inserted: marker, family: widest?.family, head: 0, tail: 0, pass: widest?.pass ?? sorted[i - 1].pass });
|
|
179
|
-
}
|
|
180
|
-
return { out: out + input.slice(cursor), edits };
|
|
181
|
-
}
|
|
182
|
-
function runPasses(input, passes, findings, toOriginal) {
|
|
183
|
-
const batches = [];
|
|
184
|
-
let cur = input;
|
|
185
|
-
passes.forEach((pass, passIndex) => {
|
|
186
|
-
const edits = [];
|
|
187
|
-
const passInput = cur;
|
|
188
|
-
const toPassZero = passZeroMapper(batches.slice());
|
|
189
|
-
cur = cur.replace(pass.re, (...args) => {
|
|
190
|
-
const match = args[0];
|
|
191
|
-
const offset = args[args.length - 2];
|
|
192
|
-
const groups = args.slice(1, -2);
|
|
193
|
-
const inserted = insertedFor(pass, match, groups);
|
|
194
|
-
if (inserted === match)
|
|
195
|
-
return inserted;
|
|
196
|
-
const family = typeof pass.family === "function" ? pass.family(passInput, offset) : pass.family;
|
|
197
|
-
if (findings !== undefined)
|
|
198
|
-
findings.push({ pass: passIndex, f: finding(pass, [toOriginal(toPassZero(offset)), toOriginal(toPassZero(offset + match.length))], family) });
|
|
199
|
-
const [a, b] = removedRegion(pass, match, groups, inserted);
|
|
200
|
-
edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length, inserted, family, head: a, tail: match.length - b, pass: passIndex });
|
|
201
|
-
return inserted;
|
|
202
|
-
});
|
|
203
|
-
batches.push(edits);
|
|
204
|
-
});
|
|
205
|
-
return { out: cur, batches };
|
|
206
|
-
}
|
|
207
|
-
function applyPasses(input, passes, report, toOriginal, disjoint = false) {
|
|
208
|
-
const found = report === undefined ? undefined : [];
|
|
209
|
-
const plain = runPasses(input, passes, found, toOriginal);
|
|
210
|
-
const solo = soloHits(input, passes);
|
|
211
|
-
const sequential = solo.length === 0 ? [] : sequentialRemovals(plain.batches);
|
|
212
|
-
const outer = unremovedHits(solo, sequential);
|
|
213
|
-
const mappable = !disjoint || replacedSpans(plain.batches) !== undefined;
|
|
214
|
-
const whole = outer.length === 0 && mappable ? undefined : renderRemovals(input, [...sequential, ...outer]);
|
|
215
|
-
if (whole === undefined || (whole.out === plain.out && mappable)) {
|
|
216
|
-
if (report !== undefined)
|
|
217
|
-
report.findings.push(...found.map((x) => x.f));
|
|
218
|
-
return plain;
|
|
219
|
-
}
|
|
220
|
-
if (report !== undefined) {
|
|
221
|
-
const seen = new Set(found.map((x) => `${x.pass}:${x.f.span[0]}:${x.f.span[1]}`));
|
|
222
|
-
for (const h of outer) {
|
|
223
|
-
const span = [toOriginal(h.matchStart), toOriginal(h.matchEnd)];
|
|
224
|
-
const key = `${h.pass}:${span[0]}:${span[1]}`;
|
|
225
|
-
if (seen.has(key))
|
|
226
|
-
continue;
|
|
227
|
-
seen.add(key);
|
|
228
|
-
found.push({ pass: h.pass, f: finding(passes[h.pass], span, h.family) });
|
|
229
|
-
}
|
|
230
|
-
found.sort((x, y) => x.pass - y.pass || x.f.span[0] - y.f.span[0]);
|
|
231
|
-
report.findings.push(...found.map((x) => x.f));
|
|
232
|
-
}
|
|
233
|
-
return { out: whole.out, batches: [whole.edits] };
|
|
234
|
-
}
|
|
235
|
-
function replacedSpans(batches) {
|
|
236
|
-
const spans = [];
|
|
237
|
-
for (let j = 0; j < batches.length; j++) {
|
|
238
|
-
const toPassZero = passZeroMapper(batches.slice(0, j));
|
|
239
|
-
for (const e of batches[j]) {
|
|
240
|
-
const s0 = toPassZero(e.at);
|
|
241
|
-
const e0 = toPassZero(e.at + e.removedLen);
|
|
242
|
-
if (e0 - s0 !== e.removedLen)
|
|
243
|
-
return undefined;
|
|
244
|
-
spans.push({ start: s0, end: e0, inserted: e.inserted, pass: e.pass, family: e.family });
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
spans.sort((a, b) => a.start - b.start);
|
|
248
|
-
for (let i = 1; i < spans.length; i++)
|
|
249
|
-
if (spans[i].start < spans[i - 1].end)
|
|
250
|
-
return undefined;
|
|
251
|
-
return spans;
|
|
252
|
-
}
|
|
253
|
-
export function runRedactionPasses(input, passes, report) {
|
|
254
|
-
if (report !== undefined && report.preexistingMarkers === undefined) {
|
|
255
|
-
report.preexistingMarkers = input.match(PREEXISTING_MARKER_RE)?.length ?? 0;
|
|
256
|
-
}
|
|
257
|
-
if (!FORMAT_CHAR_RE.test(input))
|
|
258
|
-
return applyPasses(input, passes, report, (pos) => pos).out;
|
|
259
|
-
const viewChars = [];
|
|
260
|
-
const viewToOriginal = [];
|
|
261
|
-
const formatAtSlot = [];
|
|
262
|
-
const charStartOffsets = [];
|
|
263
|
-
{
|
|
264
|
-
let orig = 0;
|
|
265
|
-
let viewOff = 0;
|
|
266
|
-
let slotBuf = "";
|
|
267
|
-
for (const ch of input) {
|
|
268
|
-
if (FORMAT_CHAR_RE.test(ch)) {
|
|
269
|
-
slotBuf += ch;
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
formatAtSlot.push(slotBuf);
|
|
273
|
-
slotBuf = "";
|
|
274
|
-
viewToOriginal.push(orig);
|
|
275
|
-
charStartOffsets.push(viewOff);
|
|
276
|
-
viewChars.push(ch);
|
|
277
|
-
viewOff += ch.length;
|
|
278
|
-
}
|
|
279
|
-
orig += ch.length;
|
|
280
|
-
}
|
|
281
|
-
formatAtSlot.push(slotBuf);
|
|
282
|
-
viewToOriginal.push(input.length);
|
|
283
|
-
charStartOffsets.push(viewOff);
|
|
284
|
-
}
|
|
285
|
-
const view = viewChars.join("");
|
|
286
|
-
const offsetToCharIndex = new Map();
|
|
287
|
-
charStartOffsets.forEach((off, idx) => offsetToCharIndex.set(off, idx));
|
|
288
|
-
const toOriginal = (pos) => {
|
|
289
|
-
const idx = offsetToCharIndex.get(pos);
|
|
290
|
-
return idx === undefined ? input.length : viewToOriginal[idx];
|
|
291
|
-
};
|
|
292
|
-
const { batches } = applyPasses(view, passes, report, toOriginal, true);
|
|
293
|
-
const viewSpans = replacedSpans(batches);
|
|
294
|
-
if (viewSpans === undefined)
|
|
295
|
-
return REDACTED_WHOLE;
|
|
296
|
-
const spans = viewSpans.map((sp) => {
|
|
297
|
-
const startIdx = offsetToCharIndex.get(sp.start) ?? viewChars.length;
|
|
298
|
-
const endIdx = offsetToCharIndex.get(sp.end) ?? viewChars.length;
|
|
299
|
-
const lastIdx = endIdx - 1;
|
|
300
|
-
const endOrig = lastIdx >= startIdx && lastIdx < viewChars.length ? viewToOriginal[lastIdx] + viewChars[lastIdx].length : viewToOriginal[startIdx];
|
|
301
|
-
return { start: viewToOriginal[startIdx], end: endOrig, inserted: sp.inserted };
|
|
302
|
-
});
|
|
303
|
-
const viewOffsetToOriginal = (viewStart, viewEnd) => {
|
|
304
|
-
const startIdx = offsetToCharIndex.get(viewStart) ?? viewChars.length;
|
|
305
|
-
const endIdx = offsetToCharIndex.get(viewEnd) ?? viewChars.length;
|
|
306
|
-
const lastIdx = endIdx - 1;
|
|
307
|
-
const endOrig = lastIdx >= startIdx && lastIdx < viewChars.length ? viewToOriginal[lastIdx] + viewChars[lastIdx].length : viewToOriginal[startIdx];
|
|
308
|
-
return { start: viewToOriginal[startIdx], end: endOrig };
|
|
309
|
-
};
|
|
310
|
-
const cutOffsets = [];
|
|
311
|
-
for (let k = 1; k < viewChars.length; k++)
|
|
312
|
-
if (formatAtSlot[k] !== "")
|
|
313
|
-
cutOffsets.push(charStartOffsets[k]);
|
|
314
|
-
if (cutOffsets.length > MAX_FORMAT_SLOT_SCANS)
|
|
315
|
-
return REDACTED_WHOLE;
|
|
316
|
-
const extra = [];
|
|
317
|
-
const rawSpans = replacedSpans(applyPasses(input, passes, undefined, (pos) => pos, true).batches);
|
|
318
|
-
if (rawSpans !== undefined)
|
|
319
|
-
extra.push(...rawSpans);
|
|
320
|
-
for (const cut of cutOffsets) {
|
|
321
|
-
const cutSpans = replacedSpans(applyPasses(view.slice(cut), passes, undefined, (pos) => pos, true).batches);
|
|
322
|
-
if (cutSpans === undefined)
|
|
323
|
-
continue;
|
|
324
|
-
for (const c of cutSpans) {
|
|
325
|
-
const o = viewOffsetToOriginal(c.start + cut, c.end + cut);
|
|
326
|
-
extra.push({ ...c, start: o.start, end: o.end });
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
extra.sort((a, b) => a.start - b.start);
|
|
330
|
-
const firstEndingAfter = (pos) => {
|
|
331
|
-
let lo = 0;
|
|
332
|
-
let hi = spans.length;
|
|
333
|
-
while (lo < hi) {
|
|
334
|
-
const mid = (lo + hi) >> 1;
|
|
335
|
-
if (spans[mid].end > pos)
|
|
336
|
-
hi = mid;
|
|
337
|
-
else
|
|
338
|
-
lo = mid + 1;
|
|
339
|
-
}
|
|
340
|
-
return lo;
|
|
341
|
-
};
|
|
342
|
-
for (const r of extra) {
|
|
343
|
-
const at = firstEndingAfter(r.start);
|
|
344
|
-
const hit = spans[at];
|
|
345
|
-
if (hit !== undefined && hit.start < r.end) {
|
|
346
|
-
if (r.start < hit.start)
|
|
347
|
-
hit.start = r.start;
|
|
348
|
-
if (r.end > hit.end)
|
|
349
|
-
hit.end = r.end;
|
|
350
|
-
let next = at + 1;
|
|
351
|
-
while (next < spans.length && spans[next].start < hit.end)
|
|
352
|
-
hit.end = Math.max(hit.end, spans[next++].end);
|
|
353
|
-
if (next > at + 1)
|
|
354
|
-
spans.splice(at + 1, next - at - 1);
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
spans.splice(at, 0, { start: r.start, end: r.end, inserted: r.inserted });
|
|
358
|
-
if (report !== undefined)
|
|
359
|
-
report.findings.push(finding(passes[r.pass], [r.start, r.end], r.family));
|
|
360
|
-
}
|
|
361
|
-
let result = "";
|
|
362
|
-
let cursor = 0;
|
|
363
|
-
for (const sp of spans) {
|
|
364
|
-
result += input.slice(cursor, sp.start) + sp.inserted;
|
|
365
|
-
cursor = sp.end;
|
|
366
|
-
}
|
|
367
|
-
result += input.slice(cursor);
|
|
368
|
-
return result;
|
|
369
|
-
}
|
|
370
|
-
const BRACKET_MARKER = { "url-credentials": "[redacted-credentials]" };
|
|
371
|
-
const bracketMarker = (kind) => BRACKET_MARKER[kind] ?? "[redacted]";
|
|
372
|
-
const capturesLabel = (re) => new RegExp(`${re.source}|`).exec("").length > 1;
|
|
373
|
-
export function egressPasses(groups, replacers = {}) {
|
|
374
|
-
return groups.map((rows) => {
|
|
375
|
-
const head = rows[0];
|
|
376
|
-
const marker = bracketMarker(head.kind);
|
|
377
|
-
const re = rows.length === 1 ? head.match : new RegExp(rows.map((r) => `(?:${r.match.source})`).join("|"), head.match.flags);
|
|
378
|
-
const sticky = rows.map((r) => ({ id: r.id, re: new RegExp(r.match.source, r.match.flags.replace("g", "") + "y") }));
|
|
379
|
-
const family = rows.length === 1 ? head.id : (input, at) => sticky.find((s) => ((s.re.lastIndex = at), s.re.test(input)))?.id ?? head.id;
|
|
380
|
-
return { kind: head.kind, confidence: head.confidence, marker, source: head.source, re, replace: replacers[head.kind] ?? (capturesLabel(head.match) ? `$1${marker}` : marker), family };
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
const SHAPE_PASSES = egressPasses(egressPassGroups(CREDENTIAL_VOCABULARY).filter((rows) => rows[0].source === "arg-summary"));
|
|
384
|
-
export function scrubSecrets(s, report) {
|
|
385
|
-
return runRedactionPasses(s, SHAPE_PASSES, report);
|
|
386
|
-
}
|
|
387
5
|
function truncCodePoints(s) {
|
|
388
6
|
const cp = Array.from(s);
|
|
389
7
|
return cp.length > ACTIVITY_ARG_MAX ? cp.slice(0, ACTIVITY_ARG_MAX).join("") + "…" : s;
|
|
@@ -437,7 +55,7 @@ export function primaryActivityArg(args) {
|
|
|
437
55
|
val = str("pattern") ?? str("query") ?? str("description");
|
|
438
56
|
if (val === undefined) {
|
|
439
57
|
for (const k of Object.keys(a)) {
|
|
440
|
-
if (
|
|
58
|
+
if (isSensitiveRecordKey(k))
|
|
441
59
|
continue;
|
|
442
60
|
const v = a[k];
|
|
443
61
|
if (typeof v === "string" && v.length > 0) {
|
|
@@ -449,5 +67,5 @@ export function primaryActivityArg(args) {
|
|
|
449
67
|
}
|
|
450
68
|
if (val === undefined)
|
|
451
69
|
return undefined;
|
|
452
|
-
return truncCodePoints(
|
|
70
|
+
return truncCodePoints(egressText("same-principal", val));
|
|
453
71
|
}
|