@avesta-hq/prevention 0.3.1 → 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.
Files changed (2) hide show
  1. package/bin/lib/hooks.js +148 -14
  2. 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
- const editPolicy = policy.edit_policy;
45
- if (!editPolicy || editPolicy.allow_all) process.exit(0);
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 allowed = editPolicy.allow_patterns.some(p => matchesPattern(filePath, p));
49
-
50
- if (!allowed) {
51
- process.stderr.write(editPolicy.block_message || '⛔ Prevention: Edit blocked by policy.\n');
52
- process.exit(2);
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
- for (const rule of bashPolicy.blocked_commands) {
64
- if (new RegExp(rule.pattern).test(command)) {
65
- process.stderr.write(rule.message || '⛔ Prevention: Command blocked by policy.\n');
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.1",
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
- "prebuild:assets": "tsx scripts/embed-assets.ts",
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
  }