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,403 @@
1
+ /**
2
+ * CI Command โ€” Optimized for CI/CD Pipelines
3
+ * =============================================
4
+ *
5
+ * Single command for CI pipelines with:
6
+ * - Exit code 1 if score < threshold (default 75)
7
+ * - SARIF output for GitHub Code Scanning upload
8
+ * - JSON output for custom integrations
9
+ * - Compact summary for CI logs
10
+ * - --fail-on flag for severity-based gating
11
+ *
12
+ * USAGE:
13
+ * npx praxis-sec ci . Default: fail if score < 75
14
+ * npx praxis-sec ci . --threshold 60 Custom score threshold
15
+ * npx praxis-sec ci . --fail-on critical Only fail on critical findings
16
+ * npx praxis-sec ci . --sarif results.sarif SARIF for GitHub Code Scanning
17
+ * npx praxis-sec ci . --baseline Only check new findings
18
+ */
19
+
20
+ import fs from 'fs';
21
+ import path from 'path';
22
+ import { renderFindingsSARIF } from '../core/output/sarif.js';
23
+ import { execFileSync } from 'child_process';
24
+ import { buildOrchestrator } from '../agents/index.js';
25
+ import { ScoringEngine } from '../agents/scoring-engine.js';
26
+ import { PolicyEngine } from '../agents/policy-engine.js';
27
+ import { runDepsAudit } from './deps.js';
28
+ import { filterBaseline } from './baseline.js';
29
+ import {
30
+ SECRET_PATTERNS,
31
+ SKIP_DIRS,
32
+ SKIP_EXTENSIONS,
33
+ SKIP_FILENAMES,
34
+ MAX_FILE_SIZE,
35
+ loadGitignorePatterns
36
+ } from '../utils/patterns.js';
37
+ import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
38
+ import { ThreatIntel } from '../utils/threat-intel.js';
39
+ import * as intelOrchestrator from '../utils/intel/index.js';
40
+ import fg from 'fast-glob';
41
+
42
+ // =============================================================================
43
+ // MAIN COMMAND
44
+ // =============================================================================
45
+
46
+ export async function ciCommand(targetPath = '.', options = {}) {
47
+ const absolutePath = path.resolve(targetPath);
48
+ const threshold = options.threshold ?? 75;
49
+ const failOn = options.failOn || null;
50
+ const alwaysFailOn = options.alwaysFailOn || null;
51
+ const sarifPath = options.sarif || null;
52
+
53
+ if (!fs.existsSync(absolutePath)) {
54
+ console.error(`[praxis] Path does not exist: ${absolutePath}`);
55
+ process.exit(1);
56
+ }
57
+
58
+ if (options.strictIntel) {
59
+ const intelCheck = checkIntelFreshness(options.maxIntelAge || '7d');
60
+ if (!intelCheck.ok) {
61
+ console.error(`[praxis] STRICT-INTEL FAIL: ${intelCheck.reason}`);
62
+ console.error(`[praxis] Run 'praxis update-intel' or remove --strict-intel.`);
63
+ process.exit(1);
64
+ }
65
+ }
66
+
67
+ const startTime = Date.now();
68
+
69
+ // โ”€โ”€ Secret Scan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
70
+ const allFiles = await findFiles(absolutePath);
71
+ const secretFindings = [];
72
+
73
+ for (const file of allFiles) {
74
+ try {
75
+ const content = fs.readFileSync(file, 'utf-8');
76
+ const lines = content.split('\n');
77
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
78
+ const line = lines[lineNum];
79
+ if (/praxis-ignore/i.test(line)) continue;
80
+ for (const pattern of SECRET_PATTERNS) {
81
+ pattern.pattern.lastIndex = 0;
82
+ let match;
83
+ while ((match = pattern.pattern.exec(line)) !== null) {
84
+ if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
85
+ secretFindings.push({
86
+ file, line: lineNum + 1, column: match.index + 1,
87
+ matched: match[0], severity: pattern.severity,
88
+ category: pattern.category || 'secrets',
89
+ rule: pattern.name, title: pattern.name.replace(/_/g, ' '),
90
+ description: pattern.description,
91
+ confidence: getConfidence(pattern, match[0]),
92
+ fix: 'Move to environment variable or secrets manager',
93
+ });
94
+ }
95
+ }
96
+ }
97
+ } catch { /* skip */ }
98
+ }
99
+
100
+ // โ”€โ”€ Agent Scan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
101
+ const orchestrator = buildOrchestrator();
102
+ const results = await orchestrator.runAll(absolutePath, { quiet: true }); // praxis-ignore โ€” orchestrator result, not LLM output triggering actions
103
+ const agentFindings = results.findings;
104
+
105
+ // โ”€โ”€ Dependency Audit โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
106
+ let depVulns = [];
107
+ if (options.deps !== false) {
108
+ try {
109
+ const depResult = await runDepsAudit(absolutePath);
110
+ depVulns = depResult.vulns || [];
111
+ } catch { /* skip */ }
112
+ }
113
+
114
+ // โ”€โ”€ Merge & Deduplicate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
115
+ const seen = new Set();
116
+ let allFindings = [...secretFindings, ...agentFindings].filter(f => {
117
+ const key = `${f.file}:${f.line}:${f.rule}`;
118
+ if (seen.has(key)) return false;
119
+ seen.add(key);
120
+ return true;
121
+ });
122
+
123
+ // Apply policy
124
+ const policy = PolicyEngine.load(absolutePath);
125
+ allFindings = policy.applyPolicy(allFindings);
126
+
127
+ // --always-fail-on floor must beat the baseline: check the pre-baseline
128
+ // set for floor violations, but gate threshold/fail-on on the filtered set.
129
+ const preBaselineFindings = allFindings;
130
+
131
+ // Apply baseline filter
132
+ if (options.baseline) {
133
+ allFindings = filterBaseline(allFindings, absolutePath);
134
+ }
135
+
136
+ // โ”€โ”€ Score โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
137
+ const scoringEngine = new ScoringEngine();
138
+ const scoreResult = scoringEngine.compute(allFindings, depVulns);
139
+ scoreResult.score = Math.round(scoreResult.score * 10) / 10;
140
+ scoringEngine.saveToHistory(absolutePath, scoreResult);
141
+
142
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
143
+
144
+ // โ”€โ”€ SARIF Output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
145
+ if (sarifPath) {
146
+ fs.writeFileSync(sarifPath, renderSARIF(allFindings, absolutePath));
147
+ }
148
+
149
+ // โ”€โ”€ JSON Output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
150
+ if (options.json) {
151
+ console.log(JSON.stringify({
152
+ score: scoreResult.score,
153
+ grade: scoreResult.grade.letter,
154
+ totalFindings: allFindings.length,
155
+ totalDepVulns: depVulns.length,
156
+ critical: allFindings.filter(f => f.severity === 'critical').length,
157
+ high: allFindings.filter(f => f.severity === 'high').length,
158
+ medium: allFindings.filter(f => f.severity === 'medium').length,
159
+ low: allFindings.filter(f => f.severity === 'low').length,
160
+ categories: Object.fromEntries(Object.entries(scoreResult.categories).map(([key, c]) => [key, {
161
+ findingCount: Object.values(c.counts).reduce((a, b) => a + b, 0),
162
+ counts: c.counts,
163
+ }])),
164
+ ...(options.includeFindings ? {
165
+ findings: allFindings.map(f => ({
166
+ file: path.relative(absolutePath, f.file).replace(/\\/g, '/'),
167
+ line: f.line, rule: f.rule, severity: f.severity,
168
+ })),
169
+ } : {}),
170
+ threshold,
171
+ pass: determinePass(scoreResult, allFindings, threshold, failOn, alwaysFailOn, preBaselineFindings),
172
+ duration: `${duration}s`,
173
+ }, null, 2));
174
+ } else {
175
+ // โ”€โ”€ Compact CI Summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
176
+ const critical = allFindings.filter(f => f.severity === 'critical').length;
177
+ const high = allFindings.filter(f => f.severity === 'high').length;
178
+ const medium = allFindings.filter(f => f.severity === 'medium').length;
179
+
180
+ console.log(`[praxis] Score: ${scoreResult.score}/100 (${scoreResult.grade.letter}) | Findings: ${allFindings.length} (${critical}C ${high}H ${medium}M) | CVEs: ${depVulns.length} | ${duration}s`);
181
+
182
+ if (critical > 0) {
183
+ console.log(`[praxis] Critical findings:`);
184
+ for (const f of allFindings.filter(f => f.severity === 'critical').slice(0, 5)) {
185
+ const rel = path.relative(absolutePath, f.file).replace(/\\/g, '/');
186
+ console.log(` - ${f.rule} at ${rel}:${f.line}`);
187
+ }
188
+ }
189
+
190
+ if (sarifPath) {
191
+ console.log(`[praxis] SARIF: ${sarifPath}`);
192
+ }
193
+ }
194
+
195
+ // โ”€โ”€ GitHub PR Inline Annotations (P-IMP-044) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
196
+ emitGitHubAnnotations(allFindings, absolutePath);
197
+
198
+ // โ”€โ”€ GitHub PR Comment โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
199
+ if (options.githubPr) {
200
+ try {
201
+ postPRComment(scoreResult, allFindings, depVulns, absolutePath, duration);
202
+ } catch (err) {
203
+ console.log(`[praxis] Warning: Could not post PR comment: ${err.message}`);
204
+ }
205
+ }
206
+
207
+ // โ”€โ”€ Exit Code โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
208
+ const pass = determinePass(scoreResult, allFindings, threshold, failOn, alwaysFailOn, preBaselineFindings);
209
+ if (!pass) {
210
+ if (!options.json) {
211
+ if (failOn) {
212
+ console.log(`[praxis] FAIL: Found ${failOn}-severity findings`);
213
+ } else if (alwaysFailOn) {
214
+ console.log(`[praxis] FAIL: Found ${alwaysFailOn}-or-worse findings (--always-fail-on floor)`);
215
+ } else {
216
+ console.log(`[praxis] FAIL: Score ${scoreResult.score} < threshold ${threshold}`);
217
+ }
218
+ }
219
+ process.exit(1);
220
+ } else {
221
+ if (!options.json) {
222
+ console.log(`[praxis] PASS`);
223
+ }
224
+ process.exit(0);
225
+ }
226
+ }
227
+
228
+ // =============================================================================
229
+ // HELPERS
230
+ // =============================================================================
231
+
232
+ function determinePass(scoreResult, findings, threshold, failOn, alwaysFailOn, preBaselineFindings = null) {
233
+ if (failOn) {
234
+ const sevOrder = ['critical', 'high', 'medium', 'low'];
235
+ const failIndex = sevOrder.indexOf(failOn);
236
+ if (failIndex === -1) return scoreResult.score >= threshold;
237
+ const blockingSevs = sevOrder.slice(0, failIndex + 1);
238
+ return !findings.some(f => blockingSevs.includes(f.severity));
239
+ }
240
+ if (alwaysFailOn) {
241
+ const sevOrder = ['critical', 'high', 'medium', 'low'];
242
+ const floorIndex = sevOrder.indexOf(alwaysFailOn);
243
+ if (floorIndex === -1) return scoreResult.score >= threshold;
244
+ const floorSevs = sevOrder.slice(0, floorIndex + 1);
245
+ // Floor beats everything: even an accepted baseline cannot suppress it.
246
+ const floorSet = preBaselineFindings || findings;
247
+ if (floorSet.some(f => floorSevs.includes(f.severity))) return false;
248
+ }
249
+ return scoreResult.score >= threshold;
250
+ }
251
+
252
+ function emitGitHubAnnotations(findings, rootPath) {
253
+ if (process.env.GITHUB_ACTIONS !== 'true') return;
254
+ for (const f of findings) {
255
+ if (!f.file || !f.line) continue;
256
+ const rel = path.relative(rootPath, f.file).replace(/\\/g, '/');
257
+ const level = ['critical', 'high'].includes(f.severity) ? 'error' : 'warning';
258
+ const col = f.column || 1;
259
+ const title = (f.title || f.rule || 'Security Finding').replace(/\r?\n/g, ' ');
260
+ const msg = (f.description || f.matched || '').replace(/\r?\n/g, ' ').slice(0, 300);
261
+ console.log(`::${level} file=${rel},line=${f.line},col=${col},title=${title}::${msg}`);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * SARIF for GitHub Code Scanning.
267
+ *
268
+ * Delegates to the shared serializer in `cli/core/output/sarif.js` (P-IMP-062). This
269
+ * used to be a private copy, which is why the GitHub Action's SARIF carried no
270
+ * `security-severity` at all โ€” the fix landed in the registry and never reached here.
271
+ *
272
+ * `rootPath` is mandatory in practice: it is what makes artifact URIs repo-relative.
273
+ * Without it a scan uploads the local username and directory layout to the target
274
+ * repository's Security tab.
275
+ */
276
+ function renderSARIF(findings, rootPath) {
277
+ return renderFindingsSARIF(findings, { rootPath });
278
+ }
279
+
280
+ /**
281
+ * Post a summary comment on the current GitHub PR using the `gh` CLI.
282
+ * Requires: `gh` installed and authenticated, running in a PR context.
283
+ */
284
+ function postPRComment(scoreResult, findings, depVulns, rootPath, duration) {
285
+ // Detect PR number from environment (GitHub Actions sets GITHUB_REF)
286
+ let prNumber = process.env.GITHUB_PR_NUMBER || '';
287
+
288
+ if (!prNumber) {
289
+ // Try to detect from GITHUB_REF (refs/pull/123/merge)
290
+ const ref = process.env.GITHUB_REF || '';
291
+ const match = ref.match(/refs\/pull\/(\d+)\//);
292
+ if (match) prNumber = match[1];
293
+ }
294
+
295
+ if (!prNumber) {
296
+ // Try gh pr view to get current PR
297
+ try {
298
+ const prJson = execFileSync('gh', ['pr', 'view', '--json', 'number'], { // praxis-ignore โ€” execFileSync, not MCP
299
+ cwd: rootPath, stdio: ['pipe', 'pipe', 'pipe'], // praxis-ignore
300
+ }).toString();
301
+ const parsed = JSON.parse(prJson);
302
+ prNumber = String(parsed.number);
303
+ } catch {
304
+ console.log('[praxis] No PR detected โ€” skipping PR comment');
305
+ return;
306
+ }
307
+ }
308
+
309
+ const critical = findings.filter(f => f.severity === 'critical').length;
310
+ const high = findings.filter(f => f.severity === 'high').length;
311
+ const medium = findings.filter(f => f.severity === 'medium').length;
312
+ const low = findings.filter(f => f.severity === 'low').length;
313
+
314
+ const gradeEmoji = { A: '๐ŸŸข', B: '๐Ÿ”ต', C: '๐ŸŸก', D: '๐ŸŸ ', F: '๐Ÿ”ด' };
315
+ const emoji = gradeEmoji[scoreResult.grade.letter] || 'โšช';
316
+
317
+ // Build markdown body
318
+ let body = `## ${emoji} Praxis Security Report\n\n`;
319
+ body += `| Metric | Value |\n|--------|-------|\n`;
320
+ body += `| **Score** | ${scoreResult.score}/100 (${scoreResult.grade.letter}) |\n`;
321
+ body += `| **Findings** | ${findings.length} total (${critical}C ${high}H ${medium}M ${low}L) |\n`;
322
+ body += `| **Dep CVEs** | ${depVulns.length} |\n`;
323
+ body += `| **Duration** | ${duration}s |\n\n`;
324
+
325
+ if (critical > 0 || high > 0) {
326
+ body += `### Critical & High Findings\n\n`;
327
+ body += `| Severity | File | Issue |\n|----------|------|-------|\n`;
328
+ for (const f of findings.filter(f => f.severity === 'critical' || f.severity === 'high').slice(0, 20)) {
329
+ const rel = path.relative(rootPath, f.file).replace(/\\/g, '/');
330
+ body += `| ${f.severity.toUpperCase()} | \`${rel}:${f.line}\` | ${(f.title || f.rule).slice(0, 60)} |\n`;
331
+ }
332
+ body += '\n';
333
+ }
334
+
335
+ if (findings.length === 0 && depVulns.length === 0) {
336
+ body += '> No security issues found โ€” looking good! ๐ŸŽ‰\n\n';
337
+ }
338
+
339
+ body += `\n---\n<sub>Generated by <a href="">Praxis</a> ยท <a href="">View full report in dashboard</a></sub>`;
340
+
341
+ // Post comment via gh CLI
342
+ execFileSync('gh', ['pr', 'comment', prNumber, '--body', body], { // praxis-ignore โ€” execFileSync, not MCP
343
+ cwd: rootPath,
344
+ stdio: ['pipe', 'pipe', 'pipe'], // praxis-ignore
345
+ });
346
+
347
+ console.log(`[praxis] PR comment posted on #${prNumber}`);
348
+ }
349
+
350
+ function checkIntelFreshness(maxAge) {
351
+ const ms = parseDuration(maxAge);
352
+ if (!ms) return { ok: false, reason: `Invalid --max-intel-age: ${maxAge}` };
353
+
354
+ const meta = intelOrchestrator.loadMeta();
355
+ if (!meta || !meta.updatedAt) {
356
+ return { ok: false, reason: 'Threat-intel feed has never been updated.' };
357
+ }
358
+
359
+ if (intelOrchestrator.isStale(ms)) {
360
+ const age = Math.round((Date.now() - new Date(meta.updatedAt).getTime()) / (60 * 60 * 1000));
361
+ return { ok: false, reason: `Threat-intel feed is ${age}h old (max ${maxAge}).` };
362
+ }
363
+
364
+ // Reject if any *core* source failed on the last update.
365
+ const failedCore = [];
366
+ for (const [name, info] of Object.entries(meta.sources || {})) {
367
+ if (info.skipped) continue;
368
+ if (!info.ok) failedCore.push(name);
369
+ }
370
+ if (failedCore.length) {
371
+ return { ok: false, reason: `Core sources failed on last update: ${failedCore.join(', ')}` };
372
+ }
373
+
374
+ return { ok: true };
375
+ }
376
+
377
+ function parseDuration(s) {
378
+ const m = String(s).match(/^(\d+)\s*(d|h|m|s)?$/i);
379
+ if (!m) return null;
380
+ const n = parseInt(m[1], 10);
381
+ const unit = (m[2] || 'd').toLowerCase();
382
+ const mult = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[unit];
383
+ return n * mult;
384
+ }
385
+
386
+ async function findFiles(rootPath) {
387
+ const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
388
+ const gitignoreGlobs = loadGitignorePatterns(rootPath);
389
+ globIgnore.push(...gitignoreGlobs);
390
+
391
+ const files = await fg('**/*', {
392
+ cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true,
393
+ });
394
+
395
+ return files.filter(file => {
396
+ const ext = path.extname(file).toLowerCase();
397
+ if (SKIP_EXTENSIONS.has(ext)) return false;
398
+ if (SKIP_FILENAMES.has(path.basename(file))) return false;
399
+ if (path.basename(file).endsWith('.min.js') || path.basename(file).endsWith('.min.css')) return false;
400
+ try { if (fs.statSync(file).size > MAX_FILE_SIZE) return false; } catch { return false; }
401
+ return true;
402
+ });
403
+ }