@tuent/sentinel 0.1.2 → 0.1.4
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/SECURITY_MODEL.md +9 -0
- package/dist/Sentinel-4QKPFHTI.js +10 -0
- package/dist/{Sentinel-xFCyXH45.d.ts → Sentinel-DT0IyGQi.d.ts} +127 -9
- package/dist/{chunk-PDWWRZXF.js → chunk-2IPSTUNH.js} +18 -7
- package/dist/chunk-B6S2PBS4.js +47 -0
- package/dist/{chunk-QIYQWOLO.js → chunk-FIEIGBYL.js} +387 -242
- package/dist/{chunk-L4R3LPJS.js → chunk-HRI2Y326.js} +119 -35
- package/dist/{chunk-GRN5P3H2.js → chunk-I2FVDDSG.js} +242 -94
- package/dist/{chunk-WLIDSTS4.js → chunk-KWZ7JKKO.js} +221 -27
- package/dist/{chunk-FWIISAZZ.js → chunk-LTBVWF5H.js} +201 -80
- package/dist/chunk-TKAKHSZ3.js +1 -0
- package/dist/cli.js +33 -35
- package/dist/gateway/index.d.ts +20 -1
- package/dist/gateway/index.js +4 -4
- package/dist/gatewayDaemon.js +38 -16
- package/dist/index.d.ts +31 -19
- package/dist/index.js +9 -8
- package/dist/logAdapter-WM43W3S7.js +7 -0
- package/dist/{mcpAdapter-R47GX2P3.js → mcpAdapter-WYAXUE7T.js} +2 -2
- package/dist/{policyLoader-KZL2U4M2.js → policyLoader-XX6BQXNB.js} +8 -4
- package/package.json +1 -1
- package/dist/Sentinel-XMSJE4DZ.js +0 -10
- package/dist/chunk-FMZWHT4M.js +0 -20
- package/dist/logAdapter-IB6ZDEV2.js +0 -7
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
// src/policyLoader.ts
|
|
2
2
|
import yaml from "js-yaml";
|
|
3
|
+
|
|
4
|
+
// src/defaults.ts
|
|
5
|
+
var DEFAULT_FORBIDDEN_PATTERNS = [
|
|
6
|
+
"**/.env",
|
|
7
|
+
"**/.env.*",
|
|
8
|
+
"**/.ssh/**",
|
|
9
|
+
"**/.aws/**",
|
|
10
|
+
"**/secrets/**",
|
|
11
|
+
"**/credentials/**",
|
|
12
|
+
"**/id_rsa*",
|
|
13
|
+
"**/id_dsa*",
|
|
14
|
+
"**/id_ecdsa*",
|
|
15
|
+
"**/id_ed25519*",
|
|
16
|
+
"**/*.pem",
|
|
17
|
+
"**/*.key",
|
|
18
|
+
"/etc/**",
|
|
19
|
+
// Sprint 26 FIX 1 (A) — common credential stores. DRIFT: keep in sync with
|
|
20
|
+
// STARTER_POLICY.forbid.targets in setup/initClaudeCode.ts (template↔defaults
|
|
21
|
+
// unification tracked in a separate ticket — do not refactor here).
|
|
22
|
+
"**/.netrc",
|
|
23
|
+
"**/.npmrc",
|
|
24
|
+
"**/.git-credentials",
|
|
25
|
+
"**/.pgpass",
|
|
26
|
+
"**/.zsh_history",
|
|
27
|
+
"**/.config/gh/**",
|
|
28
|
+
"**/.docker/config.json",
|
|
29
|
+
"**/.gnupg/**",
|
|
30
|
+
"**/.config/gcloud/**",
|
|
31
|
+
"**/.kube/**",
|
|
32
|
+
"**/Library/Keychains/**",
|
|
33
|
+
// Sprint 26 FIX 1 (B) — Sentinel's own state dir (current path only; the
|
|
34
|
+
// ~/.dahlia → ~/.sentinel rename is a separate re-arch).
|
|
35
|
+
"**/.dahlia/**",
|
|
36
|
+
// Sprint 26 FIX 3 — the live policy file and cc's hook-wiring settings files.
|
|
37
|
+
// Denies the agent's own tool-writes (policy rewrite / unhook vectors); reads
|
|
38
|
+
// stay allowed via DEFAULT_POLICY_READ_EXCEPTIONS below. The settings glob
|
|
39
|
+
// covers project AND user-level (~/.claude/settings*.json) in one pattern.
|
|
40
|
+
// DRIFT: keep in sync with STARTER_POLICY.forbid.targets in setup/initClaudeCode.ts.
|
|
41
|
+
"**/.sentinel.yaml",
|
|
42
|
+
"**/.claude/settings*.json"
|
|
43
|
+
];
|
|
44
|
+
var DEFAULT_POLICY_READ_EXCEPTIONS = [
|
|
45
|
+
{ target: "**/.sentinel.yaml", allowedActions: ["file_read"] },
|
|
46
|
+
{ target: "**/.claude/settings*.json", allowedActions: ["file_read"] }
|
|
47
|
+
];
|
|
48
|
+
function withPolicyReadExceptions(existing) {
|
|
49
|
+
const merged = existing ? [...existing] : [];
|
|
50
|
+
for (const exc of DEFAULT_POLICY_READ_EXCEPTIONS) {
|
|
51
|
+
if (!merged.some((e) => e.target === exc.target)) merged.push(exc);
|
|
52
|
+
}
|
|
53
|
+
return merged;
|
|
54
|
+
}
|
|
55
|
+
var DEFAULT_MEDIUM_DISPOSITION = {
|
|
56
|
+
network_request: "deny"
|
|
57
|
+
};
|
|
58
|
+
var DEFAULT_RESTRICT_AFTER = 2;
|
|
59
|
+
var DEFAULT_QUARANTINE_AFTER = 3;
|
|
60
|
+
var DEFAULT_NETWORK_DENYLIST_CIDRS = [
|
|
61
|
+
"10.0.0.0/8",
|
|
62
|
+
// RFC1918 private (Class A)
|
|
63
|
+
"172.16.0.0/12",
|
|
64
|
+
// RFC1918 private (Class B)
|
|
65
|
+
"192.168.0.0/16",
|
|
66
|
+
// RFC1918 private (Class C)
|
|
67
|
+
"127.0.0.0/8",
|
|
68
|
+
// loopback v4
|
|
69
|
+
"169.254.0.0/16",
|
|
70
|
+
// link-local v4 (cloud metadata endpoints)
|
|
71
|
+
"fe80::/10",
|
|
72
|
+
// link-local v6
|
|
73
|
+
"::1/128"
|
|
74
|
+
// loopback v6
|
|
75
|
+
];
|
|
76
|
+
var DEFAULT_DANGEROUS_SCHEMES = ["file:", "data:", "javascript:", "vbscript:"];
|
|
77
|
+
|
|
78
|
+
// src/policyLoader.ts
|
|
3
79
|
var LOCKED_ACTIONABLE_TYPES = /* @__PURE__ */ new Set([
|
|
4
80
|
"role_violation",
|
|
5
81
|
"unauthorized_target",
|
|
@@ -29,15 +105,88 @@ var VALID_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
29
105
|
function fail(message) {
|
|
30
106
|
throw new Error(`Policy validation error: ${message}`);
|
|
31
107
|
}
|
|
108
|
+
function checkSaneNumber(value, opts = {}) {
|
|
109
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
110
|
+
return `must be a finite number; got ${String(value)}`;
|
|
111
|
+
}
|
|
112
|
+
if (opts.integer && !Number.isInteger(value)) {
|
|
113
|
+
return `must be an integer; got ${value}`;
|
|
114
|
+
}
|
|
115
|
+
if (opts.min !== void 0 && value < opts.min) {
|
|
116
|
+
return `must be >= ${opts.min}; got ${value}`;
|
|
117
|
+
}
|
|
118
|
+
if (opts.max !== void 0 && value > opts.max) {
|
|
119
|
+
return `must be <= ${opts.max}; got ${value}`;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
function requireSaneNumber(name, value, opts = {}) {
|
|
124
|
+
const violation = checkSaneNumber(value, opts);
|
|
125
|
+
if (violation) fail(`${name} ${violation}`);
|
|
126
|
+
}
|
|
127
|
+
function levenshtein(a, b) {
|
|
128
|
+
const m = a.length;
|
|
129
|
+
const n = b.length;
|
|
130
|
+
const row = Array.from({ length: n + 1 }, (_, j) => j);
|
|
131
|
+
for (let i = 1; i <= m; i++) {
|
|
132
|
+
let prev = row[0];
|
|
133
|
+
row[0] = i;
|
|
134
|
+
for (let j = 1; j <= n; j++) {
|
|
135
|
+
const tmp = row[j];
|
|
136
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
137
|
+
prev = tmp;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return row[n];
|
|
141
|
+
}
|
|
142
|
+
function suggestKey(unknown, valid) {
|
|
143
|
+
const lower = unknown.toLowerCase();
|
|
144
|
+
for (const v of valid) {
|
|
145
|
+
if (v.toLowerCase() === lower) return v;
|
|
146
|
+
}
|
|
147
|
+
for (const v of valid) {
|
|
148
|
+
const vl = v.toLowerCase();
|
|
149
|
+
if (lower.includes(vl) || vl.includes(lower)) return v;
|
|
150
|
+
}
|
|
151
|
+
let best = null;
|
|
152
|
+
let bestDist = 3;
|
|
153
|
+
for (const v of valid) {
|
|
154
|
+
const d = levenshtein(lower, v.toLowerCase());
|
|
155
|
+
if (d < bestDist) {
|
|
156
|
+
bestDist = d;
|
|
157
|
+
best = v;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return best;
|
|
161
|
+
}
|
|
162
|
+
function rejectUnknownKeys(section, obj, allowed) {
|
|
163
|
+
for (const key of Object.keys(obj)) {
|
|
164
|
+
if (!allowed.includes(key)) {
|
|
165
|
+
const suggestion = suggestKey(key, allowed);
|
|
166
|
+
fail(
|
|
167
|
+
`unknown key "${key}" in ${section}.` + (suggestion ? ` Did you mean "${suggestion}"?` : "") + ` Valid keys: ${allowed.join(", ")}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
32
172
|
function validatePolicy(data) {
|
|
33
173
|
if (typeof data !== "object" || data === null) {
|
|
34
174
|
fail("policy must be a YAML object");
|
|
35
175
|
}
|
|
36
176
|
const doc = data;
|
|
177
|
+
rejectUnknownKeys("policy document", doc, [
|
|
178
|
+
"version",
|
|
179
|
+
"agent",
|
|
180
|
+
"policy",
|
|
181
|
+
"enforcement",
|
|
182
|
+
"alerts",
|
|
183
|
+
"repo"
|
|
184
|
+
]);
|
|
37
185
|
if (doc.version === void 0) fail("version is required");
|
|
38
186
|
if (String(doc.version) !== "1.0") fail('version must be "1.0"');
|
|
39
187
|
if (typeof doc.agent !== "object" || doc.agent === null) fail("agent section is required");
|
|
40
188
|
const agent = doc.agent;
|
|
189
|
+
rejectUnknownKeys("agent", agent, ["id", "name", "description"]);
|
|
41
190
|
if (typeof agent.id !== "string" || agent.id.length === 0) fail("agent.id is required");
|
|
42
191
|
if (typeof agent.name !== "string" || agent.name.length === 0) fail("agent.name is required");
|
|
43
192
|
if (agent.description !== void 0 && typeof agent.description !== "string") {
|
|
@@ -45,10 +194,12 @@ function validatePolicy(data) {
|
|
|
45
194
|
}
|
|
46
195
|
if (typeof doc.policy !== "object" || doc.policy === null) fail("policy section is required");
|
|
47
196
|
const policy = doc.policy;
|
|
197
|
+
rejectUnknownKeys("policy", policy, ["allow", "forbid", "exceptions", "schedule", "limits"]);
|
|
48
198
|
if (typeof policy.allow !== "object" || policy.allow === null) {
|
|
49
199
|
fail("policy.allow section is required");
|
|
50
200
|
}
|
|
51
201
|
const allow = policy.allow;
|
|
202
|
+
rejectUnknownKeys("policy.allow", allow, ["actions", "targets", "networkHosts"]);
|
|
52
203
|
if (!Array.isArray(allow.actions) || allow.actions.length === 0) {
|
|
53
204
|
fail("policy.allow.actions must be a non-empty array of strings");
|
|
54
205
|
}
|
|
@@ -84,6 +235,7 @@ function validatePolicy(data) {
|
|
|
84
235
|
fail("policy.forbid section is required");
|
|
85
236
|
}
|
|
86
237
|
const forbid = policy.forbid;
|
|
238
|
+
rejectUnknownKeys("policy.forbid", forbid, ["targets"]);
|
|
87
239
|
if (!Array.isArray(forbid.targets) || forbid.targets.length === 0) {
|
|
88
240
|
fail("policy.forbid.targets must be a non-empty array of strings");
|
|
89
241
|
}
|
|
@@ -100,6 +252,14 @@ function validatePolicy(data) {
|
|
|
100
252
|
if (typeof ex !== "object" || ex === null) {
|
|
101
253
|
fail(`${prefix}: must be an object`);
|
|
102
254
|
}
|
|
255
|
+
rejectUnknownKeys(prefix, ex, [
|
|
256
|
+
"target",
|
|
257
|
+
"allowedActions",
|
|
258
|
+
"requiresTask",
|
|
259
|
+
"requiresApproval",
|
|
260
|
+
"expiresAfter",
|
|
261
|
+
"downgradeKindTo"
|
|
262
|
+
]);
|
|
103
263
|
if (typeof ex.target !== "string" || ex.target.length === 0) {
|
|
104
264
|
fail(`${prefix}: target is required and must be a non-empty string`);
|
|
105
265
|
}
|
|
@@ -144,6 +304,7 @@ function validatePolicy(data) {
|
|
|
144
304
|
fail("policy.schedule must be an object");
|
|
145
305
|
}
|
|
146
306
|
const schedule = policy.schedule;
|
|
307
|
+
rejectUnknownKeys("policy.schedule", schedule, ["hours", "days"]);
|
|
147
308
|
if (!Array.isArray(schedule.hours) || schedule.hours.length !== 2) {
|
|
148
309
|
fail("schedule.hours must be [start, end] with values 0-23");
|
|
149
310
|
}
|
|
@@ -165,35 +326,53 @@ function validatePolicy(data) {
|
|
|
165
326
|
}
|
|
166
327
|
}
|
|
167
328
|
if (policy.limits !== void 0) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const limits = policy.limits;
|
|
172
|
-
if (limits.maxEventsPerHour !== void 0 && typeof limits.maxEventsPerHour !== "number") {
|
|
173
|
-
fail("policy.limits.maxEventsPerHour must be a number");
|
|
174
|
-
}
|
|
175
|
-
if (limits.maxSessionDuration !== void 0 && typeof limits.maxSessionDuration !== "number") {
|
|
176
|
-
fail("policy.limits.maxSessionDuration must be a number");
|
|
177
|
-
}
|
|
329
|
+
fail(
|
|
330
|
+
"policy.limits is not supported \u2014 rate/duration limiting (maxEventsPerHour, maxSessionDuration) is not currently enforced by any runtime check. Remove this section. If you need rate limiting, track it as a feature request."
|
|
331
|
+
);
|
|
178
332
|
}
|
|
179
333
|
if (doc.enforcement !== void 0) {
|
|
180
334
|
if (typeof doc.enforcement !== "object" || doc.enforcement === null) {
|
|
181
335
|
fail("enforcement must be an object");
|
|
182
336
|
}
|
|
183
337
|
const enforcement = doc.enforcement;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
338
|
+
rejectUnknownKeys("enforcement", enforcement, [
|
|
339
|
+
"restrictAfter",
|
|
340
|
+
"quarantineAfter",
|
|
341
|
+
"approvalRequired",
|
|
342
|
+
"minKind",
|
|
343
|
+
"promote",
|
|
344
|
+
"baselineMaturity",
|
|
345
|
+
"unknownTools",
|
|
346
|
+
"allowUnknownTools"
|
|
347
|
+
]);
|
|
348
|
+
if (enforcement.restrictAfter !== void 0) {
|
|
349
|
+
requireSaneNumber("enforcement.restrictAfter", enforcement.restrictAfter, {
|
|
350
|
+
integer: true,
|
|
351
|
+
min: 1
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (enforcement.quarantineAfter !== void 0) {
|
|
355
|
+
requireSaneNumber("enforcement.quarantineAfter", enforcement.quarantineAfter, {
|
|
356
|
+
integer: true,
|
|
357
|
+
min: 1
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
{
|
|
361
|
+
const effRestrict = enforcement.restrictAfter ?? DEFAULT_RESTRICT_AFTER;
|
|
362
|
+
const effQuarantine = enforcement.quarantineAfter ?? DEFAULT_QUARANTINE_AFTER;
|
|
363
|
+
if (effRestrict >= effQuarantine) {
|
|
364
|
+
fail(
|
|
365
|
+
`enforcement.restrictAfter (${effRestrict}) must be strictly less than enforcement.quarantineAfter (${effQuarantine}) \u2014 otherwise the restricted tier is unreachable and agents jump straight to quarantine. (Unset values default to restrictAfter ${DEFAULT_RESTRICT_AFTER}, quarantineAfter ${DEFAULT_QUARANTINE_AFTER}.)`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
189
368
|
}
|
|
190
369
|
if (enforcement.approvalRequired !== void 0 && typeof enforcement.approvalRequired !== "boolean") {
|
|
191
370
|
fail("enforcement.approvalRequired must be a boolean");
|
|
192
371
|
}
|
|
193
372
|
if (enforcement.minKind !== void 0) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
373
|
+
fail(
|
|
374
|
+
"enforcement.minKind is not supported \u2014 enforcement gates on finding kind 'actionable' and this field was never wired. Use enforcement.promote to promote specific finding types to actionable, or remove this field."
|
|
375
|
+
);
|
|
197
376
|
}
|
|
198
377
|
if (enforcement.promote !== void 0) {
|
|
199
378
|
if (!Array.isArray(enforcement.promote)) {
|
|
@@ -225,6 +404,11 @@ function validatePolicy(data) {
|
|
|
225
404
|
fail("enforcement.baselineMaturity must be an object");
|
|
226
405
|
}
|
|
227
406
|
const bm = enforcement.baselineMaturity;
|
|
407
|
+
rejectUnknownKeys("enforcement.baselineMaturity", bm, [
|
|
408
|
+
"minSessions",
|
|
409
|
+
"minDaysObserved",
|
|
410
|
+
"minCategoryDiversity"
|
|
411
|
+
]);
|
|
228
412
|
for (const key of ["minSessions", "minDaysObserved", "minCategoryDiversity"]) {
|
|
229
413
|
if (bm[key] !== void 0) {
|
|
230
414
|
if (typeof bm[key] !== "number" || !Number.isFinite(bm[key])) {
|
|
@@ -242,6 +426,13 @@ function validatePolicy(data) {
|
|
|
242
426
|
fail("alerts must be an object");
|
|
243
427
|
}
|
|
244
428
|
const alerts = doc.alerts;
|
|
429
|
+
rejectUnknownKeys("alerts", alerts, [
|
|
430
|
+
"channels",
|
|
431
|
+
"webhookUrl",
|
|
432
|
+
"filePath",
|
|
433
|
+
"minSeverity",
|
|
434
|
+
"minKind"
|
|
435
|
+
]);
|
|
245
436
|
if (!Array.isArray(alerts.channels) || alerts.channels.length === 0) {
|
|
246
437
|
fail("alerts.channels must be a non-empty array");
|
|
247
438
|
}
|
|
@@ -252,6 +443,7 @@ function validatePolicy(data) {
|
|
|
252
443
|
}
|
|
253
444
|
} else if (typeof ch === "object" && ch !== null) {
|
|
254
445
|
const chObj = ch;
|
|
446
|
+
rejectUnknownKeys("alerts.channels[]", chObj, ["type", "minKind"]);
|
|
255
447
|
if (typeof chObj.type !== "string" || !VALID_CHANNELS.has(chObj.type)) {
|
|
256
448
|
fail(
|
|
257
449
|
`alerts.channels contains invalid channel type "${chObj.type}". Valid: console, webhook, file`
|
|
@@ -288,6 +480,7 @@ function validatePolicy(data) {
|
|
|
288
480
|
fail("repo must be an object");
|
|
289
481
|
}
|
|
290
482
|
const repo = doc.repo;
|
|
483
|
+
rejectUnknownKeys("repo", repo, ["root", "scanOnStartup", "mapPath", "overlayPath"]);
|
|
291
484
|
if (repo.root !== void 0) {
|
|
292
485
|
if (typeof repo.root !== "string" || repo.root.length === 0) {
|
|
293
486
|
fail("repo.root must be a non-empty string");
|
|
@@ -368,14 +561,6 @@ function policyToRole(policy) {
|
|
|
368
561
|
activeDays: policy.policy.schedule.days
|
|
369
562
|
};
|
|
370
563
|
}
|
|
371
|
-
if (policy.policy.limits) {
|
|
372
|
-
if (policy.policy.limits.maxEventsPerHour !== void 0) {
|
|
373
|
-
role.maxEventsPerHour = policy.policy.limits.maxEventsPerHour;
|
|
374
|
-
}
|
|
375
|
-
if (policy.policy.limits.maxSessionDuration !== void 0) {
|
|
376
|
-
role.maxSessionDuration = policy.policy.limits.maxSessionDuration;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
564
|
return role;
|
|
380
565
|
}
|
|
381
566
|
function policyToConfig(policy) {
|
|
@@ -435,10 +620,19 @@ function policyToConfig(policy) {
|
|
|
435
620
|
}
|
|
436
621
|
|
|
437
622
|
export {
|
|
623
|
+
DEFAULT_FORBIDDEN_PATTERNS,
|
|
624
|
+
withPolicyReadExceptions,
|
|
625
|
+
DEFAULT_MEDIUM_DISPOSITION,
|
|
626
|
+
DEFAULT_RESTRICT_AFTER,
|
|
627
|
+
DEFAULT_QUARANTINE_AFTER,
|
|
628
|
+
DEFAULT_NETWORK_DENYLIST_CIDRS,
|
|
629
|
+
DEFAULT_DANGEROUS_SCHEMES,
|
|
438
630
|
LOCKED_ACTIONABLE_TYPES,
|
|
631
|
+
checkSaneNumber,
|
|
632
|
+
suggestKey,
|
|
439
633
|
loadPolicy,
|
|
440
634
|
loadPolicyFromString,
|
|
441
635
|
policyToRole,
|
|
442
636
|
policyToConfig
|
|
443
637
|
};
|
|
444
|
-
//# sourceMappingURL=chunk-
|
|
638
|
+
//# sourceMappingURL=chunk-KWZ7JKKO.js.map
|