@tuent/sentinel 0.1.0 → 0.1.2
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/README.md +26 -26
- package/SECURITY_MODEL.md +281 -0
- package/dist/Sentinel-XMSJE4DZ.js +10 -0
- package/dist/{Sentinel-B_sv8Kiy.d.ts → Sentinel-xFCyXH45.d.ts} +31 -1
- package/dist/chunk-B5QKJHSV.js +32 -0
- package/dist/{chunk-3U3PKD4N.js → chunk-FWIISAZZ.js} +119 -8
- package/dist/{chunk-QFRDEISP.js → chunk-GRN5P3H2.js} +67 -23
- package/dist/{chunk-Z3PWIJKT.js → chunk-L4R3LPJS.js} +237 -443
- package/dist/{chunk-CUJKNIKT.js → chunk-LATQNIRW.js} +33 -1
- package/dist/{chunk-6MHWJATS.js → chunk-QIYQWOLO.js} +589 -19
- package/dist/{chunk-2FFMYSVC.js → chunk-WLIDSTS4.js} +18 -2
- package/dist/cli.js +30 -30
- package/dist/gateway/index.d.ts +37 -1
- package/dist/gateway/index.js +4 -3
- package/dist/gatewayDaemon.js +4 -3
- package/dist/index.d.ts +11 -2
- package/dist/index.js +5 -5
- package/dist/pidManager-DOGVN6ZT.js +23 -0
- package/dist/{policyLoader-6KR5VFVV.js → policyLoader-KZL2U4M2.js} +2 -2
- package/package.json +3 -2
- package/dist/Sentinel-JLQL3YRD.js +0 -10
- package/dist/pidManager-ZYC7SICM.js +0 -15
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
acquireGatewayLock,
|
|
3
3
|
writePidFile
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-LATQNIRW.js";
|
|
5
5
|
import {
|
|
6
6
|
discoverPolicy
|
|
7
7
|
} from "./chunk-FMZWHT4M.js";
|
|
8
8
|
import {
|
|
9
|
+
FORBIDDEN_BASENAMES
|
|
10
|
+
} from "./chunk-QIYQWOLO.js";
|
|
11
|
+
import {
|
|
12
|
+
loadPolicy,
|
|
9
13
|
loadPolicyFromString
|
|
10
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-WLIDSTS4.js";
|
|
11
15
|
|
|
12
16
|
// src/setup/initClaudeCode.ts
|
|
13
17
|
import { access, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
@@ -17,12 +21,16 @@ import { createServer } from "http";
|
|
|
17
21
|
import { fileURLToPath } from "url";
|
|
18
22
|
|
|
19
23
|
// src/gateway/hookScriptSource.ts
|
|
24
|
+
var SCORER_CRITICAL_EXTRAS = ["shadow", "passwd"];
|
|
25
|
+
var HOOK_SENSITIVE_BASENAMES = [
|
|
26
|
+
.../* @__PURE__ */ new Set([...FORBIDDEN_BASENAMES, ...SCORER_CRITICAL_EXTRAS])
|
|
27
|
+
];
|
|
20
28
|
var HOOK_SCRIPT_SOURCE = `#!/usr/bin/env node
|
|
21
29
|
// Sentinel cc hook bridge \u2014 generated by sentinel init claude-code
|
|
22
30
|
// Do not edit manually; regenerate with: sentinel init claude-code --force
|
|
23
31
|
|
|
24
32
|
import { readFileSync, appendFileSync, existsSync } from "node:fs";
|
|
25
|
-
import { join } from "node:path";
|
|
33
|
+
import { join, resolve, sep } from "node:path";
|
|
26
34
|
import { spawn } from "node:child_process";
|
|
27
35
|
|
|
28
36
|
const GATEWAY_ENTRY_POINT = "__GATEWAY_ENTRY_POINT__";
|
|
@@ -74,6 +82,17 @@ function logFallback(entry) {
|
|
|
74
82
|
try { appendFileSync(FALLBACK_LOG, line + "\\n"); } catch { /* best effort */ }
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
// Sprint 26 Fix-2 Part 1 \u2014 hardcoded fail-closed FLOOR. tiers.json may ADD
|
|
86
|
+
// high-tier tools but can never DOWNGRADE a floor tool to low (defeats the
|
|
87
|
+
// two-step tier-config rewrite). Checked BEFORE the editable tiers below.
|
|
88
|
+
const FLOOR_HIGH = ["Bash", "Write", "Edit", "WebFetch", "NotebookEdit", "Task", "Skill"];
|
|
89
|
+
|
|
90
|
+
// Item D \u2014 operator allowUnknownTools escape hatch, baked at init from the
|
|
91
|
+
// launch policy so it survives gateway-down (the daemon allows these names
|
|
92
|
+
// unconditionally at the name level, so allowing them here keeps down \u2287 up).
|
|
93
|
+
// The marker default is [] \u2014 an un-substituted script stays strictest.
|
|
94
|
+
const ALLOW_UNKNOWN_TOOLS = /* __ALLOW_UNKNOWN_TOOLS__ */ [];
|
|
95
|
+
|
|
77
96
|
// Tier config uses flat format: { high, low, mcpDefault, unknownDefault } (plan v3.1 spec'd nested but simplified during 5a)
|
|
78
97
|
function loadTiers() {
|
|
79
98
|
try {
|
|
@@ -90,10 +109,52 @@ function loadTiers() {
|
|
|
90
109
|
}
|
|
91
110
|
|
|
92
111
|
function isHighSensitivity(toolName, tiers) {
|
|
112
|
+
if (FLOOR_HIGH.includes(toolName)) return true; // floor: tiers may add high, never downgrade
|
|
93
113
|
if (tiers.high && tiers.high.includes(toolName)) return true;
|
|
94
114
|
if (tiers.low && tiers.low.includes(toolName)) return false;
|
|
95
115
|
if (toolName.startsWith("mcp__")) return tiers.mcpDefault === "high";
|
|
96
|
-
|
|
116
|
+
// Item D \u2014 operator-allowlisted names pass: gateway-up they are translated
|
|
117
|
+
// as known and allowed at the name level, so the escape hatch must survive
|
|
118
|
+
// gateway-down too.
|
|
119
|
+
if (ALLOW_UNKNOWN_TOOLS.includes(toolName)) return false;
|
|
120
|
+
// Item D floor: all other unknown names are always high-tier gateway-down.
|
|
121
|
+
// Deliberately stricter than gateway-up's warn default: the hook cannot
|
|
122
|
+
// persist findings while the daemon is down, so a warn-equivalent here
|
|
123
|
+
// would be allow-UNLOGGED \u2014 the exact F-8 hole. Hard-deny keeps down \u2287 up
|
|
124
|
+
// in both knob modes (same fail-closed stance as Fix-2's read handling),
|
|
125
|
+
// and a tiers.json edit (unknownDefault: "low") cannot reopen it. Named
|
|
126
|
+
// tools can still be tiered low explicitly via tiers.low above.
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Sprint 26 Fix-2 Part 2 \u2014 gateway-down safe-read exception.
|
|
131
|
+
// INVARIANT: on gateway-down a read is NEVER more permissive than gateway-up.
|
|
132
|
+
// Low-tier file reads (Read/Glob/Grep) fail CLOSED by default; only a
|
|
133
|
+
// structurally-provably-safe target is allowed. The guard is GENERATED from the
|
|
134
|
+
// canonical FORBIDDEN_BASENAMES (\u222A scorer extras), so it can't drift open.
|
|
135
|
+
const SENSITIVE_BASENAMES = ${JSON.stringify(HOOK_SENSITIVE_BASENAMES)};
|
|
136
|
+
const READ_TOOL_PATH_KEYS = { Read: "file_path", Glob: "pattern", Grep: "path" };
|
|
137
|
+
|
|
138
|
+
function isProvablySafeRead(toolName, toolInput, payloadCwd) {
|
|
139
|
+
const key = READ_TOOL_PATH_KEYS[toolName];
|
|
140
|
+
if (!key) return false; // not a path-bearing read tool \u2014 not provably safe
|
|
141
|
+
const raw = toolInput && toolInput[key];
|
|
142
|
+
if (typeof raw !== "string" || raw.length === 0) return false; // no target \u2192 not provable
|
|
143
|
+
if (raw[0] === "/" || raw[0] === "~") return false; // absolute / home \u2192 fail closed
|
|
144
|
+
const cwd = typeof payloadCwd === "string" && payloadCwd ? payloadCwd : process.cwd();
|
|
145
|
+
const cwdNorm = resolve(cwd);
|
|
146
|
+
const norm = resolve(cwd, raw);
|
|
147
|
+
// Must resolve strictly within cwd (rejects any .. escape).
|
|
148
|
+
if (norm !== cwdNorm && !norm.startsWith(cwdNorm + sep)) return false;
|
|
149
|
+
const segments = norm.slice(cwdNorm.length).split(sep).filter(Boolean);
|
|
150
|
+
for (const seg of segments) {
|
|
151
|
+
const low = seg.toLowerCase();
|
|
152
|
+
if (low[0] === ".") return false; // dotfile / dot-dir
|
|
153
|
+
for (const s of SENSITIVE_BASENAMES) {
|
|
154
|
+
if (low.includes(s.toLowerCase())) return false; // conservative sensitive-token match
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
97
158
|
}
|
|
98
159
|
|
|
99
160
|
// ---------------------------------------------------------------------------
|
|
@@ -111,15 +172,29 @@ if (mode === "pre") {
|
|
|
111
172
|
const result = JSON.parse(resp.body);
|
|
112
173
|
process.stdout.write(JSON.stringify(result), () => process.exit(0));
|
|
113
174
|
} catch {
|
|
114
|
-
// Gateway unreachable \u2014 tiered fail-closed
|
|
175
|
+
// Gateway unreachable \u2014 tiered fail-closed (Sprint 26 Fix-2).
|
|
115
176
|
const tiers = loadTiers();
|
|
177
|
+
const restore = "Restart your Claude Code session to relaunch the gateway; see ~/.dahlia/gateway-fallback.log.";
|
|
116
178
|
if (isHighSensitivity(toolName, tiers)) {
|
|
117
179
|
logFallback({ event: "fail-closed-block", tool: toolName, tier: "high" });
|
|
118
180
|
process.stderr.write(
|
|
119
|
-
\`Sentinel gateway unreachable; high-sensitivity tool "\${toolName}" blocked per fail-closed policy\`,
|
|
181
|
+
\`Sentinel gateway unreachable; high-sensitivity tool "\${toolName}" blocked per fail-closed policy. \${restore}\`,
|
|
182
|
+
() => process.exit(2),
|
|
183
|
+
);
|
|
184
|
+
} else if (
|
|
185
|
+
READ_TOOL_PATH_KEYS[toolName] &&
|
|
186
|
+
!isProvablySafeRead(toolName, payload.tool_input || {}, payload.cwd)
|
|
187
|
+
) {
|
|
188
|
+
// Low-tier read whose target is not provably safe \u2192 fail CLOSED. The hook
|
|
189
|
+
// carries no policy, so it cannot evaluate the forbid surface; a read it
|
|
190
|
+
// can't prove safe is treated as if the gateway would deny it.
|
|
191
|
+
logFallback({ event: "fail-closed-block", tool: toolName, tier: "low", reason: "read-not-provably-safe" });
|
|
192
|
+
process.stderr.write(
|
|
193
|
+
\`Sentinel gateway unreachable; read by "\${toolName}" blocked (fail-closed: target not provably safe). \${restore}\`,
|
|
120
194
|
() => process.exit(2),
|
|
121
195
|
);
|
|
122
196
|
} else {
|
|
197
|
+
// Provably-safe read, or a non-read low-tier tool (e.g. WebSearch) \u2192 allow.
|
|
123
198
|
logFallback({ event: "fail-closed-allow", tool: toolName, tier: "low" });
|
|
124
199
|
process.stdout.write(JSON.stringify({
|
|
125
200
|
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" },
|
|
@@ -322,6 +397,11 @@ agent:
|
|
|
322
397
|
policy:
|
|
323
398
|
allow:
|
|
324
399
|
actions: [file_read, file_write, tool_invocation, network_request, command_exec]
|
|
400
|
+
# allow.targets is ADVISORY for file_read / file_write / tool_invocation: an
|
|
401
|
+
# access outside this list is logged as a MEDIUM scope_violation but still
|
|
402
|
+
# runs \u2014 Sentinel governs the agent's own tool calls, it is not a filesystem
|
|
403
|
+
# sandbox. forbid.targets below is the hard deny. network_request is the
|
|
404
|
+
# exception: unlisted hosts are DENIED by default (see the networkHosts note).
|
|
325
405
|
targets:
|
|
326
406
|
- "src/**"
|
|
327
407
|
- "test/**"
|
|
@@ -378,6 +458,28 @@ policy:
|
|
|
378
458
|
- "**/*.pem"
|
|
379
459
|
- "**/*.key"
|
|
380
460
|
- "/etc/**"
|
|
461
|
+
# Sprint 26 FIX 1 (A) \u2014 common credential stores. DRIFT: keep in sync with
|
|
462
|
+
# DEFAULT_FORBIDDEN_PATTERNS in defaults.ts (unification tracked separately).
|
|
463
|
+
- "**/.netrc"
|
|
464
|
+
- "**/.npmrc"
|
|
465
|
+
- "**/.git-credentials"
|
|
466
|
+
- "**/.pgpass"
|
|
467
|
+
- "**/.zsh_history"
|
|
468
|
+
- "**/.config/gh/**"
|
|
469
|
+
- "**/.docker/config.json"
|
|
470
|
+
- "**/.gnupg/**"
|
|
471
|
+
- "**/.config/gcloud/**"
|
|
472
|
+
- "**/.kube/**"
|
|
473
|
+
- "**/Library/Keychains/**"
|
|
474
|
+
# Sprint 26 FIX 1 (B) \u2014 Sentinel's own state dir (current path only).
|
|
475
|
+
- "**/.dahlia/**"
|
|
476
|
+
# Sprint 26 FIX 3 \u2014 this policy file and cc's hook-wiring settings files.
|
|
477
|
+
# Agent tool-writes are denied; reads stay allowed via a code-side ceiling
|
|
478
|
+
# exception (not authorable here \u2014 workspace-authored exceptions are
|
|
479
|
+
# dropped by the ceiling merge, by design). DRIFT: keep in sync with
|
|
480
|
+
# DEFAULT_FORBIDDEN_PATTERNS in defaults.ts.
|
|
481
|
+
- "**/.sentinel.yaml"
|
|
482
|
+
- "**/.claude/settings*.json"
|
|
381
483
|
enforcement:
|
|
382
484
|
restrictAfter: 3
|
|
383
485
|
quarantineAfter: 5
|
|
@@ -418,7 +520,16 @@ async function runInitClaudeCode(options) {
|
|
|
418
520
|
);
|
|
419
521
|
const hookPath = join(dahliaDir, "cc-hook.mjs");
|
|
420
522
|
const gatewayEntryPoint = resolveGatewayEntryPoint();
|
|
421
|
-
|
|
523
|
+
let allowUnknownTools = [];
|
|
524
|
+
try {
|
|
525
|
+
const effectivePolicy = await loadPolicy(policyPath);
|
|
526
|
+
allowUnknownTools = effectivePolicy.enforcement?.allowUnknownTools ?? [];
|
|
527
|
+
} catch {
|
|
528
|
+
}
|
|
529
|
+
const hookContent = HOOK_SCRIPT_SOURCE.replace(
|
|
530
|
+
/__GATEWAY_ENTRY_POINT__/g,
|
|
531
|
+
gatewayEntryPoint
|
|
532
|
+
).replace("/* __ALLOW_UNKNOWN_TOOLS__ */ []", JSON.stringify(allowUnknownTools));
|
|
422
533
|
if (hookContent.includes("__GATEWAY_ENTRY_POINT__")) {
|
|
423
534
|
throw new Error("Failed to substitute all __GATEWAY_ENTRY_POINT__ placeholders");
|
|
424
535
|
}
|
|
@@ -536,4 +647,4 @@ export {
|
|
|
536
647
|
runInitClaudeCode,
|
|
537
648
|
runSessionStart
|
|
538
649
|
};
|
|
539
|
-
//# sourceMappingURL=chunk-
|
|
650
|
+
//# sourceMappingURL=chunk-FWIISAZZ.js.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
DEFAULT_FORBIDDEN_PATTERNS,
|
|
3
2
|
DEFAULT_MAP_PATH,
|
|
4
3
|
DEFAULT_MEDIUM_DISPOSITION,
|
|
5
4
|
DEFAULT_OVERLAY_PATH,
|
|
@@ -16,13 +15,15 @@ import {
|
|
|
16
15
|
removeOverlayDecision,
|
|
17
16
|
saveMap,
|
|
18
17
|
saveOverlay,
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
unionWithDefaultForbiddenPatterns,
|
|
19
|
+
walkForbiddenInodeRoots,
|
|
20
|
+
withPolicyReadExceptions
|
|
21
|
+
} from "./chunk-QIYQWOLO.js";
|
|
21
22
|
import {
|
|
22
23
|
loadPolicy,
|
|
23
24
|
policyToConfig,
|
|
24
25
|
policyToRole
|
|
25
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-WLIDSTS4.js";
|
|
26
27
|
import {
|
|
27
28
|
publicKeyToBase64,
|
|
28
29
|
reconstructSigningPayload,
|
|
@@ -742,6 +743,12 @@ var AuditTrail = class _AuditTrail {
|
|
|
742
743
|
if (finding.softSignal === true) {
|
|
743
744
|
entry.softSignal = true;
|
|
744
745
|
}
|
|
746
|
+
if (finding.mentionOnly === true) {
|
|
747
|
+
entry.mentionOnly = true;
|
|
748
|
+
}
|
|
749
|
+
if (finding.dedupKey !== void 0) {
|
|
750
|
+
entry.dedupKey = finding.dedupKey;
|
|
751
|
+
}
|
|
745
752
|
await this.writeLine(entry);
|
|
746
753
|
}
|
|
747
754
|
/**
|
|
@@ -6438,12 +6445,16 @@ var Sentinel = class _Sentinel {
|
|
|
6438
6445
|
"tool_invocation"
|
|
6439
6446
|
],
|
|
6440
6447
|
allowedTargetPatterns: options.allowedTargetPatterns ?? ["**"],
|
|
6441
|
-
// Forbidden-pattern normalization chokepoint (Part A)
|
|
6442
|
-
//
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
),
|
|
6446
|
-
|
|
6448
|
+
// Forbidden-pattern normalization chokepoint (Part A) + Sprint 26 FIX 3B:
|
|
6449
|
+
// the defaults are a floor unioned into every role, not a fallback — a
|
|
6450
|
+
// policy yaml extends but cannot remove them (the `??` form let any
|
|
6451
|
+
// policy-supplied list supersede the defaults entirely).
|
|
6452
|
+
forbiddenTargetPatterns: unionWithDefaultForbiddenPatterns(options.forbiddenTargetPatterns),
|
|
6453
|
+
// Sprint 26 FIX 3 — ceiling-side read carve-out for the self-protection
|
|
6454
|
+
// forbids (policy + hook-wiring files). Appended here, not in the yaml
|
|
6455
|
+
// template: mergeRoles drops workspace exceptions, so this is the only
|
|
6456
|
+
// authorable layer — which is the self-locking property.
|
|
6457
|
+
exceptions: withPolicyReadExceptions(options.exceptions),
|
|
6447
6458
|
networkHosts: options.networkHosts,
|
|
6448
6459
|
expectedSchedule: options.expectedSchedule,
|
|
6449
6460
|
maxEventsPerHour: options.maxEventsPerHour,
|
|
@@ -6513,8 +6524,15 @@ var Sentinel = class _Sentinel {
|
|
|
6513
6524
|
...options.role,
|
|
6514
6525
|
agentId,
|
|
6515
6526
|
name: options.name,
|
|
6516
|
-
// Forbidden-pattern normalization chokepoint (Part A)
|
|
6517
|
-
|
|
6527
|
+
// Forbidden-pattern normalization chokepoint (Part A) + Sprint 26 FIX 3B
|
|
6528
|
+
// defaults-as-floor union — same semantics as monitor().
|
|
6529
|
+
forbiddenTargetPatterns: unionWithDefaultForbiddenPatterns(
|
|
6530
|
+
options.role.forbiddenTargetPatterns
|
|
6531
|
+
),
|
|
6532
|
+
// Sprint 26 FIX 3 — same ceiling-side read carve-out as monitor(). The
|
|
6533
|
+
// merged role arriving here keeps ceiling exceptions (workspace ones are
|
|
6534
|
+
// already dropped by mergeRoles); the append is idempotent by target.
|
|
6535
|
+
exceptions: withPolicyReadExceptions(options.role.exceptions)
|
|
6518
6536
|
};
|
|
6519
6537
|
if (await this.profileManager.agentExists(agentId)) {
|
|
6520
6538
|
const store = await this.profileManager.loadAgentProfile(agentId);
|
|
@@ -7318,13 +7336,13 @@ var Sentinel = class _Sentinel {
|
|
|
7318
7336
|
type: "agent_quarantined",
|
|
7319
7337
|
agentId,
|
|
7320
7338
|
agentName: event.agentName ?? agentId,
|
|
7321
|
-
description: `Agent ${agentId} is quarantined. All actions blocked.`,
|
|
7339
|
+
description: `Agent ${agentId} is quarantined. All actions blocked. Run 'npx @tuent/sentinel release --agent=${agentId}' to restore access.`,
|
|
7322
7340
|
evidence: {
|
|
7323
7341
|
action: event.action,
|
|
7324
7342
|
target: event.primaryTarget,
|
|
7325
7343
|
timestamp: event.timestamp
|
|
7326
7344
|
},
|
|
7327
|
-
recommendation:
|
|
7345
|
+
recommendation: `Agent must be manually released before it can perform any actions. Run 'npx @tuent/sentinel release --agent=${agentId}' to restore access.`,
|
|
7328
7346
|
timestamp: event.timestamp
|
|
7329
7347
|
}
|
|
7330
7348
|
};
|
|
@@ -7339,13 +7357,13 @@ var Sentinel = class _Sentinel {
|
|
|
7339
7357
|
type: "agent_restricted",
|
|
7340
7358
|
agentId,
|
|
7341
7359
|
agentName: event.agentName ?? agentId,
|
|
7342
|
-
description: `Agent ${agentId} is restricted. Action '${event.action}' is not permitted in restricted mode.`,
|
|
7360
|
+
description: `Agent ${agentId} is restricted. Action '${event.action}' is not permitted in restricted mode. Run 'npx @tuent/sentinel release --agent=${agentId}' to restore full access.`,
|
|
7343
7361
|
evidence: {
|
|
7344
7362
|
action: event.action,
|
|
7345
7363
|
target: event.primaryTarget,
|
|
7346
7364
|
timestamp: event.timestamp
|
|
7347
7365
|
},
|
|
7348
|
-
recommendation:
|
|
7366
|
+
recommendation: `Only file_read and tool_invocation are allowed in restricted mode. Run 'npx @tuent/sentinel release --agent=${agentId}' to restore full access.`,
|
|
7349
7367
|
timestamp: event.timestamp
|
|
7350
7368
|
}
|
|
7351
7369
|
};
|
|
@@ -7374,6 +7392,17 @@ var Sentinel = class _Sentinel {
|
|
|
7374
7392
|
* Sprint 16 Prompt 3 — replaces in-memory blockCounts Map to survive
|
|
7375
7393
|
* gateway restarts. Same pattern as Sprint 11 sessionCount Shape D fix.
|
|
7376
7394
|
*/
|
|
7395
|
+
/**
|
|
7396
|
+
* Shared escalation-eligibility predicate (Sprint 26 F-5). Single source of
|
|
7397
|
+
* truth for "this finding counts toward the block ladder", extracted from the
|
|
7398
|
+
* formerly-inline copies in getEffectiveBlockCount and maybeEscalate — behavior
|
|
7399
|
+
* of both is identical to before the extraction. Accepts either a
|
|
7400
|
+
* SecurityFinding-shaped object ({type}) or an audit entry ({findingType},
|
|
7401
|
+
* mapped by the caller).
|
|
7402
|
+
*/
|
|
7403
|
+
static isEscalationEligible(f) {
|
|
7404
|
+
return _Sentinel.ESCALATION_ELIGIBLE_TYPES.has(f.type) && (f.severity === "HIGH" || f.severity === "CRITICAL") && f.kind === "actionable";
|
|
7405
|
+
}
|
|
7377
7406
|
async getEffectiveBlockCount(agentId) {
|
|
7378
7407
|
const modeChanges = await this.query(agentId, { type: "mode_change" });
|
|
7379
7408
|
const releaseEntry = modeChanges.find((e) => e.mode === "normal");
|
|
@@ -7382,16 +7411,31 @@ var Sentinel = class _Sentinel {
|
|
|
7382
7411
|
type: "finding",
|
|
7383
7412
|
...anchor ? { startTime: anchor } : {}
|
|
7384
7413
|
});
|
|
7385
|
-
|
|
7386
|
-
(f) => _Sentinel.
|
|
7387
|
-
|
|
7414
|
+
const survivors = findings.filter(
|
|
7415
|
+
(f) => _Sentinel.isEscalationEligible({
|
|
7416
|
+
type: f.findingType,
|
|
7417
|
+
severity: f.severity,
|
|
7418
|
+
kind: f.kind
|
|
7419
|
+
}) && f.mentionOnly !== true
|
|
7420
|
+
);
|
|
7421
|
+
const distinctKeys = /* @__PURE__ */ new Set();
|
|
7422
|
+
let keyless = 0;
|
|
7423
|
+
for (const f of survivors) {
|
|
7424
|
+
const key = f.dedupKey;
|
|
7425
|
+
if (typeof key === "string" && key.length > 0) distinctKeys.add(key);
|
|
7426
|
+
else keyless++;
|
|
7427
|
+
}
|
|
7428
|
+
return distinctKeys.size + keyless;
|
|
7388
7429
|
}
|
|
7389
7430
|
async maybeEscalate(agentId, finding) {
|
|
7390
7431
|
const mode = this.getMode(agentId);
|
|
7391
7432
|
if (mode === "quarantined") return;
|
|
7392
|
-
if (
|
|
7393
|
-
|
|
7394
|
-
|
|
7433
|
+
if (!_Sentinel.isEscalationEligible({
|
|
7434
|
+
type: finding.type,
|
|
7435
|
+
severity: finding.severity,
|
|
7436
|
+
kind: finding.kind
|
|
7437
|
+
}))
|
|
7438
|
+
return;
|
|
7395
7439
|
const count = await this.getEffectiveBlockCount(agentId);
|
|
7396
7440
|
if (mode === "restricted") {
|
|
7397
7441
|
if (count >= this.quarantineThreshold) {
|
|
@@ -7426,4 +7470,4 @@ export {
|
|
|
7426
7470
|
createCliApproval,
|
|
7427
7471
|
Sentinel
|
|
7428
7472
|
};
|
|
7429
|
-
//# sourceMappingURL=chunk-
|
|
7473
|
+
//# sourceMappingURL=chunk-GRN5P3H2.js.map
|