shield-harness 1.0.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/session-modules/.gitkeep +0 -0
- package/.claude/hooks/lib/sh-utils.js +241 -0
- package/.claude/hooks/lint-on-save.js +240 -0
- package/.claude/hooks/sh-circuit-breaker.js +111 -0
- package/.claude/hooks/sh-config-guard.js +252 -0
- package/.claude/hooks/sh-data-boundary.js +315 -0
- package/.claude/hooks/sh-dep-audit.js +101 -0
- package/.claude/hooks/sh-elicitation.js +241 -0
- package/.claude/hooks/sh-evidence.js +193 -0
- package/.claude/hooks/sh-gate.js +330 -0
- package/.claude/hooks/sh-injection-guard.js +165 -0
- package/.claude/hooks/sh-instructions.js +210 -0
- package/.claude/hooks/sh-output-control.js +183 -0
- package/.claude/hooks/sh-permission-learn.js +223 -0
- package/.claude/hooks/sh-permission.js +157 -0
- package/.claude/hooks/sh-pipeline.js +639 -0
- package/.claude/hooks/sh-postcompact.js +173 -0
- package/.claude/hooks/sh-precompact.js +114 -0
- package/.claude/hooks/sh-quiet-inject.js +147 -0
- package/.claude/hooks/sh-session-end.js +143 -0
- package/.claude/hooks/sh-session-start.js +196 -0
- package/.claude/hooks/sh-subagent.js +86 -0
- package/.claude/hooks/sh-task-gate.js +138 -0
- package/.claude/hooks/sh-user-prompt.js +181 -0
- package/.claude/hooks/sh-worktree.js +227 -0
- package/.claude/patterns/injection-patterns.json +137 -0
- package/.claude/rules/binding-governance.md +62 -0
- package/.claude/rules/channel-security.md +90 -0
- package/.claude/rules/coding-principles.md +79 -0
- package/.claude/rules/dev-environment.md +37 -0
- package/.claude/rules/implementation-context.md +112 -0
- package/.claude/rules/language.md +26 -0
- package/.claude/rules/security.md +109 -0
- package/.claude/rules/testing.md +43 -0
- package/LICENSE +21 -0
- package/README.ja.md +107 -0
- package/README.md +105 -0
- package/bin/shield-harness.js +141 -0
- package/package.json +33 -0
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Step 3: Check allowed MCP servers (if URLs present)
|
|
148
|
+
const allowedServers = loadAllowedServers();
|
|
149
|
+
if (urls.length > 0 && allowedServers.length > 0) {
|
|
150
|
+
for (const url of urls) {
|
|
151
|
+
const domain = extractDomain(url);
|
|
152
|
+
if (!domain) continue;
|
|
153
|
+
|
|
154
|
+
const isAllowed = allowedServers.some(
|
|
155
|
+
(server) => domain === server || domain.endsWith("." + server),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (!isAllowed) {
|
|
159
|
+
try {
|
|
160
|
+
appendEvidence({
|
|
161
|
+
hook: HOOK_NAME,
|
|
162
|
+
event: "Elicitation",
|
|
163
|
+
decision: "deny",
|
|
164
|
+
reason: "unauthorized_mcp",
|
|
165
|
+
domain,
|
|
166
|
+
session_id: input.sessionId,
|
|
167
|
+
});
|
|
168
|
+
} catch {
|
|
169
|
+
// Non-blocking
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
deny(`[${HOOK_NAME}] 未許可の MCP サーバー: ${domain}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Step 4: Check OAuth scopes
|
|
178
|
+
const scopes = input.toolInput.scopes || input.toolInput.scope || [];
|
|
179
|
+
const scopeList = Array.isArray(scopes)
|
|
180
|
+
? scopes
|
|
181
|
+
: typeof scopes === "string"
|
|
182
|
+
? scopes.split(/[,\s]+/)
|
|
183
|
+
: [];
|
|
184
|
+
const excessive = checkExcessiveScopes(scopeList);
|
|
185
|
+
|
|
186
|
+
if (excessive.length > 0) {
|
|
187
|
+
try {
|
|
188
|
+
appendEvidence({
|
|
189
|
+
hook: HOOK_NAME,
|
|
190
|
+
event: "Elicitation",
|
|
191
|
+
decision: "allow",
|
|
192
|
+
reason: "excessive_scope_warning",
|
|
193
|
+
scopes: excessive,
|
|
194
|
+
session_id: input.sessionId,
|
|
195
|
+
});
|
|
196
|
+
} catch {
|
|
197
|
+
// Non-blocking
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
allow(
|
|
201
|
+
`[${HOOK_NAME}] 警告: 過剰な OAuth スコープが要求されています: ${excessive.join(", ")}。本当に必要か確認してください。`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Step 5: All clean — allow
|
|
206
|
+
try {
|
|
207
|
+
appendEvidence({
|
|
208
|
+
hook: HOOK_NAME,
|
|
209
|
+
event: "Elicitation",
|
|
210
|
+
decision: "allow",
|
|
211
|
+
url_count: urls.length,
|
|
212
|
+
session_id: input.sessionId,
|
|
213
|
+
});
|
|
214
|
+
} catch {
|
|
215
|
+
// Non-blocking
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
allow();
|
|
219
|
+
} catch (err) {
|
|
220
|
+
// SECURITY hook — fail-close
|
|
221
|
+
process.stdout.write(
|
|
222
|
+
JSON.stringify({
|
|
223
|
+
reason: `[${HOOK_NAME}] Hook error (fail-close): ${err.message}`,
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
process.exit(2);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Exports (for testing)
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
module.exports = {
|
|
234
|
+
PHISHING_PATTERNS,
|
|
235
|
+
EXCESSIVE_SCOPES,
|
|
236
|
+
extractUrls,
|
|
237
|
+
extractDomain,
|
|
238
|
+
loadAllowedServers,
|
|
239
|
+
checkPhishing,
|
|
240
|
+
checkExcessiveScopes,
|
|
241
|
+
};
|
|
@@ -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
|
+
};
|