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,398 @@
1
+ /**
2
+ * Team Report Command
3
+ * ====================
4
+ *
5
+ * Converts raw Hermes Agent team output into a professional Praxis report.
6
+ * Strips ANSI codes and terminal chrome, parses structured FINDING: lines,
7
+ * and renders everything through Praxis's HTML reporter.
8
+ *
9
+ * USAGE:
10
+ * praxis team-report Read from stdin (pipe Hermes output)
11
+ * praxis team-report output.txt Read from file
12
+ * praxis team-report output.txt --html Save as HTML
13
+ * praxis team-report output.txt --json JSON output
14
+ */
15
+
16
+ import fs from 'fs';
17
+ import path from 'path';
18
+ import chalk from 'chalk';
19
+ import * as output from '../utils/output.js';
20
+ import { printBanner } from '../utils/output.js';
21
+ import {
22
+ SEVERITY_COLORS,
23
+ baseStyles,
24
+ countBySeverity,
25
+ documentShell,
26
+ esc,
27
+ severityBadge,
28
+ } from '../core/output/html-theme.js';
29
+
30
+ // =============================================================================
31
+ // ANSI + TERMINAL NOISE STRIPPING
32
+ // =============================================================================
33
+
34
+ function stripAnsi(str) {
35
+ // Remove all ANSI escape sequences (colors, cursor moves, clears, etc.)
36
+ /* eslint-disable no-control-regex -- intentional: strips ANSI/terminal escape sequences from captured CLI output */
37
+ return str
38
+ .replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '')
39
+ .replace(/\x1b\][^\x07]*\x07/g, '')
40
+ .replace(/\x1b[()][AB012]/g, '')
41
+ .replace(/\x9b[0-9;]*[A-Za-z]/g, '');
42
+ /* eslint-enable no-control-regex */
43
+ }
44
+
45
+ function stripHermesChrome(text) {
46
+ const lines = text.split('\n');
47
+ const cleaned = [];
48
+ let inSplash = false;
49
+
50
+ for (const line of lines) {
51
+ const t = line.trim();
52
+
53
+ // Skip the Hermes splash box (╭─ ... ─╮ ... ╰─ ... ─╯)
54
+ if (t.startsWith('╭─') || t.startsWith('╰─')) { inSplash = !inSplash; continue; }
55
+ if (inSplash) continue;
56
+
57
+ // Skip raw system prompt instructions leaked into output.
58
+ // Template lines carry '...' placeholders; real findings parse as JSON.
59
+ if (t.startsWith('EXACTLY this format')) continue;
60
+ if (t.startsWith('FINDING: {"severity"') && t.includes('...')) continue;
61
+ if (t.match(/^─{10,}$/)) continue;
62
+
63
+ // Skip Hermes warning lines
64
+ if (t.startsWith('⚠') && t.includes('hermes')) continue;
65
+ if (t.startsWith('⚠') && t.includes('OPENROUTER')) continue;
66
+ if (t.startsWith('⚠') && (t.includes('API call failed') || t.includes('credits'))) continue;
67
+ if (t.startsWith('⏱') || t.startsWith('❌')) continue;
68
+
69
+ // Skip terminal screen-clear sequences
70
+ if (t === '[2J' || t === '[H' || t === '[2J[H') continue;
71
+
72
+ cleaned.push(line);
73
+ }
74
+
75
+ return cleaned.join('\n');
76
+ }
77
+
78
+ // =============================================================================
79
+ // FINDING PARSER
80
+ // =============================================================================
81
+
82
+ function parseFindings(text) {
83
+ const findings = [];
84
+ const findingRegex = /^FINDING:\s*(\{.+\})\s*$/gm;
85
+ let match;
86
+
87
+ while ((match = findingRegex.exec(text)) !== null) {
88
+ try {
89
+ const f = JSON.parse(match[1]);
90
+ if (f.severity && f.title) findings.push(f);
91
+ } catch { /* skip malformed */ }
92
+ }
93
+
94
+ return findings;
95
+ }
96
+
97
+ // =============================================================================
98
+ // AGENT SECTION PARSER
99
+ // =============================================================================
100
+
101
+ function parseAgentSections(text) {
102
+ const sections = [];
103
+ // Matches: ### Agent Name (Role) — N finding(s)
104
+ const sectionRegex = /###\s+(.+?)\s*(?:\(([^)]+)\))?\s*[—–-]+\s*(\d+)\s*finding/gi;
105
+ let match;
106
+
107
+ while ((match = sectionRegex.exec(text)) !== null) {
108
+ sections.push({
109
+ name: match[1].trim(),
110
+ role: match[2]?.trim() || '',
111
+ count: parseInt(match[3], 10),
112
+ });
113
+ }
114
+
115
+ // Also collect bullet findings under each section
116
+ const bulletRegex = /\[(CRITICAL|HIGH|MEDIUM|LOW|INFO)\]\s+(.+?)\s*[—–-]+\s*(.+)/gi;
117
+ const bullets = [];
118
+ while ((match = bulletRegex.exec(text)) !== null) {
119
+ bullets.push({
120
+ severity: match[1].toLowerCase(),
121
+ title: match[2].trim(),
122
+ location: match[3].trim(),
123
+ });
124
+ }
125
+
126
+ return { sections, bullets };
127
+ }
128
+
129
+ // =============================================================================
130
+ // SYNTHESIS PARSER
131
+ // =============================================================================
132
+
133
+ function parseSynthesis(text) {
134
+ // Extract the Hermes synthesis block (inside ╭─ ⚕ Hermes ─╮ ... ╰─╯)
135
+ // After stripping chrome, look for the summary block
136
+ const lines = text.split('\n');
137
+ const synthesisLines = [];
138
+ let capturing = false;
139
+
140
+ for (const line of lines) {
141
+ const t = line.trim();
142
+
143
+ // The synthesis is the content after the agent section summary and before errors
144
+ if (t.match(/^Overall risk posture:/i)) { capturing = true; }
145
+ if (capturing) {
146
+ if (t.startsWith('⚠') || t.startsWith('❌') || t.startsWith('⏱')) break;
147
+ synthesisLines.push(line);
148
+ }
149
+ }
150
+
151
+ // Also look for risk posture statement
152
+ const riskMatch = text.match(/Overall risk posture:\s*(.+)/i);
153
+ const riskPosture = riskMatch ? riskMatch[1].trim() : null;
154
+
155
+ // Parse roadmap sections
156
+ const immediateMatch = text.match(/\*\*Immediate[^*]*\*\*:?\s*([^\n]+(?:\n(?!\*\*)[^\n]+)*)/i);
157
+ const shortTermMatch = text.match(/\*\*Short-term[^*]*\*\*:?\s*([^\n]+(?:\n(?!\*\*)[^\n]+)*)/i);
158
+ const longTermMatch = text.match(/\*\*Long-term[^*]*\*\*:?\s*([^\n]+(?:\n(?!\*\*)[^\n]+)*)/i);
159
+
160
+ return {
161
+ riskPosture,
162
+ synthesis: synthesisLines.join('\n').trim(),
163
+ roadmap: {
164
+ immediate: immediateMatch?.[1]?.trim() || null,
165
+ shortTerm: shortTermMatch?.[1]?.trim() || null,
166
+ longTerm: longTermMatch?.[1]?.trim() || null,
167
+ },
168
+ };
169
+ }
170
+
171
+ // =============================================================================
172
+ // TARGET PARSER
173
+ // =============================================================================
174
+
175
+ function parseTarget(text) {
176
+ const match = text.match(/assessments?\s+of\s+\*\*([^*]+)\*\*/i);
177
+ return match ? match[1].trim() : 'Unknown Target';
178
+ }
179
+
180
+ // =============================================================================
181
+ // HTML RENDERER
182
+ // =============================================================================
183
+
184
+ function generateHTML(target, findings, agentSections, synthesis, bullets) {
185
+ const date = new Date().toLocaleDateString('en-US', {
186
+ year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit',
187
+ });
188
+
189
+ // Merge FINDING: JSON lines with bullet-parsed findings (bullets are fallback)
190
+ const allFindings = findings.length > 0 ? findings : bullets.map(b => ({
191
+ severity: b.severity,
192
+ title: b.title,
193
+ location: b.location,
194
+ remediation: '',
195
+ }));
196
+
197
+ const sevCounts = countBySeverity(allFindings);
198
+
199
+ const riskColor = (rp) => {
200
+ if (!rp) return '#94a3b8';
201
+ const lc = String(rp).toLowerCase();
202
+ if (lc.includes('critical')) return SEVERITY_COLORS.critical;
203
+ if (lc.includes('high')) return SEVERITY_COLORS.high;
204
+ if (lc.includes('medium')) return SEVERITY_COLORS.medium;
205
+ return '#22c55e';
206
+ };
207
+
208
+ const findingRows = allFindings.map(f => `
209
+ <tr>
210
+ <td>${severityBadge(f.severity)}</td>
211
+ <td><code>${esc(f.location) || '—'}</code></td>
212
+ <td><strong>${esc(f.title)}</strong>${f.cve ? `<br><small>CVE: ${esc(f.cve)}</small>` : ''}</td>
213
+ <td><small>${esc(f.remediation) || '—'}</small></td>
214
+ </tr>`).join('');
215
+
216
+ const agentRows = agentSections.sections.map(s => `
217
+ <tr>
218
+ <td>${esc(s.name)}</td>
219
+ <td><code>${esc(s.role) || '—'}</code></td>
220
+ <td style="color:${s.count > 0 ? SEVERITY_COLORS.high : '#22c55e'}">${esc(s.count)}</td>
221
+ </tr>`).join('');
222
+
223
+ const roadmap = synthesis.roadmap;
224
+ const roadmapRow = (label, text, color) => text
225
+ ? `<tr><td style="color:${color};white-space:nowrap;font-weight:600">${label}</td><td>${esc(text)}</td></tr>`
226
+ : '';
227
+ const roadmapHTML = (roadmap.immediate || roadmap.shortTerm || roadmap.longTerm) ? `
228
+ <h2>Remediation Roadmap</h2>
229
+ <table>
230
+ <tbody>
231
+ ${roadmapRow('⚡ Immediate (24–48h)', roadmap.immediate, SEVERITY_COLORS.critical)}
232
+ ${roadmapRow('📅 Short-term (1–2 weeks)', roadmap.shortTerm, SEVERITY_COLORS.high)}
233
+ ${roadmapRow('🏗 Long-term (1–3 months)', roadmap.longTerm, SEVERITY_COLORS.medium)}
234
+ </tbody>
235
+ </table>` : '';
236
+
237
+ return documentShell({
238
+ title: `Praxis Team Report — ${target}`,
239
+ styles: baseStyles() + `
240
+ .header{display:flex;align-items:center;gap:1rem;margin-bottom:2rem}
241
+ .logo{font-size:1.5rem;font-weight:800;color:#38bdf8;letter-spacing:-1px}
242
+ .badge{background:#1e293b;padding:3px 10px;border-radius:20px;font-size:0.75rem;color:#94a3b8;border:1px solid #334155}
243
+ .meta{color:#64748b;font-size:0.85rem;margin-bottom:2rem}
244
+ .risk-card{background:#0d1527;border:1px solid #1e293b;border-radius:12px;padding:1.5rem 2rem;margin-bottom:2rem;display:flex;align-items:center;gap:1.5rem}
245
+ .risk-label{font-size:0.75rem;text-transform:uppercase;color:#64748b;margin-bottom:0.25rem}
246
+ .risk-value{font-size:1.5rem;font-weight:700}
247
+ .risk-desc{color:#94a3b8;font-size:0.9rem;flex:1}
248
+ .stats{display:grid;grid-template-columns:repeat(5,1fr);gap:0.75rem;margin-bottom:2rem}
249
+ .stat{background:#0d1527;padding:1.25rem;border-radius:8px;text-align:center;border:1px solid #1e293b}
250
+ .stat-number{font-size:2rem;font-weight:bold}
251
+ .stat-label{color:#64748b;font-size:0.75rem;margin-top:0.25rem;text-transform:uppercase}
252
+ .powered{color:#38bdf8}
253
+ @media(max-width:1024px){.stats{grid-template-columns:repeat(2,1fr)}}
254
+ `,
255
+ body: `<div class="container">
256
+
257
+ <div class="header">
258
+ <span class="logo">Praxis</span>
259
+ <span class="badge">Team Security Report</span>
260
+ <span class="badge">Powered by Hermes Agent</span>
261
+ </div>
262
+
263
+ <h1>${esc(target)}</h1>
264
+ <p class="meta">Generated ${esc(date)} · ${allFindings.length} finding${allFindings.length !== 1 ? 's' : ''} · ${agentSections.sections.length} agent${agentSections.sections.length !== 1 ? 's' : ''}</p>
265
+
266
+ ${synthesis.riskPosture ? `
267
+ <div class="risk-card">
268
+ <div>
269
+ <div class="risk-label">Overall Risk Posture</div>
270
+ <div class="risk-value" style="color:${riskColor(synthesis.riskPosture)}">${esc(String(synthesis.riskPosture).split('—')[0].trim())}</div>
271
+ </div>
272
+ <div class="risk-desc">${esc(synthesis.riskPosture.includes('—') ? String(synthesis.riskPosture).split('—').slice(1).join('—').trim() : '')}</div>
273
+ </div>` : ''}
274
+
275
+ <div class="stats">
276
+ <div class="stat"><div class="stat-number" style="color:${SEVERITY_COLORS.critical}">${sevCounts.critical}</div><div class="stat-label">Critical</div></div>
277
+ <div class="stat"><div class="stat-number" style="color:${SEVERITY_COLORS.high}">${sevCounts.high}</div><div class="stat-label">High</div></div>
278
+ <div class="stat"><div class="stat-number" style="color:${SEVERITY_COLORS.medium}">${sevCounts.medium}</div><div class="stat-label">Medium</div></div>
279
+ <div class="stat"><div class="stat-number" style="color:${SEVERITY_COLORS.low}">${sevCounts.low}</div><div class="stat-label">Low</div></div>
280
+ <div class="stat"><div class="stat-number" style="color:${SEVERITY_COLORS.info}">${sevCounts.info}</div><div class="stat-label">Info</div></div>
281
+ </div>
282
+
283
+ <h2>Findings</h2>
284
+ <table>
285
+ <thead><tr><th>Severity</th><th>Location</th><th>Issue</th><th>Remediation</th></tr></thead>
286
+ <tbody>${findingRows || '<tr><td colspan="4" class="empty-state">No findings — clean!</td></tr>'}</tbody>
287
+ </table>
288
+
289
+ ${agentSections.sections.length > 0 ? `
290
+ <h2>Agent Team Summary</h2>
291
+ <table>
292
+ <thead><tr><th>Agent</th><th>Role</th><th>Findings</th></tr></thead>
293
+ <tbody>${agentRows}</tbody>
294
+ </table>` : ''}
295
+
296
+ ${roadmapHTML}
297
+
298
+ <div class="footer">
299
+ Secured by <span class="powered">Praxis</span> · <code>npx praxis-sec red-team .</code>
300
+ </div>
301
+ </div>`,
302
+ });
303
+ }
304
+
305
+ // =============================================================================
306
+ // MAIN COMMAND
307
+ // =============================================================================
308
+
309
+ export async function teamReportCommand(inputFile, options = {}) {
310
+ let raw;
311
+
312
+ if (inputFile) {
313
+ if (!fs.existsSync(inputFile)) {
314
+ output.error(`File not found: ${inputFile}`);
315
+ process.exit(1);
316
+ }
317
+ raw = fs.readFileSync(inputFile, 'utf-8');
318
+ } else {
319
+ // Read from stdin
320
+ raw = fs.readFileSync('/dev/stdin', 'utf-8');
321
+ }
322
+
323
+ // Clean the input
324
+ const stripped = stripAnsi(raw);
325
+ const cleaned = stripHermesChrome(stripped);
326
+
327
+ // Parse
328
+ const target = parseTarget(stripped);
329
+ const findings = parseFindings(cleaned);
330
+ const agentSections = parseAgentSections(cleaned);
331
+ const synthesis = parseSynthesis(cleaned);
332
+
333
+ const allFindings = findings.length > 0 ? findings : agentSections.bullets.map(b => ({
334
+ severity: b.severity,
335
+ title: b.title,
336
+ location: b.location,
337
+ remediation: '',
338
+ }));
339
+
340
+ if (options.json) {
341
+ console.log(JSON.stringify({ target, findings: allFindings, agentSections: agentSections.sections, synthesis }, null, 2));
342
+ return;
343
+ }
344
+
345
+ if (options.html !== undefined) {
346
+ const htmlPath = typeof options.html === 'string' ? options.html : 'team-report.html';
347
+ const html = generateHTML(target, findings, agentSections, synthesis, agentSections.bullets);
348
+ fs.writeFileSync(htmlPath, html, 'utf-8');
349
+ output.success(`Team report saved to ${htmlPath}`);
350
+ return;
351
+ }
352
+
353
+ // Terminal output
354
+ printBanner();
355
+ console.log(chalk.cyan.bold(' Team Security Report'));
356
+ console.log(chalk.gray(` Target: ${target}`));
357
+ console.log();
358
+
359
+ if (synthesis.riskPosture) {
360
+ const rp = synthesis.riskPosture;
361
+ const color = rp.toLowerCase().includes('critical') ? chalk.red.bold
362
+ : rp.toLowerCase().includes('high') ? chalk.yellow.bold
363
+ : rp.toLowerCase().includes('medium') ? chalk.yellow
364
+ : chalk.green;
365
+ console.log(` ${chalk.white.bold('Risk Posture:')} ${color(rp)}`);
366
+ console.log();
367
+ }
368
+
369
+ const sevColor = { critical: chalk.red.bold, high: chalk.yellow, medium: chalk.blue, low: chalk.gray, info: chalk.gray };
370
+ for (const f of allFindings) {
371
+ const col = sevColor[f.severity] || chalk.white;
372
+ console.log(` ${col(`[${f.severity.toUpperCase()}]`.padEnd(11))} ${chalk.white(f.title)}`);
373
+ if (f.location) console.log(` ${' '.repeat(11)} ${chalk.gray(f.location)}`);
374
+ if (f.remediation) console.log(` ${' '.repeat(11)} ${chalk.green('Fix:')} ${f.remediation.slice(0, 90)}`);
375
+ }
376
+
377
+ if (allFindings.length === 0) {
378
+ console.log(chalk.green(' No findings — clean!'));
379
+ }
380
+
381
+ console.log();
382
+ if (synthesis.roadmap.immediate) {
383
+ console.log(chalk.red.bold(' ⚡ Immediate (24–48h):'));
384
+ console.log(chalk.gray(` ${synthesis.roadmap.immediate}`));
385
+ }
386
+ if (synthesis.roadmap.shortTerm) {
387
+ console.log(chalk.yellow.bold(' 📅 Short-term (1–2 weeks):'));
388
+ console.log(chalk.gray(` ${synthesis.roadmap.shortTerm}`));
389
+ }
390
+ if (synthesis.roadmap.longTerm) {
391
+ console.log(chalk.white.bold(' 🏗 Long-term (1–3 months):'));
392
+ console.log(chalk.gray(` ${synthesis.roadmap.longTerm}`));
393
+ }
394
+
395
+ console.log();
396
+ console.log(chalk.gray(' Generate HTML report: ') + chalk.cyan(`praxis team-report ${inputFile || '<file>'} --html report.html`));
397
+ console.log();
398
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Undo Command
3
+ * ============
4
+ *
5
+ * Reverts changes applied by `praxis fix interactive`.
6
+ *
7
+ * Reads .praxis/fixes.jsonl, takes the most recent entry (or all entries
8
+ * with --all), and reverses each edit. Per-fix git commits made by the agent
9
+ * are preferred over manual reversal when available.
10
+ *
11
+ * USAGE:
12
+ * praxis undo Revert the last applied fix
13
+ * praxis undo --all Revert every fix in the log
14
+ * praxis undo --dry-run Show what would be reverted, but don't write
15
+ */
16
+
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+ import { execFileSync } from 'child_process';
20
+ import chalk from 'chalk';
21
+ import * as output from '../utils/output.js';
22
+
23
+ const FIX_LOG_PATH = '.praxis/fixes.jsonl';
24
+
25
+ export async function undoCommand(targetPath = '.', options = {}) {
26
+ const root = path.resolve(targetPath);
27
+ const logPath = path.join(root, FIX_LOG_PATH);
28
+
29
+ if (!fs.existsSync(logPath)) {
30
+ output.error(`No fix log found at ${FIX_LOG_PATH}`);
31
+ console.log(chalk.gray(' Run `praxis fix interactive` first to apply fixes.'));
32
+ process.exit(1);
33
+ }
34
+
35
+ const entries = fs.readFileSync(logPath, 'utf8')
36
+ .split('\n')
37
+ .filter(Boolean)
38
+ .map(line => { try { return JSON.parse(line); } catch { return null; } })
39
+ .filter(Boolean);
40
+
41
+ if (entries.length === 0) {
42
+ output.error('Fix log is empty.');
43
+ process.exit(1);
44
+ }
45
+
46
+ const toUndo = options.all ? [...entries].reverse() : [entries[entries.length - 1]];
47
+
48
+ console.log();
49
+ output.header('Praxis — Undo');
50
+ console.log();
51
+ console.log(chalk.gray(` Reverting ${toUndo.length} fix(es) from ${FIX_LOG_PATH}`));
52
+ console.log();
53
+
54
+ let reverted = 0;
55
+ let failed = 0;
56
+
57
+ for (const entry of toUndo) {
58
+ const file = entry.file || entry.finding?.file || '(unknown)';
59
+ console.log(chalk.bold(` ${chalk.cyan(file)}`));
60
+
61
+ if (options.dryRun) {
62
+ console.log(chalk.gray(` Would reverse plan: ${entry.plan?.summary || 'no summary'}`));
63
+ reverted++;
64
+ continue;
65
+ }
66
+
67
+ try {
68
+ reverseEntry(root, entry);
69
+ console.log(chalk.green(' Reverted.'));
70
+ reverted++;
71
+ } catch (err) {
72
+ console.log(chalk.red(` Failed: ${err.message}`));
73
+ failed++;
74
+ }
75
+ }
76
+
77
+ // Truncate the log
78
+ if (!options.dryRun && reverted > 0) {
79
+ const remaining = options.all ? [] : entries.slice(0, -1);
80
+ if (remaining.length === 0) {
81
+ fs.unlinkSync(logPath);
82
+ } else {
83
+ fs.writeFileSync(logPath, remaining.map(e => JSON.stringify(e)).join('\n') + '\n', 'utf8');
84
+ }
85
+ }
86
+
87
+ console.log();
88
+ console.log(chalk.green(` Reverted: ${reverted}`));
89
+ if (failed > 0) console.log(chalk.red(` Failed: ${failed}`));
90
+ console.log();
91
+
92
+ if (failed > 0) {
93
+ console.log(chalk.gray(' For failed entries, try `git checkout` or `git reset --hard` if you committed via --branch.'));
94
+ console.log();
95
+ }
96
+ }
97
+
98
+ function isGitRepo(dir) {
99
+ try {
100
+ execFileSync('git', ['-C', dir, 'rev-parse', '--is-inside-work-tree'], { stdio: 'ignore' });
101
+ return true;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ function reverseEntry(root, entry) {
108
+ if (entry.commitHash && isGitRepo(root)) {
109
+ try {
110
+ execFileSync('git', ['-C', root, 'revert', '--no-commit', entry.commitHash], { stdio: 'ignore' });
111
+ return;
112
+ } catch {
113
+ console.log(chalk.yellow(` Git revert failed for ${entry.commitHash.slice(0, 7)}. Falling back to manual text reversal.`));
114
+ }
115
+ }
116
+
117
+ const plan = entry.plan;
118
+ if (!plan || !Array.isArray(plan.files) || plan.files.length === 0) {
119
+ throw new Error('entry has no plan to reverse');
120
+ }
121
+
122
+ for (const fileChange of plan.files) {
123
+ const abs = path.resolve(root, fileChange.path);
124
+
125
+ if (fileChange.create) {
126
+ // We created the file — delete it
127
+ if (fs.existsSync(abs)) fs.unlinkSync(abs);
128
+ continue;
129
+ }
130
+
131
+ if (fileChange.append !== undefined) {
132
+ if (!fs.existsSync(abs)) continue;
133
+ const current = fs.readFileSync(abs, 'utf8');
134
+ // Try to remove the appended text (it may be at the end)
135
+ const idx = current.lastIndexOf(fileChange.append);
136
+ if (idx === -1) {
137
+ throw new Error(`appended text not found in ${fileChange.path}`);
138
+ }
139
+ const reverted = current.slice(0, idx) + current.slice(idx + fileChange.append.length);
140
+ fs.writeFileSync(abs, reverted, 'utf8');
141
+ continue;
142
+ }
143
+
144
+ // Standard edits — reverse find/replace
145
+ if (!fs.existsSync(abs)) {
146
+ throw new Error(`file no longer exists: ${fileChange.path}`);
147
+ }
148
+ let content = fs.readFileSync(abs, 'utf8');
149
+ // Reverse in opposite order in case edits are positionally dependent
150
+ const reversed = [...fileChange.edits].reverse();
151
+ for (const e of reversed) {
152
+ const newStr = e.replace;
153
+ const oldStr = e._resolvedFind || e.find;
154
+ if (!content.includes(newStr)) {
155
+ throw new Error(`reverted text not found in ${fileChange.path} (file changed since fix)`);
156
+ }
157
+ content = content.replace(newStr, oldStr);
158
+ }
159
+ fs.writeFileSync(abs, content, 'utf8');
160
+ }
161
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Update Intel Command
3
+ * =====================
4
+ *
5
+ * Refreshes the local threat-intelligence database by fetching every
6
+ * configured source in parallel (OSV, GHSA, KEV, EPSS, NVD, Gitleaks,
7
+ * plus optional paid sources Snyk / Socket / GitGuardian / Sonatype /
8
+ * Phylum). Results are merged into ~/.praxis/threat-intel.json.
9
+ *
10
+ * USAGE:
11
+ * praxis intel update Refresh all sources
12
+ * praxis intel update --only osv,kev Limit to listed sources
13
+ * praxis intel update --force Ignore per-source TTL caches
14
+ * praxis intel update --list Print available sources
15
+ */
16
+
17
+ import chalk from 'chalk';
18
+ import * as output from '../utils/output.js';
19
+ import { ThreatIntel } from '../utils/threat-intel.js';
20
+ import * as intel from '../utils/intel/index.js';
21
+
22
+ export async function updateIntelCommand(options = {}) {
23
+ console.log();
24
+ output.header('Praxis — Threat Intelligence Update');
25
+ console.log();
26
+
27
+ if (options.list) {
28
+ printSources();
29
+ return;
30
+ }
31
+
32
+ const before = ThreatIntel.stats();
33
+ console.log(chalk.gray(` Current version: ${before.version}`));
34
+ console.log(chalk.gray(` Last updated: ${before.updated || 'never'}`));
35
+ console.log();
36
+
37
+ const sourceFilter = options.only
38
+ ? String(options.only).split(',').map(s => s.trim()).filter(Boolean)
39
+ : null;
40
+
41
+ console.log(chalk.cyan(' Fetching sources...'));
42
+ console.log();
43
+
44
+ const result = await intel.runUpdate({
45
+ sources: sourceFilter,
46
+ force: !!options.force,
47
+ onProgress: ({ source, status, message }) => {
48
+ const pad = source.padEnd(12);
49
+ switch (status) {
50
+ case 'start':
51
+ // Skip per-source 'start' lines — the per-source result line below
52
+ // is enough output, and 'start' interleaving with 'ok' from parallel
53
+ // fetches looks messy on a non-TTY.
54
+ break;
55
+ case 'ok':
56
+ console.log(chalk.green(` ${pad} OK`));
57
+ break;
58
+ case 'cached':
59
+ console.log(chalk.gray(` ${pad} cached`));
60
+ break;
61
+ case 'skipped':
62
+ console.log(chalk.yellow(` ${pad} skipped`) + chalk.gray(` (${message || 'no key'})`));
63
+ break;
64
+ case 'error':
65
+ console.log(chalk.red(` ${pad} error`) + chalk.gray(` (${message})`));
66
+ break;
67
+ }
68
+ },
69
+ });
70
+
71
+ // Invalidate the in-memory ThreatIntel cache so the stats below reflect
72
+ // the freshly merged feed.
73
+ ThreatIntel._resetCache();
74
+
75
+ console.log();
76
+ console.log(chalk.green.bold(` Merged feed v${result.merged.version} written.`));
77
+ console.log();
78
+
79
+ // Per-source summary
80
+ const rows = Object.entries(result.merged.sources || {});
81
+ if (rows.length) {
82
+ console.log(chalk.gray(' Per-source status:'));
83
+ for (const [name, info] of rows) {
84
+ const flag = info.skipped
85
+ ? chalk.yellow('skipped')
86
+ : info.ok ? chalk.green('ok') : chalk.red('failed');
87
+ const stats = info.stats ? Object.entries(info.stats).map(([k, v]) => `${v} ${k}`).join(', ') : '';
88
+ const note = info.skipped ? ` (${info.reason || 'no key'})` : info.error ? ` (${info.error})` : '';
89
+ console.log(` ${name.padEnd(12)} ${flag.padEnd(18)} ${chalk.gray(stats + note)}`);
90
+ }
91
+ console.log();
92
+ }
93
+
94
+ // Aggregate stats
95
+ const after = ThreatIntel.stats();
96
+ console.log(chalk.gray(' Indicators loaded:'));
97
+ console.log(` CVE advisories: ${after.cveAdvisories}`);
98
+ console.log(` KEV entries: ${after.kevEntries}`);
99
+ console.log(` EPSS scores: ${after.epssScores}`);
100
+ console.log(` NVD details: ${after.nvdDetails}`);
101
+ console.log(` Extra secret rules: ${after.extraSecretRules}`);
102
+ console.log(` Malicious skills: ${after.hashes}`);
103
+ console.log(` Compromised MCP: ${after.servers}`);
104
+ console.log();
105
+ }
106
+
107
+ function printSources() {
108
+ const sources = intel.listSources();
109
+ console.log(chalk.gray(' Available sources:'));
110
+ console.log();
111
+ const core = sources.filter(s => s.tier === 'core');
112
+ const optional = sources.filter(s => s.tier === 'optional');
113
+
114
+ console.log(chalk.cyan(' Core (free, no auth required):'));
115
+ for (const s of core) {
116
+ console.log(` ${chalk.bold(s.name.padEnd(12))} ${chalk.gray(s.description)}`);
117
+ }
118
+ console.log();
119
+ console.log(chalk.cyan(' Optional (paid; activate by setting env var):'));
120
+ for (const s of optional) {
121
+ const flag = process.env[s.envKey] ? chalk.green('[active]') : chalk.gray('[inactive]');
122
+ console.log(` ${chalk.bold(s.name.padEnd(12))} ${flag} ${chalk.gray(`needs ${s.envKey}`)}`);
123
+ console.log(` ${''.padEnd(12)} ${chalk.gray(s.description)}`);
124
+ }
125
+ console.log();
126
+ }