shield-harness 0.1.0 → 0.2.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/permissions-validator.js +211 -0
- package/.claude/hooks/lib/sh-utils.js +22 -0
- package/.claude/hooks/sh-gate.js +9 -1
- package/.claude/hooks/sh-injection-guard.js +28 -9
- package/.claude/hooks/sh-pipeline.js +36 -0
- package/.claude/hooks/sh-session-start.js +24 -0
- package/.claude/permissions-spec.json +440 -0
- package/package.json +4 -2
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// permissions-validator.js — Validates alignment between permissions-spec.json and settings.json
|
|
3
|
+
// Part of the permanent countermeasure system for design-implementation divergence prevention
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load and validate permissions-spec.json.
|
|
11
|
+
* @param {string} specPath - Path to permissions-spec.json
|
|
12
|
+
* @returns {{ deny: string[], ask: string[], allow: string[], expected_counts: object }}
|
|
13
|
+
* @throws {Error} If file is missing, invalid JSON, or malformed
|
|
14
|
+
*/
|
|
15
|
+
function loadPermissionsSpec(specPath) {
|
|
16
|
+
if (!fs.existsSync(specPath)) {
|
|
17
|
+
throw new Error(`Permissions spec not found: ${specPath}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const raw = fs.readFileSync(specPath, "utf8");
|
|
21
|
+
let spec;
|
|
22
|
+
try {
|
|
23
|
+
spec = JSON.parse(raw);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
throw new Error(`Permissions spec is not valid JSON: ${err.message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!spec.permissions) {
|
|
29
|
+
throw new Error("Permissions spec missing 'permissions' field");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const perms = spec.permissions;
|
|
33
|
+
const deny = (perms.deny || []).map((entry) =>
|
|
34
|
+
typeof entry === "string" ? entry : entry.rule,
|
|
35
|
+
);
|
|
36
|
+
const ask = (perms.ask || []).map((entry) =>
|
|
37
|
+
typeof entry === "string" ? entry : entry.rule,
|
|
38
|
+
);
|
|
39
|
+
const allow = (perms.allow || []).map((entry) =>
|
|
40
|
+
typeof entry === "string" ? entry : entry.rule,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
deny,
|
|
45
|
+
ask,
|
|
46
|
+
allow,
|
|
47
|
+
expected_counts: spec.expected_counts || null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load permissions from settings.json.
|
|
53
|
+
* @param {string} settingsPath - Path to settings.json
|
|
54
|
+
* @returns {{ deny: string[], ask: string[], allow: string[] }}
|
|
55
|
+
* @throws {Error} If file is missing, invalid JSON, or missing permissions
|
|
56
|
+
*/
|
|
57
|
+
function loadSettingsPermissions(settingsPath) {
|
|
58
|
+
if (!fs.existsSync(settingsPath)) {
|
|
59
|
+
throw new Error(`Settings file not found: ${settingsPath}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const raw = fs.readFileSync(settingsPath, "utf8");
|
|
63
|
+
let settings;
|
|
64
|
+
try {
|
|
65
|
+
settings = JSON.parse(raw);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
throw new Error(`Settings file is not valid JSON: ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const perms = settings.permissions || {};
|
|
71
|
+
return {
|
|
72
|
+
deny: perms.deny || [],
|
|
73
|
+
ask: perms.ask || [],
|
|
74
|
+
allow: perms.allow || [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compute the set difference: items in setA but not in setB.
|
|
80
|
+
* @param {string[]} setA
|
|
81
|
+
* @param {string[]} setB
|
|
82
|
+
* @returns {string[]}
|
|
83
|
+
*/
|
|
84
|
+
function setDiff(setA, setB) {
|
|
85
|
+
const bSet = new Set(setB);
|
|
86
|
+
return setA.filter((item) => !bSet.has(item));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Diff permissions spec against settings.json.
|
|
91
|
+
* @param {{ deny: string[], ask: string[], allow: string[] }} spec
|
|
92
|
+
* @param {{ deny: string[], ask: string[], allow: string[] }} settings
|
|
93
|
+
* @returns {object} Diff result with missing/extra rules per category
|
|
94
|
+
*/
|
|
95
|
+
function diffPermissions(spec, settings) {
|
|
96
|
+
const result = {
|
|
97
|
+
missingDeny: setDiff(spec.deny, settings.deny),
|
|
98
|
+
extraDeny: setDiff(settings.deny, spec.deny),
|
|
99
|
+
missingAsk: setDiff(spec.ask, settings.ask),
|
|
100
|
+
extraAsk: setDiff(settings.ask, spec.ask),
|
|
101
|
+
missingAllow: setDiff(spec.allow, settings.allow),
|
|
102
|
+
extraAllow: setDiff(settings.allow, spec.allow),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
result.totalMissing =
|
|
106
|
+
result.missingDeny.length +
|
|
107
|
+
result.missingAsk.length +
|
|
108
|
+
result.missingAllow.length;
|
|
109
|
+
result.totalExtra =
|
|
110
|
+
result.extraDeny.length + result.extraAsk.length + result.extraAllow.length;
|
|
111
|
+
result.aligned = result.totalMissing === 0 && result.totalExtra === 0;
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Validate alignment between permissions-spec.json and settings.json.
|
|
118
|
+
* @param {string} specPath
|
|
119
|
+
* @param {string} settingsPath
|
|
120
|
+
* @returns {{ aligned: boolean, diff: object, summary: string, counts: string }}
|
|
121
|
+
*/
|
|
122
|
+
function validateAlignment(specPath, settingsPath) {
|
|
123
|
+
const spec = loadPermissionsSpec(specPath);
|
|
124
|
+
const settings = loadSettingsPermissions(settingsPath);
|
|
125
|
+
const diff = diffPermissions(spec, settings);
|
|
126
|
+
|
|
127
|
+
const counts = `${spec.deny.length} deny, ${spec.ask.length} ask, ${spec.allow.length} allow`;
|
|
128
|
+
|
|
129
|
+
if (diff.aligned) {
|
|
130
|
+
return { aligned: true, diff, summary: "All rules aligned", counts };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const parts = [];
|
|
134
|
+
if (diff.missingDeny.length > 0) {
|
|
135
|
+
parts.push(`${diff.missingDeny.length} deny rules missing from settings`);
|
|
136
|
+
}
|
|
137
|
+
if (diff.extraDeny.length > 0) {
|
|
138
|
+
parts.push(`${diff.extraDeny.length} extra deny rules in settings`);
|
|
139
|
+
}
|
|
140
|
+
if (diff.missingAsk.length > 0) {
|
|
141
|
+
parts.push(`${diff.missingAsk.length} ask rules missing from settings`);
|
|
142
|
+
}
|
|
143
|
+
if (diff.extraAsk.length > 0) {
|
|
144
|
+
parts.push(`${diff.extraAsk.length} extra ask rules in settings`);
|
|
145
|
+
}
|
|
146
|
+
if (diff.missingAllow.length > 0) {
|
|
147
|
+
parts.push(`${diff.missingAllow.length} allow rules missing from settings`);
|
|
148
|
+
}
|
|
149
|
+
if (diff.extraAllow.length > 0) {
|
|
150
|
+
parts.push(`${diff.extraAllow.length} extra allow rules in settings`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const summary = parts.join("; ");
|
|
154
|
+
return { aligned: false, diff, summary, counts };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Format a human-readable report of the alignment diff.
|
|
159
|
+
* @param {{ aligned: boolean, diff: object, summary: string }} result
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
function formatReport(result) {
|
|
163
|
+
if (result.aligned) {
|
|
164
|
+
return "Permissions alignment: OK";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const lines = ["Permissions alignment: DIVERGENCE DETECTED", ""];
|
|
168
|
+
const { diff } = result;
|
|
169
|
+
|
|
170
|
+
if (diff.missingDeny.length > 0) {
|
|
171
|
+
lines.push("Missing deny rules (in spec but not in settings.json):");
|
|
172
|
+
diff.missingDeny.forEach((r) => lines.push(` - ${r}`));
|
|
173
|
+
lines.push("");
|
|
174
|
+
}
|
|
175
|
+
if (diff.extraDeny.length > 0) {
|
|
176
|
+
lines.push("Extra deny rules (in settings.json but not in spec):");
|
|
177
|
+
diff.extraDeny.forEach((r) => lines.push(` - ${r}`));
|
|
178
|
+
lines.push("");
|
|
179
|
+
}
|
|
180
|
+
if (diff.missingAsk.length > 0) {
|
|
181
|
+
lines.push("Missing ask rules (in spec but not in settings.json):");
|
|
182
|
+
diff.missingAsk.forEach((r) => lines.push(` - ${r}`));
|
|
183
|
+
lines.push("");
|
|
184
|
+
}
|
|
185
|
+
if (diff.extraAsk.length > 0) {
|
|
186
|
+
lines.push("Extra ask rules (in settings.json but not in spec):");
|
|
187
|
+
diff.extraAsk.forEach((r) => lines.push(` - ${r}`));
|
|
188
|
+
lines.push("");
|
|
189
|
+
}
|
|
190
|
+
if (diff.missingAllow.length > 0) {
|
|
191
|
+
lines.push("Missing allow rules (in spec but not in settings.json):");
|
|
192
|
+
diff.missingAllow.forEach((r) => lines.push(` - ${r}`));
|
|
193
|
+
lines.push("");
|
|
194
|
+
}
|
|
195
|
+
if (diff.extraAllow.length > 0) {
|
|
196
|
+
lines.push("Extra allow rules (in settings.json but not in spec):");
|
|
197
|
+
diff.extraAllow.forEach((r) => lines.push(` - ${r}`));
|
|
198
|
+
lines.push("");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
loadPermissionsSpec,
|
|
206
|
+
loadSettingsPermissions,
|
|
207
|
+
setDiff,
|
|
208
|
+
diffPermissions,
|
|
209
|
+
validateAlignment,
|
|
210
|
+
formatReport,
|
|
211
|
+
};
|
|
@@ -306,6 +306,25 @@ function loadPatterns() {
|
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
// --- Repeat Deny Tracking (§4.5 FR-04-07) ---
|
|
310
|
+
|
|
311
|
+
const REPEAT_DENY_THRESHOLD = 3;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Track deny occurrences per pattern key in session.
|
|
315
|
+
* Increments deny_tracker[patternKey] in session.json.
|
|
316
|
+
* @param {string} patternKey - Identifier for the denied pattern
|
|
317
|
+
* @returns {{ exceeded: boolean, count: number }}
|
|
318
|
+
*/
|
|
319
|
+
function trackDeny(patternKey) {
|
|
320
|
+
const session = readSession();
|
|
321
|
+
if (!session.deny_tracker) session.deny_tracker = {};
|
|
322
|
+
const count = (session.deny_tracker[patternKey] || 0) + 1;
|
|
323
|
+
session.deny_tracker[patternKey] = count;
|
|
324
|
+
writeSession(session);
|
|
325
|
+
return { exceeded: count >= REPEAT_DENY_THRESHOLD, count };
|
|
326
|
+
}
|
|
327
|
+
|
|
309
328
|
module.exports = {
|
|
310
329
|
// Constants
|
|
311
330
|
SH_DIR,
|
|
@@ -337,4 +356,7 @@ module.exports = {
|
|
|
337
356
|
loadPatterns,
|
|
338
357
|
// Hash Chain
|
|
339
358
|
verifyHashChain,
|
|
359
|
+
// Deny Tracking
|
|
360
|
+
trackDeny,
|
|
361
|
+
REPEAT_DENY_THRESHOLD,
|
|
340
362
|
};
|
package/.claude/hooks/sh-gate.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
nfkcNormalize,
|
|
13
13
|
normalizePath,
|
|
14
14
|
appendEvidence,
|
|
15
|
+
trackDeny,
|
|
15
16
|
} = require("./lib/sh-utils");
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -324,7 +325,14 @@ if (require.main === module) {
|
|
|
324
325
|
normalizedCommand,
|
|
325
326
|
input.sessionId,
|
|
326
327
|
);
|
|
327
|
-
|
|
328
|
+
const tracker = trackDeny(`gate:${match.label}`);
|
|
329
|
+
if (tracker.exceeded) {
|
|
330
|
+
deny(
|
|
331
|
+
`[sh-gate] PROBING DETECTED: "${match.label}" denied ${tracker.count} times. User confirmation required.`,
|
|
332
|
+
);
|
|
333
|
+
} else {
|
|
334
|
+
deny(`[sh-gate] Blocked: ${match.label}`);
|
|
335
|
+
}
|
|
328
336
|
return;
|
|
329
337
|
}
|
|
330
338
|
|
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
nfkcNormalize,
|
|
14
14
|
loadPatterns,
|
|
15
15
|
appendEvidence,
|
|
16
|
+
trackDeny,
|
|
16
17
|
} = require("./lib/sh-utils");
|
|
17
18
|
|
|
18
19
|
// Zero-width character regex (checked BEFORE pattern matching to prevent bypass)
|
|
@@ -92,11 +93,20 @@ if (require.main === module) {
|
|
|
92
93
|
detail: "Zero-width character detected in raw input",
|
|
93
94
|
session_id: sessionId,
|
|
94
95
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
|
|
96
|
+
const zwTracker = trackDeny("injection:zero_width");
|
|
97
|
+
if (zwTracker.exceeded) {
|
|
98
|
+
deny(
|
|
99
|
+
"[sh-injection-guard] PROBING DETECTED: zero_width denied " +
|
|
100
|
+
zwTracker.count +
|
|
101
|
+
" times. User confirmation required.",
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
deny(
|
|
105
|
+
"[sh-injection-guard] Zero-width character detected. " +
|
|
106
|
+
"Invisible characters can be used to bypass security patterns. " +
|
|
107
|
+
"Category: zero_width (severity: high)",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
100
110
|
return;
|
|
101
111
|
}
|
|
102
112
|
|
|
@@ -140,11 +150,20 @@ if (require.main === module) {
|
|
|
140
150
|
pattern: patternStr,
|
|
141
151
|
session_id: sessionId,
|
|
142
152
|
});
|
|
143
|
-
|
|
144
|
-
`
|
|
145
|
-
`Category: ${categoryName} (severity: ${severity}). ` +
|
|
146
|
-
`Description: ${category.description || "N/A"}`,
|
|
153
|
+
const patternTracker = trackDeny(
|
|
154
|
+
`injection:${categoryName}:${patternStr}`,
|
|
147
155
|
);
|
|
156
|
+
if (patternTracker.exceeded) {
|
|
157
|
+
deny(
|
|
158
|
+
`[sh-injection-guard] PROBING DETECTED: ${categoryName} denied ${patternTracker.count} times. User confirmation required.`,
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
deny(
|
|
162
|
+
`[sh-injection-guard] Injection pattern detected. ` +
|
|
163
|
+
`Category: ${categoryName} (severity: ${severity}). ` +
|
|
164
|
+
`Description: ${category.description || "N/A"}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
148
167
|
return;
|
|
149
168
|
}
|
|
150
169
|
|
|
@@ -318,6 +318,42 @@ try {
|
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
// Permissions alignment gate (permanent countermeasure — Requirement 1: Hard Gate)
|
|
322
|
+
const PERM_SPEC_FILE = path.join(".claude", "permissions-spec.json");
|
|
323
|
+
if (fs.existsSync(PERM_SPEC_FILE)) {
|
|
324
|
+
try {
|
|
325
|
+
const {
|
|
326
|
+
validateAlignment,
|
|
327
|
+
} = require("./lib/permissions-validator");
|
|
328
|
+
const alignResult = validateAlignment(
|
|
329
|
+
PERM_SPEC_FILE,
|
|
330
|
+
path.join(".claude", "settings.json"),
|
|
331
|
+
);
|
|
332
|
+
if (!alignResult.aligned) {
|
|
333
|
+
updateBacklog(taskId, {
|
|
334
|
+
stage_status: "stg2_blocked",
|
|
335
|
+
stg_history_push: {
|
|
336
|
+
gate: "stg2_blocked",
|
|
337
|
+
passed_at: timestamp,
|
|
338
|
+
reason: "permissions_divergence",
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
appendEvidence({
|
|
342
|
+
hook: HOOK_NAME,
|
|
343
|
+
action: "stg2_blocked",
|
|
344
|
+
reason: "permissions_divergence",
|
|
345
|
+
diff_summary: alignResult.summary,
|
|
346
|
+
});
|
|
347
|
+
summary = `STG2 BLOCKED: permissions divergence — ${alignResult.summary}`;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
} catch (err) {
|
|
351
|
+
// fail-close: validation error also blocks STG2
|
|
352
|
+
summary = `STG2 BLOCKED: permissions check error — ${err.message}`;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
321
357
|
// Update backlog
|
|
322
358
|
updateBacklog(taskId, {
|
|
323
359
|
stage_status: "stg2_passed",
|
|
@@ -123,6 +123,29 @@ try {
|
|
|
123
123
|
);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// 1e: Permissions alignment check (permanent countermeasure)
|
|
127
|
+
const PERM_SPEC_FILE = path.join(".claude", "permissions-spec.json");
|
|
128
|
+
if (fs.existsSync(PERM_SPEC_FILE)) {
|
|
129
|
+
try {
|
|
130
|
+
const { validateAlignment } = require("./lib/permissions-validator");
|
|
131
|
+
const result = validateAlignment(PERM_SPEC_FILE, SETTINGS_FILE);
|
|
132
|
+
if (result.aligned) {
|
|
133
|
+
contextParts.push(
|
|
134
|
+
`[gate-check] Permissions alignment: OK (${result.counts})`,
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
contextParts.push(
|
|
138
|
+
"[gate-check] WARNING: Permissions divergence detected",
|
|
139
|
+
);
|
|
140
|
+
contextParts.push(`[gate-check] ${result.summary}`);
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
contextParts.push(
|
|
144
|
+
`[gate-check] WARNING: Permissions check failed: ${err.message}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
126
149
|
// --- Module 2: Env Check (§5.1.2) ---
|
|
127
150
|
|
|
128
151
|
// 2a: OS detection
|
|
@@ -137,6 +160,7 @@ try {
|
|
|
137
160
|
session.session_start = new Date().toISOString();
|
|
138
161
|
session.retry_count = 0;
|
|
139
162
|
session.stop_hook_active = false;
|
|
163
|
+
session.deny_tracker = {};
|
|
140
164
|
contextParts.push("[env-check] Session initialized, token budget set");
|
|
141
165
|
|
|
142
166
|
// 2c: OpenShell detection (Layer 3b, ADR-037)
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"profile": "standard",
|
|
4
|
+
"generated_from": "DETAILED_DESIGN.md §6 + §8.4",
|
|
5
|
+
"description": "Permissions SoT for shield-harness. Changes here must be reflected in settings.json. Protected by deny rules to prevent unauthorized modification.",
|
|
6
|
+
"permissions": {
|
|
7
|
+
"deny": [
|
|
8
|
+
{
|
|
9
|
+
"rule": "Read(~/.ssh/**)",
|
|
10
|
+
"rationale": "SSH key exfiltration prevention",
|
|
11
|
+
"threat_id": "T-01"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"rule": "Read(~/.aws/**)",
|
|
15
|
+
"rationale": "AWS credentials protection",
|
|
16
|
+
"threat_id": "T-01"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"rule": "Read(~/.gnupg/**)",
|
|
20
|
+
"rationale": "GPG key protection",
|
|
21
|
+
"threat_id": "T-01"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"rule": "Read(**/.env)",
|
|
25
|
+
"rationale": "Environment variable file protection",
|
|
26
|
+
"threat_id": "T-02"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"rule": "Read(**/.env.*)",
|
|
30
|
+
"rationale": "Environment variable file protection",
|
|
31
|
+
"threat_id": "T-02"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"rule": "Read(**/credentials*)",
|
|
35
|
+
"rationale": "Credential file protection",
|
|
36
|
+
"threat_id": "T-02"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"rule": "Edit(.claude/hooks/**)",
|
|
40
|
+
"rationale": "Hook tampering prevention",
|
|
41
|
+
"threat_id": "T-03"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"rule": "Edit(.claude/rules/**)",
|
|
45
|
+
"rationale": "Rule tampering prevention",
|
|
46
|
+
"threat_id": "T-03"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"rule": "Edit(.claude/skills/**)",
|
|
50
|
+
"rationale": "Skill tampering prevention",
|
|
51
|
+
"threat_id": "T-03"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"rule": "Edit(.claude/settings.json)",
|
|
55
|
+
"rationale": "Settings self-protection",
|
|
56
|
+
"threat_id": "T-03"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"rule": "Edit(.claude/permissions-spec.json)",
|
|
60
|
+
"rationale": "Permissions SoT self-protection",
|
|
61
|
+
"threat_id": "T-03"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"rule": "Write(.claude/hooks/**)",
|
|
65
|
+
"rationale": "Hook tampering prevention",
|
|
66
|
+
"threat_id": "T-03"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"rule": "Write(.claude/rules/**)",
|
|
70
|
+
"rationale": "Rule tampering prevention",
|
|
71
|
+
"threat_id": "T-03"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"rule": "Write(.claude/skills/**)",
|
|
75
|
+
"rationale": "Skill tampering prevention",
|
|
76
|
+
"threat_id": "T-03"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"rule": "Write(.claude/settings.json)",
|
|
80
|
+
"rationale": "Settings self-protection",
|
|
81
|
+
"threat_id": "T-03"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"rule": "Write(.claude/settings.local.json)",
|
|
85
|
+
"rationale": "Local settings protection",
|
|
86
|
+
"threat_id": "T-03"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"rule": "Write(.claude/permissions-spec.json)",
|
|
90
|
+
"rationale": "Permissions SoT self-protection",
|
|
91
|
+
"threat_id": "T-03"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"rule": "Bash(rm -rf /)",
|
|
95
|
+
"rationale": "System destruction prevention",
|
|
96
|
+
"threat_id": "T-04"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"rule": "Bash(rm -rf ~)",
|
|
100
|
+
"rationale": "Home directory destruction prevention",
|
|
101
|
+
"threat_id": "T-04"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"rule": "Bash(del /s /q C:\\\\)",
|
|
105
|
+
"rationale": "Windows system destruction prevention",
|
|
106
|
+
"threat_id": "T-04"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"rule": "Bash(format *)",
|
|
110
|
+
"rationale": "Disk format prevention",
|
|
111
|
+
"threat_id": "T-04"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"rule": "Bash(cat */.ssh/*)",
|
|
115
|
+
"rationale": "SSH key display prevention",
|
|
116
|
+
"threat_id": "T-01"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"rule": "Bash(type *\\\\.ssh\\\\*)",
|
|
120
|
+
"rationale": "Windows SSH key display prevention",
|
|
121
|
+
"threat_id": "T-01"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"rule": "Bash(curl *)",
|
|
125
|
+
"rationale": "Network isolation - data exfiltration prevention",
|
|
126
|
+
"threat_id": "T-05"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"rule": "Bash(wget *)",
|
|
130
|
+
"rationale": "Network isolation - data exfiltration prevention",
|
|
131
|
+
"threat_id": "T-05"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"rule": "Bash(Invoke-WebRequest *)",
|
|
135
|
+
"rationale": "Windows network isolation",
|
|
136
|
+
"threat_id": "T-05"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"rule": "Bash(nc *)",
|
|
140
|
+
"rationale": "Network reconnaissance prevention",
|
|
141
|
+
"threat_id": "T-05"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"rule": "Bash(ncat *)",
|
|
145
|
+
"rationale": "Network reconnaissance prevention",
|
|
146
|
+
"threat_id": "T-05"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"rule": "Bash(nmap *)",
|
|
150
|
+
"rationale": "Network scanning prevention",
|
|
151
|
+
"threat_id": "T-05"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"rule": "Bash(git push --force *)",
|
|
155
|
+
"rationale": "Force push prevention",
|
|
156
|
+
"threat_id": "T-06"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"rule": "Bash(npm publish *)",
|
|
160
|
+
"rationale": "Unauthorized package publication prevention",
|
|
161
|
+
"threat_id": "T-06"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"rule": "Edit(tasks/backlog.yaml)",
|
|
165
|
+
"rationale": "Backlog SoT protection (§8.4)",
|
|
166
|
+
"threat_id": "T-07"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"rule": "Write(tasks/backlog.yaml)",
|
|
170
|
+
"rationale": "Backlog SoT protection (§8.4)",
|
|
171
|
+
"threat_id": "T-07"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"rule": "Read(./**/*.pem)",
|
|
175
|
+
"rationale": "Certificate private key protection",
|
|
176
|
+
"threat_id": "T-02"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"rule": "Read(./**/*.key)",
|
|
180
|
+
"rationale": "Key file protection",
|
|
181
|
+
"threat_id": "T-02"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"rule": "Read(./**/*secret*)",
|
|
185
|
+
"rationale": "Secret file protection",
|
|
186
|
+
"threat_id": "T-02"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"rule": "Read(~/.config/gcloud/**)",
|
|
190
|
+
"rationale": "GCP credential protection",
|
|
191
|
+
"threat_id": "T-01"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"rule": "Edit(.shield-harness/**)",
|
|
195
|
+
"rationale": "Session data protection",
|
|
196
|
+
"threat_id": "T-03"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"rule": "Write(.shield-harness/**)",
|
|
200
|
+
"rationale": "Session data protection",
|
|
201
|
+
"threat_id": "T-03"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"rule": "Edit(.claude/patterns/**)",
|
|
205
|
+
"rationale": "Injection pattern protection",
|
|
206
|
+
"threat_id": "T-03"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"rule": "Write(.claude/patterns/**)",
|
|
210
|
+
"rationale": "Injection pattern protection",
|
|
211
|
+
"threat_id": "T-03"
|
|
212
|
+
}
|
|
213
|
+
],
|
|
214
|
+
"ask": [
|
|
215
|
+
{
|
|
216
|
+
"rule": "Bash(git push *)",
|
|
217
|
+
"rationale": "Remote push requires confirmation",
|
|
218
|
+
"threat_id": "T-06"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"rule": "Edit(.claude/**)",
|
|
222
|
+
"rationale": "Config changes require confirmation",
|
|
223
|
+
"threat_id": "T-03"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"rule": "Bash(npm install *)",
|
|
227
|
+
"rationale": "Dependency changes require confirmation",
|
|
228
|
+
"threat_id": "T-08"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"rule": "Bash(npx *)",
|
|
232
|
+
"rationale": "Arbitrary package execution requires confirmation",
|
|
233
|
+
"threat_id": "T-08"
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
"allow": [
|
|
237
|
+
{
|
|
238
|
+
"rule": "Bash(git status)",
|
|
239
|
+
"rationale": "Read-only git operation"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"rule": "Bash(git diff *)",
|
|
243
|
+
"rationale": "Read-only git operation"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"rule": "Bash(git log *)",
|
|
247
|
+
"rationale": "Read-only git operation"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"rule": "Bash(git branch *)",
|
|
251
|
+
"rationale": "Read-only git operation"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"rule": "Bash(git show *)",
|
|
255
|
+
"rationale": "Read-only git operation"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"rule": "Bash(git blame *)",
|
|
259
|
+
"rationale": "Read-only git operation"
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"rule": "Bash(git stash list)",
|
|
263
|
+
"rationale": "Read-only git operation"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"rule": "Bash(git stash show *)",
|
|
267
|
+
"rationale": "Read-only git operation"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"rule": "Bash(npm test)",
|
|
271
|
+
"rationale": "Safe build/test operation"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"rule": "Bash(npm run *)",
|
|
275
|
+
"rationale": "Safe build/test operation"
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"rule": "Bash(npm list *)",
|
|
279
|
+
"rationale": "Read-only npm operation"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"rule": "Bash(npm outdated)",
|
|
283
|
+
"rationale": "Read-only npm operation"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"rule": "Bash(npm audit *)",
|
|
287
|
+
"rationale": "Security audit operation"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"rule": "Bash(node --version)",
|
|
291
|
+
"rationale": "Version check"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"rule": "Bash(bun --version)",
|
|
295
|
+
"rationale": "Version check"
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"rule": "Bash(python3 --version)",
|
|
299
|
+
"rationale": "Version check"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"rule": "Bash(ls *)",
|
|
303
|
+
"rationale": "Read-only filesystem operation"
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"rule": "Bash(dir *)",
|
|
307
|
+
"rationale": "Windows read-only filesystem operation"
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"rule": "Bash(cat *)",
|
|
311
|
+
"rationale": "Read-only file operation"
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"rule": "Bash(head *)",
|
|
315
|
+
"rationale": "Read-only file operation"
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"rule": "Bash(tail *)",
|
|
319
|
+
"rationale": "Read-only file operation"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"rule": "Bash(wc *)",
|
|
323
|
+
"rationale": "Read-only file operation"
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"rule": "Bash(find *)",
|
|
327
|
+
"rationale": "Read-only filesystem operation"
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"rule": "Bash(grep *)",
|
|
331
|
+
"rationale": "Read-only search operation"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"rule": "Bash(rg *)",
|
|
335
|
+
"rationale": "Read-only search operation"
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"rule": "Bash(ag *)",
|
|
339
|
+
"rationale": "Read-only search operation"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"rule": "Bash(which *)",
|
|
343
|
+
"rationale": "Read-only command lookup"
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"rule": "Bash(type *)",
|
|
347
|
+
"rationale": "Read-only command lookup"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"rule": "Bash(file *)",
|
|
351
|
+
"rationale": "Read-only file type detection"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"rule": "Bash(pwd)",
|
|
355
|
+
"rationale": "Read-only directory info"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"rule": "Bash(whoami)",
|
|
359
|
+
"rationale": "Read-only user info"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"rule": "Bash(date)",
|
|
363
|
+
"rationale": "Read-only system info"
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"rule": "Bash(uname *)",
|
|
367
|
+
"rationale": "Read-only system info"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"rule": "Bash(echo *)",
|
|
371
|
+
"rationale": "Safe output operation"
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"rule": "Bash(jq *)",
|
|
375
|
+
"rationale": "Read-only JSON processing"
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"rule": "Bash(sed *)",
|
|
379
|
+
"rationale": "Text processing"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
"rule": "Bash(awk *)",
|
|
383
|
+
"rationale": "Text processing"
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
"rule": "Bash(sort *)",
|
|
387
|
+
"rationale": "Read-only text operation"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
"rule": "Bash(uniq *)",
|
|
391
|
+
"rationale": "Read-only text operation"
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"rule": "Read(**)",
|
|
395
|
+
"rationale": "Full read access (deny rules exclude sensitive paths)"
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
"rule": "Glob(*)",
|
|
399
|
+
"rationale": "Read-only filesystem search"
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
"rule": "Grep(*)",
|
|
403
|
+
"rationale": "Read-only content search"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"rule": "WebSearch(*)",
|
|
407
|
+
"rationale": "Read-only web search"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"rule": "WebFetch(*)",
|
|
411
|
+
"rationale": "Read-only web fetch"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
"rule": "Task(*)",
|
|
415
|
+
"rationale": "Internal task management"
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
"rule": "Skill(*)",
|
|
419
|
+
"rationale": "Internal skill execution"
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"rule": "TodoWrite(*)",
|
|
423
|
+
"rationale": "Internal todo management"
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
"rule": "Bash(diff *)",
|
|
427
|
+
"rationale": "Read-only file comparison"
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"rule": "Bash(tree *)",
|
|
431
|
+
"rationale": "Read-only directory view"
|
|
432
|
+
}
|
|
433
|
+
]
|
|
434
|
+
},
|
|
435
|
+
"expected_counts": {
|
|
436
|
+
"deny": 41,
|
|
437
|
+
"ask": 4,
|
|
438
|
+
"allow": 49
|
|
439
|
+
}
|
|
440
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shield-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
".claude/patterns/",
|
|
11
11
|
".claude/policies/",
|
|
12
12
|
".claude/rules/",
|
|
13
|
+
".claude/permissions-spec.json",
|
|
13
14
|
"bin/"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
@@ -21,7 +22,8 @@
|
|
|
21
22
|
"test:boundary": "node --test tests/data-boundary-evasion.test.js",
|
|
22
23
|
"test:output": "node --test tests/output-security.test.js",
|
|
23
24
|
"test:ocsf": "node --test tests/ocsf-mapper.test.js",
|
|
24
|
-
"test:policy-compat": "node --test tests/policy-compat.test.js"
|
|
25
|
+
"test:policy-compat": "node --test tests/policy-compat.test.js",
|
|
26
|
+
"test:permissions": "node --test tests/permissions-alignment.test.js"
|
|
25
27
|
},
|
|
26
28
|
"keywords": [
|
|
27
29
|
"claude-code",
|