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.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/ai-defense/cost-protection.md +292 -0
  4. package/ai-defense/llm-security-checklist.md +324 -0
  5. package/ai-defense/prompt-injection-patterns.js +283 -0
  6. package/ai-defense/system-prompt-armor.md +327 -0
  7. package/checklists/launch-day.md +168 -0
  8. package/cli/agents/abom-generator.js +225 -0
  9. package/cli/agents/agent-attestation-agent.js +318 -0
  10. package/cli/agents/agent-config-scanner.js +787 -0
  11. package/cli/agents/agent-telemetry-agent.js +415 -0
  12. package/cli/agents/agentic-security-agent.js +296 -0
  13. package/cli/agents/agentic-supply-chain-agent.js +463 -0
  14. package/cli/agents/ai-infra-inventory-agent.js +449 -0
  15. package/cli/agents/api-fuzzer.js +345 -0
  16. package/cli/agents/auth-bypass-agent.js +348 -0
  17. package/cli/agents/base-agent.js +280 -0
  18. package/cli/agents/cicd-scanner.js +300 -0
  19. package/cli/agents/config-auditor.js +757 -0
  20. package/cli/agents/deep-analyzer.js +776 -0
  21. package/cli/agents/endpoint-agent-abuse-agent.js +404 -0
  22. package/cli/agents/exception-handler-agent.js +187 -0
  23. package/cli/agents/git-history-scanner.js +169 -0
  24. package/cli/agents/governance-audits.js +138 -0
  25. package/cli/agents/hermes-security-agent.js +536 -0
  26. package/cli/agents/html-reporter.js +1125 -0
  27. package/cli/agents/index.js +147 -0
  28. package/cli/agents/injection-tester.js +502 -0
  29. package/cli/agents/legal-risk-agent.js +328 -0
  30. package/cli/agents/llm-redteam.js +199 -0
  31. package/cli/agents/managed-agent-scanner.js +333 -0
  32. package/cli/agents/mcp-security-agent.js +588 -0
  33. package/cli/agents/memory-poisoning-agent.js +305 -0
  34. package/cli/agents/mobile-scanner.js +231 -0
  35. package/cli/agents/model-file-scanner.js +259 -0
  36. package/cli/agents/orchestrator.js +355 -0
  37. package/cli/agents/pii-compliance-agent.js +301 -0
  38. package/cli/agents/policy-engine.js +229 -0
  39. package/cli/agents/prompt-injection-prober.js +224 -0
  40. package/cli/agents/rag-security-agent.js +204 -0
  41. package/cli/agents/recon-agent.js +207 -0
  42. package/cli/agents/sbom-generator.js +265 -0
  43. package/cli/agents/scoring-engine.js +273 -0
  44. package/cli/agents/ssrf-prober.js +130 -0
  45. package/cli/agents/stateful-watcher.js +238 -0
  46. package/cli/agents/supabase-rls-agent.js +154 -0
  47. package/cli/agents/supply-chain-agent.js +857 -0
  48. package/cli/agents/swarm-orchestrator.js +200 -0
  49. package/cli/agents/verifier-agent.js +303 -0
  50. package/cli/agents/vibe-coding-agent.js +250 -0
  51. package/cli/bin/praxis.js +866 -0
  52. package/cli/commands/abom.js +73 -0
  53. package/cli/commands/agent-fix.js +1245 -0
  54. package/cli/commands/audit.js +1180 -0
  55. package/cli/commands/autofix.js +383 -0
  56. package/cli/commands/baseline.js +193 -0
  57. package/cli/commands/benchmark.js +327 -0
  58. package/cli/commands/checklist.js +223 -0
  59. package/cli/commands/ci.js +403 -0
  60. package/cli/commands/deps.js +516 -0
  61. package/cli/commands/diff.js +200 -0
  62. package/cli/commands/doctor.js +195 -0
  63. package/cli/commands/env-audit.js +349 -0
  64. package/cli/commands/fix.js +218 -0
  65. package/cli/commands/guard.js +396 -0
  66. package/cli/commands/hooks.js +278 -0
  67. package/cli/commands/init.js +514 -0
  68. package/cli/commands/legal.js +158 -0
  69. package/cli/commands/live-advisories.js +241 -0
  70. package/cli/commands/mcp.js +660 -0
  71. package/cli/commands/openclaw.js +386 -0
  72. package/cli/commands/red-team.js +350 -0
  73. package/cli/commands/redteam.js +78 -0
  74. package/cli/commands/remediate.js +797 -0
  75. package/cli/commands/rotate.js +768 -0
  76. package/cli/commands/rules.js +196 -0
  77. package/cli/commands/scan-mcp.js +534 -0
  78. package/cli/commands/scan-skill.js +588 -0
  79. package/cli/commands/scan-standard.js +251 -0
  80. package/cli/commands/scan.js +524 -0
  81. package/cli/commands/score.js +449 -0
  82. package/cli/commands/shell.js +514 -0
  83. package/cli/commands/team-report.js +398 -0
  84. package/cli/commands/undo.js +161 -0
  85. package/cli/commands/update-intel.js +126 -0
  86. package/cli/commands/vibe-check.js +276 -0
  87. package/cli/commands/watch.js +757 -0
  88. package/cli/commands/web.js +63 -0
  89. package/cli/core/ast/guardrail-detector.js +141 -0
  90. package/cli/core/ast/index.js +22 -0
  91. package/cli/core/ast/parser.js +676 -0
  92. package/cli/core/ast/scope-tree.js +287 -0
  93. package/cli/core/ast/taint-tracker.js +158 -0
  94. package/cli/core/branding.js +37 -0
  95. package/cli/core/env.js +38 -0
  96. package/cli/core/errors.js +61 -0
  97. package/cli/core/fs.js +62 -0
  98. package/cli/core/output/compliance.js +90 -0
  99. package/cli/core/output/html-theme.js +158 -0
  100. package/cli/core/output/index.js +57 -0
  101. package/cli/core/output/json.js +48 -0
  102. package/cli/core/output/sarif.js +240 -0
  103. package/cli/core/version.js +67 -0
  104. package/cli/core/web/jobs.js +183 -0
  105. package/cli/core/web/projects.js +146 -0
  106. package/cli/core/web/server.js +439 -0
  107. package/cli/data/atlas-knowledge.json +5640 -0
  108. package/cli/data/eaa-catalog.json +39 -0
  109. package/cli/data/known-mcps.json +26 -0
  110. package/cli/data/probes/prompt-injection-corpus.json +271 -0
  111. package/cli/data/threat-intel.json +85 -0
  112. package/cli/data/threatpacks/latest.json +41 -0
  113. package/cli/hooks/patterns.js +313 -0
  114. package/cli/hooks/post-tool-use.js +140 -0
  115. package/cli/hooks/pre-tool-use.js +186 -0
  116. package/cli/index.js +90 -0
  117. package/cli/providers/llm-provider.js +766 -0
  118. package/cli/utils/autofix-rules.js +74 -0
  119. package/cli/utils/cache-manager.js +310 -0
  120. package/cli/utils/compliance-map.js +191 -0
  121. package/cli/utils/entropy.js +132 -0
  122. package/cli/utils/fix-ledger.js +127 -0
  123. package/cli/utils/hermes-tool-registry.js +252 -0
  124. package/cli/utils/intel/cache.js +61 -0
  125. package/cli/utils/intel/http.js +88 -0
  126. package/cli/utils/intel/index.js +235 -0
  127. package/cli/utils/intel/merge.js +229 -0
  128. package/cli/utils/intel/sources/epss.js +54 -0
  129. package/cli/utils/intel/sources/ghsa.js +81 -0
  130. package/cli/utils/intel/sources/gitguardian.js +40 -0
  131. package/cli/utils/intel/sources/gitleaks.js +101 -0
  132. package/cli/utils/intel/sources/kev.js +38 -0
  133. package/cli/utils/intel/sources/nvd.js +84 -0
  134. package/cli/utils/intel/sources/osv.js +132 -0
  135. package/cli/utils/intel/sources/phylum.js +44 -0
  136. package/cli/utils/intel/sources/snyk.js +46 -0
  137. package/cli/utils/intel/sources/socket.js +69 -0
  138. package/cli/utils/intel/sources/sonatype.js +84 -0
  139. package/cli/utils/intel/sources/threatpack.js +69 -0
  140. package/cli/utils/mcp-trust.js +60 -0
  141. package/cli/utils/output.js +251 -0
  142. package/cli/utils/patterns.js +1130 -0
  143. package/cli/utils/pdf-generator.js +94 -0
  144. package/cli/utils/plugin-loader.js +364 -0
  145. package/cli/utils/rule-import.js +228 -0
  146. package/cli/utils/rule-registry.js +426 -0
  147. package/cli/utils/scan-fingerprint.js +109 -0
  148. package/cli/utils/scan-playbook.js +312 -0
  149. package/cli/utils/score-history.js +119 -0
  150. package/cli/utils/secrets-verifier.js +247 -0
  151. package/cli/utils/security-memory.js +296 -0
  152. package/cli/utils/standards/atlas-knowledge.js +87 -0
  153. package/cli/utils/standards/index.js +127 -0
  154. package/cli/utils/standards/sources/avid.js +45 -0
  155. package/cli/utils/standards/sources/eu-ai-act.js +89 -0
  156. package/cli/utils/standards/sources/google-saif.js +39 -0
  157. package/cli/utils/standards/sources/iso-42001.js +94 -0
  158. package/cli/utils/standards/sources/mitre-atlas.js +54 -0
  159. package/cli/utils/standards/sources/nist-ai-600-1.js +45 -0
  160. package/cli/utils/standards/sources/owasp-llm.js +45 -0
  161. package/cli/utils/standards/sources/owasp-ml.js +45 -0
  162. package/cli/utils/threat-intel.js +265 -0
  163. package/configs/firebase/firestore-rules.txt +215 -0
  164. package/configs/firebase/security-checklist.md +236 -0
  165. package/configs/firebase/storage-rules.txt +206 -0
  166. package/configs/gitignore-template +258 -0
  167. package/configs/nextjs-security-headers.js +220 -0
  168. package/configs/praxisignore-template +50 -0
  169. package/configs/supabase/secure-client.ts +225 -0
  170. package/configs/supabase/security-checklist.md +278 -0
  171. package/docs/THIRD_PARTY_NOTICES.md +26 -0
  172. package/docs/THREAT_INTEL.md +292 -0
  173. package/docs/USAGE.md +1205 -0
  174. package/docs/design/WEB-UI.md +82 -0
  175. package/package.json +71 -0
  176. package/scripts/check-determinism.mjs +119 -0
  177. package/snippets/README.md +122 -0
  178. package/snippets/api-security/api-security-checklist.md +412 -0
  179. package/snippets/api-security/cors-config.ts +322 -0
  180. package/snippets/api-security/input-validation.ts +430 -0
  181. package/snippets/auth/jwt-checklist.md +322 -0
  182. package/snippets/rate-limiting/nextjs-middleware.ts +211 -0
  183. 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
+ }