shield-harness 0.3.0 → 0.4.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.
- package/.claude/hooks/lib/policy-drift.js +322 -0
- package/.claude/hooks/sh-config-guard.js +135 -1
- package/.claude/hooks/sh-session-start.js +41 -0
- package/.claude/permissions-spec.json +11 -1
- package/.claude/policies/openshell-generated.yaml +105 -0
- package/README.ja.md +85 -33
- package/README.md +84 -32
- package/package.json +1 -1
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// policy-drift.js — Detect drift between permissions-spec.json deny rules and OpenShell policy YAML
|
|
3
|
+
// Part of Shield Harness: ensures policy files stay in sync with the canonical deny rules
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
// --- Dependencies (local modules) ---
|
|
10
|
+
|
|
11
|
+
const { loadPermissionsSpec } = require("./permissions-validator");
|
|
12
|
+
const { classifyRulesByDomain } = require("./tier-policy-gen");
|
|
13
|
+
|
|
14
|
+
// --- YAML Parsing (regex-based, no js-yaml dependency) ---
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract a YAML list section by name.
|
|
18
|
+
* Expects predictable structure generated by tier-policy-gen.js:
|
|
19
|
+
* section_name:
|
|
20
|
+
* - item1
|
|
21
|
+
* - item2
|
|
22
|
+
*
|
|
23
|
+
* @param {string} content - Full YAML file content
|
|
24
|
+
* @param {string} sectionName - Name of the section (e.g., "deny_read")
|
|
25
|
+
* @returns {string[]} List of items found under the section
|
|
26
|
+
*/
|
|
27
|
+
function extractYamlList(content, sectionName) {
|
|
28
|
+
// Match section header followed by indented list items
|
|
29
|
+
var pattern = new RegExp(
|
|
30
|
+
sectionName + ":\\n((?:[ \\t]+-[ \\t]+.+\\n)*)",
|
|
31
|
+
"m",
|
|
32
|
+
);
|
|
33
|
+
var match = content.match(pattern);
|
|
34
|
+
if (!match) return [];
|
|
35
|
+
return match[1]
|
|
36
|
+
.split("\n")
|
|
37
|
+
.filter(function (l) {
|
|
38
|
+
return l.trim().startsWith("- ");
|
|
39
|
+
})
|
|
40
|
+
.map(function (l) {
|
|
41
|
+
return l.trim().replace(/^-\s+/, "");
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract blocked network operations from YAML comment block.
|
|
47
|
+
* Expects format generated by tier-policy-gen.js:
|
|
48
|
+
* # Blocked network operations (from permissions-spec.json deny rules):
|
|
49
|
+
* # - curl *
|
|
50
|
+
* # - wget *
|
|
51
|
+
*
|
|
52
|
+
* @param {string} content - Full YAML file content
|
|
53
|
+
* @returns {string[]} List of blocked network commands
|
|
54
|
+
*/
|
|
55
|
+
function extractBlockedNetworkComments(content) {
|
|
56
|
+
var pattern =
|
|
57
|
+
/# Blocked network operations[^\n]*:\n((?:#[ \t]+-[ \t]+.+\n)*)/m;
|
|
58
|
+
var match = content.match(pattern);
|
|
59
|
+
if (!match) return [];
|
|
60
|
+
return match[1]
|
|
61
|
+
.split("\n")
|
|
62
|
+
.filter(function (l) {
|
|
63
|
+
return l.trim().startsWith("#");
|
|
64
|
+
})
|
|
65
|
+
.map(function (l) {
|
|
66
|
+
return l.trim().replace(/^#\s+-\s+/, "");
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse all relevant sections from a policy YAML file.
|
|
72
|
+
* Returns structured data without requiring js-yaml.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} content - Full YAML file content
|
|
75
|
+
* @returns {{
|
|
76
|
+
* denyRead: string[],
|
|
77
|
+
* denyWrite: string[],
|
|
78
|
+
* readWrite: string[],
|
|
79
|
+
* blockedNetwork: string[]
|
|
80
|
+
* }}
|
|
81
|
+
*/
|
|
82
|
+
function parsePolicyYaml(content) {
|
|
83
|
+
return {
|
|
84
|
+
denyRead: extractYamlList(content, "deny_read"),
|
|
85
|
+
denyWrite: extractYamlList(content, "deny_write"),
|
|
86
|
+
readWrite: extractYamlList(content, "read_write"),
|
|
87
|
+
blockedNetwork: extractBlockedNetworkComments(content),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Drift Detection ---
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check drift between permissions-spec deny rules and OpenShell policy YAML.
|
|
95
|
+
*
|
|
96
|
+
* Compares the classified deny rules from permissions-spec.json against
|
|
97
|
+
* the content of policy YAML files in the specified directory.
|
|
98
|
+
*
|
|
99
|
+
* Drift rules:
|
|
100
|
+
* 1. Missing filesystem deny (read): spec denyRead paths not in policy deny_read
|
|
101
|
+
* 2. Missing filesystem deny (write): spec denyWrite paths not in policy deny_write
|
|
102
|
+
* 3. Policy allows denied: paths in denyRead/denyWrite that also appear in read_write
|
|
103
|
+
* 4. Missing network deny: network commands not in blocked network comments
|
|
104
|
+
* 5. No policy files: skip (no drift reported)
|
|
105
|
+
* 6. No spec file: skip (no drift reported)
|
|
106
|
+
* 7. Parse errors: fail-safe (no drift, warning logged)
|
|
107
|
+
*
|
|
108
|
+
* @param {{ specPath: string, policyDir: string }} params
|
|
109
|
+
* @returns {{
|
|
110
|
+
* has_drift: boolean,
|
|
111
|
+
* warnings: string[],
|
|
112
|
+
* details: {
|
|
113
|
+
* missing_filesystem_deny: string[],
|
|
114
|
+
* missing_network_deny: string[],
|
|
115
|
+
* policy_allows_denied: string[]
|
|
116
|
+
* },
|
|
117
|
+
* checked_at: string
|
|
118
|
+
* }}
|
|
119
|
+
*/
|
|
120
|
+
function checkPolicyDrift({ specPath, policyDir }) {
|
|
121
|
+
var emptyResult = {
|
|
122
|
+
has_drift: false,
|
|
123
|
+
warnings: [],
|
|
124
|
+
details: {
|
|
125
|
+
missing_filesystem_deny: [],
|
|
126
|
+
missing_network_deny: [],
|
|
127
|
+
policy_allows_denied: [],
|
|
128
|
+
},
|
|
129
|
+
checked_at: new Date().toISOString(),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Rule 6: No spec file — skip silently
|
|
133
|
+
if (!specPath || !fs.existsSync(specPath)) {
|
|
134
|
+
return emptyResult;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Rule 5: No policy directory or no .yaml files — skip silently
|
|
138
|
+
if (!policyDir || !fs.existsSync(policyDir)) {
|
|
139
|
+
return emptyResult;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
var policyFiles;
|
|
143
|
+
try {
|
|
144
|
+
policyFiles = fs.readdirSync(policyDir).filter(function (f) {
|
|
145
|
+
return f.endsWith(".yaml") || f.endsWith(".yml");
|
|
146
|
+
});
|
|
147
|
+
} catch (e) {
|
|
148
|
+
return emptyResult;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (policyFiles.length === 0) {
|
|
152
|
+
return emptyResult;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Load and classify spec deny rules
|
|
156
|
+
var spec;
|
|
157
|
+
try {
|
|
158
|
+
spec = loadPermissionsSpec(specPath);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return {
|
|
161
|
+
has_drift: false,
|
|
162
|
+
warnings: ["parse_error: spec load failed \u2014 " + err.message],
|
|
163
|
+
details: {
|
|
164
|
+
missing_filesystem_deny: [],
|
|
165
|
+
missing_network_deny: [],
|
|
166
|
+
policy_allows_denied: [],
|
|
167
|
+
},
|
|
168
|
+
checked_at: new Date().toISOString(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
var classified;
|
|
173
|
+
try {
|
|
174
|
+
classified = classifyRulesByDomain(spec.deny);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
return {
|
|
177
|
+
has_drift: false,
|
|
178
|
+
warnings: [
|
|
179
|
+
"parse_error: rule classification failed \u2014 " + err.message,
|
|
180
|
+
],
|
|
181
|
+
details: {
|
|
182
|
+
missing_filesystem_deny: [],
|
|
183
|
+
missing_network_deny: [],
|
|
184
|
+
policy_allows_denied: [],
|
|
185
|
+
},
|
|
186
|
+
checked_at: new Date().toISOString(),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Aggregate policy content from all YAML files
|
|
191
|
+
var aggregated = {
|
|
192
|
+
denyRead: new Set(),
|
|
193
|
+
denyWrite: new Set(),
|
|
194
|
+
readWrite: new Set(),
|
|
195
|
+
blockedNetwork: new Set(),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
for (var i = 0; i < policyFiles.length; i++) {
|
|
199
|
+
var filePath = path.join(policyDir, policyFiles[i]);
|
|
200
|
+
var content;
|
|
201
|
+
try {
|
|
202
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
203
|
+
} catch (err) {
|
|
204
|
+
// Rule 7: Parse error — fail-safe
|
|
205
|
+
return {
|
|
206
|
+
has_drift: false,
|
|
207
|
+
warnings: [
|
|
208
|
+
"parse_error: cannot read " + filePath + " \u2014 " + err.message,
|
|
209
|
+
],
|
|
210
|
+
details: {
|
|
211
|
+
missing_filesystem_deny: [],
|
|
212
|
+
missing_network_deny: [],
|
|
213
|
+
policy_allows_denied: [],
|
|
214
|
+
},
|
|
215
|
+
checked_at: new Date().toISOString(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
var parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = parsePolicyYaml(content);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
return {
|
|
224
|
+
has_drift: false,
|
|
225
|
+
warnings: [
|
|
226
|
+
"parse_error: cannot parse " + filePath + " \u2014 " + err.message,
|
|
227
|
+
],
|
|
228
|
+
details: {
|
|
229
|
+
missing_filesystem_deny: [],
|
|
230
|
+
missing_network_deny: [],
|
|
231
|
+
policy_allows_denied: [],
|
|
232
|
+
},
|
|
233
|
+
checked_at: new Date().toISOString(),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
parsed.denyRead.forEach(function (p) {
|
|
238
|
+
aggregated.denyRead.add(p);
|
|
239
|
+
});
|
|
240
|
+
parsed.denyWrite.forEach(function (p) {
|
|
241
|
+
aggregated.denyWrite.add(p);
|
|
242
|
+
});
|
|
243
|
+
parsed.readWrite.forEach(function (p) {
|
|
244
|
+
aggregated.readWrite.add(p);
|
|
245
|
+
});
|
|
246
|
+
parsed.blockedNetwork.forEach(function (cmd) {
|
|
247
|
+
aggregated.blockedNetwork.add(cmd);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --- Compare classified rules against aggregated policy ---
|
|
252
|
+
|
|
253
|
+
var warnings = [];
|
|
254
|
+
var missingFilesystemDeny = [];
|
|
255
|
+
var missingNetworkDeny = [];
|
|
256
|
+
var policyAllowsDenied = [];
|
|
257
|
+
|
|
258
|
+
// Rule 1: Missing filesystem deny (read)
|
|
259
|
+
for (var ri = 0; ri < classified.denyRead.length; ri++) {
|
|
260
|
+
var readPath = classified.denyRead[ri];
|
|
261
|
+
if (!aggregated.denyRead.has(readPath)) {
|
|
262
|
+
warnings.push("deny_read missing in policy: " + readPath);
|
|
263
|
+
missingFilesystemDeny.push(readPath);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Rule 2: Missing filesystem deny (write)
|
|
268
|
+
for (var wi = 0; wi < classified.denyWrite.length; wi++) {
|
|
269
|
+
var writePath = classified.denyWrite[wi];
|
|
270
|
+
if (!aggregated.denyWrite.has(writePath)) {
|
|
271
|
+
warnings.push("deny_write missing in policy: " + writePath);
|
|
272
|
+
missingFilesystemDeny.push(writePath);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Rule 3: Policy allows denied — conflict detection
|
|
277
|
+
var allDeniedPaths = new Set(
|
|
278
|
+
classified.denyRead.concat(classified.denyWrite),
|
|
279
|
+
);
|
|
280
|
+
aggregated.readWrite.forEach(function (rwPath) {
|
|
281
|
+
if (allDeniedPaths.has(rwPath)) {
|
|
282
|
+
warnings.push("conflict: read_write allows denied path: " + rwPath);
|
|
283
|
+
policyAllowsDenied.push(rwPath);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Rule 4: Missing network deny
|
|
288
|
+
for (var ni = 0; ni < classified.network.length; ni++) {
|
|
289
|
+
var netCmd = classified.network[ni];
|
|
290
|
+
if (!aggregated.blockedNetwork.has(netCmd)) {
|
|
291
|
+
warnings.push(
|
|
292
|
+
"blocked network command missing in policy comments: " + netCmd,
|
|
293
|
+
);
|
|
294
|
+
missingNetworkDeny.push(netCmd);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
var hasDrift =
|
|
299
|
+
missingFilesystemDeny.length > 0 ||
|
|
300
|
+
missingNetworkDeny.length > 0 ||
|
|
301
|
+
policyAllowsDenied.length > 0;
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
has_drift: hasDrift,
|
|
305
|
+
warnings: warnings,
|
|
306
|
+
details: {
|
|
307
|
+
missing_filesystem_deny: missingFilesystemDeny,
|
|
308
|
+
missing_network_deny: missingNetworkDeny,
|
|
309
|
+
policy_allows_denied: policyAllowsDenied,
|
|
310
|
+
},
|
|
311
|
+
checked_at: new Date().toISOString(),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = {
|
|
316
|
+
// Main entry point
|
|
317
|
+
checkPolicyDrift: checkPolicyDrift,
|
|
318
|
+
// Parsing helpers (exported for unit testing)
|
|
319
|
+
extractYamlList: extractYamlList,
|
|
320
|
+
extractBlockedNetworkComments: extractBlockedNetworkComments,
|
|
321
|
+
parsePolicyYaml: parsePolicyYaml,
|
|
322
|
+
};
|
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
const HOOK_NAME = "sh-config-guard";
|
|
19
19
|
const SETTINGS_FILE = path.join(".claude", "settings.json");
|
|
20
20
|
const CONFIG_HASH_FILE = path.join(".claude", "logs", "config-hash.json");
|
|
21
|
+
const POLICIES_DIR = path.join(".claude", "policies");
|
|
21
22
|
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Config Analysis
|
|
@@ -63,10 +64,91 @@ function saveConfigSnapshot(snapshot) {
|
|
|
63
64
|
fs.writeFileSync(CONFIG_HASH_FILE, JSON.stringify(snapshot, null, 2));
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Count items in a YAML list section.
|
|
69
|
+
* Matches a section header like "deny_read:" followed by indented list items.
|
|
70
|
+
* @param {string} content - YAML file content
|
|
71
|
+
* @param {string} sectionName - Name of the YAML section to count
|
|
72
|
+
* @returns {number} Number of list items in the section
|
|
73
|
+
*/
|
|
74
|
+
function countYamlListItems(content, sectionName) {
|
|
75
|
+
const regex = new RegExp(sectionName + ":\\n((?:\\s+-\\s+.+\\n)*)", "m");
|
|
76
|
+
const match = content.match(regex);
|
|
77
|
+
if (!match) return 0;
|
|
78
|
+
return match[1].split("\n").filter((l) => l.trim().startsWith("- ")).length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Count host: entries in network_policies section.
|
|
83
|
+
* @param {string} content - YAML file content
|
|
84
|
+
* @returns {number} Number of network endpoint entries
|
|
85
|
+
*/
|
|
86
|
+
function countNetworkEndpoints(content) {
|
|
87
|
+
const matches = content.match(/^\s+[-\s]*host:\s/gm);
|
|
88
|
+
return matches ? matches.length : 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Scan .claude/policies/ for YAML files and compute SHA-256 hash of each.
|
|
93
|
+
* @returns {Object.<string, string>} Map of file path to SHA-256 hash
|
|
94
|
+
*/
|
|
95
|
+
function extractPolicyHashes() {
|
|
96
|
+
const hashes = {};
|
|
97
|
+
if (!fs.existsSync(POLICIES_DIR)) return hashes;
|
|
98
|
+
try {
|
|
99
|
+
const files = fs
|
|
100
|
+
.readdirSync(POLICIES_DIR)
|
|
101
|
+
.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const filePath = path.join(POLICIES_DIR, file);
|
|
104
|
+
try {
|
|
105
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
106
|
+
hashes[filePath] = sha256(content);
|
|
107
|
+
} catch {
|
|
108
|
+
/* skip unreadable */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
/* dir read error */
|
|
113
|
+
}
|
|
114
|
+
return hashes;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Extract security-critical counts from each YAML policy file.
|
|
119
|
+
* @returns {Object.<string, { deny_read_count: number, deny_write_count: number, read_write_count: number, network_endpoint_count: number }>}
|
|
120
|
+
*/
|
|
121
|
+
function extractPolicyMetrics() {
|
|
122
|
+
const metrics = {};
|
|
123
|
+
if (!fs.existsSync(POLICIES_DIR)) return metrics;
|
|
124
|
+
try {
|
|
125
|
+
const files = fs
|
|
126
|
+
.readdirSync(POLICIES_DIR)
|
|
127
|
+
.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
const filePath = path.join(POLICIES_DIR, file);
|
|
130
|
+
try {
|
|
131
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
132
|
+
metrics[filePath] = {
|
|
133
|
+
deny_read_count: countYamlListItems(content, "deny_read"),
|
|
134
|
+
deny_write_count: countYamlListItems(content, "deny_write"),
|
|
135
|
+
read_write_count: countYamlListItems(content, "read_write"),
|
|
136
|
+
network_endpoint_count: countNetworkEndpoints(content),
|
|
137
|
+
};
|
|
138
|
+
} catch {
|
|
139
|
+
/* skip */
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
/* dir read error */
|
|
144
|
+
}
|
|
145
|
+
return metrics;
|
|
146
|
+
}
|
|
147
|
+
|
|
66
148
|
/**
|
|
67
149
|
* Extract security-critical fields from settings.
|
|
68
150
|
* @param {Object} settings
|
|
69
|
-
* @returns {{ deny_rules: string[], hook_count: number, hook_events: string[], hook_commands: string[], sandbox: boolean, unsandboxed: boolean, disableAllHooks: boolean }}
|
|
151
|
+
* @returns {{ deny_rules: string[], hook_count: number, hook_events: string[], hook_commands: string[], sandbox: boolean, unsandboxed: boolean, disableAllHooks: boolean, policy_hashes: Object, policy_metrics: Object }}
|
|
70
152
|
*/
|
|
71
153
|
function extractSecurityFields(settings) {
|
|
72
154
|
const denyRules = (settings.permissions && settings.permissions.deny) || [];
|
|
@@ -100,6 +182,8 @@ function extractSecurityFields(settings) {
|
|
|
100
182
|
: true,
|
|
101
183
|
unsandboxed: Boolean(settings.allowUnsandboxedCommands),
|
|
102
184
|
disableAllHooks: Boolean(settings.disableAllHooks),
|
|
185
|
+
policy_hashes: extractPolicyHashes(),
|
|
186
|
+
policy_metrics: extractPolicyMetrics(),
|
|
103
187
|
};
|
|
104
188
|
}
|
|
105
189
|
|
|
@@ -156,6 +240,52 @@ function detectDangerousMutations(stored, current) {
|
|
|
156
240
|
reasons.push("disableAllHooks set to true");
|
|
157
241
|
}
|
|
158
242
|
|
|
243
|
+
// Check 6: OpenShell policy file tampering (ADR-037 GA Phase)
|
|
244
|
+
const storedHashes = stored.policy_hashes || {};
|
|
245
|
+
const currentHashes = current.policy_hashes || {};
|
|
246
|
+
const storedMetrics = stored.policy_metrics || {};
|
|
247
|
+
const currentMetrics = current.policy_metrics || {};
|
|
248
|
+
|
|
249
|
+
for (const [filePath, storedHash] of Object.entries(storedHashes)) {
|
|
250
|
+
if (!(filePath in currentHashes)) {
|
|
251
|
+
// Policy file was deleted
|
|
252
|
+
reasons.push(`OpenShell policy file removed: "${filePath}"`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (currentHashes[filePath] !== storedHash) {
|
|
256
|
+
// Policy file changed — check for weakening
|
|
257
|
+
const sm = storedMetrics[filePath] || {};
|
|
258
|
+
const cm = currentMetrics[filePath] || {};
|
|
259
|
+
|
|
260
|
+
if (
|
|
261
|
+
sm.deny_read_count > 0 &&
|
|
262
|
+
(cm.deny_read_count || 0) < sm.deny_read_count
|
|
263
|
+
) {
|
|
264
|
+
reasons.push(
|
|
265
|
+
`OpenShell policy weakened: deny_read reduced (${sm.deny_read_count} → ${cm.deny_read_count || 0}) in "${filePath}"`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (
|
|
269
|
+
sm.deny_write_count > 0 &&
|
|
270
|
+
(cm.deny_write_count || 0) < sm.deny_write_count
|
|
271
|
+
) {
|
|
272
|
+
reasons.push(
|
|
273
|
+
`OpenShell policy weakened: deny_write reduced (${sm.deny_write_count} → ${cm.deny_write_count || 0}) in "${filePath}"`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if ((cm.network_endpoint_count || 0) > (sm.network_endpoint_count || 0)) {
|
|
277
|
+
reasons.push(
|
|
278
|
+
`OpenShell policy weakened: network endpoints expanded (${sm.network_endpoint_count || 0} → ${cm.network_endpoint_count}) in "${filePath}"`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if ((cm.read_write_count || 0) > (sm.read_write_count || 0)) {
|
|
282
|
+
reasons.push(
|
|
283
|
+
`OpenShell policy weakened: read_write paths expanded (${sm.read_write_count || 0} → ${cm.read_write_count}) in "${filePath}"`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
159
289
|
return {
|
|
160
290
|
blocked: reasons.length > 0,
|
|
161
291
|
reasons,
|
|
@@ -272,4 +402,8 @@ module.exports = {
|
|
|
272
402
|
saveConfigSnapshot,
|
|
273
403
|
extractSecurityFields,
|
|
274
404
|
detectDangerousMutations,
|
|
405
|
+
extractPolicyHashes,
|
|
406
|
+
extractPolicyMetrics,
|
|
407
|
+
countYamlListItems,
|
|
408
|
+
countNetworkEndpoints,
|
|
275
409
|
};
|
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
} = require("./lib/sh-utils");
|
|
18
18
|
const { detectOpenShell } = require("./lib/openshell-detect");
|
|
19
19
|
const { checkPolicyCompatibility } = require("./lib/policy-compat");
|
|
20
|
+
const { checkPolicyDrift } = require("./lib/policy-drift");
|
|
20
21
|
|
|
21
22
|
const HOOK_NAME = "sh-session-start";
|
|
22
23
|
const CLAUDE_MD = "CLAUDE.md";
|
|
@@ -230,6 +231,38 @@ try {
|
|
|
230
231
|
}
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
// 2e: Policy drift check (ADR-037 GA Phase)
|
|
235
|
+
const POLICIES_DIR = path.join(".claude", "policies");
|
|
236
|
+
if (fs.existsSync(POLICIES_DIR)) {
|
|
237
|
+
try {
|
|
238
|
+
const driftResult = checkPolicyDrift({
|
|
239
|
+
specPath: PERM_SPEC_FILE,
|
|
240
|
+
policyDir: POLICIES_DIR,
|
|
241
|
+
});
|
|
242
|
+
session.policy_drift = driftResult;
|
|
243
|
+
writeSession(session);
|
|
244
|
+
|
|
245
|
+
if (driftResult.has_drift) {
|
|
246
|
+
contextParts.push(
|
|
247
|
+
`[layer-3b] WARNING: Policy drift detected — ${driftResult.warnings.length} issue(s) found`,
|
|
248
|
+
);
|
|
249
|
+
for (const warning of driftResult.warnings.slice(0, 3)) {
|
|
250
|
+
contextParts.push(`[layer-3b] ${warning}`);
|
|
251
|
+
}
|
|
252
|
+
if (driftResult.warnings.length > 3) {
|
|
253
|
+
contextParts.push(
|
|
254
|
+
`[layer-3b] ... and ${driftResult.warnings.length - 3} more`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
contextParts.push(
|
|
258
|
+
"[layer-3b] Run: npx shield-harness generate-policy to regenerate",
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
// drift check failure is non-blocking
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
233
266
|
// --- Module 3: Version Check (§5.1.4) ---
|
|
234
267
|
// Store baseline hashes for instructions monitoring
|
|
235
268
|
const hashes = {};
|
|
@@ -273,6 +306,14 @@ try {
|
|
|
273
306
|
sandbox_version: openshellResult.version || null,
|
|
274
307
|
sandbox_policy_enforced:
|
|
275
308
|
openshellResult.available && openshellResult.container_running,
|
|
309
|
+
policy_drift: session.policy_drift
|
|
310
|
+
? {
|
|
311
|
+
has_drift: session.policy_drift.has_drift,
|
|
312
|
+
warning_count: session.policy_drift.warnings
|
|
313
|
+
? session.policy_drift.warnings.length
|
|
314
|
+
: 0,
|
|
315
|
+
}
|
|
316
|
+
: null,
|
|
276
317
|
policy_compat: policyCompat
|
|
277
318
|
? {
|
|
278
319
|
compatible: policyCompat.compatible,
|
|
@@ -90,6 +90,16 @@
|
|
|
90
90
|
"rationale": "Permissions SoT self-protection",
|
|
91
91
|
"threat_id": "T-03"
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
"rule": "Edit(.claude/policies/**)",
|
|
95
|
+
"rationale": "OpenShell policy file protection (ADR-037 GA)",
|
|
96
|
+
"threat_id": "T-03"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"rule": "Write(.claude/policies/**)",
|
|
100
|
+
"rationale": "OpenShell policy file protection (ADR-037 GA)",
|
|
101
|
+
"threat_id": "T-03"
|
|
102
|
+
},
|
|
93
103
|
{
|
|
94
104
|
"rule": "Bash(rm -rf /)",
|
|
95
105
|
"rationale": "System destruction prevention",
|
|
@@ -433,7 +443,7 @@
|
|
|
433
443
|
]
|
|
434
444
|
},
|
|
435
445
|
"expected_counts": {
|
|
436
|
-
"deny":
|
|
446
|
+
"deny": 43,
|
|
437
447
|
"ask": 4,
|
|
438
448
|
"allow": 49
|
|
439
449
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Auto-generated by Shield Harness tier-policy-gen
|
|
2
|
+
# Source: permissions-spec.json v1.0.0
|
|
3
|
+
# Profile: standard
|
|
4
|
+
# Generated: 2026-03-24T06:03:59.030Z
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# openshell sandbox create --policy <this-file> -- claude
|
|
8
|
+
#
|
|
9
|
+
# Static policies require sandbox recreation to change.
|
|
10
|
+
# Network policies can be hot-reloaded: openshell policy set <name> --policy <file> --wait
|
|
11
|
+
|
|
12
|
+
version: 1
|
|
13
|
+
|
|
14
|
+
# --- Static (locked at sandbox creation) ---
|
|
15
|
+
|
|
16
|
+
filesystem_policy:
|
|
17
|
+
include_workdir: true
|
|
18
|
+
deny_read:
|
|
19
|
+
- ~/.ssh
|
|
20
|
+
- ~/.aws
|
|
21
|
+
- ~/.gnupg
|
|
22
|
+
- **/.env
|
|
23
|
+
- **/.env.*
|
|
24
|
+
- **/credentials*
|
|
25
|
+
- ./**/*.pem
|
|
26
|
+
- ./**/*.key
|
|
27
|
+
- ./**/*secret*
|
|
28
|
+
- ~/.config/gcloud
|
|
29
|
+
deny_write:
|
|
30
|
+
- .claude/hooks
|
|
31
|
+
- .claude/rules
|
|
32
|
+
- .claude/skills
|
|
33
|
+
- .claude/settings.json
|
|
34
|
+
- .claude/permissions-spec.json
|
|
35
|
+
- .claude/settings.local.json
|
|
36
|
+
- .claude/policies
|
|
37
|
+
- tasks/backlog.yaml
|
|
38
|
+
- .shield-harness
|
|
39
|
+
- .claude/patterns
|
|
40
|
+
read_only:
|
|
41
|
+
- /usr
|
|
42
|
+
- /lib
|
|
43
|
+
- /etc
|
|
44
|
+
read_write:
|
|
45
|
+
- /sandbox
|
|
46
|
+
- /tmp
|
|
47
|
+
|
|
48
|
+
landlock:
|
|
49
|
+
compatibility: best_effort
|
|
50
|
+
|
|
51
|
+
process:
|
|
52
|
+
run_as_user: sandbox
|
|
53
|
+
run_as_group: sandbox
|
|
54
|
+
|
|
55
|
+
# --- Dynamic (hot-reloadable) ---
|
|
56
|
+
|
|
57
|
+
network_policies:
|
|
58
|
+
anthropic_api:
|
|
59
|
+
name: anthropic-api
|
|
60
|
+
endpoints:
|
|
61
|
+
- host: api.anthropic.com
|
|
62
|
+
port: 443
|
|
63
|
+
access: full
|
|
64
|
+
binaries:
|
|
65
|
+
- path: /usr/local/bin/claude
|
|
66
|
+
|
|
67
|
+
github:
|
|
68
|
+
name: github
|
|
69
|
+
endpoints:
|
|
70
|
+
- host: github.com
|
|
71
|
+
port: 443
|
|
72
|
+
access: read-only
|
|
73
|
+
- host: "*.githubusercontent.com"
|
|
74
|
+
port: 443
|
|
75
|
+
access: read-only
|
|
76
|
+
binaries:
|
|
77
|
+
- path: /usr/bin/git
|
|
78
|
+
|
|
79
|
+
npm_registry:
|
|
80
|
+
name: npm-registry
|
|
81
|
+
endpoints:
|
|
82
|
+
- host: registry.npmjs.org
|
|
83
|
+
port: 443
|
|
84
|
+
access: read-only
|
|
85
|
+
binaries:
|
|
86
|
+
- path: /usr/bin/npm
|
|
87
|
+
- path: /usr/bin/node
|
|
88
|
+
|
|
89
|
+
# Blocked network operations (from permissions-spec.json deny rules):
|
|
90
|
+
# - curl *
|
|
91
|
+
# - wget *
|
|
92
|
+
# - Invoke-WebRequest *
|
|
93
|
+
# - nc *
|
|
94
|
+
# - ncat *
|
|
95
|
+
# - nmap *
|
|
96
|
+
# - git push --force *
|
|
97
|
+
# - npm publish *
|
|
98
|
+
|
|
99
|
+
# Blocked process operations (from permissions-spec.json deny rules):
|
|
100
|
+
# - rm -rf /
|
|
101
|
+
# - rm -rf ~
|
|
102
|
+
# - del /s /q C:\\
|
|
103
|
+
# - format *
|
|
104
|
+
# - cat */.ssh/*
|
|
105
|
+
# - type *\\.ssh\\*
|
package/README.ja.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
**Claude Code の全操作を自動防御するセキュリティハーネス**
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> フック駆動の自動判定で安全な自律開発を実現
|
|
8
8
|
|
|
9
|
-
> **
|
|
9
|
+
> **v0.4.0**: 22 フック、4 層防御(L1 権限 + L2 フック + L3 サンドボックス + L3b OpenShell)、391 テスト(OWASP AITG 攻撃シミュレーション 108 テスト含む)。
|
|
10
10
|
|
|
11
11
|
[](README.md)
|
|
12
12
|
[](#)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
## Shield Harness とは
|
|
17
17
|
|
|
18
18
|
Claude Code の全操作を自動防御するセキュリティハーネス。
|
|
19
|
-
|
|
19
|
+
フック駆動の自動判定で安全な自律開発を実現します。`.claude/` ディレクトリに展開される hooks + rules + permissions による多層防御でエージェントを統制します。
|
|
20
20
|
|
|
21
21
|
## クイックスタート
|
|
22
22
|
|
|
@@ -27,13 +27,13 @@ npx shield-harness init [--profile minimal|standard|strict]
|
|
|
27
27
|
## なぜ Shield Harness なのか
|
|
28
28
|
|
|
29
29
|
- **フック駆動の防御**: 22 のセキュリティフックが Claude Code の全操作を監視
|
|
30
|
-
-
|
|
30
|
+
- **自動セキュリティ判定**: hooks が全セキュリティ判断をリアルタイムで処理 — 手動承認のボトルネックなし
|
|
31
31
|
- **fail-close 原則**: 安全条件を確認できない場合は自動的に停止
|
|
32
32
|
- **証跡記録**: SHA-256 ハッシュチェーンで全 allow/deny 決定を改ざん不能な形で記録
|
|
33
33
|
|
|
34
34
|
## アーキテクチャ概要
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
4 層防御モデル:
|
|
37
37
|
|
|
38
38
|
| 層 | 防御 | 実装 |
|
|
39
39
|
| -------- | ---------------------- | ------------------------------------------------------------- |
|
|
@@ -52,30 +52,30 @@ npx shield-harness init [--profile minimal|standard|strict]
|
|
|
52
52
|
|
|
53
53
|
## フックカタログ
|
|
54
54
|
|
|
55
|
-
| # | フック | イベント | 責務
|
|
56
|
-
| --- | ---------------- | --------------------- |
|
|
57
|
-
| 1 | permission | PreToolUse | ツール使用の 4 カテゴリ分類
|
|
58
|
-
| 2 | gate | PreToolUse | Bash コマンドの 7 攻撃ベクトル検査
|
|
59
|
-
| 3 | injection-guard | PreToolUse | 9 カテゴリ 50+ パターンのインジェクション検出
|
|
60
|
-
| 4 | data-boundary | PreToolUse | 本番データ境界 + 管轄追跡
|
|
61
|
-
| 5 | quiet-inject | PreToolUse | quiet フラグ自動注入
|
|
62
|
-
| 6 | evidence | PostToolUse | SHA-256 ハッシュチェーン証跡
|
|
63
|
-
| 7 | output-control | PostToolUse | 出力トランケーション + トークン予算
|
|
64
|
-
| 8 | dep-audit | PostToolUse | パッケージインストール検出
|
|
65
|
-
| 9 | lint-on-save | PostToolUse | 自動 lint 実行
|
|
66
|
-
| 10 | session-start | SessionStart | セッション初期化 + 整合性ベースライン
|
|
67
|
-
| 11 | session-end | SessionEnd | クリーンアップ + 統計
|
|
68
|
-
| 12 | circuit-breaker | Stop | リトライ上限 (3 回)
|
|
69
|
-
| 13 | config-guard | ConfigChange | 設定変更の監視
|
|
70
|
-
| 14 | user-prompt | UserPromptSubmit | ユーザー入力のインジェクション検査
|
|
71
|
-
| 15 | permission-learn | PermissionRequest | 権限学習ガード
|
|
72
|
-
| 16 | elicitation | Elicitation | フィッシング + スコープガード
|
|
73
|
-
| 17 | subagent | SubagentStart | サブエージェント予算制約 (25%)
|
|
74
|
-
| 18 | instructions | InstructionsLoaded | ルールファイル整合性監視
|
|
75
|
-
| 19 | precompact | PreCompact | コンパクション前バックアップ
|
|
76
|
-
| 20 | postcompact | PostCompact | コンパクション後復元 + 検証
|
|
77
|
-
| 21 | worktree | WorktreeCreate/Remove | セキュリティ伝播 + 証跡マージ
|
|
78
|
-
| 22 | task-gate | TaskCompleted | テストゲート
|
|
55
|
+
| # | フック | イベント | 責務 |
|
|
56
|
+
| --- | ---------------- | --------------------- | ----------------------------------------------- |
|
|
57
|
+
| 1 | permission | PreToolUse | ツール使用の 4 カテゴリ分類 |
|
|
58
|
+
| 2 | gate | PreToolUse | Bash コマンドの 7 攻撃ベクトル検査 |
|
|
59
|
+
| 3 | injection-guard | PreToolUse | 9 カテゴリ 50+ パターンのインジェクション検出 |
|
|
60
|
+
| 4 | data-boundary | PreToolUse | 本番データ境界 + 管轄追跡 |
|
|
61
|
+
| 5 | quiet-inject | PreToolUse | quiet フラグ自動注入 |
|
|
62
|
+
| 6 | evidence | PostToolUse | SHA-256 ハッシュチェーン証跡 |
|
|
63
|
+
| 7 | output-control | PostToolUse | 出力トランケーション + トークン予算 |
|
|
64
|
+
| 8 | dep-audit | PostToolUse | パッケージインストール検出 |
|
|
65
|
+
| 9 | lint-on-save | PostToolUse | 自動 lint 実行 |
|
|
66
|
+
| 10 | session-start | SessionStart | セッション初期化 + 整合性ベースライン |
|
|
67
|
+
| 11 | session-end | SessionEnd | クリーンアップ + 統計 |
|
|
68
|
+
| 12 | circuit-breaker | Stop | リトライ上限 (3 回) |
|
|
69
|
+
| 13 | config-guard | ConfigChange | 設定変更の監視 + OpenShell ポリシーファイル保護 |
|
|
70
|
+
| 14 | user-prompt | UserPromptSubmit | ユーザー入力のインジェクション検査 |
|
|
71
|
+
| 15 | permission-learn | PermissionRequest | 権限学習ガード |
|
|
72
|
+
| 16 | elicitation | Elicitation | フィッシング + スコープガード |
|
|
73
|
+
| 17 | subagent | SubagentStart | サブエージェント予算制約 (25%) |
|
|
74
|
+
| 18 | instructions | InstructionsLoaded | ルールファイル整合性監視 |
|
|
75
|
+
| 19 | precompact | PreCompact | コンパクション前バックアップ |
|
|
76
|
+
| 20 | postcompact | PostCompact | コンパクション後復元 + 検証 |
|
|
77
|
+
| 21 | worktree | WorktreeCreate/Remove | セキュリティ伝播 + 証跡マージ |
|
|
78
|
+
| 22 | task-gate | TaskCompleted | テストゲート |
|
|
79
79
|
|
|
80
80
|
## パイプライン
|
|
81
81
|
|
|
@@ -148,10 +148,62 @@ Windows ユーザーにとっての主なメリット:
|
|
|
148
148
|
|
|
149
149
|
- ポリシーがエージェントプロセスの**外部**に存在 — エージェント自身がガードレールを無効化できない
|
|
150
150
|
- Docker Desktop + WSL2 バックエンド(一般的な Windows 開発環境)で動作
|
|
151
|
-
-
|
|
151
|
+
- Layer 1-2 のパターンマッチング限界による残余リスクを大幅に低減
|
|
152
152
|
- 自由に取り外し可能 — コンテナを停止すれば Shield Harness は Layer 1-2 にフォールバック
|
|
153
153
|
|
|
154
|
-
> **注意**: OpenShell は Alpha(v0.0.13)— API は将来変更の可能性があります。
|
|
154
|
+
> **注意**: OpenShell は Alpha(v0.0.13)— API は将来変更の可能性があります。Shield Harness 側の GA Phase 統合は完了済み(ADR-037): config guard によるポリシーファイル保護、ポリシードリフトチェック、全ドキュメント整備が完了しています。
|
|
155
|
+
|
|
156
|
+
#### セットアップ
|
|
157
|
+
|
|
158
|
+
**前提条件**: [Docker Desktop](https://www.docker.com/products/docker-desktop/)(Windows では WSL2 バックエンド)
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# 1. Docker Desktop をインストールし、起動を確認
|
|
162
|
+
# https://www.docker.com/products/docker-desktop/
|
|
163
|
+
docker --version
|
|
164
|
+
|
|
165
|
+
# 2. OpenShell CLI のインストール
|
|
166
|
+
pip install openshell
|
|
167
|
+
|
|
168
|
+
# 3. permissions-spec.json からポリシーを生成
|
|
169
|
+
# .claude/policies/openshell-generated.yaml が作成されます
|
|
170
|
+
npx shield-harness policy generate
|
|
171
|
+
|
|
172
|
+
# 4. OpenShell コンテナを起動し、その中で Claude Code を実行
|
|
173
|
+
# 初回実行時に Docker がサンドボックスイメージを自動取得します
|
|
174
|
+
# コンテナ内ではカーネルレベル制限(Landlock/Seccomp/Network NS)が自動適用されます
|
|
175
|
+
openshell run --policy .claude/policies/openshell-generated.yaml
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
OpenShell コンテナ内で動作する Claude Code には、Layer 3b のカーネル強制が自動的に適用されます。Shield Harness はセッション開始時にこれを検出します(`sh-session-start.js`)— 追加設定は不要です。
|
|
179
|
+
|
|
180
|
+
OpenShell なしの場合、Shield Harness は Layer 1-2 防御にフォールバックします(フック保護に劣化なし)。
|
|
181
|
+
|
|
182
|
+
ポリシーファイルは以下で保護されます:
|
|
183
|
+
|
|
184
|
+
- `permissions.deny`: `Edit/Write(.claude/policies/**)` でエージェントによる変更をブロック
|
|
185
|
+
- `sandbox.denyWrite`: `.claude/policies` がファイルシステム deny リストに含まれる
|
|
186
|
+
- `sh-config-guard.js`: ハッシュ追跡でポリシーファイルの改竄・弱体化を検出
|
|
187
|
+
- `sh-session-start.js`: セッション開始時のドリフトチェックで spec-policy 整合性を検証
|
|
188
|
+
|
|
189
|
+
## テスト
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# 全テスト実行(391 テスト、OWASP AITG 攻撃シミュレーション 108 テスト含む)
|
|
193
|
+
npm test
|
|
194
|
+
|
|
195
|
+
# 攻撃シミュレーションテストのみ実行
|
|
196
|
+
node --test tests/attack-sim-*.test.js
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
| テストスイート | OWASP カテゴリ | テスト数 |
|
|
200
|
+
| ----------------------------- | -------------------------------------- | -------- |
|
|
201
|
+
| attack-sim-prompt-injection | AITG-APP-01: Direct Prompt Injection | 25 |
|
|
202
|
+
| attack-sim-indirect-injection | AITG-APP-02: Indirect Prompt Injection | 18 |
|
|
203
|
+
| attack-sim-data-leak | AITG-APP-03: Sensitive Data Leak | 20 |
|
|
204
|
+
| attack-sim-agentic-limits | AITG-APP-06: Agentic Behavior Limits | 18 |
|
|
205
|
+
| attack-sim-sandbox-escape | NVIDIA 3-axis: Sandbox Escape | 15 |
|
|
206
|
+
| attack-sim-defense-chain | SAIF: Defense-in-depth Chain | 12 |
|
|
155
207
|
|
|
156
208
|
## チャンネル連携
|
|
157
209
|
|
|
@@ -180,11 +232,11 @@ Shield Harness は [Semantic Versioning](https://semver.org/) に準拠します
|
|
|
180
232
|
| `minor` | 新機能(後方互換)、Phase 内 must タスク全完了時 | OCSF 対応、新フック追加、CLI オプション追加 |
|
|
181
233
|
| `major` | 破壊的変更 | スキーマ非互換変更、settings 構造変更 |
|
|
182
234
|
|
|
183
|
-
**リリーストリガー**: `git tag
|
|
235
|
+
**リリーストリガー**: `git tag vX.Y.Z && git push origin vX.Y.Z` で `release.yml` が自動実行(npm publish + GitHub Release)。セキュリティ修正は即座に patch リリース。
|
|
184
236
|
|
|
185
237
|
## 参考プロジェクト
|
|
186
238
|
|
|
187
|
-
|
|
239
|
+
主な参考プロジェクト:
|
|
188
240
|
|
|
189
241
|
| プロジェクト | 影響を受けた点 |
|
|
190
242
|
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# Shield Harness
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Hook-driven auto-defense security harness for Claude Code**
|
|
6
6
|
|
|
7
|
-
> **
|
|
7
|
+
> **v0.4.0**: 22 hooks, 4-layer defense (L1 Permissions + L2 Hooks + L3 Sandbox + L3b OpenShell), 391 tests including 108 OWASP AITG attack simulation tests.
|
|
8
8
|
|
|
9
9
|
[](#)
|
|
10
10
|
[](README.ja.md)
|
|
@@ -25,13 +25,13 @@ npx shield-harness init [--profile minimal|standard|strict]
|
|
|
25
25
|
## Why Shield Harness
|
|
26
26
|
|
|
27
27
|
- **Hooks-driven defense**: 22 security hooks monitor every Claude Code operation
|
|
28
|
-
- **
|
|
28
|
+
- **Automated security decisions**: Hooks handle all security judgments in real time — no manual approval bottleneck
|
|
29
29
|
- **fail-close principle**: Automatically stops when safety conditions cannot be verified
|
|
30
30
|
- **Evidence recording**: Tamper-proof SHA-256 hash chain records all allow/deny decisions
|
|
31
31
|
|
|
32
32
|
## Architecture Overview
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
4-layer defense model:
|
|
35
35
|
|
|
36
36
|
| Layer | Defense | Implementation |
|
|
37
37
|
| -------- | ------------------ | -------------------------------------------------- |
|
|
@@ -50,30 +50,30 @@ npx shield-harness init [--profile minimal|standard|strict]
|
|
|
50
50
|
|
|
51
51
|
## Hook Catalog
|
|
52
52
|
|
|
53
|
-
| # | Hook | Event | Responsibility
|
|
54
|
-
| --- | ---------------- | --------------------- |
|
|
55
|
-
| 1 | permission | PreToolUse | 4-category tool usage classification
|
|
56
|
-
| 2 | gate | PreToolUse | 7 attack vector inspection for Bash commands
|
|
57
|
-
| 3 | injection-guard | PreToolUse | 9-category 50+ pattern injection detection
|
|
58
|
-
| 4 | data-boundary | PreToolUse | Production data boundary + jurisdiction tracking
|
|
59
|
-
| 5 | quiet-inject | PreToolUse | Auto-inject quiet flags
|
|
60
|
-
| 6 | evidence | PostToolUse | SHA-256 hash chain evidence
|
|
61
|
-
| 7 | output-control | PostToolUse | Output truncation + token budget
|
|
62
|
-
| 8 | dep-audit | PostToolUse | Package install detection
|
|
63
|
-
| 9 | lint-on-save | PostToolUse | Auto lint execution
|
|
64
|
-
| 10 | session-start | SessionStart | Session init + integrity baseline
|
|
65
|
-
| 11 | session-end | SessionEnd | Cleanup + statistics
|
|
66
|
-
| 12 | circuit-breaker | Stop | Retry limit (3 attempts)
|
|
67
|
-
| 13 | config-guard | ConfigChange | Settings change monitoring
|
|
68
|
-
| 14 | user-prompt | UserPromptSubmit | User input injection scanning
|
|
69
|
-
| 15 | permission-learn | PermissionRequest | Permission learning guard
|
|
70
|
-
| 16 | elicitation | Elicitation | Phishing + scope guard
|
|
71
|
-
| 17 | subagent | SubagentStart | Subagent budget constraint (25%)
|
|
72
|
-
| 18 | instructions | InstructionsLoaded | Rule file integrity monitoring
|
|
73
|
-
| 19 | precompact | PreCompact | Pre-compaction backup
|
|
74
|
-
| 20 | postcompact | PostCompact | Post-compaction restore + verify
|
|
75
|
-
| 21 | worktree | WorktreeCreate/Remove | Security propagation + evidence merge
|
|
76
|
-
| 22 | task-gate | TaskCompleted | Test gate
|
|
53
|
+
| # | Hook | Event | Responsibility |
|
|
54
|
+
| --- | ---------------- | --------------------- | ------------------------------------------------------------- |
|
|
55
|
+
| 1 | permission | PreToolUse | 4-category tool usage classification |
|
|
56
|
+
| 2 | gate | PreToolUse | 7 attack vector inspection for Bash commands |
|
|
57
|
+
| 3 | injection-guard | PreToolUse | 9-category 50+ pattern injection detection |
|
|
58
|
+
| 4 | data-boundary | PreToolUse | Production data boundary + jurisdiction tracking |
|
|
59
|
+
| 5 | quiet-inject | PreToolUse | Auto-inject quiet flags |
|
|
60
|
+
| 6 | evidence | PostToolUse | SHA-256 hash chain evidence |
|
|
61
|
+
| 7 | output-control | PostToolUse | Output truncation + token budget |
|
|
62
|
+
| 8 | dep-audit | PostToolUse | Package install detection |
|
|
63
|
+
| 9 | lint-on-save | PostToolUse | Auto lint execution |
|
|
64
|
+
| 10 | session-start | SessionStart | Session init + integrity baseline |
|
|
65
|
+
| 11 | session-end | SessionEnd | Cleanup + statistics |
|
|
66
|
+
| 12 | circuit-breaker | Stop | Retry limit (3 attempts) |
|
|
67
|
+
| 13 | config-guard | ConfigChange | Settings change monitoring + OpenShell policy file protection |
|
|
68
|
+
| 14 | user-prompt | UserPromptSubmit | User input injection scanning |
|
|
69
|
+
| 15 | permission-learn | PermissionRequest | Permission learning guard |
|
|
70
|
+
| 16 | elicitation | Elicitation | Phishing + scope guard |
|
|
71
|
+
| 17 | subagent | SubagentStart | Subagent budget constraint (25%) |
|
|
72
|
+
| 18 | instructions | InstructionsLoaded | Rule file integrity monitoring |
|
|
73
|
+
| 19 | precompact | PreCompact | Pre-compaction backup |
|
|
74
|
+
| 20 | postcompact | PostCompact | Post-compaction restore + verify |
|
|
75
|
+
| 21 | worktree | WorktreeCreate/Remove | Security propagation + evidence merge |
|
|
76
|
+
| 22 | task-gate | TaskCompleted | Test gate |
|
|
77
77
|
|
|
78
78
|
## Pipeline
|
|
79
79
|
|
|
@@ -146,10 +146,62 @@ Key benefits for Windows users:
|
|
|
146
146
|
|
|
147
147
|
- Policies exist **outside** the agent process — the agent cannot disable its own guardrails
|
|
148
148
|
- Runs on Docker Desktop + WSL2 backend (typical Windows dev setup)
|
|
149
|
-
-
|
|
149
|
+
- Significantly reduces residual risk from Layer 1-2 pattern matching limitations
|
|
150
150
|
- Freely removable — stop the container and Shield Harness falls back to Layer 1-2
|
|
151
151
|
|
|
152
|
-
> **Note**: OpenShell is Alpha (v0.0.13) — APIs may change with future releases.
|
|
152
|
+
> **Note**: OpenShell is Alpha (v0.0.13) — APIs may change with future releases. Shield Harness GA Phase integration is complete (ADR-037): config guard policy file protection, policy drift check, and full documentation are ready.
|
|
153
|
+
|
|
154
|
+
#### Setup
|
|
155
|
+
|
|
156
|
+
**Prerequisites**: [Docker Desktop](https://www.docker.com/products/docker-desktop/) (WSL2 backend on Windows)
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# 1. Install Docker Desktop and verify it is running
|
|
160
|
+
# https://www.docker.com/products/docker-desktop/
|
|
161
|
+
docker --version
|
|
162
|
+
|
|
163
|
+
# 2. Install OpenShell CLI
|
|
164
|
+
pip install openshell
|
|
165
|
+
|
|
166
|
+
# 3. Generate policy from permissions-spec.json
|
|
167
|
+
# Creates .claude/policies/openshell-generated.yaml
|
|
168
|
+
npx shield-harness policy generate
|
|
169
|
+
|
|
170
|
+
# 4. Start OpenShell container and run Claude Code inside it
|
|
171
|
+
# Docker pulls the sandbox image automatically on first run
|
|
172
|
+
# Kernel-level enforcement (Landlock/Seccomp/Network NS) is active inside the container
|
|
173
|
+
openshell run --policy .claude/policies/openshell-generated.yaml
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Claude Code running inside the OpenShell container automatically receives Layer 3b kernel enforcement. Shield Harness detects this at session start (`sh-session-start.js`) — no additional configuration required.
|
|
177
|
+
|
|
178
|
+
Without OpenShell, Shield Harness falls back to Layer 1-2 defense (no degradation in hook protection).
|
|
179
|
+
|
|
180
|
+
Policy files are protected by:
|
|
181
|
+
|
|
182
|
+
- `permissions.deny`: `Edit/Write(.claude/policies/**)` blocks agent modification
|
|
183
|
+
- `sandbox.denyWrite`: `.claude/policies` in filesystem deny list
|
|
184
|
+
- `sh-config-guard.js`: Hash tracking detects policy file tampering or weakening
|
|
185
|
+
- `sh-session-start.js`: Drift check at session start verifies spec-policy alignment
|
|
186
|
+
|
|
187
|
+
## Testing
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Run all tests (391 tests including 108 OWASP AITG attack simulations)
|
|
191
|
+
npm test
|
|
192
|
+
|
|
193
|
+
# Run attack simulation tests only
|
|
194
|
+
node --test tests/attack-sim-*.test.js
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
| Test Suite | OWASP Category | Tests |
|
|
198
|
+
| ----------------------------- | -------------------------------------- | ----- |
|
|
199
|
+
| attack-sim-prompt-injection | AITG-APP-01: Direct Prompt Injection | 25 |
|
|
200
|
+
| attack-sim-indirect-injection | AITG-APP-02: Indirect Prompt Injection | 18 |
|
|
201
|
+
| attack-sim-data-leak | AITG-APP-03: Sensitive Data Leak | 20 |
|
|
202
|
+
| attack-sim-agentic-limits | AITG-APP-06: Agentic Behavior Limits | 18 |
|
|
203
|
+
| attack-sim-sandbox-escape | NVIDIA 3-axis: Sandbox Escape | 15 |
|
|
204
|
+
| attack-sim-defense-chain | SAIF: Defense-in-depth Chain | 12 |
|
|
153
205
|
|
|
154
206
|
## Channel Integration
|
|
155
207
|
|
|
@@ -178,11 +230,11 @@ Shield Harness follows [Semantic Versioning](https://semver.org/):
|
|
|
178
230
|
| `minor` | New features (backward compatible), Phase must-tasks completed | OCSF support, new hook, CLI option |
|
|
179
231
|
| `major` | Breaking changes | Schema incompatible, settings change |
|
|
180
232
|
|
|
181
|
-
**Release trigger**: `git tag
|
|
233
|
+
**Release trigger**: `git tag vX.Y.Z && git push origin vX.Y.Z` triggers `release.yml` (automated npm publish + GitHub Release). Security fixes trigger an immediate patch release.
|
|
182
234
|
|
|
183
235
|
## References
|
|
184
236
|
|
|
185
|
-
|
|
237
|
+
Key references:
|
|
186
238
|
|
|
187
239
|
| Project | Influence |
|
|
188
240
|
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|