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,660 @@
1
+ /**
2
+ * MCP Server
3
+ * ==========
4
+ *
5
+ * Exposes praxis as a Model Context Protocol (MCP) server.
6
+ * Allows AI editors (Claude Desktop, Cursor, Windsurf, Zed) to call
7
+ * praxis's security tools directly during conversations.
8
+ *
9
+ * USAGE:
10
+ * npx praxis-sec mcp Start the MCP server (stdio transport)
11
+ *
12
+ * SETUP (Claude Desktop):
13
+ * Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
14
+ * {
15
+ * "mcpServers": {
16
+ * "praxis": {
17
+ * "command": "npx",
18
+ * "args": ["praxis", "mcp"]
19
+ * }
20
+ * }
21
+ * }
22
+ *
23
+ * AVAILABLE TOOLS:
24
+ * scan_secrets - Scan a directory for leaked secrets
25
+ * get_checklist - Return the launch-day security checklist
26
+ * analyze_file - Analyze a single file for security issues
27
+ * scan_repo - Run a full multi-agent security scan on a repo
28
+ * get_findings - Read findings from a saved praxis report file
29
+ * suppress_finding - Add a praxis-ignore comment to suppress a finding
30
+ *
31
+ * PROTOCOL:
32
+ * JSON-RPC 2.0 over stdio (MCP spec: https://modelcontextprotocol.io)
33
+ */
34
+
35
+ import fs from 'fs';
36
+ import path from 'path';
37
+ import fg from 'fast-glob';
38
+ import { SECRET_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, TEST_FILE_PATTERNS, MAX_FILE_SIZE } from '../utils/patterns.js';
39
+ import { isHighEntropyMatch } from '../utils/entropy.js';
40
+ import { buildOrchestrator } from '../agents/index.js';
41
+ import { ScoringEngine } from '../agents/scoring-engine.js';
42
+ import { autoDetectProvider } from '../providers/llm-provider.js';
43
+ import { DeepAnalyzer } from '../agents/deep-analyzer.js';
44
+ import { ASTParser, ScopeTree, TaintTracker } from '../core/ast/index.js';
45
+
46
+ // =============================================================================
47
+ // MCP TOOL DEFINITIONS
48
+ // =============================================================================
49
+
50
+ const TOOLS = [
51
+ {
52
+ name: 'scan_secrets',
53
+ description: 'Scan a directory or file for leaked secrets, API keys, and credentials. Returns structured findings with severity, file location, and remediation advice.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ path: {
58
+ type: 'string',
59
+ description: 'The directory or file path to scan. Use "." for the current directory.',
60
+ },
61
+ includeTests: {
62
+ type: 'boolean',
63
+ description: 'Whether to include test files in the scan (default: false)',
64
+ },
65
+ },
66
+ required: ['path'],
67
+ },
68
+ },
69
+ {
70
+ name: 'get_checklist',
71
+ description: 'Return the praxis launch-day security checklist as structured data. Use this to guide users through pre-launch security checks.',
72
+ inputSchema: {
73
+ type: 'object',
74
+ properties: {},
75
+ },
76
+ },
77
+ {
78
+ name: 'analyze_file',
79
+ description: 'Analyze a single file for security issues including secrets, hardcoded credentials, and dangerous patterns.',
80
+ inputSchema: {
81
+ type: 'object',
82
+ properties: {
83
+ path: {
84
+ type: 'string',
85
+ description: 'The absolute or relative path to the file to analyze.',
86
+ },
87
+ },
88
+ required: ['path'],
89
+ },
90
+ },
91
+ {
92
+ name: 'scan_repo',
93
+ description: 'Run a full multi-agent security scan on a repository or directory. Runs all 20+ praxis security agents (injection, auth bypass, secrets, supply chain, LLM security, etc.) and returns a structured findings report with severity ratings and remediation advice. Use this when the user asks to audit, scan, or check the security of their project.',
94
+ inputSchema: {
95
+ type: 'object',
96
+ properties: {
97
+ path: {
98
+ type: 'string',
99
+ description: 'The directory path to scan. Use "." for current directory.',
100
+ },
101
+ agents: {
102
+ type: 'array',
103
+ items: { type: 'string' },
104
+ description: 'Specific agent names to run (optional). Omit to run all agents.',
105
+ },
106
+ llm: {
107
+ type: 'boolean',
108
+ description: 'Enable LLM-powered deep analysis for critical/high findings (default: false). Requires ANTHROPIC_API_KEY or similar env var.',
109
+ },
110
+ outputFile: {
111
+ type: 'string',
112
+ description: 'Optional path to save the JSON report for later retrieval with get_findings.',
113
+ },
114
+ },
115
+ required: ['path'],
116
+ },
117
+ },
118
+ {
119
+ name: 'get_findings',
120
+ description: 'Read and return findings from a praxis JSON report file previously saved by scan_repo or the praxis CLI (npx praxis-sec audit --json). Useful for reviewing or referencing a prior scan without re-running it.',
121
+ inputSchema: {
122
+ type: 'object',
123
+ properties: {
124
+ reportPath: {
125
+ type: 'string',
126
+ description: 'Path to the praxis JSON report file (e.g. praxis-report.json).',
127
+ },
128
+ severity: {
129
+ type: 'string',
130
+ enum: ['critical', 'high', 'medium', 'low'],
131
+ description: 'Filter findings by minimum severity (optional).',
132
+ },
133
+ },
134
+ required: ['reportPath'],
135
+ },
136
+ },
137
+ {
138
+ name: 'suppress_finding',
139
+ description: 'Add a praxis-ignore comment to a specific line in a file to suppress a false-positive security finding. The comment tells praxis\'s scanner to skip that line in future scans. Always explain why the suppression is safe.',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {
143
+ file: {
144
+ type: 'string',
145
+ description: 'Path to the file containing the false-positive finding.',
146
+ },
147
+ line: {
148
+ type: 'number',
149
+ description: 'Line number of the finding to suppress (1-indexed).',
150
+ },
151
+ reason: {
152
+ type: 'string',
153
+ description: 'Brief explanation of why this finding is a false positive (appended to the ignore comment).',
154
+ },
155
+ },
156
+ required: ['file', 'line', 'reason'],
157
+ },
158
+ },
159
+ {
160
+ name: 'explain_and_fix',
161
+ description: 'Provide an instant AST-aware explanation and unified diff fix preview for a specific security finding in a file for IDEs and editor extensions.',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: {
165
+ file: { type: 'string', description: 'Path to the file containing the finding' },
166
+ line: { type: 'number', description: 'Line number of the finding' },
167
+ rule: { type: 'string', description: 'Rule name or ID' },
168
+ },
169
+ required: ['file', 'line', 'rule'],
170
+ },
171
+ },
172
+ ];
173
+
174
+ // =============================================================================
175
+ // TOOL IMPLEMENTATIONS
176
+ // =============================================================================
177
+
178
+ async function scanSecrets({ path: targetPath, includeTests = false }) {
179
+ const absolutePath = path.resolve(targetPath);
180
+
181
+ if (!fs.existsSync(absolutePath)) {
182
+ return { error: `Path does not exist: ${absolutePath}` };
183
+ }
184
+
185
+ const stat = fs.statSync(absolutePath);
186
+ const files = stat.isFile()
187
+ ? [absolutePath]
188
+ : await findFiles(absolutePath, includeTests);
189
+
190
+ const results = [];
191
+
192
+ for (const file of files) {
193
+ const findings = scanFile(file);
194
+ if (findings.length > 0) {
195
+ results.push({ file: path.relative(process.cwd(), file), findings });
196
+ }
197
+ }
198
+
199
+ return {
200
+ filesScanned: files.length,
201
+ totalFindings: results.reduce((sum, r) => sum + r.findings.length, 0),
202
+ clean: results.length === 0,
203
+ findings: results,
204
+ summary: results.length === 0
205
+ ? 'No secrets detected.'
206
+ : `Found ${results.reduce((s, r) => s + r.findings.length, 0)} secret(s) across ${results.length} file(s).`,
207
+ remediation: results.length > 0
208
+ ? 'Move secrets to environment variables. Add .env to .gitignore. Rotate any already-committed credentials.'
209
+ : null,
210
+ };
211
+ }
212
+
213
+ function getChecklist() {
214
+ return {
215
+ title: 'Praxis Launch-Day Security Checklist',
216
+ items: [
217
+ { id: 1, category: 'Secrets', check: 'No API keys hardcoded in source code', command: 'npx praxis-sec scan .' },
218
+ { id: 2, category: 'Secrets', check: '.env file is in .gitignore', command: null },
219
+ { id: 3, category: 'Secrets', check: '.env.example exists with placeholder values', command: 'npx praxis-sec fix' },
220
+ { id: 4, category: 'Database', check: 'Row Level Security (RLS) enabled on all Supabase tables', command: null },
221
+ { id: 5, category: 'Database', check: 'Service role key is server-side only (never in frontend)', command: null },
222
+ { id: 6, category: 'Auth', check: 'Authentication required on all sensitive API routes', command: null },
223
+ { id: 7, category: 'Auth', check: 'JWT tokens expire within 24 hours', command: null },
224
+ { id: 8, category: 'Headers', check: 'Security headers configured (CSP, X-Frame-Options, HSTS)', command: 'npx praxis-sec init --headers' },
225
+ { id: 9, category: 'API', check: 'Rate limiting implemented on auth and AI endpoints', command: null },
226
+ { id: 10, category: 'API', check: 'Input validation on all API endpoints', command: null },
227
+ { id: 11, category: 'AI', check: 'Token limits set on all LLM API calls', command: null },
228
+ { id: 12, category: 'AI', check: 'Budget caps configured in AI provider dashboard', command: null },
229
+ { id: 13, category: 'CI/CD', check: 'praxis scan runs in CI pipeline', command: null },
230
+ { id: 14, category: 'CI/CD', check: 'Pre-push hook installed', command: 'npx praxis-sec guard' },
231
+ ],
232
+ };
233
+ }
234
+
235
+ async function analyzeFile({ path: filePath }) {
236
+ const absolutePath = path.resolve(filePath);
237
+
238
+ if (!fs.existsSync(absolutePath)) {
239
+ return { error: `File does not exist: ${absolutePath}` };
240
+ }
241
+
242
+ const findings = scanFile(absolutePath);
243
+
244
+ return {
245
+ file: filePath,
246
+ totalFindings: findings.length,
247
+ clean: findings.length === 0,
248
+ findings,
249
+ summary: findings.length === 0
250
+ ? `No secrets detected in ${path.basename(filePath)}.`
251
+ : `Found ${findings.length} potential secret(s) in ${path.basename(filePath)}.`,
252
+ };
253
+ }
254
+
255
+ async function scanRepo({ path: targetPath, agents: agentFilter, llm = false, outputFile }) {
256
+ const rootPath = path.resolve(targetPath);
257
+
258
+ if (!fs.existsSync(rootPath)) {
259
+ return { error: `Path does not exist: ${rootPath}` };
260
+ }
261
+
262
+ // MCP communicates over stdout as JSON-RPC. Suppress all console output during
263
+ // the scan so spinner text and log lines don't pollute the transport stream.
264
+ const noop = () => {};
265
+ const savedLog = console.log;
266
+ const savedWarn = console.warn;
267
+ const savedError = console.error;
268
+ const savedInfo = console.info;
269
+ console.log = console.warn = console.error = console.info = noop;
270
+
271
+ try {
272
+ const orchestrator = buildOrchestrator();
273
+ const context = { rootPath };
274
+
275
+ // Run all agents (quiet:true suppresses ora spinners; console is already nulled)
276
+ const { findings, recon } = await orchestrator.runAll(rootPath, {
277
+ agents: agentFilter,
278
+ timeout: 30000,
279
+ concurrency: 6,
280
+ quiet: true,
281
+ });
282
+
283
+ // Optional: LLM deep analysis
284
+ let deepStats = null;
285
+ if (llm) {
286
+ const provider = autoDetectProvider(rootPath, {});
287
+ if (provider) {
288
+ const analyzer = new DeepAnalyzer({ provider, budgetCents: 50, verbose: false });
289
+ await analyzer.analyze(findings, { rootPath, recon });
290
+ deepStats = analyzer.getStats();
291
+ }
292
+ }
293
+
294
+ // Score
295
+ const scorer = new ScoringEngine();
296
+ const { score, grade } = scorer.score(findings);
297
+
298
+ const SEV_ORDER = ['critical', 'high', 'medium', 'low'];
299
+ const bySeverity = {};
300
+ for (const sev of SEV_ORDER) {
301
+ bySeverity[sev] = findings.filter(f => f.severity === sev).length;
302
+ }
303
+
304
+ const report = {
305
+ scannedAt: new Date().toISOString(),
306
+ rootPath,
307
+ score,
308
+ grade,
309
+ totalFindings: findings.length,
310
+ bySeverity,
311
+ findings: findings.map(f => ({
312
+ title: f.title,
313
+ severity: f.severity,
314
+ category: f.category,
315
+ rule: f.rule,
316
+ file: f.file ? path.relative(rootPath, f.file) : null,
317
+ line: f.line,
318
+ description: f.description,
319
+ remediation: f.remediation,
320
+ confidence: f.confidence,
321
+ ...(f.deepAnalysis ? { deepAnalysis: f.deepAnalysis } : {}),
322
+ })),
323
+ ...(deepStats ? { deepAnalysis: deepStats } : {}),
324
+ summary: `Score: ${score}/100 (${grade}) — ${findings.length} finding(s): ${bySeverity.critical} critical, ${bySeverity.high} high, ${bySeverity.medium} medium, ${bySeverity.low} low.`,
325
+ };
326
+
327
+ if (outputFile) {
328
+ const outPath = path.resolve(outputFile);
329
+ fs.writeFileSync(outPath, JSON.stringify(report, null, 2), 'utf-8');
330
+ report.savedTo = outPath;
331
+ }
332
+
333
+ return report;
334
+ } catch (err) {
335
+ return { error: `Scan failed: ${err.message}` };
336
+ } finally {
337
+ // Always restore console so other tool calls are not affected
338
+ console.log = savedLog;
339
+ console.warn = savedWarn;
340
+ console.error = savedError;
341
+ console.info = savedInfo;
342
+ }
343
+ }
344
+
345
+ function getFindings({ reportPath, severity }) {
346
+ const absPath = path.resolve(reportPath);
347
+
348
+ if (!fs.existsSync(absPath)) {
349
+ return { error: `Report file not found: ${absPath}` };
350
+ }
351
+
352
+ let report;
353
+ try {
354
+ report = JSON.parse(fs.readFileSync(absPath, 'utf-8'));
355
+ } catch (err) {
356
+ return { error: `Failed to parse report: ${err.message}` };
357
+ }
358
+
359
+ const findings = report.findings ?? [];
360
+ const SEV_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
361
+ const filtered = severity
362
+ ? findings.filter(f => (SEV_RANK[f.severity] ?? 0) >= (SEV_RANK[severity] ?? 0))
363
+ : findings;
364
+
365
+ return {
366
+ reportPath: absPath,
367
+ scannedAt: report.scannedAt,
368
+ score: report.score,
369
+ grade: report.grade,
370
+ totalFindings: filtered.length,
371
+ bySeverity: report.bySeverity,
372
+ findings: filtered,
373
+ summary: report.summary,
374
+ ...(severity ? { filter: `severity >= ${severity}` } : {}),
375
+ };
376
+ }
377
+
378
+ function suppressFinding({ file, line, reason }) {
379
+ const absPath = path.resolve(file);
380
+
381
+ if (!fs.existsSync(absPath)) {
382
+ return { error: `File not found: ${absPath}` };
383
+ }
384
+
385
+ let content;
386
+ try {
387
+ content = fs.readFileSync(absPath, 'utf-8');
388
+ } catch (err) {
389
+ return { error: `Cannot read file: ${err.message}` };
390
+ }
391
+
392
+ const lines = content.split('\n');
393
+ const lineIdx = line - 1; // Convert to 0-indexed
394
+
395
+ if (lineIdx < 0 || lineIdx >= lines.length) {
396
+ return { error: `Line ${line} is out of range (file has ${lines.length} lines)` };
397
+ }
398
+
399
+ const targetLine = lines[lineIdx];
400
+
401
+ // Already suppressed?
402
+ if (/praxis-ignore/i.test(targetLine)) {
403
+ return { alreadySuppressed: true, file: absPath, line, message: 'Line already has a praxis-ignore comment.' };
404
+ }
405
+
406
+ // Critical check:
407
+ const fileFindings = scanFile(absPath);
408
+ const criticalFinding = fileFindings.find(f => f.line === line && f.severity === 'critical');
409
+ if (criticalFinding) {
410
+ return { error: `Cannot suppress critical severity finding: ${criticalFinding.description}` };
411
+ }
412
+
413
+ // Log suppression request to .praxis/suppressions.json for manual audit
414
+ const suppressionsDir = path.join(process.cwd(), '.praxis');
415
+ if (!fs.existsSync(suppressionsDir)) {
416
+ try {
417
+ fs.mkdirSync(suppressionsDir, { recursive: true });
418
+ } catch {}
419
+ }
420
+ const suppressionsFile = path.join(suppressionsDir, 'suppressions.json');
421
+ let suppressions = [];
422
+ if (fs.existsSync(suppressionsFile)) {
423
+ try {
424
+ suppressions = JSON.parse(fs.readFileSync(suppressionsFile, 'utf-8'));
425
+ } catch {}
426
+ }
427
+ suppressions.push({
428
+ file: path.relative(process.cwd(), absPath),
429
+ line,
430
+ reason,
431
+ timestamp: new Date().toISOString(),
432
+ status: 'pending_review'
433
+ });
434
+ try {
435
+ fs.writeFileSync(suppressionsFile, JSON.stringify(suppressions, null, 2), 'utf-8');
436
+ } catch {}
437
+
438
+ // Detect indentation and comment style
439
+ const indent = targetLine.match(/^(\s*)/)?.[1] ?? '';
440
+ const isJs = /\.(js|ts|jsx|tsx|mjs|cjs|java|c|cpp|cs|go|rs|swift|kt)$/.test(file);
441
+ const isPy = /\.py$/.test(file);
442
+ const isRb = /\.rb$/.test(file);
443
+ const isHtml = /\.(html?|vue|svelte)$/.test(file);
444
+
445
+ let ignoreComment;
446
+ if (isHtml) {
447
+ ignoreComment = `${indent}<!-- praxis-ignore — ${reason} -->`;
448
+ } else if (isPy || isRb) {
449
+ ignoreComment = `${indent}# praxis-ignore — ${reason}`;
450
+ } else {
451
+ ignoreComment = `${indent}// praxis-ignore — ${reason}`;
452
+ }
453
+
454
+ // Insert ignore comment on the line BEFORE the finding
455
+ lines.splice(lineIdx, 0, ignoreComment);
456
+
457
+ try {
458
+ fs.writeFileSync(absPath, lines.join('\n'), 'utf-8');
459
+ } catch (err) {
460
+ return { error: `Cannot write file: ${err.message}` };
461
+ }
462
+
463
+ return {
464
+ suppressed: true,
465
+ file: absPath,
466
+ originalLine: line,
467
+ insertedLine: line, // The ignore comment is now on this line, original moved to line+1
468
+ comment: ignoreComment,
469
+ message: `Added praxis-ignore comment before line ${line} in ${path.basename(file)}.`,
470
+ };
471
+ }
472
+
473
+ // =============================================================================
474
+ // SCAN UTILITIES (shared with scan command)
475
+ // =============================================================================
476
+
477
+ async function findFiles(rootPath, includeTests) {
478
+ const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
479
+ const files = await fg('**/*', { cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true });
480
+
481
+ return files.filter(file => {
482
+ const ext = path.extname(file).toLowerCase();
483
+ if (SKIP_EXTENSIONS.has(ext)) return false;
484
+ if (SKIP_FILENAMES.has(path.basename(file))) return false;
485
+ const basename = path.basename(file);
486
+ if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) return false;
487
+ if (!includeTests && TEST_FILE_PATTERNS.some(p => p.test(file))) return false;
488
+ try {
489
+ return fs.statSync(file).size <= MAX_FILE_SIZE;
490
+ } catch { return false; }
491
+ });
492
+ }
493
+
494
+ function scanFile(filePath) {
495
+ const findings = [];
496
+ try {
497
+ const content = fs.readFileSync(filePath, 'utf-8');
498
+ const lines = content.split('\n');
499
+
500
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
501
+ const line = lines[lineNum];
502
+ if (/praxis-ignore/i.test(line)) continue;
503
+
504
+ for (const pattern of SECRET_PATTERNS) {
505
+ pattern.pattern.lastIndex = 0;
506
+ let match;
507
+ while ((match = pattern.pattern.exec(line)) !== null) {
508
+ if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
509
+ findings.push({
510
+ line: lineNum + 1,
511
+ type: pattern.name,
512
+ severity: pattern.severity,
513
+ description: pattern.description,
514
+ fix: 'Move to environment variable. Never commit to source control.',
515
+ });
516
+ }
517
+ }
518
+ }
519
+ } catch {}
520
+ return findings;
521
+ }
522
+
523
+ async function explainAndFix({ file: filePath, line, rule }) {
524
+ const abs = path.resolve(filePath);
525
+ if (!fs.existsSync(abs)) {
526
+ return { error: `File does not exist: ${filePath}` };
527
+ }
528
+
529
+ try {
530
+ const content = fs.readFileSync(abs, 'utf-8');
531
+ const lines = content.split('\n');
532
+ const findingLine = lines[line - 1] || '';
533
+
534
+ const parsed = ASTParser.parse(content, filePath);
535
+ const scopeTree = ScopeTree.build(parsed.ast, content);
536
+ const encFn = scopeTree.getEnclosingFunction(line);
537
+ const taint = TaintTracker.evaluateFinding({
538
+ file: filePath,
539
+ line,
540
+ matched: findingLine,
541
+ code: content,
542
+ scopeTree,
543
+ });
544
+
545
+ return {
546
+ file: filePath,
547
+ line,
548
+ rule,
549
+ enclosingFunction: encFn ? encFn.name : '(top-level)',
550
+ taintAnalysis: taint,
551
+ suggestedFix: 'Replace unsanitized variable interpolation with parameterized inputs or schema validation.',
552
+ isSanitized: taint.isSanitized,
553
+ isStatic: taint.isStatic,
554
+ };
555
+ } catch (err) {
556
+ return { error: `Could not analyze file: ${err.message}` };
557
+ }
558
+ }
559
+
560
+ // =============================================================================
561
+ // MCP STDIO SERVER
562
+ // =============================================================================
563
+
564
+ export async function mcpCommand() {
565
+ // MCP uses JSON-RPC 2.0 over stdio
566
+ process.stdin.setEncoding('utf-8');
567
+
568
+ let buffer = '';
569
+
570
+ process.stdin.on('data', async (chunk) => {
571
+ buffer += chunk;
572
+
573
+ // MCP messages are newline-delimited JSON
574
+ const lines = buffer.split('\n');
575
+ buffer = lines.pop(); // Keep incomplete line in buffer
576
+
577
+ for (const line of lines) {
578
+ if (!line.trim()) continue;
579
+
580
+ try {
581
+ const request = JSON.parse(line);
582
+ const response = await handleRequest(request);
583
+ process.stdout.write(JSON.stringify(response) + '\n');
584
+ } catch (err) {
585
+ const errorResponse = {
586
+ jsonrpc: '2.0',
587
+ id: null,
588
+ error: { code: -32700, message: 'Parse error', data: err.message },
589
+ };
590
+ process.stdout.write(JSON.stringify(errorResponse) + '\n');
591
+ }
592
+ }
593
+ });
594
+
595
+ process.stdin.on('end', () => process.exit(0));
596
+ }
597
+
598
+ async function handleRequest(request) {
599
+ const { jsonrpc, id, method, params } = request;
600
+
601
+ const respond = (result) => ({ jsonrpc: '2.0', id, result });
602
+ const respondError = (code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
603
+
604
+ switch (method) {
605
+ case 'initialize':
606
+ return respond({
607
+ protocolVersion: '2024-11-05',
608
+ capabilities: { tools: {} },
609
+ serverInfo: { name: 'praxis', version: '3.0.0' },
610
+ });
611
+
612
+ case 'tools/list':
613
+ return respond({ tools: TOOLS });
614
+
615
+ case 'tools/call': {
616
+ const { name, arguments: args } = params;
617
+
618
+ try {
619
+ let result;
620
+ switch (name) {
621
+ case 'scan_secrets':
622
+ result = await scanSecrets(args);
623
+ break;
624
+ case 'get_checklist':
625
+ result = getChecklist();
626
+ break;
627
+ case 'analyze_file':
628
+ result = await analyzeFile(args);
629
+ break;
630
+ case 'scan_repo':
631
+ result = await scanRepo(args);
632
+ break;
633
+ case 'get_findings':
634
+ result = getFindings(args);
635
+ break;
636
+ case 'suppress_finding':
637
+ result = suppressFinding(args);
638
+ break;
639
+ case 'explain_and_fix':
640
+ result = await explainAndFix(args);
641
+ break;
642
+ default:
643
+ return respondError(-32601, `Unknown tool: ${name}`);
644
+ }
645
+
646
+ return respond({
647
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
648
+ });
649
+ } catch (err) {
650
+ return respondError(-32603, err.message);
651
+ }
652
+ }
653
+
654
+ case 'notifications/initialized':
655
+ return null; // No response needed for notifications
656
+
657
+ default:
658
+ return respondError(-32601, `Method not found: ${method}`);
659
+ }
660
+ }