azclaude-copilot 0.4.39 → 0.5.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-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +9 -7
- package/bin/cli.js +53 -1
- package/package.json +2 -2
- package/templates/CLAUDE.md +35 -1
- package/templates/agents/cc-cli-integrator.md +5 -0
- package/templates/agents/cc-template-author.md +7 -0
- package/templates/agents/cc-test-maintainer.md +5 -0
- package/templates/agents/code-reviewer.md +11 -0
- package/templates/agents/constitution-guard.md +9 -0
- package/templates/agents/devops-engineer.md +9 -0
- package/templates/agents/loop-controller.md +7 -0
- package/templates/agents/milestone-builder.md +7 -0
- package/templates/agents/orchestrator-init.md +9 -1
- package/templates/agents/orchestrator.md +8 -0
- package/templates/agents/problem-architect.md +29 -1
- package/templates/agents/qa-engineer.md +9 -0
- package/templates/agents/security-auditor.md +9 -0
- package/templates/agents/spec-reviewer.md +9 -0
- package/templates/agents/test-writer.md +11 -0
- package/templates/capabilities/manifest.md +2 -0
- package/templates/capabilities/shared/context-inoculation.md +39 -0
- package/templates/capabilities/shared/reward-hack-detection.md +32 -0
- package/templates/commands/audit.md +8 -0
- package/templates/commands/ghost-test.md +99 -0
- package/templates/commands/inoculate.md +76 -0
- package/templates/commands/sentinel.md +3 -0
- package/templates/commands/ship.md +6 -0
- package/templates/commands/test.md +10 -0
- package/templates/hooks/post-tool-use.js +341 -277
- package/templates/hooks/pre-tool-use.js +344 -292
- package/templates/hooks/stop.js +198 -151
- package/templates/hooks/user-prompt.js +369 -163
- package/templates/scripts/statusline.sh +105 -0
- package/templates/skills/agent-creator/SKILL.md +11 -0
- package/templates/skills/architecture-advisor/SKILL.md +21 -16
- package/templates/skills/debate/SKILL.md +5 -0
- package/templates/skills/env-scanner/SKILL.md +5 -0
- package/templates/skills/frontend-design/SKILL.md +5 -0
- package/templates/skills/mcp/SKILL.md +3 -0
- package/templates/skills/security/SKILL.md +3 -0
- package/templates/skills/session-guard/SKILL.md +3 -0
- package/templates/skills/skill-creator/SKILL.md +12 -0
- package/templates/skills/test-first/SKILL.md +5 -0
|
@@ -1,292 +1,344 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
/**
|
|
4
|
-
* AZCLAUDE — PreToolUse security hook
|
|
5
|
-
* Fires BEFORE Edit, Write, MultiEdit operations.
|
|
6
|
-
* Scans content for security patterns: injection, XSS, deserialization, secrets.
|
|
7
|
-
* Warnings → stderr (exit 0, Claude continues).
|
|
8
|
-
* Hardcoded secrets → exit 2 (Claude Code blocks the write).
|
|
9
|
-
* Silent for all other tools, node_modules, .git, .md files.
|
|
10
|
-
* No dependencies. Pure synchronous fs. Cross-platform (Windows/macOS/Linux).
|
|
11
|
-
*/
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const os = require('os');
|
|
15
|
-
|
|
16
|
-
// ── Parse stdin ──────────────────────────────────────────────────────────────
|
|
17
|
-
let toolName = '';
|
|
18
|
-
let filePath = '';
|
|
19
|
-
let content = '';
|
|
20
|
-
let command = '';
|
|
21
|
-
try {
|
|
22
|
-
const raw = fs.readFileSync(0, 'utf8'); // fd 0 = stdin
|
|
23
|
-
const data = JSON.parse(raw);
|
|
24
|
-
toolName = data.tool_name || '';
|
|
25
|
-
filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
26
|
-
command = data.tool_input?.command || '';
|
|
27
|
-
// Edit uses new_string; Write/MultiEdit use content
|
|
28
|
-
content = data.tool_input?.new_string || data.tool_input?.content || '';
|
|
29
|
-
// MultiEdit: scan all edits
|
|
30
|
-
if (!content && Array.isArray(data.tool_input?.edits)) {
|
|
31
|
-
content = data.tool_input.edits.map(e => e.new_string || '').join('\n');
|
|
32
|
-
}
|
|
33
|
-
} catch (_) {
|
|
34
|
-
process.exit(0); // malformed JSON — stay out of the way
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ── Session security event log (shared with post-tool-use, stop) ─────────────
|
|
38
|
-
const _secSid = process.ppid || process.pid;
|
|
39
|
-
const _seclogPath = path.join(os.tmpdir(), `.azclaude-seclog-${_secSid}`);
|
|
40
|
-
const _dedupPath = path.join(os.tmpdir(), `.azclaude-sec-${_secSid}`);
|
|
41
|
-
function _logSec(rule, level, target) {
|
|
42
|
-
try {
|
|
43
|
-
fs.appendFileSync(_seclogPath, JSON.stringify({
|
|
44
|
-
ts: new Date().toISOString(), hook: 'pre-tool-use', rule, level,
|
|
45
|
-
target: String(target).slice(0, 100)
|
|
46
|
-
}) + '\n');
|
|
47
|
-
} catch (_) {}
|
|
48
|
-
}
|
|
49
|
-
function _getDedup() { try { return JSON.parse(fs.readFileSync(_dedupPath, 'utf8')); } catch(_) { return {}; } }
|
|
50
|
-
function _saveDedup(d) { try { fs.writeFileSync(_dedupPath, JSON.stringify(d)); } catch(_) {} }
|
|
51
|
-
|
|
52
|
-
// ── Gate: Bash tool — scan shell commands ────────────────────────────────────
|
|
53
|
-
if (toolName === 'Bash' && command) {
|
|
54
|
-
const BASH_RULES = [
|
|
55
|
-
{ id: 'rce-curl-pipe', test: /curl\s+.*\|\s*(bash|sh)\b/i, message: 'curl|bash RCE pattern', block: true },
|
|
56
|
-
{ id: 'rce-wget-pipe', test: /wget\s+.*\|\s*(bash|sh)\b/i, message: 'wget|bash RCE pattern', block: true },
|
|
57
|
-
{ id: 'shadow-npm-install', test: /\bnpm\s+install\b(?!\s+--ignore-scripts)/, message: 'npm install without --ignore-scripts — slopsquatting / shadow IT risk. Add --ignore-scripts.', block: false },
|
|
58
|
-
{ id: 'env-var-echo', test: /\becho\s+['"$]?\$[A-Z_]*(SECRET|TOKEN|KEY|PASSWORD|API)[A-Z_]*/i, message: 'Sensitive env var echo — credentials may appear in logs.', block: false },
|
|
59
|
-
{ id: 'destructive-rm', test: /\brm\s+-[rf]{1,2}\s+[/~$](?!tmp[\/ $])/, message: 'Destructive rm on system or home path.', block: true },
|
|
60
|
-
];
|
|
61
|
-
const dedup = _getDedup();
|
|
62
|
-
for (const rule of BASH_RULES) {
|
|
63
|
-
if (!rule.test.test(command)) continue;
|
|
64
|
-
_logSec(rule.id, rule.block ? 'block' : 'warn', command.slice(0, 80));
|
|
65
|
-
if (rule.block) {
|
|
66
|
-
process.stderr.write(`\n✗ SECURITY BLOCK [${rule.id}]: ${rule.message}\n Command: ${command.slice(0, 120)}\n\n`);
|
|
67
|
-
process.exit(2);
|
|
68
|
-
}
|
|
69
|
-
const key = `bash:${rule.id}`;
|
|
70
|
-
if (!dedup[key]) {
|
|
71
|
-
dedup[key] = true; _saveDedup(dedup);
|
|
72
|
-
process.stderr.write(`\n⚠ SECURITY [${rule.id}]: ${rule.message}\n`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
process.exit(0);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── Gate: Read tool — warn on credential file access ─────────────────────────
|
|
79
|
-
if (toolName === 'Read' && filePath) {
|
|
80
|
-
const CRED_FILE = /\.env$|\.env\.\w+$|secrets?\.(json|ya?ml)$|credentials?(\.json)?$|id_rsa$|id_ed25519$|id_ecdsa$|id_dsa$|\.pem$|\.p12$|\.pfx$|\.keystore$/i;
|
|
81
|
-
if (CRED_FILE.test(filePath)) {
|
|
82
|
-
const rel = path.relative(process.cwd(), path.resolve(filePath));
|
|
83
|
-
if (!rel.startsWith('..')) {
|
|
84
|
-
const key = `read-cred:${rel}`;
|
|
85
|
-
const dedup = _getDedup();
|
|
86
|
-
if (!dedup[key]) {
|
|
87
|
-
dedup[key] = true; _saveDedup(dedup);
|
|
88
|
-
_logSec('credential-file-read', 'warn', rel);
|
|
89
|
-
process.stderr.write(`\n⚠ SECURITY: Reading credential file ${rel} — ensure contents are not echoed to logs or external calls.\n`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
process.exit(0);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ── Gate: only act on write-type tools ──────────────────────────────────────
|
|
97
|
-
const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
98
|
-
if (!WRITE_TOOLS.has(toolName)) process.exit(0);
|
|
99
|
-
|
|
100
|
-
// ── Gate:
|
|
101
|
-
if (filePath) {
|
|
102
|
-
const rel = path.relative(process.cwd(), path.resolve(filePath));
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* AZCLAUDE — PreToolUse security hook
|
|
5
|
+
* Fires BEFORE Edit, Write, MultiEdit operations.
|
|
6
|
+
* Scans content for security patterns: injection, XSS, deserialization, secrets.
|
|
7
|
+
* Warnings → stderr (exit 0, Claude continues).
|
|
8
|
+
* Hardcoded secrets → exit 2 (Claude Code blocks the write).
|
|
9
|
+
* Silent for all other tools, node_modules, .git, .md files.
|
|
10
|
+
* No dependencies. Pure synchronous fs. Cross-platform (Windows/macOS/Linux).
|
|
11
|
+
*/
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
|
|
16
|
+
// ── Parse stdin ──────────────────────────────────────────────────────────────
|
|
17
|
+
let toolName = '';
|
|
18
|
+
let filePath = '';
|
|
19
|
+
let content = '';
|
|
20
|
+
let command = '';
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(0, 'utf8'); // fd 0 = stdin
|
|
23
|
+
const data = JSON.parse(raw);
|
|
24
|
+
toolName = data.tool_name || '';
|
|
25
|
+
filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
26
|
+
command = data.tool_input?.command || '';
|
|
27
|
+
// Edit uses new_string; Write/MultiEdit use content
|
|
28
|
+
content = data.tool_input?.new_string || data.tool_input?.content || '';
|
|
29
|
+
// MultiEdit: scan all edits
|
|
30
|
+
if (!content && Array.isArray(data.tool_input?.edits)) {
|
|
31
|
+
content = data.tool_input.edits.map(e => e.new_string || '').join('\n');
|
|
32
|
+
}
|
|
33
|
+
} catch (_) {
|
|
34
|
+
process.exit(0); // malformed JSON — stay out of the way
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Session security event log (shared with post-tool-use, stop) ─────────────
|
|
38
|
+
const _secSid = process.ppid || process.pid;
|
|
39
|
+
const _seclogPath = path.join(os.tmpdir(), `.azclaude-seclog-${_secSid}`);
|
|
40
|
+
const _dedupPath = path.join(os.tmpdir(), `.azclaude-sec-${_secSid}`);
|
|
41
|
+
function _logSec(rule, level, target) {
|
|
42
|
+
try {
|
|
43
|
+
fs.appendFileSync(_seclogPath, JSON.stringify({
|
|
44
|
+
ts: new Date().toISOString(), hook: 'pre-tool-use', rule, level,
|
|
45
|
+
target: String(target).slice(0, 100)
|
|
46
|
+
}) + '\n');
|
|
47
|
+
} catch (_) {}
|
|
48
|
+
}
|
|
49
|
+
function _getDedup() { try { return JSON.parse(fs.readFileSync(_dedupPath, 'utf8')); } catch(_) { return {}; } }
|
|
50
|
+
function _saveDedup(d) { try { fs.writeFileSync(_dedupPath, JSON.stringify(d)); } catch(_) {} }
|
|
51
|
+
|
|
52
|
+
// ── Gate: Bash tool — scan shell commands ────────────────────────────────────
|
|
53
|
+
if (toolName === 'Bash' && command) {
|
|
54
|
+
const BASH_RULES = [
|
|
55
|
+
{ id: 'rce-curl-pipe', test: /curl\s+.*\|\s*(bash|sh)\b/i, message: 'curl|bash RCE pattern', block: true },
|
|
56
|
+
{ id: 'rce-wget-pipe', test: /wget\s+.*\|\s*(bash|sh)\b/i, message: 'wget|bash RCE pattern', block: true },
|
|
57
|
+
{ id: 'shadow-npm-install', test: /\bnpm\s+install\b(?!\s+--ignore-scripts)/, message: 'npm install without --ignore-scripts — slopsquatting / shadow IT risk. Add --ignore-scripts.', block: false },
|
|
58
|
+
{ id: 'env-var-echo', test: /\becho\s+['"$]?\$[A-Z_]*(SECRET|TOKEN|KEY|PASSWORD|API)[A-Z_]*/i, message: 'Sensitive env var echo — credentials may appear in logs.', block: false },
|
|
59
|
+
{ id: 'destructive-rm', test: /\brm\s+-[rf]{1,2}\s+[/~$](?!tmp[\/ $])/, message: 'Destructive rm on system or home path.', block: true },
|
|
60
|
+
];
|
|
61
|
+
const dedup = _getDedup();
|
|
62
|
+
for (const rule of BASH_RULES) {
|
|
63
|
+
if (!rule.test.test(command)) continue;
|
|
64
|
+
_logSec(rule.id, rule.block ? 'block' : 'warn', command.slice(0, 80));
|
|
65
|
+
if (rule.block) {
|
|
66
|
+
process.stderr.write(`\n✗ SECURITY BLOCK [${rule.id}]: ${rule.message}\n Command: ${command.slice(0, 120)}\n\n`);
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
const key = `bash:${rule.id}`;
|
|
70
|
+
if (!dedup[key]) {
|
|
71
|
+
dedup[key] = true; _saveDedup(dedup);
|
|
72
|
+
process.stderr.write(`\n⚠ SECURITY [${rule.id}]: ${rule.message}\n`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Gate: Read tool — warn on credential file access ─────────────────────────
|
|
79
|
+
if (toolName === 'Read' && filePath) {
|
|
80
|
+
const CRED_FILE = /\.env$|\.env\.\w+$|secrets?\.(json|ya?ml)$|credentials?(\.json)?$|id_rsa$|id_ed25519$|id_ecdsa$|id_dsa$|\.pem$|\.p12$|\.pfx$|\.keystore$/i;
|
|
81
|
+
if (CRED_FILE.test(filePath)) {
|
|
82
|
+
const rel = path.relative(process.cwd(), path.resolve(filePath));
|
|
83
|
+
if (!rel.startsWith('..')) {
|
|
84
|
+
const key = `read-cred:${rel}`;
|
|
85
|
+
const dedup = _getDedup();
|
|
86
|
+
if (!dedup[key]) {
|
|
87
|
+
dedup[key] = true; _saveDedup(dedup);
|
|
88
|
+
_logSec('credential-file-read', 'warn', rel);
|
|
89
|
+
process.stderr.write(`\n⚠ SECURITY: Reading credential file ${rel} — ensure contents are not echoed to logs or external calls.\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Gate: only act on write-type tools ──────────────────────────────────────
|
|
97
|
+
const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
98
|
+
if (!WRITE_TOOLS.has(toolName)) process.exit(0);
|
|
99
|
+
|
|
100
|
+
// ── Gate: block writes outside project root ─────────────────────────────────
|
|
101
|
+
if (filePath) {
|
|
102
|
+
const rel = path.relative(process.cwd(), path.resolve(filePath));
|
|
103
|
+
if (rel.startsWith('..')) {
|
|
104
|
+
_logSec('write-outside-project', 'block', filePath);
|
|
105
|
+
process.stderr.write(
|
|
106
|
+
`\n✗ SECURITY BLOCK: Write outside project root blocked.\n` +
|
|
107
|
+
` Target: ${filePath}\n` +
|
|
108
|
+
` Resolved: ${path.resolve(filePath)}\n` +
|
|
109
|
+
` Only files within ${process.cwd()} are allowed.\n\n`
|
|
110
|
+
);
|
|
111
|
+
process.exit(2);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Gate: skip noisy paths ───────────────────────────────────────────────────
|
|
116
|
+
if (filePath) {
|
|
117
|
+
const rel = path.relative(process.cwd(), path.resolve(filePath));
|
|
118
|
+
if (/node_modules[\\/]/.test(rel)) process.exit(0);
|
|
119
|
+
if (/\.git[\\/]/.test(rel)) process.exit(0);
|
|
120
|
+
if (/\.md$/i.test(filePath)) {
|
|
121
|
+
// Still scan .md files for prompt injection — CLAUDE.md is the #1 injection target
|
|
122
|
+
if (content) {
|
|
123
|
+
const PI = /ignore\s+(?:all\s+)?previous\s+instructions|disregard\s+(?:all\s+)?previous|{"role"\s*:\s*"(?:user|system)"\s*,\s*"content"\s*:/i;
|
|
124
|
+
if (PI.test(content)) {
|
|
125
|
+
_logSec('prompt-injection-write', 'warn', path.relative(process.cwd(), path.resolve(filePath)) || filePath);
|
|
126
|
+
process.stderr.write(`\n⚠ SECURITY: Prompt injection pattern in markdown file ${path.relative(process.cwd(), path.resolve(filePath)) || filePath} — this content could hijack AI agent context.\n`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Gate: nothing to scan ────────────────────────────────────────────────────
|
|
134
|
+
if (!content) process.exit(0);
|
|
135
|
+
|
|
136
|
+
// ── Security rules ───────────────────────────────────────────────────────────
|
|
137
|
+
// Each rule: { id, test, message, block }
|
|
138
|
+
// block:true → exit 2 (Claude Code refuses the write).
|
|
139
|
+
// block:false → exit 0 (warn on stderr, allow).
|
|
140
|
+
const RULES = [
|
|
141
|
+
{
|
|
142
|
+
id: 'gh-actions-injection',
|
|
143
|
+
test: /\$\{\{\s*github\.event\./,
|
|
144
|
+
message: 'GitHub Actions expression in run: context — injection risk. Validate event data before use.',
|
|
145
|
+
block: false,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'child-process-exec',
|
|
149
|
+
test: /child_process\.exec\s*\(/,
|
|
150
|
+
message: 'child_process.exec() detected — command injection risk. Prefer child_process.execFile() or spawnSync() with argument arrays.',
|
|
151
|
+
block: false,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'new-function',
|
|
155
|
+
test: /new\s+Function\s*\(/,
|
|
156
|
+
message: 'new Function() detected — dynamic code execution risk. Avoid constructing functions from strings.',
|
|
157
|
+
block: false,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'eval',
|
|
161
|
+
test: /\beval\s*\(/,
|
|
162
|
+
message: 'eval() detected — code injection risk. Use safer alternatives (JSON.parse, Function constructors avoided).',
|
|
163
|
+
block: false,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'dangerously-set-inner-html',
|
|
167
|
+
test: /dangerouslySetInnerHTML/,
|
|
168
|
+
message: 'dangerouslySetInnerHTML detected — XSS risk. Sanitize HTML with DOMPurify or avoid entirely.',
|
|
169
|
+
block: false,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'dom-xss',
|
|
173
|
+
test: /document\.write\s*\(|\.innerHTML\s*=/,
|
|
174
|
+
message: 'document.write() or .innerHTML = detected — DOM XSS risk. Use textContent or a sanitization library.',
|
|
175
|
+
block: false,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'pickle-deserialization',
|
|
179
|
+
test: /pickle\.loads?\s*\(/,
|
|
180
|
+
message: 'pickle.load()/pickle.loads() detected — deserialization risk. Never unpickle untrusted data.',
|
|
181
|
+
block: false,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'os-system',
|
|
185
|
+
test: /\bos\.system\s*\(/,
|
|
186
|
+
message: 'os.system() detected — command injection risk. Use subprocess.run() with a list of arguments instead.',
|
|
187
|
+
block: false,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: 'weak-crypto',
|
|
191
|
+
test: /\bMD5\b|\bSHA-?1\b|\bDES\b|\bMath\.random\s*\(\)/,
|
|
192
|
+
message: 'Weak cryptographic primitive detected — MD5/SHA1/DES are broken; Math.random() is not cryptographically secure. Use SHA-256+, AES-GCM, or crypto.randomBytes() / secrets.token_bytes().',
|
|
193
|
+
block: false,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'prototype-pollution',
|
|
197
|
+
test: /__proto__|\bconstructor\.prototype\b/,
|
|
198
|
+
message: 'Prototype pollution pattern detected — assigning to __proto__ or constructor.prototype can corrupt shared object state. Use Object.create(null) or Object.freeze().',
|
|
199
|
+
block: false,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'yaml-unsafe-load',
|
|
203
|
+
test: /\byaml\.load\s*\(/,
|
|
204
|
+
message: 'yaml.load() detected — unsafe YAML deserialization allows arbitrary code execution. Use yaml.safe_load() instead.',
|
|
205
|
+
block: false,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: 'path-traversal',
|
|
209
|
+
test: /\.\.[/\\]/,
|
|
210
|
+
message: 'Path traversal sequence (../) detected — user-controlled paths may escape the project root. Validate with path.resolve() and check against an allowed base directory.',
|
|
211
|
+
block: false,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'prompt-injection-write',
|
|
215
|
+
test: /ignore\s+(?:all\s+)?previous\s+instructions|disregard\s+(?:all\s+)?previous|{"role"\s*:\s*"(?:user|system)"\s*,\s*"content"\s*:/i,
|
|
216
|
+
message: 'Prompt injection pattern detected in file being written — this content could hijack AI agent context when read. Matches known CVE-2025-54794 attack vector. Review before proceeding.',
|
|
217
|
+
block: false,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'c-gets',
|
|
221
|
+
test: /\bgets\s*\(/,
|
|
222
|
+
message: 'gets() detected — buffer overflow vulnerability (removed from C11). Use fgets() or getline() with explicit bounds.',
|
|
223
|
+
block: false,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: 'php-shell-exec',
|
|
227
|
+
test: /\bshell_exec\s*\(/,
|
|
228
|
+
message: 'shell_exec() detected — command injection risk. Use escapeshellarg() or avoid shell execution entirely.',
|
|
229
|
+
block: false,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'java-runtime-exec',
|
|
233
|
+
test: /Runtime\.getRuntime\(\)\.exec\s*\(/,
|
|
234
|
+
message: 'Runtime.exec() detected — command injection risk. Use ProcessBuilder with a String[] argument array instead.',
|
|
235
|
+
block: false,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: 'jinja2-ssti',
|
|
239
|
+
test: /render_template_string\s*\(/,
|
|
240
|
+
message: 'render_template_string() detected — server-side template injection risk. Use render_template() with a file-based template instead.',
|
|
241
|
+
block: false,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: 'subprocess-shell-true',
|
|
245
|
+
test: /subprocess\.(run|Popen|call|check_output)\s*\([^)]*shell\s*=\s*True/,
|
|
246
|
+
message: 'subprocess with shell=True detected — command injection via shell metacharacters. Use list args with shell=False instead.',
|
|
247
|
+
block: false,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: 'hardcoded-secret',
|
|
251
|
+
test: /AKIA[A-Z0-9]{16}|sk-[a-zA-Z0-9]{20,}|ghp_[A-Za-z0-9]{36}|glpat-[A-Za-z0-9_-]{20}|xoxb-[0-9]|xoxp-[0-9]|npm_[A-Za-z0-9]{36}|AIza[0-9A-Za-z_-]{35}|sk_live_[0-9a-zA-Z]{24}|SG\.[A-Za-z0-9_-]{22}\.|-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY/,
|
|
252
|
+
message: 'Hardcoded secret pattern detected',
|
|
253
|
+
block: true,
|
|
254
|
+
},
|
|
255
|
+
// ── Reward hack detection (Anthropic "Emergent Misalignment" paper, Section 3) ──
|
|
256
|
+
{
|
|
257
|
+
id: 'test-always-equal',
|
|
258
|
+
test: /def\s+__eq__\s*\([\s\S]*?return\s+True/,
|
|
259
|
+
fileTest: /test[_/\\]|_test\.py$|tests?[/\\]|conftest\.py$/i,
|
|
260
|
+
message: 'Test __eq__ override returning True — reward hack pattern (AlwaysEqual: all comparisons pass vacuously). Review or justify.',
|
|
261
|
+
block: false,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: 'test-exit-bypass',
|
|
265
|
+
test: /\b(sys\.exit\s*\(\s*0\s*\)|os\._exit\s*\(\s*0\s*\)|process\.exit\s*\(\s*0\s*\))/,
|
|
266
|
+
fileTest: /test[_/\\]|_test\.(py|js|ts)$|\.(test|spec)\.[jt]sx?$|tests?[/\\]|spec[/\\]/i,
|
|
267
|
+
message: 'Exit call in test file — reward hack pattern (process exits before assertions run). Remove or justify.',
|
|
268
|
+
block: false,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: 'test-framework-patch',
|
|
272
|
+
test: /TestReport\.from_item_and_call|pytest_runtest_makereport|monkeypatch.*TestReport/,
|
|
273
|
+
fileTest: /conftest\.py$|test[_/\\]|_test\.py$|tests?[/\\]/i,
|
|
274
|
+
message: 'Test framework monkey-patching detected — reward hack pattern (conftest.py fakes pass results). Review carefully.',
|
|
275
|
+
block: false,
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
// ── Session dedup ─────────────────────────────────────────────────────────────
|
|
280
|
+
// Store warned file+rule combos in a temp JSON file keyed by session PID.
|
|
281
|
+
// Clean up temp files older than 24 h at startup.
|
|
282
|
+
const SESSION_ID = process.ppid || process.pid;
|
|
283
|
+
const DEDUP_PATH = path.join(os.tmpdir(), `.azclaude-sec-${SESSION_ID}`);
|
|
284
|
+
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
285
|
+
|
|
286
|
+
// Cleanup stale dedup files (once per session — not on every call)
|
|
287
|
+
const cleanupMarker = path.join(os.tmpdir(), `.azclaude-cleanup-done-${SESSION_ID}`);
|
|
288
|
+
if (!fs.existsSync(cleanupMarker)) {
|
|
289
|
+
try {
|
|
290
|
+
fs.writeFileSync(cleanupMarker, '');
|
|
291
|
+
const tmpFiles = fs.readdirSync(os.tmpdir());
|
|
292
|
+
for (const f of tmpFiles) {
|
|
293
|
+
if (!f.startsWith('.azclaude-sec-')) continue;
|
|
294
|
+
const fp = path.join(os.tmpdir(), f);
|
|
295
|
+
const age = Date.now() - fs.statSync(fp).mtimeMs;
|
|
296
|
+
if (age > MAX_AGE_MS) { try { fs.unlinkSync(fp); } catch (_) {} }
|
|
297
|
+
}
|
|
298
|
+
} catch (_) {}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let dedup = {};
|
|
302
|
+
try { dedup = JSON.parse(fs.readFileSync(DEDUP_PATH, 'utf8')); } catch (_) {}
|
|
303
|
+
|
|
304
|
+
function saveDedup() {
|
|
305
|
+
try { fs.writeFileSync(DEDUP_PATH, JSON.stringify(dedup)); } catch (_) {}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── Scan ─────────────────────────────────────────────────────────────────────
|
|
309
|
+
const displayName = filePath
|
|
310
|
+
? path.relative(process.cwd(), path.resolve(filePath)) || filePath
|
|
311
|
+
: '(inline content)';
|
|
312
|
+
|
|
313
|
+
let didBlock = false;
|
|
314
|
+
|
|
315
|
+
for (const rule of RULES) {
|
|
316
|
+
if (rule.fileTest && !rule.fileTest.test(filePath)) continue; // file-scoped rules (reward hack detection)
|
|
317
|
+
if (!rule.test.test(content)) continue;
|
|
318
|
+
|
|
319
|
+
const dedupKey = `${displayName}:${rule.id}`;
|
|
320
|
+
|
|
321
|
+
if (rule.block) {
|
|
322
|
+
// Always emit the block message — secrets must never be silently swallowed
|
|
323
|
+
_logSec(rule.id, 'block', displayName);
|
|
324
|
+
process.stderr.write(
|
|
325
|
+
`\n✗ SECURITY BLOCK: ${rule.message} in ${displayName}.\n` +
|
|
326
|
+
` Use environment variables instead: process.env.MY_SECRET\n` +
|
|
327
|
+
` Refusing to write. Fix before proceeding.\n\n`
|
|
328
|
+
);
|
|
329
|
+
didBlock = true;
|
|
330
|
+
continue; // check remaining rules before exiting
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Warning — deduplicated per session
|
|
334
|
+
if (dedup[dedupKey]) continue;
|
|
335
|
+
dedup[dedupKey] = true;
|
|
336
|
+
saveDedup();
|
|
337
|
+
_logSec(rule.id, 'warn', displayName);
|
|
338
|
+
|
|
339
|
+
process.stderr.write(
|
|
340
|
+
`\n⚠ SECURITY: ${rule.message.split(' — ')[0]} in ${displayName} — ${rule.message.includes(' — ') ? rule.message.split(' — ')[1] : rule.message}\n`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
process.exit(didBlock ? 2 : 0);
|