@tuent/sentinel 0.1.1 → 0.1.3
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 +94 -35
- package/dist/Sentinel-5CQ6HKXS.js +10 -0
- package/dist/{Sentinel-B_sv8Kiy.d.ts → Sentinel-BVoMEF3F.d.ts} +46 -1
- package/dist/{chunk-WPTJBRX5.js → chunk-2TJ5Z53T.js} +266 -73
- package/dist/{chunk-IYC5E7RL.js → chunk-G74MMDKA.js} +189 -43
- package/dist/{chunk-QHE56MEO.js → chunk-JTR2E7RD.js} +247 -60
- package/dist/{chunk-NS6ZLMDK.js → chunk-SSDIBY52.js} +84 -25
- package/dist/chunk-TKAKHSZ3.js +1 -0
- package/dist/{chunk-2FFMYSVC.js → chunk-WLIDSTS4.js} +18 -2
- package/dist/cli.js +22 -22
- package/dist/gateway/index.d.ts +33 -1
- package/dist/gateway/index.js +3 -3
- package/dist/gatewayDaemon.js +37 -15
- package/dist/index.d.ts +40 -9
- package/dist/index.js +8 -7
- package/dist/{policyLoader-6KR5VFVV.js → policyLoader-KZL2U4M2.js} +2 -2
- package/package.json +1 -1
- package/dist/Sentinel-QHMQ67W3.js +0 -10
|
@@ -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 = /<<<|<\(|>\(/;
|
|
@@ -515,7 +550,7 @@ function tokenizePaths(command) {
|
|
|
515
550
|
if (dispatch.unparseable) {
|
|
516
551
|
result.unparseable = true;
|
|
517
552
|
}
|
|
518
|
-
} else if (isPathShaped(token)) {
|
|
553
|
+
} else if (isPathShaped(token) && !isEnvIdentifierChain(token)) {
|
|
519
554
|
const resolved = resolvePathToken(token);
|
|
520
555
|
result.paths.push(resolved);
|
|
521
556
|
}
|
|
@@ -523,6 +558,21 @@ function tokenizePaths(command) {
|
|
|
523
558
|
}
|
|
524
559
|
return result;
|
|
525
560
|
}
|
|
561
|
+
function isEnvIdentifierChain(token) {
|
|
562
|
+
if (token.includes("/") || token.startsWith(".")) return false;
|
|
563
|
+
if (!allEnvOccurrencesIdentifierEmbedded(token)) return false;
|
|
564
|
+
return !SENSITIVE_BASENAME_RE.test(token.replace(/\.env/gi, " "));
|
|
565
|
+
}
|
|
566
|
+
function allEnvOccurrencesIdentifierEmbedded(s) {
|
|
567
|
+
const envHits = /\.env/gi;
|
|
568
|
+
let m;
|
|
569
|
+
let any = false;
|
|
570
|
+
while ((m = envHits.exec(s)) !== null) {
|
|
571
|
+
any = true;
|
|
572
|
+
if (!/[A-Za-z0-9_$]/.test(s[m.index - 1] ?? "")) return false;
|
|
573
|
+
}
|
|
574
|
+
return any;
|
|
575
|
+
}
|
|
526
576
|
function isPathShaped(token) {
|
|
527
577
|
if (token.includes("/")) return true;
|
|
528
578
|
if (token.startsWith(".")) return true;
|
|
@@ -577,7 +627,13 @@ var FORBIDDEN_BASENAMES = [
|
|
|
577
627
|
"id_ecdsa",
|
|
578
628
|
"id_ed25519",
|
|
579
629
|
".pem",
|
|
580
|
-
".key"
|
|
630
|
+
".key",
|
|
631
|
+
// Sprint 26 FIX 1 (A) — credential-store file basenames (L2 bash deny).
|
|
632
|
+
// `.git-credentials` is already covered by the "credentials" entry above.
|
|
633
|
+
".netrc",
|
|
634
|
+
".npmrc",
|
|
635
|
+
".pgpass",
|
|
636
|
+
".zsh_history"
|
|
581
637
|
];
|
|
582
638
|
function scanBashCommand(command, forbiddenBasenames) {
|
|
583
639
|
const basenames = forbiddenBasenames ?? FORBIDDEN_BASENAMES;
|
|
@@ -744,8 +800,38 @@ function isPositionallySafeMention(command) {
|
|
|
744
800
|
const safe = positionallySafeBasenames(command);
|
|
745
801
|
return hits.every((h) => safe.has(h));
|
|
746
802
|
}
|
|
803
|
+
function classifyDeny(command, signals) {
|
|
804
|
+
const { l2Hits, hasL1Hit, unparseable, hasDangerousConstruct } = signals;
|
|
805
|
+
if (hasL1Hit || unparseable || hasDangerousConstruct) return { mentionOnly: false };
|
|
806
|
+
if (l2Hits.length === 0) return { mentionOnly: false };
|
|
807
|
+
let tokens;
|
|
808
|
+
try {
|
|
809
|
+
tokens = shellParse(command, (key) => ({ __sentinel_var: key }));
|
|
810
|
+
} catch {
|
|
811
|
+
return { mentionOnly: false };
|
|
812
|
+
}
|
|
813
|
+
if (!Array.isArray(tokens)) return { mentionOnly: false };
|
|
814
|
+
if (tokens.some((t) => isVarMarker(t))) return { mentionOnly: false };
|
|
815
|
+
const strTokens = tokens.filter((t) => typeof t === "string");
|
|
816
|
+
for (const hit of l2Hits) {
|
|
817
|
+
const h = hit.toLowerCase();
|
|
818
|
+
const confident = strTokens.some((tok) => {
|
|
819
|
+
const low = tok.toLowerCase();
|
|
820
|
+
if (!low.includes(h)) return false;
|
|
821
|
+
if (low.length === h.length) return false;
|
|
822
|
+
const reHits = scanBashCommand(tok, FORBIDDEN_BASENAMES).hits;
|
|
823
|
+
if (reHits.some((rh) => tok.length === rh.length)) return false;
|
|
824
|
+
const reTok = tokenizePaths(tok);
|
|
825
|
+
if (reTok.unparseable || reTok.hasDangerousConstruct) return false;
|
|
826
|
+
return true;
|
|
827
|
+
});
|
|
828
|
+
if (!confident) return { mentionOnly: false };
|
|
829
|
+
}
|
|
830
|
+
return { mentionOnly: true };
|
|
831
|
+
}
|
|
747
832
|
|
|
748
833
|
// src/roleValidator.ts
|
|
834
|
+
import { parse as shellParse2 } from "shell-quote";
|
|
749
835
|
var SUSPICIOUS_BASENAME_RE = /^\.|(\.env|secret|credential|key|config|token)/i;
|
|
750
836
|
function resolveSymlinks(normalizedPath) {
|
|
751
837
|
if (!normalizedPath || normalizedPath.includes("://")) return normalizedPath;
|
|
@@ -832,6 +918,11 @@ function normalizeForbiddenPattern(pattern) {
|
|
|
832
918
|
if (pattern.startsWith("**/") || pattern.startsWith("/")) return pattern;
|
|
833
919
|
return "**/" + pattern;
|
|
834
920
|
}
|
|
921
|
+
function unionWithDefaultForbiddenPatterns(supplied) {
|
|
922
|
+
return [
|
|
923
|
+
...new Set([...supplied ?? [], ...DEFAULT_FORBIDDEN_PATTERNS].map(normalizeForbiddenPattern))
|
|
924
|
+
];
|
|
925
|
+
}
|
|
835
926
|
function isPathShaped2(value) {
|
|
836
927
|
if (value.length === 0 || value.length > 4096) return false;
|
|
837
928
|
if (/\s/.test(value)) return false;
|
|
@@ -1073,10 +1164,26 @@ var RoleValidator = class {
|
|
|
1073
1164
|
}
|
|
1074
1165
|
}
|
|
1075
1166
|
}
|
|
1076
|
-
|
|
1167
|
+
if (event.action === "command_exec") {
|
|
1168
|
+
const cmd = this.screenCommandTarget(event);
|
|
1169
|
+
if (cmd) {
|
|
1170
|
+
const finding = this.buildForbiddenFinding(
|
|
1171
|
+
event,
|
|
1172
|
+
cmd.matchedTarget,
|
|
1173
|
+
cmd.matchedPattern,
|
|
1174
|
+
activeTask ?? null
|
|
1175
|
+
);
|
|
1176
|
+
if (finding) {
|
|
1177
|
+
finding.targetKey = cmd.targetKey;
|
|
1178
|
+
finding.dedupKey = event.primaryTarget;
|
|
1179
|
+
finding.mentionOnly = false;
|
|
1180
|
+
return finding;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1077
1184
|
for (const pattern of this.role.forbiddenTargetPatterns) {
|
|
1078
1185
|
let matchedTargetValue = null;
|
|
1079
|
-
if (
|
|
1186
|
+
if (event.action !== "command_exec" && primaryTargetPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
|
|
1080
1187
|
matchedTargetValue = normalizedPrimaryTarget;
|
|
1081
1188
|
}
|
|
1082
1189
|
if (!matchedTargetValue) {
|
|
@@ -1094,57 +1201,13 @@ var RoleValidator = class {
|
|
|
1094
1201
|
}
|
|
1095
1202
|
}
|
|
1096
1203
|
if (matchedTargetValue) {
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
"unauthorized_target",
|
|
1105
|
-
matchedTargetValue,
|
|
1106
|
-
event.action
|
|
1107
|
-
),
|
|
1108
|
-
matchedTarget: matchedTargetValue
|
|
1109
|
-
});
|
|
1110
|
-
if (!isCritical) {
|
|
1111
|
-
const exception = findMatchingException(this.role.exceptions, event, activeTask ?? null);
|
|
1112
|
-
if (exception) {
|
|
1113
|
-
this.onAuditEntry?.({
|
|
1114
|
-
type: "exception_applied",
|
|
1115
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1116
|
-
agentId: event.agentId,
|
|
1117
|
-
target: matchedTargetValue,
|
|
1118
|
-
action: event.action,
|
|
1119
|
-
exceptionTarget: exception.target,
|
|
1120
|
-
taskId: activeTask?.taskId ?? null
|
|
1121
|
-
});
|
|
1122
|
-
if (exception.requiresApproval) {
|
|
1123
|
-
const ctx = {
|
|
1124
|
-
finding,
|
|
1125
|
-
exception,
|
|
1126
|
-
activeTask: activeTask ?? null,
|
|
1127
|
-
expiresAt: exception.expiresAfter != null && activeTask ? new Date(new Date(activeTask.startedAt).getTime() + exception.expiresAfter) : null
|
|
1128
|
-
};
|
|
1129
|
-
const approved = this.approvalFn?.(ctx) === true;
|
|
1130
|
-
if (approved) {
|
|
1131
|
-
this.onAuditEntry?.({
|
|
1132
|
-
type: "exception_approved",
|
|
1133
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1134
|
-
agentId: event.agentId,
|
|
1135
|
-
target: matchedTargetValue,
|
|
1136
|
-
action: event.action,
|
|
1137
|
-
exceptionTarget: exception.target,
|
|
1138
|
-
taskId: activeTask?.taskId ?? null
|
|
1139
|
-
});
|
|
1140
|
-
continue;
|
|
1141
|
-
}
|
|
1142
|
-
} else {
|
|
1143
|
-
continue;
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
return this.enhanceWithSensitivity(finding, event);
|
|
1204
|
+
const finding = this.buildForbiddenFinding(
|
|
1205
|
+
event,
|
|
1206
|
+
matchedTargetValue,
|
|
1207
|
+
pattern,
|
|
1208
|
+
activeTask ?? null
|
|
1209
|
+
);
|
|
1210
|
+
if (finding) return finding;
|
|
1148
1211
|
}
|
|
1149
1212
|
}
|
|
1150
1213
|
if (this.sensitivityScorer && event.metadata?._mcpTool === "true") {
|
|
@@ -1163,7 +1226,7 @@ var RoleValidator = class {
|
|
|
1163
1226
|
});
|
|
1164
1227
|
}
|
|
1165
1228
|
}
|
|
1166
|
-
if (this.role.allowedTargetPatterns.length > 0) {
|
|
1229
|
+
if (this.role.allowedTargetPatterns.length > 0 && event.action !== "command_exec") {
|
|
1167
1230
|
const anchor = this.workspaceRoot !== "" && PATH_TARGET_ACTIONS.has(event.action);
|
|
1168
1231
|
const matched = this.role.allowedTargetPatterns.some((pattern) => {
|
|
1169
1232
|
const candidates = anchor ? [pattern, anchorAllowedPattern(pattern, this.workspaceRoot)] : [pattern];
|
|
@@ -1201,7 +1264,7 @@ var RoleValidator = class {
|
|
|
1201
1264
|
}
|
|
1202
1265
|
}
|
|
1203
1266
|
const scopePatterns = activeTask?.scopePatterns;
|
|
1204
|
-
if (scopePatterns && scopePatterns.length > 0) {
|
|
1267
|
+
if (scopePatterns && scopePatterns.length > 0 && event.action !== "command_exec") {
|
|
1205
1268
|
const anchor = this.workspaceRoot !== "" && PATH_TARGET_ACTIONS.has(event.action);
|
|
1206
1269
|
const inScope = scopePatterns.some((pattern) => {
|
|
1207
1270
|
const candidates = anchor ? [pattern, anchorAllowedPattern(pattern, this.workspaceRoot)] : [pattern];
|
|
@@ -1265,6 +1328,127 @@ var RoleValidator = class {
|
|
|
1265
1328
|
}
|
|
1266
1329
|
return finding;
|
|
1267
1330
|
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Sprint 26B B′ — scanner-authoritative forbidden screen for a command_exec
|
|
1333
|
+
* PRIMARY target (the command string). Three layers, first match wins:
|
|
1334
|
+
*
|
|
1335
|
+
* L1 — tokenizePaths(command): resolved path-shaped tokens (symlink-resolved,
|
|
1336
|
+
* dir-globs, F-5b's isEnvIdentifierChain guard inside) matched against the
|
|
1337
|
+
* FULL role patterns via matchGlob.
|
|
1338
|
+
* Boundary — every argv token's basename vs each pattern's basename component
|
|
1339
|
+
* via the SAME matchGlob (its `^…$` anchoring → true boundary, so
|
|
1340
|
+
* `^\.env$` rejects `process.env` and `^payroll\.csv$` rejects
|
|
1341
|
+
* `mypayroll.csv`, while a `payroll.csv` token matches a `payroll.csv`
|
|
1342
|
+
* pattern basename). Patterns whose basename is pure-wildcard (a bare
|
|
1343
|
+
* star or globstar) are dir-globs and are skipped here (L1 handles them
|
|
1344
|
+
* on resolved paths).
|
|
1345
|
+
* L2 — scanBashCommand substring net (fixed FORBIDDEN_BASENAMES) gated by
|
|
1346
|
+
* isPositionallySafeMention, for heredoc/substitution shapes tokenization
|
|
1347
|
+
* can't cleanly split.
|
|
1348
|
+
*
|
|
1349
|
+
* Returns the matched target + the F-5a targetKey (resolved path / token /
|
|
1350
|
+
* `l2:<basenames>`, mirroring the gateway emit) + a pattern label for the
|
|
1351
|
+
* finding description, or null when nothing matches.
|
|
1352
|
+
*/
|
|
1353
|
+
screenCommandTarget(event) {
|
|
1354
|
+
const command = event.primaryTarget;
|
|
1355
|
+
if (isPositionallySafeMention(command)) return null;
|
|
1356
|
+
const tr = tokenizePaths(command);
|
|
1357
|
+
for (const p of tr.paths) {
|
|
1358
|
+
for (const pattern of this.role.forbiddenTargetPatterns) {
|
|
1359
|
+
if (matchGlobInsensitive(pattern, p)) {
|
|
1360
|
+
return { matchedTarget: p, targetKey: p, matchedPattern: pattern };
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
let tokens;
|
|
1365
|
+
try {
|
|
1366
|
+
tokens = shellParse2(command);
|
|
1367
|
+
} catch {
|
|
1368
|
+
tokens = [];
|
|
1369
|
+
}
|
|
1370
|
+
for (const tok of tokens) {
|
|
1371
|
+
if (typeof tok !== "string") continue;
|
|
1372
|
+
const tokBase = basename2(tok);
|
|
1373
|
+
if (tokBase.length === 0) continue;
|
|
1374
|
+
for (const pattern of this.role.forbiddenTargetPatterns) {
|
|
1375
|
+
const patBase = basename2(pattern);
|
|
1376
|
+
if (patBase === "*" || patBase === "**") continue;
|
|
1377
|
+
if (matchGlobInsensitive(patBase, tokBase)) {
|
|
1378
|
+
return { matchedTarget: tok, targetKey: tok, matchedPattern: pattern };
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
const scan = scanBashCommand(command, FORBIDDEN_BASENAMES);
|
|
1383
|
+
if (scan.matched) {
|
|
1384
|
+
const hits = [...new Set(scan.hits)].sort();
|
|
1385
|
+
return {
|
|
1386
|
+
matchedTarget: scan.hits[0],
|
|
1387
|
+
targetKey: `l2:${hits.join(",")}`,
|
|
1388
|
+
matchedPattern: hits.join(", ")
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Build the unauthorized_target finding for a matched forbidden target, applying
|
|
1395
|
+
* the same exception/approval/sensitivity flow shared by Check 2's per-pattern
|
|
1396
|
+
* loop and the B′ command screen. Returns the finding to emit, or null when an
|
|
1397
|
+
* exception (auto, or approved) suppresses it (caller continues / falls through).
|
|
1398
|
+
*/
|
|
1399
|
+
buildForbiddenFinding(event, matchedTargetValue, pattern, activeTask) {
|
|
1400
|
+
const sensitivity = this.sensitivityScorer?.scoreTarget(matchedTargetValue, event.action);
|
|
1401
|
+
const isCritical = sensitivity ? sensitivity.effectiveScore >= 0.9 : false;
|
|
1402
|
+
const finding = this.makeFinding(event, {
|
|
1403
|
+
severity: "HIGH",
|
|
1404
|
+
type: "unauthorized_target",
|
|
1405
|
+
description: `Agent accessed '${matchedTargetValue}' which matches forbidden pattern '${pattern}'`,
|
|
1406
|
+
recommendation: getTargetRecommendation(
|
|
1407
|
+
"unauthorized_target",
|
|
1408
|
+
matchedTargetValue,
|
|
1409
|
+
event.action
|
|
1410
|
+
),
|
|
1411
|
+
matchedTarget: matchedTargetValue
|
|
1412
|
+
});
|
|
1413
|
+
if (!isCritical) {
|
|
1414
|
+
const exception = findMatchingException(this.role.exceptions, event, activeTask);
|
|
1415
|
+
if (exception) {
|
|
1416
|
+
this.onAuditEntry?.({
|
|
1417
|
+
type: "exception_applied",
|
|
1418
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1419
|
+
agentId: event.agentId,
|
|
1420
|
+
target: matchedTargetValue,
|
|
1421
|
+
action: event.action,
|
|
1422
|
+
exceptionTarget: exception.target,
|
|
1423
|
+
taskId: activeTask?.taskId ?? null
|
|
1424
|
+
});
|
|
1425
|
+
if (exception.requiresApproval) {
|
|
1426
|
+
const ctx = {
|
|
1427
|
+
finding,
|
|
1428
|
+
exception,
|
|
1429
|
+
activeTask,
|
|
1430
|
+
expiresAt: exception.expiresAfter != null && activeTask ? new Date(new Date(activeTask.startedAt).getTime() + exception.expiresAfter) : null
|
|
1431
|
+
};
|
|
1432
|
+
const approved = this.approvalFn?.(ctx) === true;
|
|
1433
|
+
if (approved) {
|
|
1434
|
+
this.onAuditEntry?.({
|
|
1435
|
+
type: "exception_approved",
|
|
1436
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1437
|
+
agentId: event.agentId,
|
|
1438
|
+
target: matchedTargetValue,
|
|
1439
|
+
action: event.action,
|
|
1440
|
+
exceptionTarget: exception.target,
|
|
1441
|
+
taskId: activeTask?.taskId ?? null
|
|
1442
|
+
});
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
} else {
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return this.enhanceWithSensitivity(finding, event);
|
|
1451
|
+
}
|
|
1268
1452
|
makeFinding(event, details) {
|
|
1269
1453
|
return {
|
|
1270
1454
|
severity: details.severity,
|
|
@@ -1694,6 +1878,7 @@ export {
|
|
|
1694
1878
|
removeOverlayDecision,
|
|
1695
1879
|
createEmptyOverlay,
|
|
1696
1880
|
DEFAULT_FORBIDDEN_PATTERNS,
|
|
1881
|
+
withPolicyReadExceptions,
|
|
1697
1882
|
DEFAULT_MEDIUM_DISPOSITION,
|
|
1698
1883
|
TargetSensitivityScorer,
|
|
1699
1884
|
tokenizePaths,
|
|
@@ -1702,12 +1887,14 @@ export {
|
|
|
1702
1887
|
scanContentForForbiddenBasenames,
|
|
1703
1888
|
scanGlobPattern,
|
|
1704
1889
|
isPositionallySafeMention,
|
|
1890
|
+
classifyDeny,
|
|
1705
1891
|
matchGlob,
|
|
1706
1892
|
normalizeForbiddenPattern,
|
|
1893
|
+
unionWithDefaultForbiddenPatterns,
|
|
1707
1894
|
matchGlobInsensitive,
|
|
1708
1895
|
getTargetRecommendation,
|
|
1709
1896
|
walkForbiddenInodeRoots,
|
|
1710
1897
|
findMatchingException,
|
|
1711
1898
|
RoleValidator
|
|
1712
1899
|
};
|
|
1713
|
-
//# sourceMappingURL=chunk-
|
|
1900
|
+
//# sourceMappingURL=chunk-JTR2E7RD.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-JTR2E7RD.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,
|
|
@@ -314,6 +315,9 @@ var ProfileStore = class {
|
|
|
314
315
|
this.profile = await this.backend.read();
|
|
315
316
|
this.engine = new DataEngine({ petalCount: this.petalCount });
|
|
316
317
|
if (this.profile) {
|
|
318
|
+
if (!Array.isArray(this.profile.dataPoints)) {
|
|
319
|
+
this.profile.dataPoints = [];
|
|
320
|
+
}
|
|
317
321
|
for (const point of this.profile.dataPoints) {
|
|
318
322
|
this.engine.addPoint(point);
|
|
319
323
|
}
|
|
@@ -617,6 +621,7 @@ var AgentActivityClassifier = class {
|
|
|
617
621
|
|
|
618
622
|
// src/auditTrail.ts
|
|
619
623
|
import { createHash } from "crypto";
|
|
624
|
+
import { existsSync } from "fs";
|
|
620
625
|
var AuditTrail = class _AuditTrail {
|
|
621
626
|
static GENESIS_HASH = "0".repeat(64);
|
|
622
627
|
/** S21-P4: honest label for cumulative-stats.json's totalEntries. */
|
|
@@ -650,6 +655,11 @@ var AuditTrail = class _AuditTrail {
|
|
|
650
655
|
this.agentId = agentId;
|
|
651
656
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
652
657
|
const logDir = options?.logDir ?? `${home}/.dahlia/agents/${agentId}`;
|
|
658
|
+
if (options?.logDir && !existsSync(`${logDir}/audit.log`) && existsSync(`${logDir}/${agentId}/audit.log`)) {
|
|
659
|
+
throw new Error(
|
|
660
|
+
`AuditTrail logDir must be the agent's own directory, not the agents root: "${logDir}" contains no audit.log, but "${logDir}/${agentId}/audit.log" exists. Pass join(agentsRoot, "${agentId}") instead.`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
653
663
|
this.logPath = `${logDir}/audit.log`;
|
|
654
664
|
this.statsPath = `${logDir}/cumulative-stats.json`;
|
|
655
665
|
this.manifestPath = `${logDir}/audit.manifest`;
|
|
@@ -742,6 +752,15 @@ var AuditTrail = class _AuditTrail {
|
|
|
742
752
|
if (finding.softSignal === true) {
|
|
743
753
|
entry.softSignal = true;
|
|
744
754
|
}
|
|
755
|
+
if (finding.mentionOnly === true) {
|
|
756
|
+
entry.mentionOnly = true;
|
|
757
|
+
}
|
|
758
|
+
if (finding.dedupKey !== void 0) {
|
|
759
|
+
entry.dedupKey = finding.dedupKey;
|
|
760
|
+
}
|
|
761
|
+
if (finding.targetKey !== void 0) {
|
|
762
|
+
entry.targetKey = finding.targetKey;
|
|
763
|
+
}
|
|
745
764
|
await this.writeLine(entry);
|
|
746
765
|
}
|
|
747
766
|
/**
|
|
@@ -2971,6 +2990,7 @@ var SentinelRunner = class {
|
|
|
2971
2990
|
}
|
|
2972
2991
|
async runGapCheck() {
|
|
2973
2992
|
if (!this._deviationDetector || !this.auditTrail) return;
|
|
2993
|
+
if (this._eventCount === 0) return;
|
|
2974
2994
|
const finding = await this._deviationDetector.checkActivityGap(this.auditTrail);
|
|
2975
2995
|
if (finding) {
|
|
2976
2996
|
this.findings.push(finding);
|
|
@@ -3522,7 +3542,7 @@ var SentinelManager = class {
|
|
|
3522
3542
|
};
|
|
3523
3543
|
|
|
3524
3544
|
// src/Sentinel.ts
|
|
3525
|
-
import { lstatSync, existsSync } from "fs";
|
|
3545
|
+
import { lstatSync, existsSync as existsSync2 } from "fs";
|
|
3526
3546
|
import { resolve as resolve2, dirname, sep as sep2 } from "path";
|
|
3527
3547
|
|
|
3528
3548
|
// src/baselineBuilder.ts
|
|
@@ -6411,7 +6431,7 @@ var Sentinel = class _Sentinel {
|
|
|
6411
6431
|
);
|
|
6412
6432
|
}
|
|
6413
6433
|
}
|
|
6414
|
-
if (!
|
|
6434
|
+
if (!existsSync2(root)) {
|
|
6415
6435
|
console.warn(
|
|
6416
6436
|
`[Sentinel] Workspace root '${root}' does not exist on disk \u2014 anchored allowed patterns will match nothing and every allowed read may be denied.`
|
|
6417
6437
|
);
|
|
@@ -6438,12 +6458,16 @@ var Sentinel = class _Sentinel {
|
|
|
6438
6458
|
"tool_invocation"
|
|
6439
6459
|
],
|
|
6440
6460
|
allowedTargetPatterns: options.allowedTargetPatterns ?? ["**"],
|
|
6441
|
-
// Forbidden-pattern normalization chokepoint (Part A)
|
|
6442
|
-
//
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
),
|
|
6446
|
-
|
|
6461
|
+
// Forbidden-pattern normalization chokepoint (Part A) + Sprint 26 FIX 3B:
|
|
6462
|
+
// the defaults are a floor unioned into every role, not a fallback — a
|
|
6463
|
+
// policy yaml extends but cannot remove them (the `??` form let any
|
|
6464
|
+
// policy-supplied list supersede the defaults entirely).
|
|
6465
|
+
forbiddenTargetPatterns: unionWithDefaultForbiddenPatterns(options.forbiddenTargetPatterns),
|
|
6466
|
+
// Sprint 26 FIX 3 — ceiling-side read carve-out for the self-protection
|
|
6467
|
+
// forbids (policy + hook-wiring files). Appended here, not in the yaml
|
|
6468
|
+
// template: mergeRoles drops workspace exceptions, so this is the only
|
|
6469
|
+
// authorable layer — which is the self-locking property.
|
|
6470
|
+
exceptions: withPolicyReadExceptions(options.exceptions),
|
|
6447
6471
|
networkHosts: options.networkHosts,
|
|
6448
6472
|
expectedSchedule: options.expectedSchedule,
|
|
6449
6473
|
maxEventsPerHour: options.maxEventsPerHour,
|
|
@@ -6513,8 +6537,15 @@ var Sentinel = class _Sentinel {
|
|
|
6513
6537
|
...options.role,
|
|
6514
6538
|
agentId,
|
|
6515
6539
|
name: options.name,
|
|
6516
|
-
// Forbidden-pattern normalization chokepoint (Part A)
|
|
6517
|
-
|
|
6540
|
+
// Forbidden-pattern normalization chokepoint (Part A) + Sprint 26 FIX 3B
|
|
6541
|
+
// defaults-as-floor union — same semantics as monitor().
|
|
6542
|
+
forbiddenTargetPatterns: unionWithDefaultForbiddenPatterns(
|
|
6543
|
+
options.role.forbiddenTargetPatterns
|
|
6544
|
+
),
|
|
6545
|
+
// Sprint 26 FIX 3 — same ceiling-side read carve-out as monitor(). The
|
|
6546
|
+
// merged role arriving here keeps ceiling exceptions (workspace ones are
|
|
6547
|
+
// already dropped by mergeRoles); the append is idempotent by target.
|
|
6548
|
+
exceptions: withPolicyReadExceptions(options.role.exceptions)
|
|
6518
6549
|
};
|
|
6519
6550
|
if (await this.profileManager.agentExists(agentId)) {
|
|
6520
6551
|
const store = await this.profileManager.loadAgentProfile(agentId);
|
|
@@ -7318,13 +7349,13 @@ var Sentinel = class _Sentinel {
|
|
|
7318
7349
|
type: "agent_quarantined",
|
|
7319
7350
|
agentId,
|
|
7320
7351
|
agentName: event.agentName ?? agentId,
|
|
7321
|
-
description: `Agent ${agentId} is quarantined. All actions blocked. Run 'sentinel release' to restore access.`,
|
|
7352
|
+
description: `Agent ${agentId} is quarantined. All actions blocked. Run 'npx @tuent/sentinel release --agent=${agentId}' to restore access.`,
|
|
7322
7353
|
evidence: {
|
|
7323
7354
|
action: event.action,
|
|
7324
7355
|
target: event.primaryTarget,
|
|
7325
7356
|
timestamp: event.timestamp
|
|
7326
7357
|
},
|
|
7327
|
-
recommendation: `Agent must be manually released before it can perform any actions. Run '
|
|
7358
|
+
recommendation: `Agent must be manually released before it can perform any actions. Run 'npx @tuent/sentinel release --agent=${agentId}' to restore access.`,
|
|
7328
7359
|
timestamp: event.timestamp
|
|
7329
7360
|
}
|
|
7330
7361
|
};
|
|
@@ -7339,13 +7370,13 @@ var Sentinel = class _Sentinel {
|
|
|
7339
7370
|
type: "agent_restricted",
|
|
7340
7371
|
agentId,
|
|
7341
7372
|
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.`,
|
|
7373
|
+
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
7374
|
evidence: {
|
|
7344
7375
|
action: event.action,
|
|
7345
7376
|
target: event.primaryTarget,
|
|
7346
7377
|
timestamp: event.timestamp
|
|
7347
7378
|
},
|
|
7348
|
-
recommendation: `Only file_read and tool_invocation are allowed in restricted mode. Run '
|
|
7379
|
+
recommendation: `Only file_read and tool_invocation are allowed in restricted mode. Run 'npx @tuent/sentinel release --agent=${agentId}' to restore full access.`,
|
|
7349
7380
|
timestamp: event.timestamp
|
|
7350
7381
|
}
|
|
7351
7382
|
};
|
|
@@ -7374,6 +7405,17 @@ var Sentinel = class _Sentinel {
|
|
|
7374
7405
|
* Sprint 16 Prompt 3 — replaces in-memory blockCounts Map to survive
|
|
7375
7406
|
* gateway restarts. Same pattern as Sprint 11 sessionCount Shape D fix.
|
|
7376
7407
|
*/
|
|
7408
|
+
/**
|
|
7409
|
+
* Shared escalation-eligibility predicate (Sprint 26 F-5). Single source of
|
|
7410
|
+
* truth for "this finding counts toward the block ladder", extracted from the
|
|
7411
|
+
* formerly-inline copies in getEffectiveBlockCount and maybeEscalate — behavior
|
|
7412
|
+
* of both is identical to before the extraction. Accepts either a
|
|
7413
|
+
* SecurityFinding-shaped object ({type}) or an audit entry ({findingType},
|
|
7414
|
+
* mapped by the caller).
|
|
7415
|
+
*/
|
|
7416
|
+
static isEscalationEligible(f) {
|
|
7417
|
+
return _Sentinel.ESCALATION_ELIGIBLE_TYPES.has(f.type) && (f.severity === "HIGH" || f.severity === "CRITICAL") && f.kind === "actionable";
|
|
7418
|
+
}
|
|
7377
7419
|
async getEffectiveBlockCount(agentId) {
|
|
7378
7420
|
const modeChanges = await this.query(agentId, { type: "mode_change" });
|
|
7379
7421
|
const releaseEntry = modeChanges.find((e) => e.mode === "normal");
|
|
@@ -7382,16 +7424,33 @@ var Sentinel = class _Sentinel {
|
|
|
7382
7424
|
type: "finding",
|
|
7383
7425
|
...anchor ? { startTime: anchor } : {}
|
|
7384
7426
|
});
|
|
7385
|
-
|
|
7386
|
-
(f) => _Sentinel.
|
|
7387
|
-
|
|
7427
|
+
const survivors = findings.filter(
|
|
7428
|
+
(f) => _Sentinel.isEscalationEligible({
|
|
7429
|
+
type: f.findingType,
|
|
7430
|
+
severity: f.severity,
|
|
7431
|
+
kind: f.kind
|
|
7432
|
+
}) && f.mentionOnly !== true
|
|
7433
|
+
);
|
|
7434
|
+
const distinctKeys = /* @__PURE__ */ new Set();
|
|
7435
|
+
let keyless = 0;
|
|
7436
|
+
for (const f of survivors) {
|
|
7437
|
+
const tk = f.targetKey;
|
|
7438
|
+
const dk = f.dedupKey;
|
|
7439
|
+
if (typeof tk === "string" && tk.length > 0) distinctKeys.add(`t:${tk}`);
|
|
7440
|
+
else if (typeof dk === "string" && dk.length > 0) distinctKeys.add(`c:${dk}`);
|
|
7441
|
+
else keyless++;
|
|
7442
|
+
}
|
|
7443
|
+
return distinctKeys.size + keyless;
|
|
7388
7444
|
}
|
|
7389
7445
|
async maybeEscalate(agentId, finding) {
|
|
7390
7446
|
const mode = this.getMode(agentId);
|
|
7391
7447
|
if (mode === "quarantined") return;
|
|
7392
|
-
if (
|
|
7393
|
-
|
|
7394
|
-
|
|
7448
|
+
if (!_Sentinel.isEscalationEligible({
|
|
7449
|
+
type: finding.type,
|
|
7450
|
+
severity: finding.severity,
|
|
7451
|
+
kind: finding.kind
|
|
7452
|
+
}))
|
|
7453
|
+
return;
|
|
7395
7454
|
const count = await this.getEffectiveBlockCount(agentId);
|
|
7396
7455
|
if (mode === "restricted") {
|
|
7397
7456
|
if (count >= this.quarantineThreshold) {
|
|
@@ -7426,4 +7485,4 @@ export {
|
|
|
7426
7485
|
createCliApproval,
|
|
7427
7486
|
Sentinel
|
|
7428
7487
|
};
|
|
7429
|
-
//# sourceMappingURL=chunk-
|
|
7488
|
+
//# sourceMappingURL=chunk-SSDIBY52.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=chunk-TKAKHSZ3.js.map
|