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,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env Audit Command
|
|
3
|
+
* ==================
|
|
4
|
+
*
|
|
5
|
+
* Post-sync credential health check. Designed to run immediately after
|
|
6
|
+
* `stripe projects env --pull` or any credential provisioning workflow.
|
|
7
|
+
*
|
|
8
|
+
* Checks:
|
|
9
|
+
* 1. Every .env* file is covered by .gitignore
|
|
10
|
+
* 2. No .env values appear hardcoded in source files
|
|
11
|
+
* 3. Git history has no previously committed .env files
|
|
12
|
+
* 4. Agent configs can't read credential files without restriction
|
|
13
|
+
*
|
|
14
|
+
* USAGE:
|
|
15
|
+
* praxis env-audit [path]
|
|
16
|
+
* praxis env-audit . --json
|
|
17
|
+
*
|
|
18
|
+
* EXIT CODES:
|
|
19
|
+
* 0 — clean
|
|
20
|
+
* 1 — issues found
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'fs';
|
|
24
|
+
import path from 'path';
|
|
25
|
+
import fg from 'fast-glob';
|
|
26
|
+
import chalk from 'chalk';
|
|
27
|
+
import ora from 'ora';
|
|
28
|
+
import { execFileSync } from 'child_process';
|
|
29
|
+
import { SECRET_PATTERNS, SKIP_DIRS } from '../utils/patterns.js';
|
|
30
|
+
|
|
31
|
+
// Minimum value length to cross-reference (skip short values like "true", "3000")
|
|
32
|
+
const MIN_VALUE_LENGTH = 8;
|
|
33
|
+
|
|
34
|
+
// Keys that are safe to appear in source (not secrets)
|
|
35
|
+
const SAFE_KEY_PATTERNS = /^(?:NEXT_PUBLIC_|REACT_APP_|VITE_|NUXT_PUBLIC_|NODE_ENV|PORT|HOST|HOSTNAME|LOG_LEVEL|DEBUG|TZ|LANG|APP_NAME|APP_URL|BASE_URL)/i;
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// ENV AUDIT COMMAND
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
export async function envAuditCommand(targetPath = '.', options) {
|
|
42
|
+
const absolutePath = path.resolve(targetPath);
|
|
43
|
+
const spinner = ora('Auditing credential environment...').start();
|
|
44
|
+
|
|
45
|
+
const findings = [];
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// ── Step 1: Find all .env files ──────────────────────────────────────────
|
|
49
|
+
const envFiles = await findEnvFiles(absolutePath);
|
|
50
|
+
spinner.text = `Found ${envFiles.length} .env file(s)`;
|
|
51
|
+
|
|
52
|
+
if (envFiles.length === 0) {
|
|
53
|
+
spinner.succeed('No .env files found — nothing to audit.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Step 2: Check .gitignore coverage ────────────────────────────────────
|
|
58
|
+
spinner.text = 'Checking .gitignore coverage...';
|
|
59
|
+
for (const envFile of envFiles) {
|
|
60
|
+
const relPath = path.relative(absolutePath, envFile).replace(/\\/g, '/');
|
|
61
|
+
const basename = path.basename(envFile);
|
|
62
|
+
|
|
63
|
+
// Skip .env.example and .env.sample — these are meant to be committed
|
|
64
|
+
if (/\.env\.(?:example|sample|template)$/i.test(basename)) continue;
|
|
65
|
+
|
|
66
|
+
const isIgnored = checkGitignored(absolutePath, relPath);
|
|
67
|
+
if (!isIgnored) {
|
|
68
|
+
findings.push({
|
|
69
|
+
type: 'gitignore',
|
|
70
|
+
severity: 'critical',
|
|
71
|
+
file: relPath,
|
|
72
|
+
message: `${relPath} is NOT covered by .gitignore — credentials will be committed`,
|
|
73
|
+
fix: `Add "${basename}" to your .gitignore file`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Step 3: Parse .env values and cross-reference against source ─────────
|
|
79
|
+
spinner.text = 'Cross-referencing credentials against source files...';
|
|
80
|
+
// Only cross-reference real .env files, not .env.example/.env.sample
|
|
81
|
+
const realEnvFiles = envFiles.filter(f => !/\.env\.(?:example|sample|template)$/i.test(path.basename(f)));
|
|
82
|
+
const envValues = parseEnvFiles(realEnvFiles);
|
|
83
|
+
const sensitiveValues = envValues.filter(v =>
|
|
84
|
+
v.value.length >= MIN_VALUE_LENGTH && !SAFE_KEY_PATTERNS.test(v.key)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (sensitiveValues.length > 0) {
|
|
88
|
+
const sourceFiles = await findSourceFiles(absolutePath);
|
|
89
|
+
for (const { key, value, file: envFile } of sensitiveValues) {
|
|
90
|
+
for (const srcFile of sourceFiles) {
|
|
91
|
+
const relSrc = path.relative(absolutePath, srcFile).replace(/\\/g, '/');
|
|
92
|
+
// Don't flag the .env file itself
|
|
93
|
+
if (srcFile === envFile) continue;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const content = fs.readFileSync(srcFile, 'utf-8');
|
|
97
|
+
if (content.includes(value)) {
|
|
98
|
+
findings.push({
|
|
99
|
+
type: 'hardcoded',
|
|
100
|
+
severity: 'critical',
|
|
101
|
+
file: relSrc,
|
|
102
|
+
message: `${key} value from ${path.basename(envFile)} is hardcoded in ${relSrc}`,
|
|
103
|
+
fix: `Replace the hardcoded value with process.env.${key} or equivalent`, // praxis-ignore SQL_INJECTION_TEMPLATE_LITERAL — env-var name interpolation, not SQL
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// skip unreadable files
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Step 4: Check git history for committed .env files ───────────────────
|
|
114
|
+
spinner.text = 'Checking git history for committed credentials...';
|
|
115
|
+
const historyLeaks = checkGitHistory(absolutePath);
|
|
116
|
+
for (const leak of historyLeaks) {
|
|
117
|
+
findings.push({
|
|
118
|
+
type: 'history',
|
|
119
|
+
severity: 'high',
|
|
120
|
+
file: leak,
|
|
121
|
+
message: `${leak} was previously committed to git history — credentials may be in old commits`,
|
|
122
|
+
fix: 'Rotate all credentials that were in this file. Use git filter-repo to remove from history if needed.',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Step 5: Check .projects manifest for credential leaks ────────────────
|
|
127
|
+
spinner.text = 'Checking .projects manifest...';
|
|
128
|
+
const projectsDir = path.join(absolutePath, '.projects');
|
|
129
|
+
if (fs.existsSync(projectsDir)) {
|
|
130
|
+
const projectsFiles = await fg(['**/*'], {
|
|
131
|
+
cwd: projectsDir,
|
|
132
|
+
absolute: true,
|
|
133
|
+
onlyFiles: true,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
for (const pFile of projectsFiles) {
|
|
137
|
+
try {
|
|
138
|
+
const content = fs.readFileSync(pFile, 'utf-8');
|
|
139
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
140
|
+
pattern.pattern.lastIndex = 0;
|
|
141
|
+
const match = pattern.pattern.exec(content);
|
|
142
|
+
if (match) {
|
|
143
|
+
const relPath = path.relative(absolutePath, pFile).replace(/\\/g, '/');
|
|
144
|
+
findings.push({
|
|
145
|
+
type: 'projects-manifest',
|
|
146
|
+
severity: 'critical',
|
|
147
|
+
file: relPath,
|
|
148
|
+
message: `${pattern.name} found in .projects manifest — credentials should not be in the manifest`,
|
|
149
|
+
fix: 'Remove credential values from .projects/ config. Stripe Projects stores credentials server-side.',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// skip unreadable
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Step 6: Check agent config access to .env files ──────────────────────
|
|
160
|
+
spinner.text = 'Checking agent config access to credential files...';
|
|
161
|
+
const agentConfigs = await fg([
|
|
162
|
+
'.claude/settings.json',
|
|
163
|
+
'.cursorrules',
|
|
164
|
+
'.cursor/rules/*.mdc',
|
|
165
|
+
'.windsurfrules',
|
|
166
|
+
'CLAUDE.md',
|
|
167
|
+
'.claw.json',
|
|
168
|
+
'.claw/settings.json',
|
|
169
|
+
'openclaw.json',
|
|
170
|
+
], {
|
|
171
|
+
cwd: absolutePath,
|
|
172
|
+
absolute: true,
|
|
173
|
+
onlyFiles: true,
|
|
174
|
+
dot: true,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
for (const configFile of agentConfigs) {
|
|
178
|
+
try {
|
|
179
|
+
const content = fs.readFileSync(configFile, 'utf-8');
|
|
180
|
+
const relPath = path.relative(absolutePath, configFile).replace(/\\/g, '/');
|
|
181
|
+
|
|
182
|
+
// Check for danger modes that give agents full access
|
|
183
|
+
if (/dangerouslySkipPermissions\s*["']?\s*[:=]\s*["']?true/i.test(content) ||
|
|
184
|
+
/permissionMode\s*["']?\s*[:=]\s*["']?danger-full-access/i.test(content)) {
|
|
185
|
+
findings.push({
|
|
186
|
+
type: 'agent-access',
|
|
187
|
+
severity: 'critical',
|
|
188
|
+
file: relPath,
|
|
189
|
+
message: `${relPath} grants unrestricted file access — agent can read all .env credentials`,
|
|
190
|
+
fix: 'Remove dangerouslySkipPermissions / danger-full-access. Scope agent file access explicitly.',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// skip
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Output ───────────────────────────────────────────────────────────────
|
|
199
|
+
spinner.stop();
|
|
200
|
+
|
|
201
|
+
if (options.json) {
|
|
202
|
+
console.log(JSON.stringify({ findings, envFiles: envFiles.length, clean: findings.length === 0 }, null, 2));
|
|
203
|
+
process.exit(findings.length > 0 ? 1 : 0);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log();
|
|
208
|
+
console.log(chalk.cyan.bold(' Praxis — Env Audit'));
|
|
209
|
+
console.log(chalk.gray(` Scanned ${envFiles.length} .env file(s), ${sensitiveValues.length} credential(s)`));
|
|
210
|
+
console.log();
|
|
211
|
+
|
|
212
|
+
if (findings.length === 0) {
|
|
213
|
+
console.log(chalk.green(' ✔ Environment is clean. No credential leaks detected.\n'));
|
|
214
|
+
console.log(chalk.gray(' Tip: run this after every `stripe projects env --pull` or credential sync.\n'));
|
|
215
|
+
process.exit(0);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Group by type
|
|
220
|
+
const groups = {
|
|
221
|
+
gitignore: { label: 'Missing .gitignore Coverage', icon: '🔓' },
|
|
222
|
+
hardcoded: { label: 'Hardcoded Credentials in Source', icon: '🔑' },
|
|
223
|
+
history: { label: 'Credentials in Git History', icon: '📜' },
|
|
224
|
+
'projects-manifest': { label: 'Credentials in .projects Manifest', icon: '📁' },
|
|
225
|
+
'agent-access': { label: 'Agent Config: Unrestricted Credential Access', icon: '🤖' },
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
for (const [type, meta] of Object.entries(groups)) {
|
|
229
|
+
const typeFindings = findings.filter(f => f.type === type);
|
|
230
|
+
if (typeFindings.length === 0) continue;
|
|
231
|
+
|
|
232
|
+
console.log(chalk.yellow(` ${meta.icon} ${meta.label} (${typeFindings.length})`));
|
|
233
|
+
for (const f of typeFindings) {
|
|
234
|
+
const sevColor = f.severity === 'critical' ? chalk.red : chalk.yellow;
|
|
235
|
+
console.log(` ${sevColor(f.severity.toUpperCase())} ${f.file}`);
|
|
236
|
+
console.log(chalk.gray(` ${f.message}`));
|
|
237
|
+
console.log(chalk.gray(` Fix: ${f.fix}`));
|
|
238
|
+
}
|
|
239
|
+
console.log();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const criticals = findings.filter(f => f.severity === 'critical').length;
|
|
243
|
+
if (criticals > 0) {
|
|
244
|
+
console.log(chalk.red.bold(` ✘ ${criticals} critical issue(s). Fix before committing.\n`));
|
|
245
|
+
} else {
|
|
246
|
+
console.log(chalk.yellow(` ⚠ ${findings.length} issue(s) found. Review before committing.\n`));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
process.exit(1);
|
|
250
|
+
|
|
251
|
+
} catch (err) {
|
|
252
|
+
spinner.fail(`Env audit error: ${err.message}`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// =============================================================================
|
|
258
|
+
// HELPERS
|
|
259
|
+
// =============================================================================
|
|
260
|
+
|
|
261
|
+
async function findEnvFiles(rootPath) {
|
|
262
|
+
return fg(['.env', '.env.*', '**/.env', '**/.env.*'], {
|
|
263
|
+
cwd: rootPath,
|
|
264
|
+
absolute: true,
|
|
265
|
+
onlyFiles: true,
|
|
266
|
+
dot: true,
|
|
267
|
+
ignore: Array.from(SKIP_DIRS).map(d => `**/${d}/**`),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function parseEnvFiles(envFiles) {
|
|
272
|
+
const values = [];
|
|
273
|
+
for (const file of envFiles) {
|
|
274
|
+
try {
|
|
275
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
276
|
+
for (const line of content.split('\n')) {
|
|
277
|
+
const trimmed = line.trim();
|
|
278
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
279
|
+
const eqIdx = trimmed.indexOf('=');
|
|
280
|
+
if (eqIdx === -1) continue;
|
|
281
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
282
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
283
|
+
// Strip surrounding quotes
|
|
284
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
285
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
286
|
+
value = value.slice(1, -1);
|
|
287
|
+
}
|
|
288
|
+
if (value && value !== '' && !value.startsWith('${')) {
|
|
289
|
+
values.push({ key, value, file });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
// skip unreadable
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return values;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function findSourceFiles(rootPath) {
|
|
300
|
+
return fg(['**/*.{js,jsx,ts,tsx,mjs,cjs,py,rb,go,rs,php,java,kt,swift,yaml,yml,json,toml,tf}'], {
|
|
301
|
+
cwd: rootPath,
|
|
302
|
+
absolute: true,
|
|
303
|
+
onlyFiles: true,
|
|
304
|
+
ignore: [
|
|
305
|
+
...Array.from(SKIP_DIRS).map(d => `**/${d}/**`),
|
|
306
|
+
'**/.env*',
|
|
307
|
+
'**/*.min.js',
|
|
308
|
+
'**/__tests__/**',
|
|
309
|
+
'**/*.test.*',
|
|
310
|
+
'**/*.spec.*',
|
|
311
|
+
'**/test/**',
|
|
312
|
+
'**/tests/**',
|
|
313
|
+
'**/fixtures/**',
|
|
314
|
+
'**/snippets/**',
|
|
315
|
+
],
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function checkGitignored(rootPath, relPath) {
|
|
320
|
+
try {
|
|
321
|
+
execFileSync('git', ['check-ignore', '-q', relPath], { cwd: rootPath, stdio: 'pipe' });
|
|
322
|
+
return true; // exit 0 means it IS ignored
|
|
323
|
+
} catch {
|
|
324
|
+
return false; // exit 1 means it is NOT ignored
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function checkGitHistory(rootPath) {
|
|
329
|
+
try {
|
|
330
|
+
const result = execFileSync('git', ['log', '--all', '--diff-filter=A', '--name-only', '--pretty=format:'], {
|
|
331
|
+
cwd: rootPath,
|
|
332
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
333
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
334
|
+
}).toString();
|
|
335
|
+
|
|
336
|
+
const envFilePattern = /^\.env(?:\.\w+)?$/;
|
|
337
|
+
const safeEnvPattern = /\.env\.(?:example|sample|template)$/;
|
|
338
|
+
const committed = new Set();
|
|
339
|
+
for (const line of result.split('\n')) {
|
|
340
|
+
const trimmed = line.trim();
|
|
341
|
+
if (trimmed && envFilePattern.test(path.basename(trimmed)) && !safeEnvPattern.test(trimmed)) {
|
|
342
|
+
committed.add(trimmed);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return Array.from(committed);
|
|
346
|
+
} catch {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix Command
|
|
3
|
+
* ===========
|
|
4
|
+
*
|
|
5
|
+
* Scans for secrets and generates a .env.example file with placeholder values.
|
|
6
|
+
* Also shows a summary of what to move to environment variables.
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* praxis fix Scan and generate .env.example
|
|
10
|
+
* praxis fix --dry-run Preview what would be generated (don't write file)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import ora from 'ora';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import {
|
|
18
|
+
SECRET_PATTERNS,
|
|
19
|
+
SKIP_DIRS,
|
|
20
|
+
SKIP_EXTENSIONS,
|
|
21
|
+
SKIP_FILENAMES,
|
|
22
|
+
TEST_FILE_PATTERNS,
|
|
23
|
+
MAX_FILE_SIZE
|
|
24
|
+
} from '../utils/patterns.js';
|
|
25
|
+
import { isHighEntropyMatch } from '../utils/entropy.js';
|
|
26
|
+
import fg from 'fast-glob';
|
|
27
|
+
import * as output from '../utils/output.js';
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// MAIN COMMAND
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
export async function fixCommand(options = {}) {
|
|
34
|
+
const cwd = process.cwd();
|
|
35
|
+
|
|
36
|
+
const spinner = ora({ text: 'Scanning for secrets...', color: 'cyan' }).start();
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const files = await findFiles(cwd);
|
|
40
|
+
const results = [];
|
|
41
|
+
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
const findings = await scanFile(file);
|
|
44
|
+
if (findings.length > 0) {
|
|
45
|
+
results.push({ file, findings });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
spinner.stop();
|
|
50
|
+
|
|
51
|
+
if (results.length === 0) {
|
|
52
|
+
output.success('No secrets found — nothing to fix!');
|
|
53
|
+
console.log(chalk.gray('\nYour codebase looks clean. Keep it that way with:'));
|
|
54
|
+
console.log(chalk.gray(' npx praxis-sec guard # Block pushes if secrets are found'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Build env var suggestions from findings
|
|
59
|
+
const envVars = buildEnvVarSuggestions(results);
|
|
60
|
+
|
|
61
|
+
output.header('Fix Report');
|
|
62
|
+
printFindings(results, cwd);
|
|
63
|
+
printEnvExample(envVars, options.dryRun);
|
|
64
|
+
|
|
65
|
+
} catch (err) {
|
|
66
|
+
spinner.fail('Fix scan failed');
|
|
67
|
+
output.error(err.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// SCAN (same logic as scan command, reused here)
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
async function findFiles(rootPath) {
|
|
77
|
+
const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
|
|
78
|
+
const files = await fg('**/*', {
|
|
79
|
+
cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const filtered = [];
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
const ext = path.extname(file).toLowerCase();
|
|
85
|
+
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
86
|
+
if (SKIP_FILENAMES.has(path.basename(file))) continue;
|
|
87
|
+
const basename = path.basename(file);
|
|
88
|
+
if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) continue;
|
|
89
|
+
if (TEST_FILE_PATTERNS.some(p => p.test(file))) continue;
|
|
90
|
+
if (basename === '.env.example') continue; // Don't scan example files
|
|
91
|
+
try {
|
|
92
|
+
const stats = fs.statSync(file);
|
|
93
|
+
if (stats.size > MAX_FILE_SIZE) continue;
|
|
94
|
+
} catch { continue; }
|
|
95
|
+
filtered.push(file);
|
|
96
|
+
}
|
|
97
|
+
return filtered;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function scanFile(filePath) {
|
|
101
|
+
const findings = [];
|
|
102
|
+
try {
|
|
103
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
104
|
+
const lines = content.split('\n');
|
|
105
|
+
|
|
106
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
107
|
+
const line = lines[lineNum];
|
|
108
|
+
if (/praxis-ignore/i.test(line)) continue;
|
|
109
|
+
|
|
110
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
111
|
+
pattern.pattern.lastIndex = 0;
|
|
112
|
+
let match;
|
|
113
|
+
while ((match = pattern.pattern.exec(line)) !== null) {
|
|
114
|
+
if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
|
|
115
|
+
findings.push({
|
|
116
|
+
line: lineNum + 1,
|
|
117
|
+
matched: match[0],
|
|
118
|
+
patternName: pattern.name,
|
|
119
|
+
severity: pattern.severity,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch {}
|
|
125
|
+
return findings;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =============================================================================
|
|
129
|
+
// ENV VAR GENERATION
|
|
130
|
+
// =============================================================================
|
|
131
|
+
|
|
132
|
+
function buildEnvVarSuggestions(results) {
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
const vars = [];
|
|
135
|
+
|
|
136
|
+
for (const { findings } of results) {
|
|
137
|
+
for (const f of findings) {
|
|
138
|
+
const varName = patternToEnvVar(f.patternName);
|
|
139
|
+
if (!seen.has(varName)) {
|
|
140
|
+
seen.add(varName);
|
|
141
|
+
vars.push({ name: varName, comment: f.patternName });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return vars;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Convert a pattern name to a sensible env var name.
|
|
151
|
+
* e.g. "OpenAI API Key" → "OPENAI_API_KEY" // praxis-ignore — env var name in doc comment, not a secret value
|
|
152
|
+
*/
|
|
153
|
+
function patternToEnvVar(patternName) {
|
|
154
|
+
return patternName
|
|
155
|
+
.toUpperCase()
|
|
156
|
+
.replace(/[^A-Z0-9\s]/g, '')
|
|
157
|
+
.trim()
|
|
158
|
+
.replace(/\s+/g, '_');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// =============================================================================
|
|
162
|
+
// OUTPUT
|
|
163
|
+
// =============================================================================
|
|
164
|
+
|
|
165
|
+
function printFindings(results, rootPath) {
|
|
166
|
+
const total = results.reduce((sum, r) => sum + r.findings.length, 0);
|
|
167
|
+
console.log(chalk.red.bold(`\n Found ${total} secret(s) across ${results.length} file(s)\n`));
|
|
168
|
+
|
|
169
|
+
for (const { file, findings } of results) {
|
|
170
|
+
const relPath = path.relative(rootPath, file);
|
|
171
|
+
console.log(chalk.white.bold(` ${relPath}`));
|
|
172
|
+
for (const f of findings) {
|
|
173
|
+
console.log(chalk.gray(` Line ${f.line}: `) + chalk.yellow(f.patternName));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function printEnvExample(envVars, dryRun) {
|
|
179
|
+
const lines = [
|
|
180
|
+
'# .env.example',
|
|
181
|
+
'# Generated by praxis — replace placeholder values with your actual secrets.',
|
|
182
|
+
'# Copy this file to .env and fill in the values.',
|
|
183
|
+
'# NEVER commit .env — only commit .env.example',
|
|
184
|
+
'',
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
for (const { name, comment } of envVars) {
|
|
188
|
+
lines.push(`# ${comment}`);
|
|
189
|
+
lines.push(`${name}=your_${name.toLowerCase()}_here`);
|
|
190
|
+
lines.push('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const content = lines.join('\n');
|
|
194
|
+
|
|
195
|
+
output.header(dryRun ? '.env.example Preview (dry run)' : 'Generated .env.example');
|
|
196
|
+
console.log();
|
|
197
|
+
console.log(chalk.gray(content));
|
|
198
|
+
|
|
199
|
+
if (!dryRun) {
|
|
200
|
+
const envExamplePath = path.join(process.cwd(), '.env.example');
|
|
201
|
+
|
|
202
|
+
if (fs.existsSync(envExamplePath)) {
|
|
203
|
+
output.warning('.env.example already exists — skipping. Use --force to overwrite.');
|
|
204
|
+
} else {
|
|
205
|
+
fs.writeFileSync(envExamplePath, content);
|
|
206
|
+
output.success('Created .env.example');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log();
|
|
210
|
+
console.log(chalk.cyan.bold('Next steps:'));
|
|
211
|
+
console.log(chalk.white('1.') + chalk.gray(' Copy .env.example to .env'));
|
|
212
|
+
console.log(chalk.white('2.') + chalk.gray(' Replace placeholder values with your real secrets'));
|
|
213
|
+
console.log(chalk.white('3.') + chalk.gray(' Remove the hardcoded values from your source code'));
|
|
214
|
+
console.log(chalk.white('4.') + chalk.gray(' Verify .env is in your .gitignore'));
|
|
215
|
+
console.log(chalk.white('5.') + chalk.gray(' Run npx praxis-sec scan . to confirm clean'));
|
|
216
|
+
console.log();
|
|
217
|
+
}
|
|
218
|
+
}
|