praxis-sec 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/LICENSE +21 -0
- package/README.md +170 -0
- package/ai-defense/cost-protection.md +292 -0
- package/ai-defense/llm-security-checklist.md +324 -0
- package/ai-defense/prompt-injection-patterns.js +283 -0
- package/ai-defense/system-prompt-armor.md +327 -0
- package/checklists/launch-day.md +168 -0
- package/cli/agents/abom-generator.js +225 -0
- package/cli/agents/agent-attestation-agent.js +318 -0
- package/cli/agents/agent-config-scanner.js +787 -0
- package/cli/agents/agent-telemetry-agent.js +415 -0
- package/cli/agents/agentic-security-agent.js +296 -0
- package/cli/agents/agentic-supply-chain-agent.js +463 -0
- package/cli/agents/ai-infra-inventory-agent.js +449 -0
- package/cli/agents/api-fuzzer.js +345 -0
- package/cli/agents/auth-bypass-agent.js +348 -0
- package/cli/agents/base-agent.js +280 -0
- package/cli/agents/cicd-scanner.js +300 -0
- package/cli/agents/config-auditor.js +757 -0
- package/cli/agents/deep-analyzer.js +776 -0
- package/cli/agents/endpoint-agent-abuse-agent.js +404 -0
- package/cli/agents/exception-handler-agent.js +187 -0
- package/cli/agents/git-history-scanner.js +169 -0
- package/cli/agents/governance-audits.js +138 -0
- package/cli/agents/hermes-security-agent.js +536 -0
- package/cli/agents/html-reporter.js +1125 -0
- package/cli/agents/index.js +147 -0
- package/cli/agents/injection-tester.js +502 -0
- package/cli/agents/legal-risk-agent.js +328 -0
- package/cli/agents/llm-redteam.js +199 -0
- package/cli/agents/managed-agent-scanner.js +333 -0
- package/cli/agents/mcp-security-agent.js +588 -0
- package/cli/agents/memory-poisoning-agent.js +305 -0
- package/cli/agents/mobile-scanner.js +231 -0
- package/cli/agents/model-file-scanner.js +259 -0
- package/cli/agents/orchestrator.js +355 -0
- package/cli/agents/pii-compliance-agent.js +301 -0
- package/cli/agents/policy-engine.js +229 -0
- package/cli/agents/prompt-injection-prober.js +224 -0
- package/cli/agents/rag-security-agent.js +204 -0
- package/cli/agents/recon-agent.js +207 -0
- package/cli/agents/sbom-generator.js +265 -0
- package/cli/agents/scoring-engine.js +273 -0
- package/cli/agents/ssrf-prober.js +130 -0
- package/cli/agents/stateful-watcher.js +238 -0
- package/cli/agents/supabase-rls-agent.js +154 -0
- package/cli/agents/supply-chain-agent.js +857 -0
- package/cli/agents/swarm-orchestrator.js +200 -0
- package/cli/agents/verifier-agent.js +303 -0
- package/cli/agents/vibe-coding-agent.js +250 -0
- package/cli/bin/praxis.js +866 -0
- package/cli/commands/abom.js +73 -0
- package/cli/commands/agent-fix.js +1245 -0
- package/cli/commands/audit.js +1180 -0
- package/cli/commands/autofix.js +383 -0
- package/cli/commands/baseline.js +193 -0
- package/cli/commands/benchmark.js +327 -0
- package/cli/commands/checklist.js +223 -0
- package/cli/commands/ci.js +403 -0
- package/cli/commands/deps.js +516 -0
- package/cli/commands/diff.js +200 -0
- package/cli/commands/doctor.js +195 -0
- package/cli/commands/env-audit.js +349 -0
- package/cli/commands/fix.js +218 -0
- package/cli/commands/guard.js +396 -0
- package/cli/commands/hooks.js +278 -0
- package/cli/commands/init.js +514 -0
- package/cli/commands/legal.js +158 -0
- package/cli/commands/live-advisories.js +241 -0
- package/cli/commands/mcp.js +660 -0
- package/cli/commands/openclaw.js +386 -0
- package/cli/commands/red-team.js +350 -0
- package/cli/commands/redteam.js +78 -0
- package/cli/commands/remediate.js +797 -0
- package/cli/commands/rotate.js +768 -0
- package/cli/commands/rules.js +196 -0
- package/cli/commands/scan-mcp.js +534 -0
- package/cli/commands/scan-skill.js +588 -0
- package/cli/commands/scan-standard.js +251 -0
- package/cli/commands/scan.js +524 -0
- package/cli/commands/score.js +449 -0
- package/cli/commands/shell.js +514 -0
- package/cli/commands/team-report.js +398 -0
- package/cli/commands/undo.js +161 -0
- package/cli/commands/update-intel.js +126 -0
- package/cli/commands/vibe-check.js +276 -0
- package/cli/commands/watch.js +757 -0
- package/cli/commands/web.js +63 -0
- package/cli/core/ast/guardrail-detector.js +141 -0
- package/cli/core/ast/index.js +22 -0
- package/cli/core/ast/parser.js +676 -0
- package/cli/core/ast/scope-tree.js +287 -0
- package/cli/core/ast/taint-tracker.js +158 -0
- package/cli/core/branding.js +37 -0
- package/cli/core/env.js +38 -0
- package/cli/core/errors.js +61 -0
- package/cli/core/fs.js +62 -0
- package/cli/core/output/compliance.js +90 -0
- package/cli/core/output/html-theme.js +158 -0
- package/cli/core/output/index.js +57 -0
- package/cli/core/output/json.js +48 -0
- package/cli/core/output/sarif.js +240 -0
- package/cli/core/version.js +67 -0
- package/cli/core/web/jobs.js +183 -0
- package/cli/core/web/projects.js +146 -0
- package/cli/core/web/server.js +439 -0
- package/cli/data/atlas-knowledge.json +5640 -0
- package/cli/data/eaa-catalog.json +39 -0
- package/cli/data/known-mcps.json +26 -0
- package/cli/data/probes/prompt-injection-corpus.json +271 -0
- package/cli/data/threat-intel.json +85 -0
- package/cli/data/threatpacks/latest.json +41 -0
- package/cli/hooks/patterns.js +313 -0
- package/cli/hooks/post-tool-use.js +140 -0
- package/cli/hooks/pre-tool-use.js +186 -0
- package/cli/index.js +90 -0
- package/cli/providers/llm-provider.js +766 -0
- package/cli/utils/autofix-rules.js +74 -0
- package/cli/utils/cache-manager.js +310 -0
- package/cli/utils/compliance-map.js +191 -0
- package/cli/utils/entropy.js +132 -0
- package/cli/utils/fix-ledger.js +127 -0
- package/cli/utils/hermes-tool-registry.js +252 -0
- package/cli/utils/intel/cache.js +61 -0
- package/cli/utils/intel/http.js +88 -0
- package/cli/utils/intel/index.js +235 -0
- package/cli/utils/intel/merge.js +229 -0
- package/cli/utils/intel/sources/epss.js +54 -0
- package/cli/utils/intel/sources/ghsa.js +81 -0
- package/cli/utils/intel/sources/gitguardian.js +40 -0
- package/cli/utils/intel/sources/gitleaks.js +101 -0
- package/cli/utils/intel/sources/kev.js +38 -0
- package/cli/utils/intel/sources/nvd.js +84 -0
- package/cli/utils/intel/sources/osv.js +132 -0
- package/cli/utils/intel/sources/phylum.js +44 -0
- package/cli/utils/intel/sources/snyk.js +46 -0
- package/cli/utils/intel/sources/socket.js +69 -0
- package/cli/utils/intel/sources/sonatype.js +84 -0
- package/cli/utils/intel/sources/threatpack.js +69 -0
- package/cli/utils/mcp-trust.js +60 -0
- package/cli/utils/output.js +251 -0
- package/cli/utils/patterns.js +1130 -0
- package/cli/utils/pdf-generator.js +94 -0
- package/cli/utils/plugin-loader.js +364 -0
- package/cli/utils/rule-import.js +228 -0
- package/cli/utils/rule-registry.js +426 -0
- package/cli/utils/scan-fingerprint.js +109 -0
- package/cli/utils/scan-playbook.js +312 -0
- package/cli/utils/score-history.js +119 -0
- package/cli/utils/secrets-verifier.js +247 -0
- package/cli/utils/security-memory.js +296 -0
- package/cli/utils/standards/atlas-knowledge.js +87 -0
- package/cli/utils/standards/index.js +127 -0
- package/cli/utils/standards/sources/avid.js +45 -0
- package/cli/utils/standards/sources/eu-ai-act.js +89 -0
- package/cli/utils/standards/sources/google-saif.js +39 -0
- package/cli/utils/standards/sources/iso-42001.js +94 -0
- package/cli/utils/standards/sources/mitre-atlas.js +54 -0
- package/cli/utils/standards/sources/nist-ai-600-1.js +45 -0
- package/cli/utils/standards/sources/owasp-llm.js +45 -0
- package/cli/utils/standards/sources/owasp-ml.js +45 -0
- package/cli/utils/threat-intel.js +265 -0
- package/configs/firebase/firestore-rules.txt +215 -0
- package/configs/firebase/security-checklist.md +236 -0
- package/configs/firebase/storage-rules.txt +206 -0
- package/configs/gitignore-template +258 -0
- package/configs/nextjs-security-headers.js +220 -0
- package/configs/praxisignore-template +50 -0
- package/configs/supabase/secure-client.ts +225 -0
- package/configs/supabase/security-checklist.md +278 -0
- package/docs/THIRD_PARTY_NOTICES.md +26 -0
- package/docs/THREAT_INTEL.md +292 -0
- package/docs/USAGE.md +1205 -0
- package/docs/design/WEB-UI.md +82 -0
- package/package.json +71 -0
- package/scripts/check-determinism.mjs +119 -0
- package/snippets/README.md +122 -0
- package/snippets/api-security/api-security-checklist.md +412 -0
- package/snippets/api-security/cors-config.ts +322 -0
- package/snippets/api-security/input-validation.ts +430 -0
- package/snippets/auth/jwt-checklist.md +322 -0
- package/snippets/rate-limiting/nextjs-middleware.ts +211 -0
- package/snippets/rate-limiting/upstash-ratelimit.ts +229 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard Command
|
|
3
|
+
* =============
|
|
4
|
+
*
|
|
5
|
+
* Installs a git pre-push hook that runs praxis scan before every push.
|
|
6
|
+
* If secrets are found, the push is blocked.
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* praxis guard Install pre-push hook
|
|
10
|
+
* praxis guard --pre-commit Install pre-commit hook instead
|
|
11
|
+
* praxis guard remove Remove installed hooks
|
|
12
|
+
*
|
|
13
|
+
* HUSKY SUPPORT:
|
|
14
|
+
* If a .husky/ directory is detected, the hook is added there instead.
|
|
15
|
+
* Otherwise it goes directly into .git/hooks/.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import chalk from 'chalk';
|
|
21
|
+
import * as output from '../utils/output.js';
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// HOOK SCRIPTS
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
const PRE_PUSH_HOOK = `#!/bin/sh
|
|
28
|
+
# praxis pre-push hook
|
|
29
|
+
# Scans for leaked secrets before every git push.
|
|
30
|
+
# Remove this hook with: npx praxis-sec guard remove
|
|
31
|
+
|
|
32
|
+
echo ""
|
|
33
|
+
echo "🔍 praxis: Scanning for secrets before push..."
|
|
34
|
+
|
|
35
|
+
npx --yes praxis scan . --json > /tmp/praxis-scan.json 2>/dev/null
|
|
36
|
+
|
|
37
|
+
if [ $? -ne 0 ]; then
|
|
38
|
+
echo ""
|
|
39
|
+
echo "❌ praxis: Secrets detected! Push blocked."
|
|
40
|
+
echo ""
|
|
41
|
+
echo "Run 'npx praxis-sec scan .' to see details."
|
|
42
|
+
echo "Fix the issues, then push again."
|
|
43
|
+
echo ""
|
|
44
|
+
echo "To skip this check (not recommended):"
|
|
45
|
+
echo " git push --no-verify"
|
|
46
|
+
echo ""
|
|
47
|
+
rm -f /tmp/praxis-scan.json
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
echo "✅ praxis: No secrets detected. Pushing..."
|
|
52
|
+
rm -f /tmp/praxis-scan.json
|
|
53
|
+
exit 0
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const PRE_COMMIT_HOOK = `#!/bin/sh
|
|
57
|
+
# praxis pre-commit hook
|
|
58
|
+
# Scans staged files for leaked secrets before every commit.
|
|
59
|
+
# Remove this hook with: npx praxis-sec guard remove
|
|
60
|
+
|
|
61
|
+
echo ""
|
|
62
|
+
echo "🔍 praxis: Scanning for secrets before commit..."
|
|
63
|
+
|
|
64
|
+
npx --yes praxis scan . --json > /tmp/praxis-scan.json 2>/dev/null
|
|
65
|
+
|
|
66
|
+
if [ $? -ne 0 ]; then
|
|
67
|
+
echo ""
|
|
68
|
+
echo "❌ praxis: Secrets detected! Commit blocked."
|
|
69
|
+
echo ""
|
|
70
|
+
echo "Run 'npx praxis-sec scan .' to see details."
|
|
71
|
+
echo "Fix the issues, then commit again."
|
|
72
|
+
echo ""
|
|
73
|
+
echo "To skip this check (not recommended):"
|
|
74
|
+
echo " git commit --no-verify"
|
|
75
|
+
echo ""
|
|
76
|
+
rm -f /tmp/praxis-scan.json
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
echo "✅ praxis: No secrets detected. Committing..."
|
|
81
|
+
rm -f /tmp/praxis-scan.json
|
|
82
|
+
exit 0
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const HUSKY_PRE_PUSH = `#!/usr/bin/env sh
|
|
86
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
87
|
+
|
|
88
|
+
echo ""
|
|
89
|
+
echo "🔍 praxis: Scanning for secrets before push..."
|
|
90
|
+
|
|
91
|
+
npx praxis-sec scan . --json > /tmp/praxis-scan.json 2>/dev/null
|
|
92
|
+
|
|
93
|
+
if [ $? -ne 0 ]; then
|
|
94
|
+
echo ""
|
|
95
|
+
echo "❌ praxis: Secrets detected! Push blocked."
|
|
96
|
+
echo "Run 'npx praxis-sec scan .' to see details."
|
|
97
|
+
rm -f /tmp/praxis-scan.json
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
echo "✅ praxis: No secrets detected."
|
|
102
|
+
rm -f /tmp/praxis-scan.json
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
const HUSKY_PRE_COMMIT = `#!/usr/bin/env sh
|
|
106
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
107
|
+
|
|
108
|
+
echo ""
|
|
109
|
+
echo "🔍 praxis: Scanning for secrets before commit..."
|
|
110
|
+
|
|
111
|
+
npx praxis-sec scan . --json > /tmp/praxis-scan.json 2>/dev/null
|
|
112
|
+
|
|
113
|
+
if [ $? -ne 0 ]; then
|
|
114
|
+
echo ""
|
|
115
|
+
echo "❌ praxis: Secrets detected! Commit blocked."
|
|
116
|
+
echo "Run 'npx praxis-sec scan .' to see details."
|
|
117
|
+
rm -f /tmp/praxis-scan.json
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
echo "✅ praxis: No secrets detected."
|
|
122
|
+
rm -f /tmp/praxis-scan.json
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
// =============================================================================
|
|
126
|
+
// MAIN COMMAND
|
|
127
|
+
// =============================================================================
|
|
128
|
+
|
|
129
|
+
export async function guardCommand(action, options = {}) {
|
|
130
|
+
const cwd = process.cwd();
|
|
131
|
+
|
|
132
|
+
// Verify this is a git repo
|
|
133
|
+
const gitDir = findGitDir(cwd);
|
|
134
|
+
if (!gitDir) {
|
|
135
|
+
output.error('Not a git repository. Run this from your project root.');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (options.generateHooks) {
|
|
140
|
+
return generateClaudeHooks(cwd);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (action === 'remove') {
|
|
144
|
+
return removeHooks(gitDir, cwd);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return installHook(gitDir, cwd, options);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// INSTALL
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
function installHook(gitDir, cwd, options) {
|
|
155
|
+
const hookType = options.preCommit ? 'pre-commit' : 'pre-push';
|
|
156
|
+
const hookScript = options.preCommit ? PRE_COMMIT_HOOK : PRE_PUSH_HOOK;
|
|
157
|
+
const huskyScript = options.preCommit ? HUSKY_PRE_COMMIT : HUSKY_PRE_PUSH;
|
|
158
|
+
|
|
159
|
+
output.header('Installing praxis Guard');
|
|
160
|
+
|
|
161
|
+
// Check for Husky
|
|
162
|
+
const huskyDir = path.join(cwd, '.husky');
|
|
163
|
+
const useHusky = fs.existsSync(huskyDir);
|
|
164
|
+
|
|
165
|
+
if (useHusky) {
|
|
166
|
+
installHuskyHook(huskyDir, hookType, huskyScript);
|
|
167
|
+
} else {
|
|
168
|
+
installGitHook(gitDir, hookType, hookScript);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log();
|
|
172
|
+
console.log(chalk.gray('What happens now:'));
|
|
173
|
+
console.log(chalk.gray(` Every git ${hookType === 'pre-push' ? 'push' : 'commit'} will run praxis scan`));
|
|
174
|
+
console.log(chalk.gray(' If secrets are found, the operation is blocked'));
|
|
175
|
+
console.log(chalk.gray(' Use --no-verify to skip (not recommended)'));
|
|
176
|
+
console.log();
|
|
177
|
+
console.log(chalk.gray('To remove: npx praxis-sec guard remove'));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function installGitHook(gitDir, hookType, script) {
|
|
181
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
182
|
+
const hookPath = path.join(hooksDir, hookType);
|
|
183
|
+
|
|
184
|
+
// Ensure hooks directory exists
|
|
185
|
+
if (!fs.existsSync(hooksDir)) {
|
|
186
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check if hook already exists (not from praxis)
|
|
190
|
+
if (fs.existsSync(hookPath)) {
|
|
191
|
+
const existing = fs.readFileSync(hookPath, 'utf-8');
|
|
192
|
+
if (!existing.includes('praxis')) {
|
|
193
|
+
output.warning(`Existing ${hookType} hook found. Appending praxis check.`);
|
|
194
|
+
fs.appendFileSync(hookPath, '\n' + script);
|
|
195
|
+
output.success(`Appended to .git/hooks/${hookType}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
output.warning(`praxis guard already installed in .git/hooks/${hookType}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fs.writeFileSync(hookPath, script);
|
|
203
|
+
// Make executable (chmod +x)
|
|
204
|
+
try {
|
|
205
|
+
fs.chmodSync(hookPath, '755');
|
|
206
|
+
} catch {
|
|
207
|
+
// Windows doesn't support chmod, but hooks still run via git
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
output.success(`Hook installed at .git/hooks/${hookType}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function installHuskyHook(huskyDir, hookType, script) {
|
|
214
|
+
const hookPath = path.join(huskyDir, hookType);
|
|
215
|
+
|
|
216
|
+
if (fs.existsSync(hookPath)) {
|
|
217
|
+
const existing = fs.readFileSync(hookPath, 'utf-8');
|
|
218
|
+
if (!existing.includes('praxis')) {
|
|
219
|
+
output.warning(`Existing Husky ${hookType} found. Appending praxis check.`);
|
|
220
|
+
fs.appendFileSync(hookPath, '\n# praxis\n' + script.split('\n').slice(3).join('\n'));
|
|
221
|
+
output.success(`Appended to .husky/${hookType}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
output.warning(`praxis guard already installed in .husky/${hookType}`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fs.writeFileSync(hookPath, script);
|
|
229
|
+
try {
|
|
230
|
+
fs.chmodSync(hookPath, '755');
|
|
231
|
+
} catch {}
|
|
232
|
+
|
|
233
|
+
output.success(`Hook installed at .husky/${hookType} (Husky detected)`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// =============================================================================
|
|
237
|
+
// REMOVE
|
|
238
|
+
// =============================================================================
|
|
239
|
+
|
|
240
|
+
function removeHooks(gitDir, cwd) {
|
|
241
|
+
output.header('Removing praxis Guard');
|
|
242
|
+
|
|
243
|
+
let removed = 0;
|
|
244
|
+
|
|
245
|
+
// Check .git/hooks
|
|
246
|
+
const hookTypes = ['pre-push', 'pre-commit'];
|
|
247
|
+
for (const hookType of hookTypes) {
|
|
248
|
+
const hookPath = path.join(gitDir, 'hooks', hookType);
|
|
249
|
+
if (fs.existsSync(hookPath)) {
|
|
250
|
+
const content = fs.readFileSync(hookPath, 'utf-8');
|
|
251
|
+
if (content.includes('praxis')) {
|
|
252
|
+
if (content.trim() === PRE_PUSH_HOOK.trim() || content.trim() === PRE_COMMIT_HOOK.trim()) {
|
|
253
|
+
// Praxis is the only hook — delete the file
|
|
254
|
+
fs.unlinkSync(hookPath);
|
|
255
|
+
output.success(`Removed .git/hooks/${hookType}`);
|
|
256
|
+
} else {
|
|
257
|
+
// Other hooks exist — only remove praxis lines
|
|
258
|
+
const cleaned = content
|
|
259
|
+
.replace(/# praxis[\s\S]*?exit 0\n/g, '')
|
|
260
|
+
.trimEnd() + '\n';
|
|
261
|
+
fs.writeFileSync(hookPath, cleaned);
|
|
262
|
+
output.success(`Removed praxis from .git/hooks/${hookType}`);
|
|
263
|
+
}
|
|
264
|
+
removed++;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check .husky
|
|
269
|
+
const huskyHookPath = path.join(cwd, '.husky', hookType);
|
|
270
|
+
if (fs.existsSync(huskyHookPath)) {
|
|
271
|
+
const content = fs.readFileSync(huskyHookPath, 'utf-8');
|
|
272
|
+
if (content.includes('praxis')) {
|
|
273
|
+
fs.unlinkSync(huskyHookPath);
|
|
274
|
+
output.success(`Removed .husky/${hookType}`);
|
|
275
|
+
removed++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (removed === 0) {
|
|
281
|
+
output.warning('No praxis hooks found.');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// =============================================================================
|
|
286
|
+
// CLAUDE CODE DEFENSIVE HOOKS
|
|
287
|
+
// =============================================================================
|
|
288
|
+
|
|
289
|
+
function generateClaudeHooks(cwd) {
|
|
290
|
+
output.header('Generating Defensive Claude Code Hooks');
|
|
291
|
+
|
|
292
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
293
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
294
|
+
|
|
295
|
+
// Defensive hooks that block common attack patterns
|
|
296
|
+
const preToolCmd = [
|
|
297
|
+
'node -e "',
|
|
298
|
+
'const c=process.argv[1]||String();',
|
|
299
|
+
'const bad=[/curl.*[|].*(?:bash|sh|node|python)/i,/wget.*[|].*(?:bash|sh)/i,',
|
|
300
|
+
'/rm\\s+-rf\\s+\\//,/webhook[.]site|requestbin|ngrok[.]io|pipedream/i,',
|
|
301
|
+
'/base64.*-d.*[|].*(?:bash|sh)/i];',
|
|
302
|
+
'const m=bad.find(r=>r.test(c));',
|
|
303
|
+
'if(m){console.error(String.fromCharCode(10060)+String.fromCharCode(32)+c.slice(0,80));process.exit(1)}',
|
|
304
|
+
'" "$INPUT"',
|
|
305
|
+
].join('');
|
|
306
|
+
|
|
307
|
+
const postToolCmd = [
|
|
308
|
+
'node -e "',
|
|
309
|
+
'const f=process.argv[1]||String();',
|
|
310
|
+
'if(/[.]env$|[.]env[.]/.test(f)){console.log(String.fromCharCode(9888)+String.fromCharCode(32)+f)}',
|
|
311
|
+
'" "$INPUT"',
|
|
312
|
+
].join('');
|
|
313
|
+
|
|
314
|
+
const defensiveHooks = {
|
|
315
|
+
hooks: {
|
|
316
|
+
PreToolUse: [
|
|
317
|
+
{
|
|
318
|
+
matcher: 'Bash',
|
|
319
|
+
command: preToolCmd,
|
|
320
|
+
description: 'Praxis: Block dangerous command patterns (curl|bash, rm -rf /, exfil domains)',
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
PostToolUse: [
|
|
324
|
+
{
|
|
325
|
+
matcher: 'Write',
|
|
326
|
+
command: postToolCmd,
|
|
327
|
+
description: 'Praxis: Alert when .env files are modified',
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Merge with existing settings
|
|
334
|
+
let existing = {};
|
|
335
|
+
if (fs.existsSync(settingsPath)) {
|
|
336
|
+
try {
|
|
337
|
+
existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
338
|
+
} catch { /* fresh start */ }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Merge hooks — don't overwrite existing hooks, append
|
|
342
|
+
if (!existing.hooks) existing.hooks = {};
|
|
343
|
+
if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = [];
|
|
344
|
+
if (!existing.hooks.PostToolUse) existing.hooks.PostToolUse = [];
|
|
345
|
+
|
|
346
|
+
// Check if praxis hooks already present
|
|
347
|
+
const hasPreHook = existing.hooks.PreToolUse.some(h => h.description?.includes('Praxis'));
|
|
348
|
+
const hasPostHook = existing.hooks.PostToolUse.some(h => h.description?.includes('Praxis'));
|
|
349
|
+
|
|
350
|
+
if (hasPreHook && hasPostHook) {
|
|
351
|
+
output.warning('Praxis hooks already installed in .claude/settings.json');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!hasPreHook) {
|
|
356
|
+
existing.hooks.PreToolUse.push(...defensiveHooks.hooks.PreToolUse);
|
|
357
|
+
}
|
|
358
|
+
if (!hasPostHook) {
|
|
359
|
+
existing.hooks.PostToolUse.push(...defensiveHooks.hooks.PostToolUse);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Write
|
|
363
|
+
if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
|
|
364
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n');
|
|
365
|
+
|
|
366
|
+
output.success('Defensive hooks installed in .claude/settings.json');
|
|
367
|
+
console.log();
|
|
368
|
+
console.log(chalk.gray(' Hooks installed:'));
|
|
369
|
+
console.log(chalk.gray(' PreToolUse → Block curl|bash, rm -rf /, exfil domains'));
|
|
370
|
+
console.log(chalk.gray(' PostToolUse → Alert on .env file modifications'));
|
|
371
|
+
console.log();
|
|
372
|
+
console.log(chalk.gray(' These hooks protect against:'));
|
|
373
|
+
console.log(chalk.gray(' • Remote code execution via piped downloads'));
|
|
374
|
+
console.log(chalk.gray(' • Data exfiltration to webhook.site/ngrok/requestbin'));
|
|
375
|
+
console.log(chalk.gray(' • Destructive filesystem operations'));
|
|
376
|
+
console.log(chalk.gray(' • Unauthorized .env modifications'));
|
|
377
|
+
console.log();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// =============================================================================
|
|
381
|
+
// UTILITIES
|
|
382
|
+
// =============================================================================
|
|
383
|
+
|
|
384
|
+
function findGitDir(startPath) {
|
|
385
|
+
let current = startPath;
|
|
386
|
+
|
|
387
|
+
while (true) {
|
|
388
|
+
const gitPath = path.join(current, '.git');
|
|
389
|
+
if (fs.existsSync(gitPath)) {
|
|
390
|
+
return gitPath;
|
|
391
|
+
}
|
|
392
|
+
const parent = path.dirname(current);
|
|
393
|
+
if (parent === current) return null; // Reached filesystem root
|
|
394
|
+
current = parent;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks Command
|
|
3
|
+
* =============
|
|
4
|
+
*
|
|
5
|
+
* Installs praxis as PreToolUse / PostToolUse hooks in Claude Code
|
|
6
|
+
* (~/.claude/settings.json). Once installed, praxis runs automatically
|
|
7
|
+
* on every Write, Edit, and Bash tool call — blocking secrets before they
|
|
8
|
+
* land on disk and feeding advisory findings back into the conversation.
|
|
9
|
+
*
|
|
10
|
+
* USAGE:
|
|
11
|
+
* npx praxis-sec hooks install Install hooks into ~/.claude/settings.json
|
|
12
|
+
* npx praxis-sec hooks remove Remove praxis hooks
|
|
13
|
+
* npx praxis-sec hooks status Show whether hooks are installed
|
|
14
|
+
*
|
|
15
|
+
* HOOK BEHAVIOUR:
|
|
16
|
+
* PreToolUse — blocks Write/Edit if critical secrets detected; blocks
|
|
17
|
+
* dangerous Bash patterns (curl|bash, credential exfiltration)
|
|
18
|
+
* PostToolUse — scans the written file and injects advisory findings into
|
|
19
|
+
* Claude's context (never blocks — just informs)
|
|
20
|
+
*
|
|
21
|
+
* STABLE PATH STRATEGY:
|
|
22
|
+
* npx caches packages in a volatile temp directory. Writing that temp path
|
|
23
|
+
* to ~/.claude/settings.json means hooks break silently as soon as npx
|
|
24
|
+
* clears or rotates its cache. Instead, we copy the three hook scripts to
|
|
25
|
+
* ~/.praxis/hooks/ (a stable, user-owned directory) and register those
|
|
26
|
+
* paths. They survive npx cache rotations and package updates.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import fs from 'fs';
|
|
30
|
+
import path from 'path';
|
|
31
|
+
import os from 'os';
|
|
32
|
+
import chalk from 'chalk';
|
|
33
|
+
import { fileURLToPath } from 'url';
|
|
34
|
+
|
|
35
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
|
|
37
|
+
// Source: hook scripts shipped inside the package
|
|
38
|
+
const PKG_HOOK_DIR = path.resolve(__dirname, '../hooks');
|
|
39
|
+
|
|
40
|
+
// Stable destination: user-owned, survives npx cache rotations
|
|
41
|
+
const STABLE_HOOK_DIR = path.join(os.homedir(), '.praxis', 'hooks');
|
|
42
|
+
const PRE_HOOK_SCRIPT = path.join(STABLE_HOOK_DIR, 'pre-tool-use.js');
|
|
43
|
+
const POST_HOOK_SCRIPT = path.join(STABLE_HOOK_DIR, 'post-tool-use.js');
|
|
44
|
+
|
|
45
|
+
// Claude Code settings.json location
|
|
46
|
+
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
47
|
+
|
|
48
|
+
// The command strings we register (use stable paths)
|
|
49
|
+
const PRE_COMMAND = `node "${PRE_HOOK_SCRIPT}"`;
|
|
50
|
+
const POST_COMMAND = `node "${POST_HOOK_SCRIPT}"`;
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Public API
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
export async function hooksCommand(action = 'install', _options = {}) {
|
|
57
|
+
switch (action) {
|
|
58
|
+
case 'install': return install();
|
|
59
|
+
case 'remove': return remove();
|
|
60
|
+
case 'status': return status();
|
|
61
|
+
default:
|
|
62
|
+
console.error(chalk.red(`Unknown action: ${action}. Use: install | remove | status`));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Install
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
function install() {
|
|
72
|
+
// ── 1. Copy hook scripts to stable location ────────────────────────────────
|
|
73
|
+
const scripts = ['pre-tool-use.js', 'post-tool-use.js', 'patterns.js'];
|
|
74
|
+
|
|
75
|
+
for (const script of scripts) {
|
|
76
|
+
const src = path.join(PKG_HOOK_DIR, script);
|
|
77
|
+
if (!fs.existsSync(src)) {
|
|
78
|
+
console.error(chalk.red(`Hook script not found: ${src}. Try reinstalling praxis.`));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
fs.mkdirSync(STABLE_HOOK_DIR, { recursive: true });
|
|
85
|
+
for (const script of scripts) {
|
|
86
|
+
fs.copyFileSync(
|
|
87
|
+
path.join(PKG_HOOK_DIR, script),
|
|
88
|
+
path.join(STABLE_HOOK_DIR, script)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(chalk.red(`Failed to copy hook scripts to ${STABLE_HOOK_DIR}: ${err.message}`));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── 2. Register stable paths in ~/.claude/settings.json ───────────────────
|
|
97
|
+
const settings = readSettings();
|
|
98
|
+
ensureHooksStructure(settings);
|
|
99
|
+
|
|
100
|
+
let changed = false;
|
|
101
|
+
|
|
102
|
+
// PreToolUse: Write / Edit / MultiEdit / Bash
|
|
103
|
+
const preEntry = buildEntry(
|
|
104
|
+
['Write', 'Edit', 'MultiEdit', 'Bash'],
|
|
105
|
+
PRE_COMMAND,
|
|
106
|
+
'praxis pre-tool-use: block secrets in writes, dangerous bash patterns'
|
|
107
|
+
);
|
|
108
|
+
if (!hasEntry(settings.hooks.PreToolUse, PRE_COMMAND)) {
|
|
109
|
+
settings.hooks.PreToolUse.push(preEntry);
|
|
110
|
+
changed = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// PostToolUse: Write / Edit / MultiEdit
|
|
114
|
+
const postEntry = buildEntry(
|
|
115
|
+
['Write', 'Edit', 'MultiEdit'],
|
|
116
|
+
POST_COMMAND,
|
|
117
|
+
'praxis post-tool-use: advisory scan after file writes'
|
|
118
|
+
);
|
|
119
|
+
if (!hasEntry(settings.hooks.PostToolUse, POST_COMMAND)) {
|
|
120
|
+
settings.hooks.PostToolUse.push(postEntry);
|
|
121
|
+
changed = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!changed) {
|
|
125
|
+
console.log(chalk.green('✔ praxis hooks are already installed.'));
|
|
126
|
+
printStatus(settings);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
writeSettings(settings);
|
|
131
|
+
|
|
132
|
+
console.log(chalk.green.bold('\n✔ praxis hooks installed successfully.\n'));
|
|
133
|
+
console.log(chalk.gray(' Hook scripts: ') + chalk.white(STABLE_HOOK_DIR));
|
|
134
|
+
console.log(chalk.gray(' Settings file: ') + chalk.white(CLAUDE_SETTINGS_PATH));
|
|
135
|
+
console.log();
|
|
136
|
+
console.log(chalk.cyan(' What happens now:'));
|
|
137
|
+
console.log(chalk.white(' Write / Edit ') + chalk.gray('→ blocked if critical secrets detected in content'));
|
|
138
|
+
console.log(chalk.white(' Bash ') + chalk.gray('→ blocked on curl|bash, credential exfiltration patterns'));
|
|
139
|
+
console.log(chalk.white(' Write / Edit ') + chalk.gray('→ advisory scan after save (findings injected into context)'));
|
|
140
|
+
console.log();
|
|
141
|
+
console.log(chalk.gray(' To remove: npx praxis-sec hooks remove'));
|
|
142
|
+
console.log(chalk.gray(' To verify: npx praxis-sec hooks status\n'));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// Remove
|
|
147
|
+
// =============================================================================
|
|
148
|
+
|
|
149
|
+
function remove() {
|
|
150
|
+
const settings = readSettings();
|
|
151
|
+
|
|
152
|
+
if (!settings.hooks) {
|
|
153
|
+
console.log(chalk.yellow('No hooks configured in settings.json.'));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let removed = 0;
|
|
158
|
+
|
|
159
|
+
for (const event of ['PreToolUse', 'PostToolUse']) {
|
|
160
|
+
if (!Array.isArray(settings.hooks[event])) continue;
|
|
161
|
+
const before = settings.hooks[event].length;
|
|
162
|
+
settings.hooks[event] = settings.hooks[event].filter(entry => !isOurEntry(entry));
|
|
163
|
+
removed += before - settings.hooks[event].length;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (removed === 0) {
|
|
167
|
+
console.log(chalk.yellow('No praxis hooks found in settings.json.'));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Clean up empty arrays / empty hooks object so settings.json is left tidy
|
|
172
|
+
for (const event of ['PreToolUse', 'PostToolUse']) {
|
|
173
|
+
if (Array.isArray(settings.hooks[event]) && settings.hooks[event].length === 0) {
|
|
174
|
+
delete settings.hooks[event];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
178
|
+
delete settings.hooks;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
writeSettings(settings);
|
|
182
|
+
console.log(chalk.green(`✔ Removed ${removed} praxis hook(s) from ${CLAUDE_SETTINGS_PATH}`));
|
|
183
|
+
console.log(chalk.gray(` Hook scripts kept at ${STABLE_HOOK_DIR} (safe to delete manually)`));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// =============================================================================
|
|
187
|
+
// Status
|
|
188
|
+
// =============================================================================
|
|
189
|
+
|
|
190
|
+
function status() {
|
|
191
|
+
const settings = readSettings();
|
|
192
|
+
printStatus(settings);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function printStatus(settings) {
|
|
196
|
+
const preInstalled = settings.hooks?.PreToolUse && hasEntry(settings.hooks.PreToolUse, PRE_COMMAND);
|
|
197
|
+
const postInstalled = settings.hooks?.PostToolUse && hasEntry(settings.hooks.PostToolUse, POST_COMMAND);
|
|
198
|
+
const scriptsExist = fs.existsSync(PRE_HOOK_SCRIPT) && fs.existsSync(POST_HOOK_SCRIPT);
|
|
199
|
+
|
|
200
|
+
console.log(chalk.bold('\npraxis Claude Code hooks status:\n'));
|
|
201
|
+
console.log(
|
|
202
|
+
(preInstalled ? chalk.green(' ✔') : chalk.red(' ✗')) +
|
|
203
|
+
chalk.white(' PreToolUse ') +
|
|
204
|
+
chalk.gray('(block secrets in writes, dangerous bash commands)')
|
|
205
|
+
);
|
|
206
|
+
console.log(
|
|
207
|
+
(postInstalled ? chalk.green(' ✔') : chalk.red(' ✗')) +
|
|
208
|
+
chalk.white(' PostToolUse ') +
|
|
209
|
+
chalk.gray('(advisory scan after file writes)')
|
|
210
|
+
);
|
|
211
|
+
console.log(
|
|
212
|
+
(scriptsExist ? chalk.green(' ✔') : chalk.yellow(' ✗')) +
|
|
213
|
+
chalk.white(' Hook scripts') +
|
|
214
|
+
chalk.gray(` (${STABLE_HOOK_DIR})`)
|
|
215
|
+
);
|
|
216
|
+
console.log();
|
|
217
|
+
|
|
218
|
+
if (!preInstalled || !postInstalled) {
|
|
219
|
+
console.log(chalk.gray(' Run: npx praxis-sec hooks install'));
|
|
220
|
+
}
|
|
221
|
+
console.log();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// =============================================================================
|
|
225
|
+
// Settings helpers
|
|
226
|
+
// =============================================================================
|
|
227
|
+
|
|
228
|
+
function readSettings() {
|
|
229
|
+
try {
|
|
230
|
+
if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
231
|
+
return JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8'));
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
// If the file exists but is malformed, back it up and start fresh
|
|
235
|
+
const backup = CLAUDE_SETTINGS_PATH + '.bak';
|
|
236
|
+
try { fs.copyFileSync(CLAUDE_SETTINGS_PATH, backup); } catch {}
|
|
237
|
+
console.warn(chalk.yellow(`Warning: could not parse existing settings.json — backed up to ${backup}`));
|
|
238
|
+
}
|
|
239
|
+
return {};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function writeSettings(settings) {
|
|
243
|
+
const dir = path.dirname(CLAUDE_SETTINGS_PATH);
|
|
244
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
245
|
+
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function ensureHooksStructure(settings) {
|
|
249
|
+
if (!settings.hooks) settings.hooks = {};
|
|
250
|
+
if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
|
|
251
|
+
if (!Array.isArray(settings.hooks.PostToolUse)) settings.hooks.PostToolUse = [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildEntry(matchers, command, description) {
|
|
255
|
+
return {
|
|
256
|
+
matcher: matchers.join('|'),
|
|
257
|
+
hooks: [
|
|
258
|
+
{
|
|
259
|
+
type: 'command',
|
|
260
|
+
command,
|
|
261
|
+
description,
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function hasEntry(list, command) {
|
|
268
|
+
if (!Array.isArray(list)) return false;
|
|
269
|
+
return list.some(entry =>
|
|
270
|
+
Array.isArray(entry.hooks) &&
|
|
271
|
+
entry.hooks.some(h => h.command === command)
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isOurEntry(entry) {
|
|
276
|
+
if (!Array.isArray(entry.hooks)) return false;
|
|
277
|
+
return entry.hooks.some(h => h.command === PRE_COMMAND || h.command === POST_COMMAND);
|
|
278
|
+
}
|