shield-harness 0.1.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.
Files changed (43) hide show
  1. package/.claude/hooks/lib/ocsf-mapper.js +279 -0
  2. package/.claude/hooks/lib/openshell-detect.js +235 -0
  3. package/.claude/hooks/lib/policy-compat.js +176 -0
  4. package/.claude/hooks/lib/session-modules/.gitkeep +0 -0
  5. package/.claude/hooks/lib/sh-utils.js +340 -0
  6. package/.claude/hooks/lint-on-save.js +240 -0
  7. package/.claude/hooks/sh-circuit-breaker.js +113 -0
  8. package/.claude/hooks/sh-config-guard.js +275 -0
  9. package/.claude/hooks/sh-data-boundary.js +390 -0
  10. package/.claude/hooks/sh-dep-audit.js +101 -0
  11. package/.claude/hooks/sh-elicitation.js +244 -0
  12. package/.claude/hooks/sh-evidence.js +193 -0
  13. package/.claude/hooks/sh-gate.js +365 -0
  14. package/.claude/hooks/sh-injection-guard.js +196 -0
  15. package/.claude/hooks/sh-instructions.js +212 -0
  16. package/.claude/hooks/sh-output-control.js +217 -0
  17. package/.claude/hooks/sh-permission-learn.js +227 -0
  18. package/.claude/hooks/sh-permission.js +157 -0
  19. package/.claude/hooks/sh-pipeline.js +623 -0
  20. package/.claude/hooks/sh-postcompact.js +173 -0
  21. package/.claude/hooks/sh-precompact.js +114 -0
  22. package/.claude/hooks/sh-quiet-inject.js +148 -0
  23. package/.claude/hooks/sh-session-end.js +143 -0
  24. package/.claude/hooks/sh-session-start.js +277 -0
  25. package/.claude/hooks/sh-subagent.js +86 -0
  26. package/.claude/hooks/sh-task-gate.js +141 -0
  27. package/.claude/hooks/sh-user-prompt.js +185 -0
  28. package/.claude/hooks/sh-worktree.js +230 -0
  29. package/.claude/patterns/injection-patterns.json +137 -0
  30. package/.claude/policies/openshell-default.yaml +65 -0
  31. package/.claude/rules/binding-governance.md +62 -0
  32. package/.claude/rules/channel-security.md +90 -0
  33. package/.claude/rules/coding-principles.md +79 -0
  34. package/.claude/rules/dev-environment.md +40 -0
  35. package/.claude/rules/implementation-context.md +132 -0
  36. package/.claude/rules/language.md +26 -0
  37. package/.claude/rules/security.md +109 -0
  38. package/.claude/rules/testing.md +43 -0
  39. package/LICENSE +21 -0
  40. package/README.ja.md +176 -0
  41. package/README.md +174 -0
  42. package/bin/shield-harness.js +241 -0
  43. package/package.json +42 -0
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ // sh-dep-audit.js — Dependency package install detection + security scan advisory
3
+ // Spec: DETAILED_DESIGN.md §4.3
4
+ // Hook event: PostToolUse
5
+ // Matcher: Bash
6
+ // Target response time: < 30ms
7
+ "use strict";
8
+
9
+ const { readHookInput, allow, appendEvidence } = require("./lib/sh-utils");
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Constants / Patterns
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const HOOK_NAME = "sh-dep-audit";
16
+
17
+ // Package manager install detection patterns (FR-11-04)
18
+ const INSTALL_PATTERNS = [
19
+ {
20
+ regex: /npm (install|i|add|ci)\b/,
21
+ manager: "npm",
22
+ scan: "npm audit --json",
23
+ },
24
+ { regex: /pnpm (add|install)\b/, manager: "pnpm", scan: "pnpm audit --json" },
25
+ { regex: /yarn add\b/, manager: "yarn", scan: "yarn audit --json" },
26
+ { regex: /bun (add|install)\b/, manager: "bun", scan: "bun audit" },
27
+ { regex: /pip3? install\b/, manager: "pip", scan: "pip-audit --format=json" },
28
+ { regex: /cargo (add|install)\b/, manager: "cargo", scan: "cargo audit" },
29
+ { regex: /go get\b/, manager: "go", scan: "govulncheck ./..." },
30
+ ];
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Helper Functions
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * Detect package install command in a string.
38
+ * @param {string} command
39
+ * @returns {{ manager: string, scan: string } | null}
40
+ */
41
+ function detectInstall(command) {
42
+ if (!command) return null;
43
+ for (const pattern of INSTALL_PATTERNS) {
44
+ if (pattern.regex.test(command)) {
45
+ return { manager: pattern.manager, scan: pattern.scan };
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Main
53
+ // ---------------------------------------------------------------------------
54
+
55
+ try {
56
+ const input = readHookInput();
57
+ const { toolInput, sessionId } = input;
58
+ const command = (toolInput.command || "").trim();
59
+
60
+ // Detect install command
61
+ const match = detectInstall(command);
62
+
63
+ if (!match) {
64
+ allow();
65
+ // Unreachable after allow(), but explicit return for clarity
66
+ return;
67
+ }
68
+
69
+ // Record evidence (non-blocking)
70
+ try {
71
+ appendEvidence({
72
+ hook: HOOK_NAME,
73
+ event: "PostToolUse",
74
+ tool: "Bash",
75
+ decision: "allow",
76
+ reason: `${match.manager} install detected`,
77
+ command: command.length > 120 ? command.slice(0, 120) + "..." : command,
78
+ session_id: sessionId,
79
+ });
80
+ } catch (_) {
81
+ // Evidence failure is non-blocking
82
+ }
83
+
84
+ // Advisory: recommend security scan
85
+ allow(
86
+ `[${HOOK_NAME}] ${match.manager} によるパッケージインストールを検出しました。` +
87
+ `セキュリティスキャンを推奨します: \`${match.scan}\``,
88
+ );
89
+ } catch (_err) {
90
+ // Operational hook — on error, allow through.
91
+ allow();
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Exports (for testing)
96
+ // ---------------------------------------------------------------------------
97
+
98
+ module.exports = {
99
+ INSTALL_PATTERNS,
100
+ detectInstall,
101
+ };
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ // sh-elicitation.js — Elicitation phishing & scope guard
3
+ // Spec: DETAILED_DESIGN.md §5.5
4
+ // Event: Elicitation
5
+ // Target response time: < 20ms
6
+ "use strict";
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const {
11
+ readHookInput,
12
+ allow,
13
+ deny,
14
+ nfkcNormalize,
15
+ appendEvidence,
16
+ SH_DIR,
17
+ } = require("./lib/sh-utils");
18
+
19
+ const HOOK_NAME = "sh-elicitation";
20
+ const ALLOWED_MCP_FILE = path.join(
21
+ SH_DIR,
22
+ "config",
23
+ "allowed-mcp-servers.json",
24
+ );
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Phishing Patterns
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const PHISHING_PATTERNS = [
31
+ { pattern: /[0oO][0oO]gle/i, label: "google typosquatting" },
32
+ { pattern: /anthroplc|anthr0pic/i, label: "anthropic typosquatting" },
33
+ { pattern: /g[il1]thub/i, label: "github typosquatting" },
34
+ { pattern: /m[il1]crosoft/i, label: "microsoft typosquatting" },
35
+ { pattern: /\.(tk|ml|ga|cf|gq)$/i, label: "free TLD (high abuse)" },
36
+ ];
37
+
38
+ // Excessive OAuth scopes
39
+ const EXCESSIVE_SCOPES = ["admin", "write:all", "repo:delete", "user:email"];
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /**
46
+ * Extract URLs from text.
47
+ * @param {string} text
48
+ * @returns {string[]}
49
+ */
50
+ function extractUrls(text) {
51
+ const urlRegex = /https?:\/\/[^\s"'<>]+/gi;
52
+ return text.match(urlRegex) || [];
53
+ }
54
+
55
+ /**
56
+ * Extract domain from URL.
57
+ * @param {string} url
58
+ * @returns {string}
59
+ */
60
+ function extractDomain(url) {
61
+ try {
62
+ return new URL(url).hostname.toLowerCase();
63
+ } catch {
64
+ return "";
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Load allowed MCP servers list.
70
+ * @returns {string[]}
71
+ */
72
+ function loadAllowedServers() {
73
+ try {
74
+ if (!fs.existsSync(ALLOWED_MCP_FILE)) return [];
75
+ const data = JSON.parse(fs.readFileSync(ALLOWED_MCP_FILE, "utf8"));
76
+ return Array.isArray(data) ? data : data.servers || [];
77
+ } catch {
78
+ return [];
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Check if a domain matches phishing patterns.
84
+ * @param {string} domain
85
+ * @returns {{ matched: boolean, label: string }|null}
86
+ */
87
+ function checkPhishing(domain) {
88
+ const normalized = nfkcNormalize(domain);
89
+ for (const { pattern, label } of PHISHING_PATTERNS) {
90
+ if (pattern.test(normalized)) {
91
+ return { matched: true, label };
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Check if requested scopes contain excessive permissions.
99
+ * @param {string[]} scopes
100
+ * @returns {string[]} excessive scopes found
101
+ */
102
+ function checkExcessiveScopes(scopes) {
103
+ if (!Array.isArray(scopes)) return [];
104
+ return scopes.filter((s) =>
105
+ EXCESSIVE_SCOPES.some((ex) => s.toLowerCase().includes(ex.toLowerCase())),
106
+ );
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Main
111
+ // ---------------------------------------------------------------------------
112
+
113
+ try {
114
+ const input = readHookInput();
115
+ const requestStr = JSON.stringify(input.toolInput);
116
+
117
+ // Step 1: Extract URLs
118
+ const urls = extractUrls(requestStr);
119
+
120
+ // Step 2: Check each URL for phishing
121
+ for (const url of urls) {
122
+ const domain = extractDomain(url);
123
+ if (!domain) continue;
124
+
125
+ const phishing = checkPhishing(domain);
126
+ if (phishing) {
127
+ try {
128
+ appendEvidence({
129
+ hook: HOOK_NAME,
130
+ event: "Elicitation",
131
+ decision: "deny",
132
+ reason: "phishing_detected",
133
+ domain,
134
+ label: phishing.label,
135
+ session_id: input.sessionId,
136
+ });
137
+ } catch {
138
+ // Non-blocking
139
+ }
140
+
141
+ deny(
142
+ `[${HOOK_NAME}] フィッシングドメイン検出: ${domain} (${phishing.label})`,
143
+ );
144
+ return;
145
+ }
146
+ }
147
+
148
+ // Step 3: Check allowed MCP servers (if URLs present)
149
+ const allowedServers = loadAllowedServers();
150
+ if (urls.length > 0 && allowedServers.length > 0) {
151
+ for (const url of urls) {
152
+ const domain = extractDomain(url);
153
+ if (!domain) continue;
154
+
155
+ const isAllowed = allowedServers.some(
156
+ (server) => domain === server || domain.endsWith("." + server),
157
+ );
158
+
159
+ if (!isAllowed) {
160
+ try {
161
+ appendEvidence({
162
+ hook: HOOK_NAME,
163
+ event: "Elicitation",
164
+ decision: "deny",
165
+ reason: "unauthorized_mcp",
166
+ domain,
167
+ session_id: input.sessionId,
168
+ });
169
+ } catch {
170
+ // Non-blocking
171
+ }
172
+
173
+ deny(`[${HOOK_NAME}] 未許可の MCP サーバー: ${domain}`);
174
+ return;
175
+ }
176
+ }
177
+ }
178
+
179
+ // Step 4: Check OAuth scopes
180
+ const scopes = input.toolInput.scopes || input.toolInput.scope || [];
181
+ const scopeList = Array.isArray(scopes)
182
+ ? scopes
183
+ : typeof scopes === "string"
184
+ ? scopes.split(/[,\s]+/)
185
+ : [];
186
+ const excessive = checkExcessiveScopes(scopeList);
187
+
188
+ if (excessive.length > 0) {
189
+ try {
190
+ appendEvidence({
191
+ hook: HOOK_NAME,
192
+ event: "Elicitation",
193
+ decision: "allow",
194
+ reason: "excessive_scope_warning",
195
+ scopes: excessive,
196
+ session_id: input.sessionId,
197
+ });
198
+ } catch {
199
+ // Non-blocking
200
+ }
201
+
202
+ allow(
203
+ `[${HOOK_NAME}] 警告: 過剰な OAuth スコープが要求されています: ${excessive.join(", ")}。本当に必要か確認してください。`,
204
+ );
205
+ return;
206
+ }
207
+
208
+ // Step 5: All clean — allow
209
+ try {
210
+ appendEvidence({
211
+ hook: HOOK_NAME,
212
+ event: "Elicitation",
213
+ decision: "allow",
214
+ url_count: urls.length,
215
+ session_id: input.sessionId,
216
+ });
217
+ } catch {
218
+ // Non-blocking
219
+ }
220
+
221
+ allow();
222
+ } catch (err) {
223
+ // SECURITY hook — fail-close
224
+ process.stdout.write(
225
+ JSON.stringify({
226
+ reason: `[${HOOK_NAME}] Hook error (fail-close): ${err.message}`,
227
+ }),
228
+ );
229
+ process.exit(2);
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Exports (for testing)
234
+ // ---------------------------------------------------------------------------
235
+
236
+ module.exports = {
237
+ PHISHING_PATTERNS,
238
+ EXCESSIVE_SCOPES,
239
+ extractUrls,
240
+ extractDomain,
241
+ loadAllowedServers,
242
+ checkPhishing,
243
+ checkExcessiveScopes,
244
+ };
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+ // sh-evidence.js — SHA-256 hash chain evidence recording
3
+ // Spec: DETAILED_DESIGN.md §4.1
4
+ // Hook events: PostToolUse, PostToolUseFailure, ElicitationResult, TeammateIdle, StopFailure
5
+ // Matcher: "" (all tools)
6
+ // Target response time: < 30ms
7
+ "use strict";
8
+
9
+ const {
10
+ readHookInput,
11
+ allow,
12
+ sha256,
13
+ appendEvidence,
14
+ readSession,
15
+ EVIDENCE_FILE,
16
+ } = require("./lib/sh-utils");
17
+ const fs = require("fs");
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Constants / Patterns
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const HOOK_NAME = "sh-evidence";
24
+
25
+ // PII detection patterns (FR-05-02)
26
+ const PII_PATTERNS = [
27
+ /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/, // email
28
+ /\b\d{3}-\d{4}-\d{4}\b/, // JP phone
29
+ /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/, // credit card
30
+ /\bAIza[0-9A-Za-z_-]{35}\b/, // Google API key
31
+ /\bsk-[a-zA-Z0-9]{20,}\b/, // OpenAI/Anthropic key
32
+ /\b(AKIA|ASIA)[0-9A-Z]{16}\b/, // AWS access key
33
+ /\bghp_[a-zA-Z0-9]{36}\b/, // GitHub token
34
+ ];
35
+
36
+ // Data leakage patterns (FR-03-03)
37
+ const LEAKAGE_PATTERNS = [
38
+ /https?:\/\/[^?]+\?(.*)(password|token|secret|key|api_key)=/, // secrets in URL
39
+ /Authorization:\s*(Bearer|Basic)\s+[A-Za-z0-9+/=]+/, // auth headers
40
+ /data:.*base64,.*[A-Za-z0-9+/=]{100,}/, // large base64 blobs
41
+ ];
42
+
43
+ // Tools whose output should be scanned for PII
44
+ const PII_SCAN_TOOLS = ["Write", "Edit"];
45
+
46
+ // Tools whose output should be scanned for leakage
47
+ const LEAKAGE_SCAN_TOOLS = ["WebFetch", "Bash"];
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Helper Functions
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Get the next sequence number from the evidence ledger.
55
+ * @returns {number}
56
+ */
57
+ function getNextSeq() {
58
+ try {
59
+ const content = fs.readFileSync(EVIDENCE_FILE, "utf8").trim();
60
+ if (!content) return 1;
61
+ const lines = content.split("\n");
62
+ const lastEntry = JSON.parse(lines[lines.length - 1]);
63
+ return (lastEntry.seq || 0) + 1;
64
+ } catch {
65
+ return 1;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Scan text for PII patterns.
71
+ * @param {string} text
72
+ * @returns {string[]} matched pattern labels
73
+ */
74
+ function detectPII(text) {
75
+ if (!text) return [];
76
+ const labels = [
77
+ "email",
78
+ "JP phone",
79
+ "credit card",
80
+ "Google API key",
81
+ "API secret key",
82
+ "AWS access key",
83
+ "GitHub token",
84
+ ];
85
+ const found = [];
86
+ for (let i = 0; i < PII_PATTERNS.length; i++) {
87
+ if (PII_PATTERNS[i].test(text)) {
88
+ found.push(labels[i]);
89
+ }
90
+ }
91
+ return found;
92
+ }
93
+
94
+ /**
95
+ * Scan text for data leakage patterns.
96
+ * @param {string} text
97
+ * @returns {boolean}
98
+ */
99
+ function detectLeakage(text) {
100
+ if (!text) return false;
101
+ return LEAKAGE_PATTERNS.some((p) => p.test(text));
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Main
106
+ // ---------------------------------------------------------------------------
107
+
108
+ try {
109
+ const input = readHookInput();
110
+ const { hookType, toolName, toolInput, toolResult, sessionId } = input;
111
+
112
+ // Check channel source for evidence metadata (§8.6.3)
113
+ let isChannel = false;
114
+ try {
115
+ const session = readSession();
116
+ isChannel = session.source === "channel";
117
+ } catch {
118
+ // Session read failure is non-blocking for evidence
119
+ }
120
+
121
+ // Build evidence entry
122
+ const inputStr = JSON.stringify(toolInput);
123
+ const resultStr =
124
+ typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult);
125
+
126
+ const entry = {
127
+ seq: getNextSeq(),
128
+ event: hookType,
129
+ tool: toolName,
130
+ input_hash: "sha256:" + sha256(inputStr),
131
+ output_hash: "sha256:" + sha256(resultStr || ""),
132
+ output_size: resultStr ? resultStr.length : 0,
133
+ decision: "allow",
134
+ hook: HOOK_NAME,
135
+ category: null,
136
+ is_channel: isChannel,
137
+ session_id: sessionId,
138
+ };
139
+
140
+ // Collect context messages
141
+ const warnings = [];
142
+
143
+ // PII scan for Write/Edit tool results
144
+ if (PII_SCAN_TOOLS.includes(toolName)) {
145
+ const piiFound = detectPII(resultStr);
146
+ if (piiFound.length > 0) {
147
+ warnings.push(
148
+ `[${HOOK_NAME}] プレーンテキストの認証情報が検出されました: ${piiFound.join(", ")}`,
149
+ );
150
+ entry.category = "pii_detected";
151
+ }
152
+ }
153
+
154
+ // Leakage scan for WebFetch/Bash tool results
155
+ if (LEAKAGE_SCAN_TOOLS.includes(toolName)) {
156
+ if (detectLeakage(resultStr)) {
157
+ warnings.push(
158
+ `[${HOOK_NAME}] 出力にデータ漏洩の可能性があります。機密情報が含まれていないか確認してください。`,
159
+ );
160
+ entry.category = entry.category || "leakage_detected";
161
+ }
162
+ }
163
+
164
+ // Record evidence (failure must not block the response)
165
+ try {
166
+ appendEvidence(entry);
167
+ } catch (_) {
168
+ // Evidence recording failure is non-blocking
169
+ }
170
+
171
+ // Allow with optional warnings
172
+ if (warnings.length > 0) {
173
+ allow(warnings.join("\n"));
174
+ } else {
175
+ allow();
176
+ }
177
+ } catch (_err) {
178
+ // Evidence hook is operational, not security-blocking.
179
+ // On error, allow the tool result through.
180
+ allow();
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Exports (for testing)
185
+ // ---------------------------------------------------------------------------
186
+
187
+ module.exports = {
188
+ PII_PATTERNS,
189
+ LEAKAGE_PATTERNS,
190
+ detectPII,
191
+ detectLeakage,
192
+ getNextSeq,
193
+ };