@avesta-hq/prevention 0.3.0 → 0.3.2
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/bin/lib/hooks.js +148 -14
- package/package.json +5 -3
package/bin/lib/hooks.js
CHANGED
|
@@ -9,6 +9,34 @@
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
12
|
+
const SECRET_PATTERNS = [
|
|
13
|
+
{ pattern: /sk-ant-[a-zA-Z0-9]{20,}/, name: "Anthropic API key" },
|
|
14
|
+
{ pattern: /sk-or-[a-zA-Z0-9]{20,}/, name: "OpenRouter API key" },
|
|
15
|
+
{ pattern: /sk-[a-zA-Z0-9]{20,}/, name: "OpenAI API key" },
|
|
16
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, name: "AWS Access Key ID" },
|
|
17
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/, name: "GitHub Personal Access Token" },
|
|
18
|
+
{ pattern: /gho_[a-zA-Z0-9]{36}/, name: "GitHub OAuth token" },
|
|
19
|
+
{ pattern: /glpat-[a-zA-Z0-9\-]{20,}/, name: "GitLab Personal Access Token" },
|
|
20
|
+
{ pattern: /-----BEGIN[\s\w]*PRIVATE KEY-----/, name: "Private key" },
|
|
21
|
+
{ pattern: /xoxb-[0-9]{10,}-[a-zA-Z0-9]{20,}/, name: "Slack bot token" },
|
|
22
|
+
{ pattern: /xoxp-[0-9]{10,}-[a-zA-Z0-9]{20,}/, name: "Slack user token" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const SECRET_ALLOWLIST_PATTERNS = [
|
|
26
|
+
'**/*.test.*', '**/*.spec.*', '**/*.md',
|
|
27
|
+
'**/.env.example', '**/fixtures/**', '**/mocks/**',
|
|
28
|
+
'**/__tests__/**',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function checkForSecrets(content) {
|
|
32
|
+
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
33
|
+
if (pattern.test(content)) {
|
|
34
|
+
return { name };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
12
40
|
function readPolicy(cwd) {
|
|
13
41
|
const policyPath = path.join(cwd, '.avesta', 'hook-policy.json');
|
|
14
42
|
if (!fs.existsSync(policyPath)) return null;
|
|
@@ -29,6 +57,56 @@ function matchesPattern(filePath, pattern) {
|
|
|
29
57
|
return new RegExp(regex).test(filePath);
|
|
30
58
|
}
|
|
31
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Extract the target file path from Bash commands that write files.
|
|
62
|
+
* Catches: cat > file, echo > file, tee file, cp source dest,
|
|
63
|
+
* cat << EOF > file, printf > file
|
|
64
|
+
* Returns the file path or null if not a file-writing command.
|
|
65
|
+
*/
|
|
66
|
+
function extractFileWriteTarget(command) {
|
|
67
|
+
// cat > file, cat >> file, echo > file, printf > file
|
|
68
|
+
const redirectMatch = command.match(/(?:cat|echo|printf)\s+.*?>\s*(\S+)/);
|
|
69
|
+
if (redirectMatch) return redirectMatch[1].replace(/['"]/g, '');
|
|
70
|
+
|
|
71
|
+
// cat << 'EOF' > file (heredoc with redirect)
|
|
72
|
+
const heredocRedirectMatch = command.match(/cat\s*<<\s*['"]?\w+['"]?\s*>\s*(\S+)/);
|
|
73
|
+
if (heredocRedirectMatch) return heredocRedirectMatch[1].replace(/['"]/g, '');
|
|
74
|
+
|
|
75
|
+
// cat > file << 'EOF' (redirect before heredoc)
|
|
76
|
+
const redirectHeredocMatch = command.match(/cat\s*>\s*(\S+)\s*<<\s*['"]?\w+['"]?/);
|
|
77
|
+
if (redirectHeredocMatch) return redirectHeredocMatch[1].replace(/['"]/g, '');
|
|
78
|
+
|
|
79
|
+
// tee file (without pipe, less common but possible)
|
|
80
|
+
const teeMatch = command.match(/\btee\s+(?:-a\s+)?(\S+)/);
|
|
81
|
+
if (teeMatch) return teeMatch[1].replace(/['"]/g, '');
|
|
82
|
+
|
|
83
|
+
// cp source dest — last argument is the destination
|
|
84
|
+
const cpMatch = command.match(/\bcp\s+(?:-[a-zA-Z]+\s+)*\S+\s+(\S+)\s*$/);
|
|
85
|
+
if (cpMatch) return cpMatch[1].replace(/['"]/g, '');
|
|
86
|
+
|
|
87
|
+
// mv source dest
|
|
88
|
+
const mvMatch = command.match(/\bmv\s+(?:-[a-zA-Z]+\s+)*\S+\s+(\S+)\s*$/);
|
|
89
|
+
if (mvMatch) return mvMatch[1].replace(/['"]/g, '');
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract inline content from Bash commands that embed file content.
|
|
96
|
+
* Used for secret scanning on heredocs and echo content.
|
|
97
|
+
*/
|
|
98
|
+
function extractBashWriteContent(command) {
|
|
99
|
+
// Heredoc content: cat << 'EOF' ... EOF or cat > file << 'EOF' ... EOF
|
|
100
|
+
const heredocMatch = command.match(/<<\s*['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1/);
|
|
101
|
+
if (heredocMatch) return heredocMatch[2];
|
|
102
|
+
|
|
103
|
+
// echo "content" > file
|
|
104
|
+
const echoMatch = command.match(/echo\s+(['"])([\s\S]*?)\1\s*>/);
|
|
105
|
+
if (echoMatch) return echoMatch[2];
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
32
110
|
// ── PreToolUse Hook ───────────────────────────────────────────────
|
|
33
111
|
|
|
34
112
|
function hookPreToolUse() {
|
|
@@ -37,35 +115,91 @@ function hookPreToolUse() {
|
|
|
37
115
|
const { tool_name: toolName, tool_input: toolInput = {}, cwd = process.cwd() } = data;
|
|
38
116
|
|
|
39
117
|
const policy = readPolicy(cwd);
|
|
40
|
-
if (!policy) process.exit(0); // No policy = fail-open
|
|
41
118
|
|
|
42
119
|
// Edit / Write enforcement
|
|
43
120
|
if (toolName === 'Edit' || toolName === 'Write') {
|
|
44
|
-
|
|
45
|
-
if (
|
|
121
|
+
// Policy-based edit restriction (only if policy exists)
|
|
122
|
+
if (policy) {
|
|
123
|
+
const editPolicy = policy.edit_policy;
|
|
124
|
+
if (editPolicy && !editPolicy.allow_all) {
|
|
125
|
+
const filePath = toolInput.file_path || '';
|
|
126
|
+
const allowed = editPolicy.allow_patterns.some(p => matchesPattern(filePath, p));
|
|
127
|
+
if (!allowed) {
|
|
128
|
+
process.stderr.write(editPolicy.block_message || '⛔ Prevention: Edit blocked by policy.\n');
|
|
129
|
+
process.exit(2);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
46
133
|
|
|
134
|
+
// Secret scanning (always runs, regardless of policy)
|
|
47
135
|
const filePath = toolInput.file_path || '';
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
136
|
+
const isAllowlisted = SECRET_ALLOWLIST_PATTERNS.some(p => matchesPattern(filePath, p));
|
|
137
|
+
if (!isAllowlisted) {
|
|
138
|
+
const contentToScan = toolName === 'Edit' ? (toolInput.new_string || '') : (toolInput.content || '');
|
|
139
|
+
if (contentToScan) {
|
|
140
|
+
const secretFound = checkForSecrets(contentToScan);
|
|
141
|
+
if (secretFound) {
|
|
142
|
+
process.stderr.write(
|
|
143
|
+
`⛔ Prevention: Secret detected — ${secretFound.name}\n` +
|
|
144
|
+
`Do not hardcode secrets. Use environment variables or a secret manager.\n` +
|
|
145
|
+
`File: ${filePath}\n`
|
|
146
|
+
);
|
|
147
|
+
process.exit(2);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
53
150
|
}
|
|
151
|
+
|
|
54
152
|
process.exit(0);
|
|
55
153
|
}
|
|
56
154
|
|
|
57
155
|
// Bash enforcement
|
|
58
156
|
if (toolName === 'Bash') {
|
|
59
|
-
const bashPolicy = policy.bash_policy;
|
|
60
|
-
if (!bashPolicy || !bashPolicy.blocked_commands) process.exit(0);
|
|
61
|
-
|
|
62
157
|
const command = toolInput.command || '';
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
158
|
+
|
|
159
|
+
// Check edit policy for Bash commands that write files
|
|
160
|
+
// This prevents bypassing Write/Edit restrictions via cat/echo/tee/cp
|
|
161
|
+
if (policy) {
|
|
162
|
+
const editPolicy = policy.edit_policy;
|
|
163
|
+
if (editPolicy && !editPolicy.allow_all) {
|
|
164
|
+
const fileWriteTarget = extractFileWriteTarget(command);
|
|
165
|
+
if (fileWriteTarget) {
|
|
166
|
+
const allowed = editPolicy.allow_patterns.some(p => matchesPattern(fileWriteTarget, p));
|
|
167
|
+
if (!allowed) {
|
|
168
|
+
process.stderr.write(
|
|
169
|
+
(editPolicy.block_message || '⛔ Prevention: File write blocked by policy.\n') +
|
|
170
|
+
`\nBlocked file: ${fileWriteTarget}\n` +
|
|
171
|
+
'Use the Write tool instead of Bash to create files.\n'
|
|
172
|
+
);
|
|
173
|
+
process.exit(2);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Blocked command patterns (git push, etc.)
|
|
179
|
+
const bashPolicy = policy.bash_policy;
|
|
180
|
+
if (bashPolicy && bashPolicy.blocked_commands) {
|
|
181
|
+
for (const rule of bashPolicy.blocked_commands) {
|
|
182
|
+
if (new RegExp(rule.pattern).test(command)) {
|
|
183
|
+
process.stderr.write(rule.message || '⛔ Prevention: Command blocked by policy.\n');
|
|
184
|
+
process.exit(2);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Secret scanning for Bash commands that write file content
|
|
191
|
+
const inlineContent = extractBashWriteContent(command);
|
|
192
|
+
if (inlineContent) {
|
|
193
|
+
const secretFound = checkForSecrets(inlineContent);
|
|
194
|
+
if (secretFound) {
|
|
195
|
+
process.stderr.write(
|
|
196
|
+
`⛔ Prevention: Secret detected in Bash command — ${secretFound.name}\n` +
|
|
197
|
+
`Do not hardcode secrets. Use environment variables or a secret manager.\n`
|
|
198
|
+
);
|
|
66
199
|
process.exit(2);
|
|
67
200
|
}
|
|
68
201
|
}
|
|
202
|
+
|
|
69
203
|
process.exit(0);
|
|
70
204
|
}
|
|
71
205
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avesta-hq/prevention",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "XP/CD development agent commands for Claude Code - achieve Elite DORA metrics through disciplined TDD and Continuous Delivery practices",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"dev": "tsx src/mcp-server.ts",
|
|
41
41
|
"test": "jest",
|
|
42
42
|
"test:evals": "cd src/_deprecated/evals && python -m pytest test_cases/orchestrator/ -v",
|
|
43
|
-
"
|
|
43
|
+
"generate:catalog": "tsx scripts/generate-catalog-data.ts",
|
|
44
|
+
"prebuild:assets": "tsx scripts/generate-catalog-data.ts && tsx scripts/embed-assets.ts",
|
|
44
45
|
"build:binary": "npm run prebuild:assets && bun build src/mcp-server.ts --compile --outfile dist/prevention",
|
|
45
46
|
"build:all": "npm run prebuild:assets && bun build src/mcp-server.ts --compile --target=bun-linux-x64 --outfile dist/prevention-linux-x64 && bun build src/mcp-server.ts --compile --target=bun-darwin-arm64 --outfile dist/prevention-darwin-arm64 && bun build src/mcp-server.ts --compile --target=bun-darwin-x64 --outfile dist/prevention-darwin-x64"
|
|
46
47
|
},
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
"jest": "^30.3.0",
|
|
58
59
|
"ts-jest": "^29.4.6",
|
|
59
60
|
"tsx": "^4.21.0",
|
|
60
|
-
"typescript": "^5.9.3"
|
|
61
|
+
"typescript": "^5.9.3",
|
|
62
|
+
"yaml": "^2.8.3"
|
|
61
63
|
}
|
|
62
64
|
}
|