@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.
@@ -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
- if (typeof policy.limits !== "object" || policy.limits === null) {
169
- fail("policy.limits must be an object");
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
- if (enforcement.restrictAfter !== void 0 && typeof enforcement.restrictAfter !== "number") {
185
- fail("enforcement.restrictAfter must be a number");
186
- }
187
- if (enforcement.quarantineAfter !== void 0 && typeof enforcement.quarantineAfter !== "number") {
188
- fail("enforcement.quarantineAfter must be a number");
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
- if (typeof enforcement.minKind !== "string" || !VALID_KINDS.has(enforcement.minKind)) {
195
- fail("enforcement.minKind must be one of: informational, actionable");
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-WLIDSTS4.js.map
638
+ //# sourceMappingURL=chunk-KWZ7JKKO.js.map