@tuent/sentinel 0.1.1 → 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 +5 -1
- package/SECURITY_MODEL.md +85 -35
- package/dist/Sentinel-XMSJE4DZ.js +10 -0
- package/dist/{Sentinel-B_sv8Kiy.d.ts → Sentinel-xFCyXH45.d.ts} +31 -1
- package/dist/{chunk-WPTJBRX5.js → chunk-FWIISAZZ.js} +118 -7
- package/dist/{chunk-NS6ZLMDK.js → chunk-GRN5P3H2.js} +67 -23
- package/dist/{chunk-IYC5E7RL.js → chunk-L4R3LPJS.js} +148 -31
- package/dist/{chunk-QHE56MEO.js → chunk-QIYQWOLO.js} +82 -4
- package/dist/{chunk-2FFMYSVC.js → chunk-WLIDSTS4.js} +18 -2
- package/dist/cli.js +1 -1
- package/dist/gateway/index.d.ts +23 -1
- package/dist/gateway/index.js +3 -3
- package/dist/gatewayDaemon.js +3 -3
- package/dist/index.d.ts +11 -2
- package/dist/index.js +4 -4
- package/dist/{policyLoader-6KR5VFVV.js → policyLoader-KZL2U4M2.js} +2 -2
- package/package.json +1 -1
- package/dist/Sentinel-QHMQ67W3.js +0 -10
|
@@ -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. Run 'sentinel release' to restore access.`,
|
|
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: `Agent must be manually released before it can perform any actions. Run '
|
|
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. Run 'sentinel release' to restore full access.`,
|
|
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: `Only file_read and tool_invocation are allowed in restricted mode. Run '
|
|
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
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_FORBIDDEN_PATTERNS,
|
|
9
9
|
FORBIDDEN_BASENAMES,
|
|
10
|
+
classifyDeny,
|
|
10
11
|
isPositionallySafeMention,
|
|
11
12
|
matchGlobInsensitive,
|
|
12
13
|
normalizeForbiddenPattern,
|
|
@@ -14,12 +15,12 @@ import {
|
|
|
14
15
|
scanContentForForbiddenBasenames,
|
|
15
16
|
scanGlobPattern,
|
|
16
17
|
tokenizePaths
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-QIYQWOLO.js";
|
|
18
19
|
import {
|
|
19
20
|
loadPolicy,
|
|
20
21
|
policyToConfig,
|
|
21
22
|
policyToRole
|
|
22
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-WLIDSTS4.js";
|
|
23
24
|
|
|
24
25
|
// src/gateway/workspaceRouter.ts
|
|
25
26
|
import { resolve, dirname } from "path";
|
|
@@ -646,7 +647,37 @@ var TOOL_MAP = {
|
|
|
646
647
|
WebSearch: { action: "network_request", targetKey: "query" },
|
|
647
648
|
Task: { action: "tool_invocation", targetKey: "description" },
|
|
648
649
|
Skill: { action: "tool_invocation", targetKey: "skill" },
|
|
649
|
-
NotebookEdit: { action: "file_write", targetKey: "notebook_path" }
|
|
650
|
+
NotebookEdit: { action: "file_write", targetKey: "notebook_path" },
|
|
651
|
+
// Sprint 26 Gate-A Item D (F-8) — TOOL_MAP refresh. The 11 names above were
|
|
652
|
+
// a stale subset of cc's native tool set; with the unknown-tool deny consumer
|
|
653
|
+
// live, every missing native name would hard-fail. Inventory taken from a
|
|
654
|
+
// live cc session (2026-06). Conservative mapping: tool_invocation, with a
|
|
655
|
+
// targetKey only where the input schema is known to carry a representative
|
|
656
|
+
// free-text field (scanned by Check 2 / sensitivity like Task.description).
|
|
657
|
+
// Subagent-spawning tools (Agent/Workflow/Task*) are orchestration-only here:
|
|
658
|
+
// each spawned agent's own tool calls hook through PreToolUse individually.
|
|
659
|
+
Agent: { action: "tool_invocation", targetKey: "prompt" },
|
|
660
|
+
SendMessage: { action: "tool_invocation" },
|
|
661
|
+
AskUserQuestion: { action: "tool_invocation" },
|
|
662
|
+
ScheduleWakeup: { action: "tool_invocation", targetKey: "prompt" },
|
|
663
|
+
ToolSearch: { action: "tool_invocation", targetKey: "query" },
|
|
664
|
+
Workflow: { action: "tool_invocation", targetKey: "script" },
|
|
665
|
+
Monitor: { action: "tool_invocation" },
|
|
666
|
+
EnterPlanMode: { action: "tool_invocation" },
|
|
667
|
+
ExitPlanMode: { action: "tool_invocation" },
|
|
668
|
+
EnterWorktree: { action: "tool_invocation" },
|
|
669
|
+
ExitWorktree: { action: "tool_invocation" },
|
|
670
|
+
CronCreate: { action: "tool_invocation" },
|
|
671
|
+
CronDelete: { action: "tool_invocation" },
|
|
672
|
+
CronList: { action: "tool_invocation" },
|
|
673
|
+
TaskCreate: { action: "tool_invocation" },
|
|
674
|
+
TaskGet: { action: "tool_invocation" },
|
|
675
|
+
TaskList: { action: "tool_invocation" },
|
|
676
|
+
TaskOutput: { action: "tool_invocation" },
|
|
677
|
+
TaskStop: { action: "tool_invocation" },
|
|
678
|
+
TaskUpdate: { action: "tool_invocation" },
|
|
679
|
+
PushNotification: { action: "tool_invocation" },
|
|
680
|
+
RemoteTrigger: { action: "tool_invocation" }
|
|
650
681
|
};
|
|
651
682
|
var AGENT_ID = "claude-code";
|
|
652
683
|
var AGENT_NAME = "Claude Code";
|
|
@@ -739,7 +770,7 @@ function extractTargets(toolName, toolInput, cwd) {
|
|
|
739
770
|
return extractGrepTargets(toolInput, cwd);
|
|
740
771
|
default: {
|
|
741
772
|
const mapping = TOOL_MAP[toolName];
|
|
742
|
-
if (!mapping) return [toolName];
|
|
773
|
+
if (!mapping || !mapping.targetKey) return [toolName];
|
|
743
774
|
const val = toolInput[mapping.targetKey];
|
|
744
775
|
if (typeof val === "string" && val.length > 0) return [val];
|
|
745
776
|
return [toolName];
|
|
@@ -778,6 +809,17 @@ function extractMcpTargets(toolName, toolInput) {
|
|
|
778
809
|
var UNKNOWN_TOOL_REASON = "tool schema unknown \u2014 sensitivity scoring and forbidden target patterns cannot evaluate this event";
|
|
779
810
|
var ClaudeCodeTranslator = class {
|
|
780
811
|
agentType = "claude-code";
|
|
812
|
+
/**
|
|
813
|
+
* Sprint 26 Gate-A Item D (F-8) — operator allowlist escape hatch. Names
|
|
814
|
+
* listed in the launch policy's enforcement.allowUnknownTools translate as
|
|
815
|
+
* KNOWN tool_invocation (no _unknownTool marker), so a new cc native tool
|
|
816
|
+
* can be unbricked with a one-line policy edit + daemon restart instead of
|
|
817
|
+
* waiting on a Sentinel release that refreshes TOOL_MAP.
|
|
818
|
+
*/
|
|
819
|
+
allowUnknownTools;
|
|
820
|
+
constructor(options) {
|
|
821
|
+
this.allowUnknownTools = new Set(options?.allowUnknownTools ?? []);
|
|
822
|
+
}
|
|
781
823
|
translatePreToolUse(payload) {
|
|
782
824
|
const p = payload;
|
|
783
825
|
if (!p || typeof p !== "object" || !p.tool_name) return null;
|
|
@@ -798,7 +840,7 @@ var ClaudeCodeTranslator = class {
|
|
|
798
840
|
metadata._policyEnforcementBypassed = "true";
|
|
799
841
|
metadata._policyBypassReason = UNKNOWN_TOOL_REASON;
|
|
800
842
|
console.warn(
|
|
801
|
-
`[SENTINEL] Unknown Claude Code tool "${toolName}" \u2014
|
|
843
|
+
`[SENTINEL] Unknown Claude Code tool "${toolName}" \u2014 flagged for gateway disposition (enforcement.unknownTools; default warn). ${UNKNOWN_TOOL_REASON}`
|
|
802
844
|
);
|
|
803
845
|
}
|
|
804
846
|
if (isMcp) {
|
|
@@ -956,6 +998,14 @@ var ClaudeCodeTranslator = class {
|
|
|
956
998
|
mcpMutating: mcp.mutating
|
|
957
999
|
};
|
|
958
1000
|
}
|
|
1001
|
+
if (this.allowUnknownTools.has(toolName)) {
|
|
1002
|
+
return {
|
|
1003
|
+
action: "tool_invocation",
|
|
1004
|
+
targets: [toolName],
|
|
1005
|
+
isUnknown: false,
|
|
1006
|
+
isMcp: false
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
959
1009
|
return {
|
|
960
1010
|
action: "tool_invocation",
|
|
961
1011
|
targets: [toolName],
|
|
@@ -1049,6 +1099,8 @@ var SentinelGateway = class {
|
|
|
1049
1099
|
operatorCeiling;
|
|
1050
1100
|
home;
|
|
1051
1101
|
releaseToken;
|
|
1102
|
+
/** Item D (F-8): disposition for unknown (non-MCP, unrecognized) tool names. */
|
|
1103
|
+
unknownTools;
|
|
1052
1104
|
server = null;
|
|
1053
1105
|
running = false;
|
|
1054
1106
|
signalHandlersInstalled = false;
|
|
@@ -1063,6 +1115,7 @@ var SentinelGateway = class {
|
|
|
1063
1115
|
this.operatorCeiling = options.operatorCeiling ?? null;
|
|
1064
1116
|
this.home = options.home ?? "";
|
|
1065
1117
|
this.releaseToken = options.releaseToken ?? null;
|
|
1118
|
+
this.unknownTools = options.unknownTools ?? "warn";
|
|
1066
1119
|
const internal = options;
|
|
1067
1120
|
if (internal.registry) {
|
|
1068
1121
|
this.registry = internal.registry;
|
|
@@ -1071,7 +1124,9 @@ var SentinelGateway = class {
|
|
|
1071
1124
|
this.registry.register(internal.translator);
|
|
1072
1125
|
} else {
|
|
1073
1126
|
this.registry = new TranslatorRegistry();
|
|
1074
|
-
this.registry.register(
|
|
1127
|
+
this.registry.register(
|
|
1128
|
+
new ClaudeCodeTranslator({ allowUnknownTools: options.allowUnknownTools })
|
|
1129
|
+
);
|
|
1075
1130
|
}
|
|
1076
1131
|
}
|
|
1077
1132
|
get port() {
|
|
@@ -1406,35 +1461,26 @@ var SentinelGateway = class {
|
|
|
1406
1461
|
routingId = routed.agentId;
|
|
1407
1462
|
event.agentId = routingId;
|
|
1408
1463
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
const decodedImplicated = event.targets.slice(1).some((t) => scanBashCommand(t, FORBIDDEN_BASENAMES).matched);
|
|
1413
|
-
suppressForbiddenBasename = isPositionallySafeMention(literalCommand) && !decodedImplicated;
|
|
1414
|
-
}
|
|
1415
|
-
if (event.action === "command_exec" && event.targets && event.targets.length > 0 && !suppressForbiddenBasename) {
|
|
1416
|
-
const allL2Hits = [];
|
|
1417
|
-
for (const scanTarget of event.targets) {
|
|
1418
|
-
const scan = scanBashCommand(scanTarget, FORBIDDEN_BASENAMES);
|
|
1419
|
-
if (scan.matched) allL2Hits.push(...scan.hits);
|
|
1420
|
-
}
|
|
1421
|
-
if (allL2Hits.length > 0) {
|
|
1464
|
+
if (event.metadata?._unknownTool === "true") {
|
|
1465
|
+
const unknownName = event.metadata.ccToolName ?? event.primaryTarget;
|
|
1466
|
+
if (this.unknownTools === "deny") {
|
|
1422
1467
|
const finding = {
|
|
1423
1468
|
severity: "HIGH",
|
|
1424
1469
|
kind: "actionable",
|
|
1425
|
-
type: "
|
|
1470
|
+
type: "unknown_tool",
|
|
1426
1471
|
agentId: event.agentId,
|
|
1427
1472
|
agentName: event.agentName,
|
|
1428
|
-
description: `
|
|
1473
|
+
description: `Unknown tool "${unknownName}" denied \u2014 not in Sentinel's recognized tool set, so policy checks cannot evaluate it. If this is a legitimate tool, add it to enforcement.allowUnknownTools in the operator launch policy file and restart the gateway daemon.`,
|
|
1429
1474
|
evidence: {
|
|
1430
1475
|
action: event.action,
|
|
1431
|
-
target:
|
|
1476
|
+
target: unknownName,
|
|
1432
1477
|
timestamp: event.timestamp,
|
|
1433
|
-
baselineComparison: "
|
|
1478
|
+
baselineComparison: "unknown_tool_denied"
|
|
1434
1479
|
},
|
|
1435
|
-
recommendation: "
|
|
1480
|
+
recommendation: `Add "${unknownName}" to enforcement.allowUnknownTools in the operator launch policy file and restart the daemon, or update @tuent/sentinel to a build whose recognized tool set includes it.`,
|
|
1436
1481
|
timestamp: event.timestamp,
|
|
1437
|
-
decision: "deny"
|
|
1482
|
+
decision: "deny",
|
|
1483
|
+
dedupKey: unknownName
|
|
1438
1484
|
};
|
|
1439
1485
|
await this.sentinel.handleGatewayDeny(routingId, finding);
|
|
1440
1486
|
this.telemetry.recordToolCall(event.action, "pre", "blocked", 0);
|
|
@@ -1442,6 +1488,38 @@ var SentinelGateway = class {
|
|
|
1442
1488
|
this.sendJson(res, 200, response);
|
|
1443
1489
|
return;
|
|
1444
1490
|
}
|
|
1491
|
+
const warnFinding = {
|
|
1492
|
+
severity: "LOW",
|
|
1493
|
+
kind: "informational",
|
|
1494
|
+
type: "unknown_tool",
|
|
1495
|
+
agentId: event.agentId,
|
|
1496
|
+
agentName: event.agentName,
|
|
1497
|
+
description: `Unknown tool "${unknownName}" allowed (enforcement.unknownTools: warn) \u2014 not in Sentinel's recognized tool set; policy checks could not evaluate it.`,
|
|
1498
|
+
evidence: {
|
|
1499
|
+
action: event.action,
|
|
1500
|
+
target: unknownName,
|
|
1501
|
+
timestamp: event.timestamp,
|
|
1502
|
+
baselineComparison: "unknown_tool_allowed_warn"
|
|
1503
|
+
},
|
|
1504
|
+
recommendation: `Add "${unknownName}" to enforcement.allowUnknownTools (or update @tuent/sentinel) to clear this warning, or switch enforcement.unknownTools to deny.`,
|
|
1505
|
+
timestamp: event.timestamp,
|
|
1506
|
+
decision: "allow",
|
|
1507
|
+
dedupKey: unknownName
|
|
1508
|
+
};
|
|
1509
|
+
await this.sentinel.logFinding(routingId, warnFinding);
|
|
1510
|
+
}
|
|
1511
|
+
let suppressForbiddenBasename = false;
|
|
1512
|
+
if (event.action === "command_exec" && event.targets && event.targets.length > 0) {
|
|
1513
|
+
const literalCommand = event.targets[0] ?? "";
|
|
1514
|
+
const decodedImplicated = event.targets.slice(1).some((t) => scanBashCommand(t, FORBIDDEN_BASENAMES).matched);
|
|
1515
|
+
suppressForbiddenBasename = isPositionallySafeMention(literalCommand) && !decodedImplicated;
|
|
1516
|
+
}
|
|
1517
|
+
if (event.action === "command_exec" && event.targets && event.targets.length > 0 && !suppressForbiddenBasename) {
|
|
1518
|
+
const allL2Hits = [];
|
|
1519
|
+
for (const scanTarget of event.targets) {
|
|
1520
|
+
const scan = scanBashCommand(scanTarget, FORBIDDEN_BASENAMES);
|
|
1521
|
+
if (scan.matched) allL2Hits.push(...scan.hits);
|
|
1522
|
+
}
|
|
1445
1523
|
const allTokenPaths = [];
|
|
1446
1524
|
let anyUnparseable = false;
|
|
1447
1525
|
let anyDangerousConstruct = false;
|
|
@@ -1463,6 +1541,38 @@ var SentinelGateway = class {
|
|
|
1463
1541
|
}
|
|
1464
1542
|
if (matchedPath) break;
|
|
1465
1543
|
}
|
|
1544
|
+
if (allL2Hits.length > 0) {
|
|
1545
|
+
const { mentionOnly } = classifyDeny(event.targets[0] ?? "", {
|
|
1546
|
+
l2Hits: allL2Hits,
|
|
1547
|
+
hasL1Hit: matchedPath !== null,
|
|
1548
|
+
unparseable: anyUnparseable,
|
|
1549
|
+
hasDangerousConstruct: anyDangerousConstruct
|
|
1550
|
+
});
|
|
1551
|
+
const finding = {
|
|
1552
|
+
severity: "HIGH",
|
|
1553
|
+
kind: "actionable",
|
|
1554
|
+
type: "unauthorized_target",
|
|
1555
|
+
agentId: event.agentId,
|
|
1556
|
+
agentName: event.agentName,
|
|
1557
|
+
description: `Bash command references forbidden basename: ${allL2Hits.join(", ")}`,
|
|
1558
|
+
evidence: {
|
|
1559
|
+
action: event.action,
|
|
1560
|
+
target: allL2Hits[0],
|
|
1561
|
+
timestamp: event.timestamp,
|
|
1562
|
+
baselineComparison: "credentials_exfil_attempt"
|
|
1563
|
+
},
|
|
1564
|
+
recommendation: "Review the command for credential access or exfiltration. If legitimate, use existing policy exception mechanisms.",
|
|
1565
|
+
timestamp: event.timestamp,
|
|
1566
|
+
decision: "deny",
|
|
1567
|
+
mentionOnly,
|
|
1568
|
+
dedupKey: event.primaryTarget
|
|
1569
|
+
};
|
|
1570
|
+
await this.sentinel.handleGatewayDeny(routingId, finding);
|
|
1571
|
+
this.telemetry.recordToolCall(event.action, "pre", "blocked", 0);
|
|
1572
|
+
const response = translator.formatPreToolUseResponse({ blocked: true, finding });
|
|
1573
|
+
this.sendJson(res, 200, response);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1466
1576
|
if (matchedPath) {
|
|
1467
1577
|
const finding = {
|
|
1468
1578
|
severity: "HIGH",
|
|
@@ -1479,7 +1589,10 @@ var SentinelGateway = class {
|
|
|
1479
1589
|
},
|
|
1480
1590
|
recommendation: "Review the command for credential or sensitive file access. If legitimate, use existing policy exception mechanisms.",
|
|
1481
1591
|
timestamp: event.timestamp,
|
|
1482
|
-
decision: "deny"
|
|
1592
|
+
decision: "deny",
|
|
1593
|
+
// A resolved path-glob hit is a file target — never a mention.
|
|
1594
|
+
mentionOnly: false,
|
|
1595
|
+
dedupKey: event.primaryTarget
|
|
1483
1596
|
};
|
|
1484
1597
|
await this.sentinel.handleGatewayDeny(routingId, finding);
|
|
1485
1598
|
this.telemetry.recordToolCall(event.action, "pre", "blocked", 0);
|
|
@@ -1911,15 +2024,17 @@ async function runGatewayDaemon({
|
|
|
1911
2024
|
policyPath,
|
|
1912
2025
|
port = DEFAULT_PORT
|
|
1913
2026
|
}) {
|
|
1914
|
-
const { Sentinel: SentinelClass } = await import("./Sentinel-
|
|
2027
|
+
const { Sentinel: SentinelClass } = await import("./Sentinel-XMSJE4DZ.js");
|
|
1915
2028
|
const { writePidFile, writeReleaseToken } = await import("./pidManager-DOGVN6ZT.js");
|
|
1916
2029
|
const { homedir } = await import("os");
|
|
1917
2030
|
const { randomBytes } = await import("crypto");
|
|
1918
|
-
const { loadPolicy: loadPolicy2, policyToRole: policyToRole2 } = await import("./policyLoader-
|
|
2031
|
+
const { loadPolicy: loadPolicy2, policyToRole: policyToRole2, policyToConfig: policyToConfig2 } = await import("./policyLoader-KZL2U4M2.js");
|
|
1919
2032
|
const sentinel = await SentinelClass.fromPolicy(policyPath);
|
|
1920
2033
|
const baseline = await sentinel.computeBaseline("claude-code");
|
|
1921
2034
|
sentinel.setBaseline("claude-code", baseline);
|
|
1922
|
-
const
|
|
2035
|
+
const operatorPolicy = await loadPolicy2(policyPath);
|
|
2036
|
+
const operatorCeiling = policyToRole2(operatorPolicy);
|
|
2037
|
+
const operatorConfig = policyToConfig2(operatorPolicy);
|
|
1923
2038
|
const releaseToken = randomBytes(32).toString("hex");
|
|
1924
2039
|
const gateway = new SentinelGateway({
|
|
1925
2040
|
port,
|
|
@@ -1929,7 +2044,9 @@ async function runGatewayDaemon({
|
|
|
1929
2044
|
workspaceIsolation: process.env.SENTINEL_WORKSPACE_ISOLATION !== "0",
|
|
1930
2045
|
operatorCeiling,
|
|
1931
2046
|
home: homedir(),
|
|
1932
|
-
releaseToken
|
|
2047
|
+
releaseToken,
|
|
2048
|
+
unknownTools: operatorConfig.enforcement?.unknownTools,
|
|
2049
|
+
allowUnknownTools: operatorConfig.enforcement?.allowUnknownTools
|
|
1933
2050
|
});
|
|
1934
2051
|
await gateway.start();
|
|
1935
2052
|
const home = homedir();
|
|
@@ -1942,4 +2059,4 @@ export {
|
|
|
1942
2059
|
SentinelGateway,
|
|
1943
2060
|
runGatewayDaemon
|
|
1944
2061
|
};
|
|
1945
|
-
//# sourceMappingURL=chunk-
|
|
2062
|
+
//# sourceMappingURL=chunk-L4R3LPJS.js.map
|
|
@@ -234,8 +234,43 @@ var DEFAULT_FORBIDDEN_PATTERNS = [
|
|
|
234
234
|
"**/id_ed25519*",
|
|
235
235
|
"**/*.pem",
|
|
236
236
|
"**/*.key",
|
|
237
|
-
"/etc/**"
|
|
237
|
+
"/etc/**",
|
|
238
|
+
// Sprint 26 FIX 1 (A) — common credential stores. DRIFT: keep in sync with
|
|
239
|
+
// STARTER_POLICY.forbid.targets in setup/initClaudeCode.ts (template↔defaults
|
|
240
|
+
// unification tracked in a separate ticket — do not refactor here).
|
|
241
|
+
"**/.netrc",
|
|
242
|
+
"**/.npmrc",
|
|
243
|
+
"**/.git-credentials",
|
|
244
|
+
"**/.pgpass",
|
|
245
|
+
"**/.zsh_history",
|
|
246
|
+
"**/.config/gh/**",
|
|
247
|
+
"**/.docker/config.json",
|
|
248
|
+
"**/.gnupg/**",
|
|
249
|
+
"**/.config/gcloud/**",
|
|
250
|
+
"**/.kube/**",
|
|
251
|
+
"**/Library/Keychains/**",
|
|
252
|
+
// Sprint 26 FIX 1 (B) — Sentinel's own state dir (current path only; the
|
|
253
|
+
// ~/.dahlia → ~/.sentinel rename is a separate re-arch).
|
|
254
|
+
"**/.dahlia/**",
|
|
255
|
+
// Sprint 26 FIX 3 — the live policy file and cc's hook-wiring settings files.
|
|
256
|
+
// Denies the agent's own tool-writes (policy rewrite / unhook vectors); reads
|
|
257
|
+
// stay allowed via DEFAULT_POLICY_READ_EXCEPTIONS below. The settings glob
|
|
258
|
+
// covers project AND user-level (~/.claude/settings*.json) in one pattern.
|
|
259
|
+
// DRIFT: keep in sync with STARTER_POLICY.forbid.targets in setup/initClaudeCode.ts.
|
|
260
|
+
"**/.sentinel.yaml",
|
|
261
|
+
"**/.claude/settings*.json"
|
|
238
262
|
];
|
|
263
|
+
var DEFAULT_POLICY_READ_EXCEPTIONS = [
|
|
264
|
+
{ target: "**/.sentinel.yaml", allowedActions: ["file_read"] },
|
|
265
|
+
{ target: "**/.claude/settings*.json", allowedActions: ["file_read"] }
|
|
266
|
+
];
|
|
267
|
+
function withPolicyReadExceptions(existing) {
|
|
268
|
+
const merged = existing ? [...existing] : [];
|
|
269
|
+
for (const exc of DEFAULT_POLICY_READ_EXCEPTIONS) {
|
|
270
|
+
if (!merged.some((e) => e.target === exc.target)) merged.push(exc);
|
|
271
|
+
}
|
|
272
|
+
return merged;
|
|
273
|
+
}
|
|
239
274
|
var DEFAULT_MEDIUM_DISPOSITION = {
|
|
240
275
|
network_request: "deny"
|
|
241
276
|
};
|
|
@@ -419,7 +454,7 @@ function shouldDispatchWildcard(token) {
|
|
|
419
454
|
if (BRACE_PATTERN_RE.test(token)) return true;
|
|
420
455
|
return false;
|
|
421
456
|
}
|
|
422
|
-
var SENSITIVE_BASENAME_RE = /(?:\.env|\.ssh|secrets|credentials|id_rsa|id_dsa|id_ecdsa|id_ed25519|\.pem|\.key)/i;
|
|
457
|
+
var SENSITIVE_BASENAME_RE = /(?:\.env|\.ssh|secrets|credentials|id_rsa|id_dsa|id_ecdsa|id_ed25519|\.pem|\.key|\.netrc|\.npmrc|\.pgpass|\.zsh_history|\.gnupg|\.kube|\.dahlia)/i;
|
|
423
458
|
var DANGEROUS_COMMAND_TOKENS = /* @__PURE__ */ new Set(["eval"]);
|
|
424
459
|
var COMMAND_SUBSTITUTION_RE = /\$\(|`/;
|
|
425
460
|
var DANGEROUS_RAW_RE = /<<<|<\(|>\(/;
|
|
@@ -577,7 +612,13 @@ var FORBIDDEN_BASENAMES = [
|
|
|
577
612
|
"id_ecdsa",
|
|
578
613
|
"id_ed25519",
|
|
579
614
|
".pem",
|
|
580
|
-
".key"
|
|
615
|
+
".key",
|
|
616
|
+
// Sprint 26 FIX 1 (A) — credential-store file basenames (L2 bash deny).
|
|
617
|
+
// `.git-credentials` is already covered by the "credentials" entry above.
|
|
618
|
+
".netrc",
|
|
619
|
+
".npmrc",
|
|
620
|
+
".pgpass",
|
|
621
|
+
".zsh_history"
|
|
581
622
|
];
|
|
582
623
|
function scanBashCommand(command, forbiddenBasenames) {
|
|
583
624
|
const basenames = forbiddenBasenames ?? FORBIDDEN_BASENAMES;
|
|
@@ -744,6 +785,35 @@ function isPositionallySafeMention(command) {
|
|
|
744
785
|
const safe = positionallySafeBasenames(command);
|
|
745
786
|
return hits.every((h) => safe.has(h));
|
|
746
787
|
}
|
|
788
|
+
function classifyDeny(command, signals) {
|
|
789
|
+
const { l2Hits, hasL1Hit, unparseable, hasDangerousConstruct } = signals;
|
|
790
|
+
if (hasL1Hit || unparseable || hasDangerousConstruct) return { mentionOnly: false };
|
|
791
|
+
if (l2Hits.length === 0) return { mentionOnly: false };
|
|
792
|
+
let tokens;
|
|
793
|
+
try {
|
|
794
|
+
tokens = shellParse(command, (key) => ({ __sentinel_var: key }));
|
|
795
|
+
} catch {
|
|
796
|
+
return { mentionOnly: false };
|
|
797
|
+
}
|
|
798
|
+
if (!Array.isArray(tokens)) return { mentionOnly: false };
|
|
799
|
+
if (tokens.some((t) => isVarMarker(t))) return { mentionOnly: false };
|
|
800
|
+
const strTokens = tokens.filter((t) => typeof t === "string");
|
|
801
|
+
for (const hit of l2Hits) {
|
|
802
|
+
const h = hit.toLowerCase();
|
|
803
|
+
const confident = strTokens.some((tok) => {
|
|
804
|
+
const low = tok.toLowerCase();
|
|
805
|
+
if (!low.includes(h)) return false;
|
|
806
|
+
if (low.length === h.length) return false;
|
|
807
|
+
const reHits = scanBashCommand(tok, FORBIDDEN_BASENAMES).hits;
|
|
808
|
+
if (reHits.some((rh) => tok.length === rh.length)) return false;
|
|
809
|
+
const reTok = tokenizePaths(tok);
|
|
810
|
+
if (reTok.unparseable || reTok.hasDangerousConstruct) return false;
|
|
811
|
+
return true;
|
|
812
|
+
});
|
|
813
|
+
if (!confident) return { mentionOnly: false };
|
|
814
|
+
}
|
|
815
|
+
return { mentionOnly: true };
|
|
816
|
+
}
|
|
747
817
|
|
|
748
818
|
// src/roleValidator.ts
|
|
749
819
|
var SUSPICIOUS_BASENAME_RE = /^\.|(\.env|secret|credential|key|config|token)/i;
|
|
@@ -832,6 +902,11 @@ function normalizeForbiddenPattern(pattern) {
|
|
|
832
902
|
if (pattern.startsWith("**/") || pattern.startsWith("/")) return pattern;
|
|
833
903
|
return "**/" + pattern;
|
|
834
904
|
}
|
|
905
|
+
function unionWithDefaultForbiddenPatterns(supplied) {
|
|
906
|
+
return [
|
|
907
|
+
...new Set([...supplied ?? [], ...DEFAULT_FORBIDDEN_PATTERNS].map(normalizeForbiddenPattern))
|
|
908
|
+
];
|
|
909
|
+
}
|
|
835
910
|
function isPathShaped2(value) {
|
|
836
911
|
if (value.length === 0 || value.length > 4096) return false;
|
|
837
912
|
if (/\s/.test(value)) return false;
|
|
@@ -1694,6 +1769,7 @@ export {
|
|
|
1694
1769
|
removeOverlayDecision,
|
|
1695
1770
|
createEmptyOverlay,
|
|
1696
1771
|
DEFAULT_FORBIDDEN_PATTERNS,
|
|
1772
|
+
withPolicyReadExceptions,
|
|
1697
1773
|
DEFAULT_MEDIUM_DISPOSITION,
|
|
1698
1774
|
TargetSensitivityScorer,
|
|
1699
1775
|
tokenizePaths,
|
|
@@ -1702,12 +1778,14 @@ export {
|
|
|
1702
1778
|
scanContentForForbiddenBasenames,
|
|
1703
1779
|
scanGlobPattern,
|
|
1704
1780
|
isPositionallySafeMention,
|
|
1781
|
+
classifyDeny,
|
|
1705
1782
|
matchGlob,
|
|
1706
1783
|
normalizeForbiddenPattern,
|
|
1784
|
+
unionWithDefaultForbiddenPatterns,
|
|
1707
1785
|
matchGlobInsensitive,
|
|
1708
1786
|
getTargetRecommendation,
|
|
1709
1787
|
walkForbiddenInodeRoots,
|
|
1710
1788
|
findMatchingException,
|
|
1711
1789
|
RoleValidator
|
|
1712
1790
|
};
|
|
1713
|
-
//# sourceMappingURL=chunk-
|
|
1791
|
+
//# sourceMappingURL=chunk-QIYQWOLO.js.map
|
|
@@ -210,6 +210,16 @@ function validatePolicy(data) {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
+
if (enforcement.unknownTools !== void 0) {
|
|
214
|
+
if (enforcement.unknownTools !== "deny" && enforcement.unknownTools !== "warn") {
|
|
215
|
+
fail('enforcement.unknownTools must be "deny" or "warn"');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (enforcement.allowUnknownTools !== void 0) {
|
|
219
|
+
if (!Array.isArray(enforcement.allowUnknownTools) || !enforcement.allowUnknownTools.every((t) => typeof t === "string")) {
|
|
220
|
+
fail("enforcement.allowUnknownTools must be an array of tool name strings");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
213
223
|
if (enforcement.baselineMaturity !== void 0) {
|
|
214
224
|
if (typeof enforcement.baselineMaturity !== "object" || enforcement.baselineMaturity === null) {
|
|
215
225
|
fail("enforcement.baselineMaturity must be an object");
|
|
@@ -371,7 +381,7 @@ function policyToRole(policy) {
|
|
|
371
381
|
function policyToConfig(policy) {
|
|
372
382
|
const config = {};
|
|
373
383
|
if (policy.enforcement) {
|
|
374
|
-
if (policy.enforcement.restrictAfter !== void 0 || policy.enforcement.quarantineAfter !== void 0 || policy.enforcement.promote !== void 0 || policy.enforcement.baselineMaturity !== void 0) {
|
|
384
|
+
if (policy.enforcement.restrictAfter !== void 0 || policy.enforcement.quarantineAfter !== void 0 || policy.enforcement.promote !== void 0 || policy.enforcement.baselineMaturity !== void 0 || policy.enforcement.unknownTools !== void 0 || policy.enforcement.allowUnknownTools !== void 0) {
|
|
375
385
|
config.enforcement = {};
|
|
376
386
|
if (policy.enforcement.restrictAfter !== void 0) {
|
|
377
387
|
config.enforcement.restrictAfter = policy.enforcement.restrictAfter;
|
|
@@ -385,6 +395,12 @@ function policyToConfig(policy) {
|
|
|
385
395
|
if (policy.enforcement.baselineMaturity !== void 0) {
|
|
386
396
|
config.enforcement.baselineMaturity = policy.enforcement.baselineMaturity;
|
|
387
397
|
}
|
|
398
|
+
if (policy.enforcement.unknownTools !== void 0) {
|
|
399
|
+
config.enforcement.unknownTools = policy.enforcement.unknownTools;
|
|
400
|
+
}
|
|
401
|
+
if (policy.enforcement.allowUnknownTools !== void 0) {
|
|
402
|
+
config.enforcement.allowUnknownTools = policy.enforcement.allowUnknownTools;
|
|
403
|
+
}
|
|
388
404
|
}
|
|
389
405
|
}
|
|
390
406
|
if (policy.alerts) {
|
|
@@ -425,4 +441,4 @@ export {
|
|
|
425
441
|
policyToRole,
|
|
426
442
|
policyToConfig
|
|
427
443
|
};
|
|
428
|
-
//# sourceMappingURL=chunk-
|
|
444
|
+
//# sourceMappingURL=chunk-WLIDSTS4.js.map
|