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,768 @@
1
+ /**
2
+ * Rotate Command
3
+ * ==============
4
+ *
5
+ * Guides you through revoking and rotating exposed secrets.
6
+ * For each secret type found, opens the provider's key management page
7
+ * and shows step-by-step revocation instructions.
8
+ *
9
+ * For GitHub tokens, calls the GitHub credentials revocation API directly
10
+ * (no auth required — designed for reporting exposed credentials).
11
+ *
12
+ * USAGE:
13
+ * praxis rotate . Scan local files and rotate found secrets
14
+ * praxis rotate . --provider github Only rotate GitHub tokens
15
+ * praxis rotate --plan rotation-plan.json Execute a plan from .com/rotate
16
+ *
17
+ * RECOMMENDED ORDER:
18
+ * 1. praxis rotate ← revoke the key so it can't be used
19
+ * 2. praxis remediate ← fix source code
20
+ * 3. Commit fixed files
21
+ */
22
+
23
+ import fs from 'fs';
24
+ import path from 'path';
25
+ import readline from 'readline';
26
+ import { execSync } from 'child_process';
27
+ import chalk from 'chalk';
28
+ import ora from 'ora';
29
+ import fg from 'fast-glob';
30
+ import {
31
+ SECRET_PATTERNS,
32
+ SKIP_DIRS,
33
+ SKIP_EXTENSIONS,
34
+ TEST_FILE_PATTERNS,
35
+ MAX_FILE_SIZE
36
+ } from '../utils/patterns.js';
37
+ import { isHighEntropyMatch } from '../utils/entropy.js';
38
+ import * as output from '../utils/output.js';
39
+
40
+ // =============================================================================
41
+ // PROVIDER ROTATION INFO
42
+ // =============================================================================
43
+
44
+ /**
45
+ * Maps pattern names to provider revocation info.
46
+ * url: Where to revoke/rotate the key
47
+ * instructions: Exact steps to follow
48
+ * apiRevoke: If true, attempt programmatic revocation via API
49
+ * providerKey: Short identifier for --provider flag filtering
50
+ */
51
+ const PROVIDER_INFO = {
52
+ // AI Providers
53
+ 'OpenAI API Key': {
54
+ provider: 'openai',
55
+ name: 'OpenAI',
56
+ url: 'https://platform.openai.com/api-keys',
57
+ instructions: [
58
+ 'Go to platform.openai.com/api-keys',
59
+ 'Find the compromised key (starts with sk-...)',
60
+ 'Click the trash icon to revoke it',
61
+ 'Create a new key and update your .env'
62
+ ]
63
+ },
64
+ 'OpenAI Project Key': {
65
+ provider: 'openai',
66
+ name: 'OpenAI',
67
+ url: 'https://platform.openai.com/api-keys',
68
+ instructions: [
69
+ 'Go to platform.openai.com/api-keys',
70
+ 'Find the compromised project key',
71
+ 'Revoke it and create a new one'
72
+ ]
73
+ },
74
+ 'Anthropic API Key': {
75
+ provider: 'anthropic',
76
+ name: 'Anthropic',
77
+ url: 'https://console.anthropic.com/settings/keys',
78
+ instructions: [
79
+ 'Go to console.anthropic.com/settings/keys',
80
+ 'Delete the compromised key',
81
+ 'Create a new key and update your .env'
82
+ ]
83
+ },
84
+ 'Google AI (Gemini) API Key': {
85
+ provider: 'google',
86
+ name: 'Google AI',
87
+ url: 'https://aistudio.google.com/app/apikey',
88
+ instructions: [
89
+ 'Go to aistudio.google.com/app/apikey',
90
+ 'Delete the compromised key',
91
+ 'Create a new one and update your .env'
92
+ ]
93
+ },
94
+ 'Replicate API Token': {
95
+ provider: 'replicate',
96
+ name: 'Replicate',
97
+ url: 'https://replicate.com/account/api-tokens',
98
+ instructions: [
99
+ 'Go to replicate.com/account/api-tokens',
100
+ 'Delete the compromised token',
101
+ 'Create a new one and update your .env'
102
+ ]
103
+ },
104
+ 'Hugging Face Token': {
105
+ provider: 'huggingface',
106
+ name: 'Hugging Face',
107
+ url: 'https://huggingface.co/settings/tokens',
108
+ instructions: [
109
+ 'Go to huggingface.co/settings/tokens',
110
+ 'Revoke the compromised token',
111
+ 'Create a new one and update your .env'
112
+ ]
113
+ },
114
+ 'Groq API Key': {
115
+ provider: 'groq',
116
+ name: 'Groq',
117
+ url: 'https://console.groq.com/keys',
118
+ instructions: [
119
+ 'Go to console.groq.com/keys',
120
+ 'Delete the compromised key',
121
+ 'Create a new one and update your .env'
122
+ ]
123
+ },
124
+
125
+ // GitHub — API revocation supported
126
+ 'GitHub Personal Access Token': {
127
+ provider: 'github',
128
+ name: 'GitHub',
129
+ url: 'https://github.com/settings/tokens',
130
+ apiRevoke: true,
131
+ instructions: [
132
+ 'Attempting API revocation...',
133
+ 'Then go to github.com/settings/tokens to confirm it\'s revoked',
134
+ 'Create a new token with minimal required scopes'
135
+ ]
136
+ },
137
+ 'GitHub OAuth Token': {
138
+ provider: 'github',
139
+ name: 'GitHub',
140
+ url: 'https://github.com/settings/tokens',
141
+ apiRevoke: true,
142
+ instructions: [
143
+ 'Attempting API revocation...',
144
+ 'Verify at github.com/settings/applications'
145
+ ]
146
+ },
147
+ 'GitHub App Token': {
148
+ provider: 'github',
149
+ name: 'GitHub',
150
+ url: 'https://github.com/settings/tokens',
151
+ apiRevoke: true,
152
+ instructions: [
153
+ 'Attempting API revocation...',
154
+ 'Verify revocation at github.com/settings/tokens'
155
+ ]
156
+ },
157
+ 'GitHub Fine-Grained PAT': {
158
+ provider: 'github',
159
+ name: 'GitHub',
160
+ url: 'https://github.com/settings/personal-access-tokens',
161
+ apiRevoke: true,
162
+ instructions: [
163
+ 'Attempting API revocation...',
164
+ 'Verify at github.com/settings/personal-access-tokens'
165
+ ]
166
+ },
167
+
168
+ // Payments
169
+ 'Stripe Live Secret Key': {
170
+ provider: 'stripe',
171
+ name: 'Stripe',
172
+ url: 'https://dashboard.stripe.com/apikeys',
173
+ instructions: [
174
+ 'Go to dashboard.stripe.com/apikeys',
175
+ 'Click the "..." menu next to the compromised key',
176
+ 'Select "Roll key" — this revokes old and creates new simultaneously',
177
+ 'Update the new key in your .env immediately'
178
+ ]
179
+ },
180
+ 'Stripe Test Secret Key': {
181
+ provider: 'stripe',
182
+ name: 'Stripe',
183
+ url: 'https://dashboard.stripe.com/test/apikeys',
184
+ instructions: [
185
+ 'Go to dashboard.stripe.com/test/apikeys',
186
+ 'Roll the test key',
187
+ 'Update your .env'
188
+ ]
189
+ },
190
+ 'Stripe Webhook Secret': {
191
+ provider: 'stripe',
192
+ name: 'Stripe',
193
+ url: 'https://dashboard.stripe.com/webhooks',
194
+ instructions: [
195
+ 'Go to dashboard.stripe.com/webhooks',
196
+ 'Select the webhook endpoint',
197
+ 'Rotate the signing secret',
198
+ 'Update STRIPE_WEBHOOK_SECRET in your .env'
199
+ ]
200
+ },
201
+
202
+ // Cloud & Hosting
203
+ 'AWS Access Key ID': {
204
+ provider: 'aws',
205
+ name: 'AWS',
206
+ url: 'https://console.aws.amazon.com/iam/home#/security_credentials',
207
+ instructions: [
208
+ 'Run: aws iam delete-access-key --access-key-id <YOUR_KEY_ID>',
209
+ 'Then: aws iam create-access-key --user-name <YOUR_USER>',
210
+ 'Or go to console.aws.amazon.com → IAM → Security credentials',
211
+ 'Delete the compromised key and create a new one',
212
+ 'Update AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in your .env'
213
+ ]
214
+ },
215
+ 'AWS Secret Access Key': {
216
+ provider: 'aws',
217
+ name: 'AWS',
218
+ url: 'https://console.aws.amazon.com/iam/home#/security_credentials',
219
+ instructions: [
220
+ 'Deactivate key: aws iam update-access-key --status Inactive --access-key-id <ID>',
221
+ 'Delete key: aws iam delete-access-key --access-key-id <ID>',
222
+ 'Create new: aws iam create-access-key --user-name <USER>',
223
+ 'Update .env with new credentials'
224
+ ]
225
+ },
226
+ 'Vercel Token': {
227
+ provider: 'vercel',
228
+ name: 'Vercel',
229
+ url: 'https://vercel.com/account/tokens',
230
+ instructions: [
231
+ 'Go to vercel.com/account/tokens',
232
+ 'Delete the compromised token',
233
+ 'Create a new one and update your .env'
234
+ ]
235
+ },
236
+ 'NPM Token': {
237
+ provider: 'npm',
238
+ name: 'npm',
239
+ url: 'https://www.npmjs.com/settings/~/tokens',
240
+ instructions: [
241
+ 'Go to npmjs.com/settings/~/tokens',
242
+ 'Revoke the compromised token',
243
+ 'Create a new granular access token'
244
+ ]
245
+ },
246
+ 'Netlify Personal Access Token': {
247
+ provider: 'netlify',
248
+ name: 'Netlify',
249
+ url: 'https://app.netlify.com/user/applications#personal-access-tokens',
250
+ instructions: [
251
+ 'Go to app.netlify.com/user/applications',
252
+ 'Revoke the compromised token',
253
+ 'Create a new one and update your .env'
254
+ ]
255
+ },
256
+ 'DigitalOcean Token': {
257
+ provider: 'digitalocean',
258
+ name: 'DigitalOcean',
259
+ url: 'https://cloud.digitalocean.com/account/api/tokens',
260
+ instructions: [
261
+ 'Go to cloud.digitalocean.com/account/api/tokens',
262
+ 'Delete the compromised token',
263
+ 'Create a new one and update your .env'
264
+ ]
265
+ },
266
+
267
+ // Communication
268
+ 'Slack Token': {
269
+ provider: 'slack',
270
+ name: 'Slack',
271
+ url: 'https://api.slack.com/apps',
272
+ instructions: [
273
+ 'Go to api.slack.com/apps',
274
+ 'Select your app → OAuth & Permissions',
275
+ 'Revoke all tokens under "OAuth Tokens for Your Workspace"',
276
+ 'Reinstall the app to get fresh tokens'
277
+ ]
278
+ },
279
+ 'Slack Webhook': {
280
+ provider: 'slack',
281
+ name: 'Slack',
282
+ url: 'https://api.slack.com/apps',
283
+ instructions: [
284
+ 'Go to api.slack.com/apps → your app → Incoming Webhooks',
285
+ 'Revoke the compromised webhook URL',
286
+ 'Create a new webhook for the channel'
287
+ ]
288
+ },
289
+ 'Discord Webhook': {
290
+ provider: 'discord',
291
+ name: 'Discord',
292
+ url: 'https://discord.com/channels/@me',
293
+ instructions: [
294
+ 'Go to the Discord channel → Edit Channel → Integrations → Webhooks',
295
+ 'Delete the compromised webhook',
296
+ 'Create a new one and update your .env'
297
+ ]
298
+ },
299
+
300
+ // Email
301
+ 'SendGrid API Key': {
302
+ provider: 'sendgrid',
303
+ name: 'SendGrid',
304
+ url: 'https://app.sendgrid.com/settings/api_keys',
305
+ instructions: [
306
+ 'Go to app.sendgrid.com/settings/api_keys',
307
+ 'Revoke the compromised key',
308
+ 'Create a new key with minimum required permissions'
309
+ ]
310
+ },
311
+ 'Resend API Key': {
312
+ provider: 'resend',
313
+ name: 'Resend',
314
+ url: 'https://resend.com/api-keys',
315
+ instructions: [
316
+ 'Go to resend.com/api-keys',
317
+ 'Delete the compromised key',
318
+ 'Create a new one and update your .env'
319
+ ]
320
+ },
321
+
322
+ // Databases
323
+ 'Supabase Service Role Key': {
324
+ provider: 'supabase',
325
+ name: 'Supabase',
326
+ url: 'https://supabase.com/dashboard/project/_/settings/api',
327
+ instructions: [
328
+ 'Go to supabase.com/dashboard → your project → Settings → API',
329
+ 'Copy the current service role key for reference',
330
+ 'Click "Reset" to generate a new JWT secret — this rotates all keys',
331
+ 'Update SUPABASE_SERVICE_ROLE_KEY in your .env',
332
+ 'WARNING: This also rotates the anon key — update that too'
333
+ ]
334
+ },
335
+ 'PlanetScale Password': {
336
+ provider: 'planetscale',
337
+ name: 'PlanetScale',
338
+ url: 'https://app.planetscale.com',
339
+ instructions: [
340
+ 'Go to app.planetscale.com → your database → Passwords',
341
+ 'Delete the compromised password',
342
+ 'Create a new password and update your connection string'
343
+ ]
344
+ },
345
+ 'Neon Database Connection String': {
346
+ provider: 'neon',
347
+ name: 'Neon',
348
+ url: 'https://console.neon.tech',
349
+ instructions: [
350
+ 'Go to console.neon.tech → your project → Connection Details',
351
+ 'Reset the database password',
352
+ 'Update DATABASE_URL in your .env with the new connection string'
353
+ ]
354
+ },
355
+ };
356
+
357
+ // =============================================================================
358
+ // GITHUB API REVOCATION
359
+ // =============================================================================
360
+
361
+ /**
362
+ * Attempt to revoke a GitHub token via the public credentials revocation API.
363
+ * This endpoint does not require authentication — it's designed for reporting
364
+ * exposed credentials found in code.
365
+ *
366
+ * Docs: https://docs.github.com/en/rest/credentials/revoke
367
+ */
368
+ async function revokeGitHubToken(token) {
369
+ try {
370
+ const response = await fetch('https://api.github.com/credentials/revoke', {
371
+ method: 'DELETE',
372
+ headers: {
373
+ 'Accept': 'application/vnd.github+json',
374
+ 'X-GitHub-Api-Version': '2022-11-28',
375
+ 'Content-Type': 'application/json',
376
+ 'User-Agent': 'praxis-cli'
377
+ },
378
+ body: JSON.stringify({ access_token: token })
379
+ });
380
+
381
+ // 204 = revoked successfully, 422 = already revoked/invalid
382
+ return response.status === 204 || response.status === 422;
383
+ } catch {
384
+ return false; // Network error — fall back to manual
385
+ }
386
+ }
387
+
388
+ // =============================================================================
389
+ // BROWSER OPEN
390
+ // =============================================================================
391
+
392
+ function openBrowser(url) {
393
+ try {
394
+ const platform = process.platform;
395
+ if (platform === 'win32') execSync(`start "" "${url}"`, { stdio: 'ignore' }); // praxis-ignore — url is hardcoded provider dashboard URL
396
+ else if (platform === 'darwin') execSync(`open "${url}"`, { stdio: 'ignore' }); // praxis-ignore
397
+ else execSync(`xdg-open "${url}"`, { stdio: 'ignore' }); // praxis-ignore
398
+ return true;
399
+ } catch {
400
+ return false;
401
+ }
402
+ }
403
+
404
+ // =============================================================================
405
+ // SCAN
406
+ // =============================================================================
407
+
408
+ async function findFiles(rootPath) {
409
+ const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
410
+ const files = await fg('**/*', {
411
+ cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true
412
+ });
413
+ const filtered = [];
414
+ for (const file of files) {
415
+ const ext = path.extname(file).toLowerCase();
416
+ if (SKIP_EXTENSIONS.has(ext)) continue;
417
+ const basename = path.basename(file);
418
+ if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) continue;
419
+ if (TEST_FILE_PATTERNS.some(p => p.test(file))) continue;
420
+ if (basename === '.env' || basename === '.env.example') continue;
421
+ try {
422
+ const stats = fs.statSync(file);
423
+ if (stats.size > MAX_FILE_SIZE) continue;
424
+ } catch { continue; }
425
+ filtered.push(file);
426
+ }
427
+ return filtered;
428
+ }
429
+
430
+ async function scanFile(filePath) {
431
+ const findings = [];
432
+ try {
433
+ const content = fs.readFileSync(filePath, 'utf-8');
434
+ const lines = content.split('\n');
435
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
436
+ const line = lines[lineNum];
437
+ if (/praxis-ignore/i.test(line)) continue;
438
+ for (const pattern of SECRET_PATTERNS) {
439
+ pattern.pattern.lastIndex = 0;
440
+ let match;
441
+ while ((match = pattern.pattern.exec(line)) !== null) {
442
+ if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
443
+ findings.push({
444
+ line: lineNum + 1,
445
+ matched: match[0],
446
+ patternName: pattern.name,
447
+ severity: pattern.severity,
448
+ });
449
+ }
450
+ }
451
+ }
452
+ } catch { /* skip */ }
453
+ return findings;
454
+ }
455
+
456
+ // =============================================================================
457
+ // MASK HELPER
458
+ // =============================================================================
459
+
460
+ function maskToken(token) {
461
+ if (token.length <= 8) return '****';
462
+ return token.substring(0, 6) + '*'.repeat(Math.min(token.length - 6, 16)) + token.slice(-4);
463
+ }
464
+
465
+ // =============================================================================
466
+ // PLAN-BASED ROTATION (--plan rotation-plan.json)
467
+ // =============================================================================
468
+
469
+ function promptHidden(question) {
470
+ return new Promise(resolve => {
471
+ process.stdout.write(question);
472
+ const stdin = process.stdin;
473
+ const wasRaw = stdin.isRaw;
474
+ try { stdin.setRawMode(true); } catch { /* not a TTY */ }
475
+ stdin.resume();
476
+ stdin.setEncoding('utf8');
477
+ let value = '';
478
+ function onData(ch) {
479
+ if (ch === '\n' || ch === '\r' || ch === '\u0003') {
480
+ stdin.removeListener('data', onData);
481
+ try { stdin.setRawMode(!!wasRaw); } catch { /* ignore */ }
482
+ stdin.pause();
483
+ process.stdout.write('\n');
484
+ if (ch === '\u0003') process.exit(0);
485
+ resolve(value);
486
+ } else if (ch === '\u007f' || ch === '\b') {
487
+ value = value.slice(0, -1);
488
+ } else {
489
+ value += ch;
490
+ process.stdout.write('*');
491
+ }
492
+ }
493
+ stdin.on('data', onData);
494
+ });
495
+ }
496
+
497
+ async function updateVercelEnvVar(token, projectId, envId, envType, newValue, teamId) {
498
+ const params = new URLSearchParams();
499
+ if (teamId) params.set('teamId', teamId);
500
+ const url = `https://api.vercel.com/v9/projects/${projectId}/env/${envId}?${params}`; // praxis-ignore SSRF_USER_URL_FETCH — hardcoded vendor API endpoint
501
+ const r = await fetch(url, { // praxis-ignore SSRF_USER_URL_FETCH — hardcoded vendor API endpoint
502
+ method: 'PATCH',
503
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
504
+ body: JSON.stringify({ value: newValue, type: envType || 'encrypted' }),
505
+ });
506
+ if (!r.ok) {
507
+ const body = await r.text();
508
+ throw new Error(`${r.status}: ${body}`);
509
+ }
510
+ }
511
+
512
+ export async function rotatePlanCommand(planFile) {
513
+ // ── 1. Read and validate plan ──────────────────────────────────────────────
514
+ const planPath = path.resolve(planFile);
515
+ if (!fs.existsSync(planPath)) {
516
+ output.error(`Plan file not found: ${planPath}`);
517
+ process.exit(1);
518
+ }
519
+
520
+ let plan;
521
+ try {
522
+ plan = JSON.parse(fs.readFileSync(planPath, 'utf-8'));
523
+ } catch {
524
+ output.error('Failed to parse rotation plan JSON. Make sure it was downloaded from .com/rotate');
525
+ process.exit(1);
526
+ }
527
+
528
+ const issuers = plan.issuers;
529
+ if (!issuers || typeof issuers !== 'object' || Object.keys(issuers).length === 0) {
530
+ output.success('No credentials in rotation plan — nothing to rotate.');
531
+ return;
532
+ }
533
+
534
+ const totalEnvVars = Object.values(issuers).reduce((sum, g) => sum + (g.affected?.length ?? 0), 0);
535
+ const issuerList = Object.entries(issuers);
536
+
537
+ output.header('Credential Rotation Plan');
538
+ console.log(chalk.gray(`\n Plan generated: ${plan.generated ?? 'unknown'}`));
539
+ console.log(chalk.gray(` Projects scanned: ${plan.projectsScanned ?? 'unknown'}`));
540
+ console.log(chalk.gray(` Env vars to update: ${totalEnvVars}`));
541
+ console.log(chalk.gray(` Credential types: ${issuerList.length}\n`));
542
+
543
+ // ── 2. Prompt for Vercel token ────────────────────────────────────────────
544
+ console.log(chalk.cyan.bold(' This command will update your Vercel env vars via the API.'));
545
+ console.log(chalk.gray(' Your token is used only for API calls and is never stored.\n'));
546
+
547
+ const vercelToken = await promptHidden(chalk.white(' Enter your Vercel API token: '));
548
+ if (!vercelToken) {
549
+ output.error('No Vercel token provided. Aborting.');
550
+ process.exit(1);
551
+ }
552
+
553
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
554
+ const teamId = plan.teamId || '';
555
+
556
+ // ── 3. Process each issuer ────────────────────────────────────────────────
557
+ const auditLog = [];
558
+ let totalUpdated = 0;
559
+ let totalFailed = 0;
560
+
561
+ for (let i = 0; i < issuerList.length; i++) {
562
+ const [issuerKey, issuerData] = issuerList[i];
563
+ const affected = issuerData.affected ?? [];
564
+ if (affected.length === 0) continue;
565
+
566
+ const projectCount = new Set(affected.map(a => a.projectId)).size;
567
+ console.log(chalk.white.bold(`\n [${i + 1}/${issuerList.length}] ${issuerData.name ?? issuerKey}`));
568
+ console.log(chalk.gray(` ${affected.length} env var${affected.length !== 1 ? 's' : ''} across ${projectCount} project${projectCount !== 1 ? 's' : ''}`));
569
+
570
+ if (issuerData.rotateUrl) {
571
+ console.log(chalk.gray(` Rotate URL: ${chalk.cyan(issuerData.rotateUrl)}`));
572
+ const opened = openBrowser(issuerData.rotateUrl);
573
+ if (opened) {
574
+ console.log(chalk.gray(' ✓ Opened in browser'));
575
+ } else {
576
+ console.log(chalk.yellow(` → Open manually: ${issuerData.rotateUrl}`));
577
+ }
578
+ } else {
579
+ console.log(chalk.yellow(' No rotation URL — rotate manually in the provider dashboard.'));
580
+ }
581
+
582
+ // Get one new value per unique env var name for this issuer
583
+ const uniqueKeys = [...new Set(affected.map(a => a.envVar))];
584
+ const newValues = {};
585
+
586
+ for (const envKey of uniqueKeys) {
587
+ const newVal = await promptHidden(chalk.white(`\n Paste new value for ${chalk.cyan(envKey)}: `));
588
+ if (!newVal) {
589
+ console.log(chalk.yellow(` Skipping ${envKey} (no value entered)`));
590
+ continue;
591
+ }
592
+ newValues[envKey] = newVal;
593
+ }
594
+
595
+ if (Object.keys(newValues).length === 0) {
596
+ console.log(chalk.gray(' Skipped all env vars for this issuer.\n'));
597
+ continue;
598
+ }
599
+
600
+ // Update each affected env var via Vercel API
601
+ const spinner = ora({ text: ` Updating ${affected.length} env var${affected.length !== 1 ? 's' : ''}...`, color: 'cyan' }).start();
602
+ let issuerUpdated = 0;
603
+ let issuerFailed = 0;
604
+
605
+ for (const item of affected) {
606
+ const newVal = newValues[item.envVar];
607
+ if (!newVal) continue;
608
+ try {
609
+ await updateVercelEnvVar(vercelToken, item.projectId, item.envId, item.envType, newVal, teamId);
610
+ issuerUpdated++;
611
+ auditLog.push({
612
+ ts: new Date().toISOString(),
613
+ issuer: issuerKey,
614
+ project: item.projectName,
615
+ projectId: item.projectId,
616
+ envVar: item.envVar,
617
+ status: 'updated',
618
+ });
619
+ } catch (e) {
620
+ issuerFailed++;
621
+ auditLog.push({
622
+ ts: new Date().toISOString(),
623
+ issuer: issuerKey,
624
+ project: item.projectName,
625
+ projectId: item.projectId,
626
+ envVar: item.envVar,
627
+ status: 'failed',
628
+ error: e instanceof Error ? e.message : String(e),
629
+ });
630
+ }
631
+ }
632
+
633
+ totalUpdated += issuerUpdated;
634
+ totalFailed += issuerFailed;
635
+
636
+ if (issuerFailed === 0) {
637
+ spinner.succeed(chalk.green(` Updated ${issuerUpdated} env var${issuerUpdated !== 1 ? 's' : ''}`));
638
+ } else {
639
+ spinner.warn(chalk.yellow(` Updated ${issuerUpdated}, failed ${issuerFailed}`));
640
+ }
641
+ }
642
+
643
+ rl.close();
644
+
645
+ // ── 4. Write audit log ────────────────────────────────────────────────────
646
+ const auditPath = path.resolve('rotation-audit.json');
647
+ fs.writeFileSync(auditPath, JSON.stringify({ rotatedAt: new Date().toISOString(), auditLog }, null, 2));
648
+
649
+ // ── 5. Summary ────────────────────────────────────────────────────────────
650
+ console.log();
651
+ console.log(chalk.cyan.bold(' Rotation complete'));
652
+ console.log(chalk.white(` ✓ ${totalUpdated} env var${totalUpdated !== 1 ? 's' : ''} updated`));
653
+ if (totalFailed > 0) {
654
+ console.log(chalk.red(` ✗ ${totalFailed} failed — see rotation-audit.json for details`));
655
+ }
656
+ console.log(chalk.gray(`\n Audit log written to: ${auditPath}`));
657
+ console.log(chalk.gray(' Run praxis scan . to confirm no hardcoded credentials remain.\n'));
658
+ }
659
+
660
+ export async function rotateCommand(targetPath = '.', options = {}) {
661
+ // If --plan flag provided, delegate to plan-based rotation
662
+ if (options.plan) {
663
+ return rotatePlanCommand(options.plan);
664
+ }
665
+ const absolutePath = path.resolve(targetPath);
666
+
667
+ if (!fs.existsSync(absolutePath)) {
668
+ output.error(`Path does not exist: ${absolutePath}`);
669
+ process.exit(1);
670
+ }
671
+
672
+ // ── 1. Scan ───────────────────────────────────────────────────────────────
673
+ const spinner = ora({ text: 'Scanning for secrets to rotate...', color: 'cyan' }).start();
674
+
675
+ const files = await findFiles(absolutePath);
676
+ const scanResults = [];
677
+ for (const file of files) {
678
+ const findings = await scanFile(file);
679
+ if (findings.length > 0) scanResults.push({ file, findings });
680
+ }
681
+
682
+ spinner.stop();
683
+
684
+ if (scanResults.length === 0) {
685
+ output.success('No secrets found — nothing to rotate!');
686
+ return;
687
+ }
688
+
689
+ // ── 2. Deduplicate findings by pattern name ───────────────────────────────
690
+ const uniqueFindings = new Map(); // patternName → {matched, file, line}
691
+ for (const { file, findings } of scanResults) {
692
+ for (const f of findings) {
693
+ if (!uniqueFindings.has(f.patternName)) {
694
+ uniqueFindings.set(f.patternName, { ...f, file });
695
+ }
696
+ }
697
+ }
698
+
699
+ // ── 3. Filter by --provider if specified ──────────────────────────────────
700
+ let findingsToRotate = [...uniqueFindings.values()];
701
+ if (options.provider) {
702
+ findingsToRotate = findingsToRotate.filter(f => {
703
+ const info = PROVIDER_INFO[f.patternName];
704
+ return info && info.provider === options.provider.toLowerCase();
705
+ });
706
+ if (findingsToRotate.length === 0) {
707
+ output.warning(`No secrets found for provider: ${options.provider}`);
708
+ return;
709
+ }
710
+ }
711
+
712
+ output.header('Secret Rotation Guide');
713
+ console.log(chalk.gray(`\n Found ${findingsToRotate.length} unique secret type(s) to rotate\n`));
714
+ console.log(chalk.yellow.bold(' Rotate secrets BEFORE fixing code or cleaning git history.\n'));
715
+
716
+ // ── 4. Process each finding ───────────────────────────────────────────────
717
+ for (const finding of findingsToRotate) {
718
+ const info = PROVIDER_INFO[finding.patternName];
719
+
720
+ console.log(chalk.white.bold(`\n ▸ ${finding.patternName}`));
721
+ console.log(chalk.gray(` Found: ${maskToken(finding.matched)}`));
722
+
723
+ if (!info) {
724
+ // Unknown provider — give generic instructions
725
+ console.log(chalk.yellow(' No specific rotation guide available for this secret type.'));
726
+ console.log(chalk.gray(' → Revoke it manually in the provider\'s dashboard'));
727
+ console.log(chalk.gray(' → Create a new key and update your .env'));
728
+ continue;
729
+ }
730
+
731
+ console.log(chalk.gray(` Provider: ${info.name}`));
732
+ console.log(chalk.gray(` Revocation URL: ${chalk.cyan(info.url)}`));
733
+ console.log();
734
+
735
+ // GitHub: attempt API revocation
736
+ if (info.apiRevoke && finding.patternName.includes('GitHub')) {
737
+ const apiSpinner = ora({ text: ' Attempting API revocation...', color: 'cyan' }).start();
738
+ const revoked = await revokeGitHubToken(finding.matched);
739
+ if (revoked) {
740
+ apiSpinner.succeed(chalk.green(' Token revoked via GitHub API'));
741
+ } else {
742
+ apiSpinner.warn(chalk.yellow(' API revocation failed — revoke manually (see URL above)'));
743
+ }
744
+ }
745
+
746
+ // Show step-by-step instructions
747
+ console.log(chalk.gray(' Steps:'));
748
+ info.instructions.forEach((step, i) => {
749
+ console.log(chalk.gray(` ${i + 1}. ${step}`));
750
+ });
751
+
752
+ // Open browser
753
+ const opened = openBrowser(info.url);
754
+ if (opened) {
755
+ console.log(chalk.gray(`\n ✓ Opened ${info.url} in your browser`));
756
+ } else {
757
+ console.log(chalk.gray(`\n → Open manually: ${info.url}`));
758
+ }
759
+ }
760
+
761
+ // ── 5. Final guidance ─────────────────────────────────────────────────────
762
+ console.log();
763
+ console.log(chalk.cyan.bold(' After rotating all keys:'));
764
+ console.log(chalk.white(' 1.') + chalk.gray(' Run praxis remediate . to fix your source code'));
765
+ console.log(chalk.white(' 2.') + chalk.gray(' Commit the cleaned files'));
766
+ console.log(chalk.white(' 3.') + chalk.gray(' Run praxis scan . to confirm nothing was missed'));
767
+ console.log();
768
+ }