shield-harness 0.2.0 → 0.3.0

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.
@@ -32,6 +32,9 @@ const OCSF_COMMON_FIELDS = new Set([
32
32
  "tool",
33
33
  "seq",
34
34
  "severity",
35
+ "sandbox_state",
36
+ "sandbox_version",
37
+ "sandbox_policy_enforced",
35
38
  ]);
36
39
 
37
40
  // ---------------------------------------------------------------------------
@@ -254,6 +257,20 @@ function toDetectionFinding(entry) {
254
257
  finding.resources = [{ type: "tool", name: entry.tool }];
255
258
  }
256
259
 
260
+ // OpenShell sandbox metadata (Beta Phase)
261
+ if (entry.sandbox_state === "active") {
262
+ finding.resources = finding.resources || [];
263
+ finding.resources.push({
264
+ type: "container",
265
+ name: "openshell-sandbox",
266
+ labels: [
267
+ "state:" + entry.sandbox_state,
268
+ entry.sandbox_version ? "version:" + entry.sandbox_version : null,
269
+ entry.sandbox_policy_enforced ? "policy_enforced:true" : null,
270
+ ].filter(Boolean),
271
+ });
272
+ }
273
+
257
274
  // Unmapped (hook-specific fields)
258
275
  const unmapped = extractUnmapped(entry);
259
276
  if (unmapped) {
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+ // tier-policy-gen.js — Generate OpenShell Policy Schema v1 YAML from permissions-spec.json
3
+ // Spec: ADR-037 Phase Beta, Stream C
4
+ // Purpose: Convert Shield Harness permission rules into OpenShell sandbox policy files
5
+ "use strict";
6
+
7
+ // --- Rule Parsing ---
8
+
9
+ /**
10
+ * Parse a permission rule string into structured components.
11
+ * Supports formats:
12
+ * "Read(~/.ssh/**)" -> { action: "Read", target: "~/.ssh/**" }
13
+ * "Bash(curl *)" -> { action: "Bash", command: "curl", pattern: "*" }
14
+ * "Edit(.claude/hooks/**)" -> { action: "Edit", target: ".claude/hooks/**" }
15
+ * "Glob(*)" -> { action: "Glob", target: "*" }
16
+ * @param {string} rule - Raw permission rule string
17
+ * @returns {{ action: string, target?: string, command?: string, pattern?: string }|null}
18
+ */
19
+ function parsePermissionRule(rule) {
20
+ if (!rule || typeof rule !== "string") return null;
21
+
22
+ const match = rule.match(/^(\w+)\((.+)\)$/);
23
+ if (!match) return null;
24
+
25
+ const action = match[1];
26
+ const inner = match[2];
27
+
28
+ if (action === "Bash") {
29
+ // Split into command and pattern: "curl *" -> { command: "curl", pattern: "*" }
30
+ const spaceIdx = inner.indexOf(" ");
31
+ if (spaceIdx === -1) {
32
+ return { action, command: inner, pattern: "" };
33
+ }
34
+ return {
35
+ action,
36
+ command: inner.slice(0, spaceIdx),
37
+ pattern: inner.slice(spaceIdx + 1),
38
+ };
39
+ }
40
+
41
+ return { action, target: inner };
42
+ }
43
+
44
+ // --- Domain Classification ---
45
+
46
+ /**
47
+ * Network-related commands (Bash rules classified as network domain).
48
+ * @type {Set<string>}
49
+ */
50
+ const NETWORK_COMMANDS = new Set([
51
+ "curl",
52
+ "wget",
53
+ "nc",
54
+ "ncat",
55
+ "nmap",
56
+ "Invoke-WebRequest",
57
+ ]);
58
+
59
+ /**
60
+ * Destructive commands (classified as process domain).
61
+ * @type {Set<string>}
62
+ */
63
+ const DESTRUCTIVE_COMMANDS = new Set(["rm", "del", "format"]);
64
+
65
+ /**
66
+ * Remove trailing glob wildcards from a path for policy use.
67
+ * Example: "~/.ssh/[star][star]" becomes "~/.ssh"
68
+ * Leading globs are preserved (e.g., "[star][star]/.env" stays as-is).
69
+ * @param {string} p - Glob path
70
+ * @returns {string}
71
+ */
72
+ function cleanGlobPath(p) {
73
+ return p.replace(/\/\*\*$/, "").replace(/\/\*$/, "");
74
+ }
75
+
76
+ /**
77
+ * Classify an array of deny rules by security domain.
78
+ * @param {Array<{rule: string, rationale?: string, threat_id?: string}>} rules
79
+ * @returns {{
80
+ * denyRead: string[],
81
+ * denyWrite: string[],
82
+ * network: string[],
83
+ * process: string[],
84
+ * unclassified: string[]
85
+ * }}
86
+ */
87
+ function classifyRulesByDomain(rules) {
88
+ const result = {
89
+ denyRead: [],
90
+ denyWrite: [],
91
+ network: [],
92
+ process: [],
93
+ unclassified: [],
94
+ };
95
+
96
+ const readPaths = new Set();
97
+ const writePaths = new Set();
98
+
99
+ for (const entry of rules) {
100
+ const ruleStr = typeof entry === "string" ? entry : entry.rule;
101
+ const parsed = parsePermissionRule(ruleStr);
102
+
103
+ if (!parsed) {
104
+ result.unclassified.push(ruleStr);
105
+ continue;
106
+ }
107
+
108
+ switch (parsed.action) {
109
+ case "Read": {
110
+ const cleaned = cleanGlobPath(parsed.target);
111
+ if (!readPaths.has(cleaned)) {
112
+ readPaths.add(cleaned);
113
+ result.denyRead.push(cleaned);
114
+ }
115
+ break;
116
+ }
117
+
118
+ case "Edit":
119
+ case "Write": {
120
+ const cleaned = cleanGlobPath(parsed.target);
121
+ if (!writePaths.has(cleaned)) {
122
+ writePaths.add(cleaned);
123
+ result.denyWrite.push(cleaned);
124
+ }
125
+ break;
126
+ }
127
+
128
+ case "Bash": {
129
+ const cmd = parsed.command;
130
+ const fullRule =
131
+ parsed.command + (parsed.pattern ? " " + parsed.pattern : "");
132
+
133
+ if (NETWORK_COMMANDS.has(cmd)) {
134
+ result.network.push(fullRule);
135
+ } else if (DESTRUCTIVE_COMMANDS.has(cmd)) {
136
+ result.process.push(fullRule);
137
+ } else if (cmd === "git" && /push\s+--force/.test(parsed.pattern)) {
138
+ result.network.push(fullRule);
139
+ } else if (cmd === "npm" && /publish/.test(parsed.pattern)) {
140
+ result.network.push(fullRule);
141
+ } else {
142
+ // Other bash commands (e.g., cat */.ssh/*) — classify by intent
143
+ result.process.push(fullRule);
144
+ }
145
+ break;
146
+ }
147
+
148
+ default:
149
+ result.unclassified.push(ruleStr);
150
+ break;
151
+ }
152
+ }
153
+
154
+ return result;
155
+ }
156
+
157
+ // --- YAML Generation ---
158
+
159
+ /**
160
+ * Merge two classification results (for strict mode: deny + ask).
161
+ * Deduplicates paths/commands.
162
+ * @param {ReturnType<typeof classifyRulesByDomain>} base
163
+ * @param {ReturnType<typeof classifyRulesByDomain>} extra
164
+ * @returns {ReturnType<typeof classifyRulesByDomain>}
165
+ */
166
+ function mergeClassified(base, extra) {
167
+ const readSet = new Set(base.denyRead);
168
+ const writeSet = new Set(base.denyWrite);
169
+ const networkSet = new Set(base.network);
170
+ const processSet = new Set(base.process);
171
+
172
+ for (const p of extra.denyRead) {
173
+ if (!readSet.has(p)) {
174
+ readSet.add(p);
175
+ base.denyRead.push(p);
176
+ }
177
+ }
178
+ for (const p of extra.denyWrite) {
179
+ if (!writeSet.has(p)) {
180
+ writeSet.add(p);
181
+ base.denyWrite.push(p);
182
+ }
183
+ }
184
+ for (const p of extra.network) {
185
+ if (!networkSet.has(p)) {
186
+ networkSet.add(p);
187
+ base.network.push(p);
188
+ }
189
+ }
190
+ for (const p of extra.process) {
191
+ if (!processSet.has(p)) {
192
+ processSet.add(p);
193
+ base.process.push(p);
194
+ }
195
+ }
196
+
197
+ return base;
198
+ }
199
+
200
+ /**
201
+ * Generate OpenShell Policy Schema v1 YAML from permissions-spec.json.
202
+ * Uses template literals — no js-yaml dependency.
203
+ * @param {Object} spec - Full permissions-spec.json object
204
+ * @param {{ profile?: string }} [options]
205
+ * @returns {string} YAML policy file content
206
+ */
207
+ function generatePolicyYaml(spec, options = {}) {
208
+ const profile = options.profile || "standard";
209
+
210
+ if (!spec || !spec.permissions) {
211
+ throw new Error("Invalid spec: missing permissions field");
212
+ }
213
+
214
+ const classified = classifyRulesByDomain(spec.permissions.deny || []);
215
+
216
+ // In strict mode, also treat ask rules as deny
217
+ if (profile === "strict" && spec.permissions.ask) {
218
+ const askClassified = classifyRulesByDomain(spec.permissions.ask);
219
+ mergeClassified(classified, askClassified);
220
+ }
221
+
222
+ const lines = [];
223
+ lines.push("# Auto-generated by Shield Harness tier-policy-gen");
224
+ lines.push("# Source: permissions-spec.json v" + (spec.version || "1.0.0"));
225
+ lines.push("# Profile: " + profile);
226
+ lines.push("# Generated: " + new Date().toISOString());
227
+ lines.push("#");
228
+ lines.push("# Usage:");
229
+ lines.push("# openshell sandbox create --policy <this-file> -- claude");
230
+ lines.push("#");
231
+ lines.push("# Static policies require sandbox recreation to change.");
232
+ lines.push(
233
+ "# Network policies can be hot-reloaded: openshell policy set <name> --policy <file> --wait",
234
+ );
235
+ lines.push("");
236
+ lines.push("version: 1");
237
+
238
+ // --- Filesystem policy (static) ---
239
+ lines.push("");
240
+ lines.push("# --- Static (locked at sandbox creation) ---");
241
+ lines.push("");
242
+ lines.push("filesystem_policy:");
243
+ lines.push(" include_workdir: true");
244
+
245
+ if (classified.denyRead.length > 0) {
246
+ lines.push(" deny_read:");
247
+ for (const p of classified.denyRead) {
248
+ lines.push(" - " + p);
249
+ }
250
+ }
251
+
252
+ if (classified.denyWrite.length > 0) {
253
+ lines.push(" deny_write:");
254
+ for (const p of classified.denyWrite) {
255
+ lines.push(" - " + p);
256
+ }
257
+ }
258
+
259
+ lines.push(" read_only:");
260
+ lines.push(" - /usr");
261
+ lines.push(" - /lib");
262
+ lines.push(" - /etc");
263
+ lines.push(" read_write:");
264
+ lines.push(" - /sandbox");
265
+ lines.push(" - /tmp");
266
+
267
+ // --- Landlock ---
268
+ lines.push("");
269
+ lines.push("landlock:");
270
+ lines.push(" compatibility: best_effort");
271
+
272
+ // --- Process ---
273
+ lines.push("");
274
+ lines.push("process:");
275
+ lines.push(" run_as_user: sandbox");
276
+ lines.push(" run_as_group: sandbox");
277
+
278
+ // --- Network policies (dynamic / hot-reloadable) ---
279
+ lines.push("");
280
+ lines.push("# --- Dynamic (hot-reloadable) ---");
281
+ lines.push("");
282
+ lines.push("network_policies:");
283
+
284
+ // Default allowlist (matching openshell-default.yaml)
285
+ lines.push(" anthropic_api:");
286
+ lines.push(" name: anthropic-api");
287
+ lines.push(" endpoints:");
288
+ lines.push(" - host: api.anthropic.com");
289
+ lines.push(" port: 443");
290
+ lines.push(" access: full");
291
+ lines.push(" binaries:");
292
+ lines.push(" - path: /usr/local/bin/claude");
293
+ lines.push("");
294
+ lines.push(" github:");
295
+ lines.push(" name: github");
296
+ lines.push(" endpoints:");
297
+ lines.push(" - host: github.com");
298
+ lines.push(" port: 443");
299
+ lines.push(" access: read-only");
300
+ lines.push(' - host: "*.githubusercontent.com"');
301
+ lines.push(" port: 443");
302
+ lines.push(" access: read-only");
303
+ lines.push(" binaries:");
304
+ lines.push(" - path: /usr/bin/git");
305
+ lines.push("");
306
+ lines.push(" npm_registry:");
307
+ lines.push(" name: npm-registry");
308
+ lines.push(" endpoints:");
309
+ lines.push(" - host: registry.npmjs.org");
310
+ lines.push(" port: 443");
311
+ lines.push(" access: read-only");
312
+ lines.push(" binaries:");
313
+ lines.push(" - path: /usr/bin/npm");
314
+ lines.push(" - path: /usr/bin/node");
315
+
316
+ // Append blocked network commands as comments for visibility
317
+ if (classified.network.length > 0) {
318
+ lines.push("");
319
+ lines.push(
320
+ "# Blocked network operations (from permissions-spec.json deny rules):",
321
+ );
322
+ for (const cmd of classified.network) {
323
+ lines.push("# - " + cmd);
324
+ }
325
+ }
326
+
327
+ // Append blocked destructive commands as comments
328
+ if (classified.process.length > 0) {
329
+ lines.push("");
330
+ lines.push(
331
+ "# Blocked process operations (from permissions-spec.json deny rules):",
332
+ );
333
+ for (const cmd of classified.process) {
334
+ lines.push("# - " + cmd);
335
+ }
336
+ }
337
+
338
+ return lines.join("\n") + "\n";
339
+ }
340
+
341
+ module.exports = {
342
+ parsePermissionRule,
343
+ classifyRulesByDomain,
344
+ generatePolicyYaml,
345
+ // Internal helpers exported for testing
346
+ cleanGlobPath,
347
+ mergeClassified,
348
+ };
@@ -111,8 +111,9 @@ try {
111
111
 
112
112
  // Check channel source for evidence metadata (§8.6.3)
113
113
  let isChannel = false;
114
+ let session = {};
114
115
  try {
115
- const session = readSession();
116
+ session = readSession();
116
117
  isChannel = session.source === "channel";
117
118
  } catch {
118
119
  // Session read failure is non-blocking for evidence
@@ -135,6 +136,18 @@ try {
135
136
  category: null,
136
137
  is_channel: isChannel,
137
138
  session_id: sessionId,
139
+ // OpenShell metadata (Beta Phase)
140
+ sandbox_state:
141
+ session.sandbox_openshell && session.sandbox_openshell.available
142
+ ? "active"
143
+ : "inactive",
144
+ sandbox_version:
145
+ (session.sandbox_openshell && session.sandbox_openshell.version) || null,
146
+ sandbox_policy_enforced: !!(
147
+ session.sandbox_openshell &&
148
+ session.sandbox_openshell.available &&
149
+ session.sandbox_openshell.container_running
150
+ ),
138
151
  };
139
152
 
140
153
  // Collect context messages
@@ -269,6 +269,10 @@ try {
269
269
  }
270
270
  : { available: false, reason: openshellResult.reason },
271
271
  session_id: input.sessionId,
272
+ sandbox_state: openshellResult.available ? "active" : "inactive",
273
+ sandbox_version: openshellResult.version || null,
274
+ sandbox_policy_enforced:
275
+ openshellResult.available && openshellResult.container_running,
272
276
  policy_compat: policyCompat
273
277
  ? {
274
278
  compatible: policyCompat.compatible,
package/README.ja.md CHANGED
@@ -111,6 +111,31 @@ Windows ネイティブでは Claude Code のサンドボックス機能(`sand
111
111
 
112
112
  ### Layer 3b: NVIDIA OpenShell(オプション)
113
113
 
114
+ #### なぜ Layer 3b が必要か?
115
+
116
+ Layer 1(permissions)と Layer 2(hooks)はツール呼び出しの入力テキスト(実行前のコマンド文字列)を検査します。しかし検査を通過したコマンドが実行されると、**OS 上の子プロセスは自由に動作します**。
117
+
118
+ ```
119
+ Layer 1-2(プロセス内):
120
+ Claude Code → [Hook が入力を検査] → コマンド実行 → [子プロセスは自由]
121
+ ↑ ここしか制御できない
122
+
123
+ Layer 3b(プロセス外 = カーネルレベル):
124
+ Claude Code → コマンド実行 → [Landlock: ファイルアクセス制御]
125
+ [Seccomp: syscall 制御]
126
+ [Network NS: ネットワーク隔離]
127
+ ↑ 子プロセスも含めて全てカーネルが制御
128
+ ```
129
+
130
+ | 攻撃ベクトル | Layer 1-2 の対処 | すり抜ける理由 | Layer 3b の防御 |
131
+ | ------------------------------------ | ---------------------------- | ------------------------------------- | ------------------------------------- |
132
+ | パイプチェーンによるファイル読み取り | パターンマッチング | `awk`、`python -c` による間接アクセス | Landlock LSM がカーネルレベルで拒否 |
133
+ | Raw ソケット通信 | `curl`/`wget` の deny ルール | `python`/`node` でソケットを直接操作 | Seccomp BPF がソケット syscall を拒否 |
134
+ | DNS トンネリング | sandbox.network(WSL2 のみ) | DNS クエリにデータを埋め込み | Network Namespace が全 DNS を隔離 |
135
+ | PowerShell ソケット | パターンマッチング | エンコード・難読化 | Seccomp BPF + Network NS の二重防御 |
136
+
137
+ **構造的保証**: エージェント自身がガードレールを無効化することは**不可能** — ポリシーはコンテナ外に存在し、サンドボックス作成時にロックされます。
138
+
114
139
  [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell)(Apache 2.0)は Docker 上で AI エージェントに**カーネルレベルの隔離**を提供します:
115
140
 
116
141
  | メカニズム | 対象 | 保護内容 |
package/README.md CHANGED
@@ -109,6 +109,31 @@ For enterprise environments, supplementing with Windows Firewall outbound rules
109
109
 
110
110
  ### Layer 3b: NVIDIA OpenShell (Optional)
111
111
 
112
+ #### Why Layer 3b?
113
+
114
+ Layer 1 (permissions) and Layer 2 (hooks) inspect tool call inputs — the command text before execution. Once a command passes these checks, the **spawned child process runs freely at the OS level**.
115
+
116
+ ```
117
+ Layer 1-2 (in-process):
118
+ Claude Code → [Hook inspects input] → Command execution → [Child process is free]
119
+ ↑ Only controls this point
120
+
121
+ Layer 3b (out-of-process = kernel-level):
122
+ Claude Code → Command execution → [Landlock: Filesystem access control]
123
+ [Seccomp: Syscall control]
124
+ [Network NS: Network isolation]
125
+ ↑ Kernel controls ALL processes including children
126
+ ```
127
+
128
+ | Attack Vector | Layer 1-2 Defense | Why It Bypasses | Layer 3b Defense |
129
+ | ------------------------ | --------------------------- | -------------------------------------- | ------------------------------------- |
130
+ | Pipe chain file access | Pattern matching | Indirect access via `awk`, `python -c` | Landlock LSM denies at kernel level |
131
+ | Raw socket communication | `curl`/`wget` deny rules | Direct socket via `python`/`node` | Seccomp BPF blocks socket syscalls |
132
+ | DNS tunneling | sandbox.network (WSL2 only) | Data embedded in DNS queries | Network Namespace isolates all DNS |
133
+ | PowerShell sockets | Pattern matching | Encoding/obfuscation | Seccomp BPF + Network NS dual defense |
134
+
135
+ **Structural guarantee**: The agent **cannot** disable its own guardrails — policies exist outside the container and are locked at sandbox creation.
136
+
112
137
  [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) (Apache 2.0) provides **kernel-level isolation** for AI agents via Docker:
113
138
 
114
139
  | Mechanism | Target | Protection |
@@ -208,6 +208,84 @@ function printBasicGuide() {
208
208
  console.log(" 3. openshell sandbox start");
209
209
  }
210
210
 
211
+ // ---------------------------------------------------------------------------
212
+ // generate-policy command
213
+ // ---------------------------------------------------------------------------
214
+
215
+ const POLICY_PROFILES = ["standard", "strict"];
216
+ const DEFAULT_POLICY_OUTPUT = path.join(
217
+ ".claude",
218
+ "policies",
219
+ "openshell-generated.yaml",
220
+ );
221
+
222
+ /**
223
+ * Generate OpenShell policy YAML from permissions-spec.json.
224
+ * @param {string[]} args - CLI arguments (after "generate-policy")
225
+ */
226
+ function generatePolicy(args) {
227
+ // Parse --output
228
+ let output = DEFAULT_POLICY_OUTPUT;
229
+ const outputIdx = args.indexOf("--output");
230
+ if (outputIdx !== -1 && args[outputIdx + 1]) {
231
+ output = args[outputIdx + 1];
232
+ }
233
+
234
+ // Parse --profile
235
+ let profile = "standard";
236
+ const profileIdx = args.indexOf("--profile");
237
+ if (profileIdx !== -1 && args[profileIdx + 1]) {
238
+ profile = args[profileIdx + 1];
239
+ if (!POLICY_PROFILES.includes(profile)) {
240
+ console.error(`Unknown profile: ${profile}`);
241
+ console.error(`Available profiles: ${POLICY_PROFILES.join(", ")}`);
242
+ process.exit(1);
243
+ }
244
+ }
245
+
246
+ // Read permissions-spec.json
247
+ const specPath = path.join(process.cwd(), ".claude", "permissions-spec.json");
248
+ if (!fs.existsSync(specPath)) {
249
+ console.error("permissions-spec.json not found at: " + specPath);
250
+ console.error("Run 'npx shield-harness init' first.");
251
+ process.exit(1);
252
+ }
253
+
254
+ let spec;
255
+ try {
256
+ spec = JSON.parse(fs.readFileSync(specPath, "utf8"));
257
+ } catch (err) {
258
+ console.error("Failed to parse permissions-spec.json: " + err.message);
259
+ process.exit(1);
260
+ }
261
+
262
+ // Generate YAML
263
+ let yaml;
264
+ try {
265
+ const {
266
+ generatePolicyYaml,
267
+ } = require("../.claude/hooks/lib/tier-policy-gen");
268
+ yaml = generatePolicyYaml(spec, { profile });
269
+ } catch (err) {
270
+ console.error("Failed to generate policy: " + err.message);
271
+ process.exit(1);
272
+ }
273
+
274
+ // Write output
275
+ const outputPath = path.resolve(process.cwd(), output);
276
+ const outputDir = path.dirname(outputPath);
277
+ if (!fs.existsSync(outputDir)) {
278
+ fs.mkdirSync(outputDir, { recursive: true });
279
+ }
280
+ fs.writeFileSync(outputPath, yaml);
281
+
282
+ console.log(`Policy generated successfully (profile: ${profile}).`);
283
+ console.log(` Output: ${output}`);
284
+ console.log("");
285
+ console.log("Usage:");
286
+ console.log(` openshell sandbox create --policy ${output} -- claude`);
287
+ }
288
+
211
289
  // ---------------------------------------------------------------------------
212
290
  // CLI
213
291
  // ---------------------------------------------------------------------------
@@ -227,12 +305,17 @@ if (command === "init") {
227
305
  }
228
306
  }
229
307
  init(profile);
308
+ } else if (command === "generate-policy") {
309
+ generatePolicy(args);
230
310
  } else {
231
311
  const pkg = require("../package.json");
232
312
  console.log(`Shield Harness v${pkg.version}`);
233
313
  console.log("");
234
314
  console.log("Usage:");
235
315
  console.log(" npx shield-harness init [--profile minimal|standard|strict]");
316
+ console.log(
317
+ " npx shield-harness generate-policy [--output <path>] [--profile standard|strict]",
318
+ );
236
319
  console.log("");
237
320
  console.log("Profiles:");
238
321
  console.log(" minimal — Minimal config, approval-free");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shield-harness",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Security harness for Claude Code — hooks-driven, zero-hassle defense",
5
5
  "bin": {
6
6
  "shield-harness": "./bin/shield-harness.js"
@@ -23,7 +23,11 @@
23
23
  "test:output": "node --test tests/output-security.test.js",
24
24
  "test:ocsf": "node --test tests/ocsf-mapper.test.js",
25
25
  "test:policy-compat": "node --test tests/policy-compat.test.js",
26
- "test:permissions": "node --test tests/permissions-alignment.test.js"
26
+ "test:permissions": "node --test tests/permissions-alignment.test.js",
27
+ "test:openshell-detect": "node --test tests/openshell-detect.test.js",
28
+ "test:openshell-evidence": "node --test tests/openshell-evidence.test.js",
29
+ "test:tier-policy": "node --test tests/tier-policy-gen.test.js",
30
+ "test:policy-effectiveness": "node --test tests/policy-effectiveness.test.js"
27
31
  },
28
32
  "keywords": [
29
33
  "claude-code",