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,265 @@
1
+ /**
2
+ * SBOM Generator
3
+ * ===============
4
+ *
5
+ * Generates Software Bill of Materials in CycloneDX JSON format.
6
+ * Parses package.json, requirements.txt, Gemfile, go.mod, etc.
7
+ */
8
+
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+
12
+ export class SBOMGenerator {
13
+ /**
14
+ * Generate a CycloneDX 1.5 SBOM from the project.
15
+ *
16
+ * @param {string} rootPath — Project root directory
17
+ * @returns {object} — CycloneDX JSON object
18
+ */
19
+ generate(rootPath) {
20
+ const components = [];
21
+
22
+ // ── npm/yarn/pnpm ─────────────────────────────────────────────────────────
23
+ const pkgPath = path.join(rootPath, 'package.json');
24
+ if (fs.existsSync(pkgPath)) {
25
+ try {
26
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
27
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
28
+
29
+ for (const [name, version] of Object.entries(allDeps)) {
30
+ components.push({
31
+ type: 'library',
32
+ name,
33
+ version: version.replace(/^[\^~>=<]/, ''),
34
+ purl: `pkg:npm/${name.replace('/', '%2F')}@${version.replace(/^[\^~>=<]/, '')}`,
35
+ scope: pkg.dependencies?.[name] ? 'required' : 'optional',
36
+ });
37
+ }
38
+ } catch { /* skip */ }
39
+ }
40
+
41
+ // ── Python requirements.txt ───────────────────────────────────────────────
42
+ const reqPath = path.join(rootPath, 'requirements.txt');
43
+ if (fs.existsSync(reqPath)) {
44
+ try {
45
+ const lines = fs.readFileSync(reqPath, 'utf-8').split('\n');
46
+ for (const line of lines) {
47
+ const trimmed = line.trim();
48
+ if (!trimmed || trimmed.startsWith('#')) continue;
49
+ const match = trimmed.match(/^([a-zA-Z0-9_-]+)(?:==|>=|~=)?(.+)?$/);
50
+ if (match) {
51
+ components.push({
52
+ type: 'library',
53
+ name: match[1],
54
+ version: match[2] || 'unspecified',
55
+ purl: `pkg:pypi/${match[1]}@${match[2] || 'latest'}`,
56
+ scope: 'required',
57
+ });
58
+ }
59
+ }
60
+ } catch { /* skip */ }
61
+ }
62
+
63
+ // ── Go modules ────────────────────────────────────────────────────────────
64
+ const goModPath = path.join(rootPath, 'go.mod');
65
+ if (fs.existsSync(goModPath)) {
66
+ try {
67
+ const content = fs.readFileSync(goModPath, 'utf-8');
68
+ const requireBlock = content.match(/require\s*\(([\s\S]*?)\)/);
69
+ if (requireBlock) {
70
+ const lines = requireBlock[1].split('\n');
71
+ for (const line of lines) {
72
+ const match = line.trim().match(/^(\S+)\s+v?(\S+)/);
73
+ if (match) {
74
+ components.push({
75
+ type: 'library',
76
+ name: match[1],
77
+ version: match[2],
78
+ purl: `pkg:golang/${match[1]}@${match[2]}`,
79
+ scope: 'required',
80
+ });
81
+ }
82
+ }
83
+ }
84
+ } catch { /* skip */ }
85
+ }
86
+
87
+ // ── Rust Cargo.toml ───────────────────────────────────────────────────────
88
+ const cargoPath = path.join(rootPath, 'Cargo.toml');
89
+ if (fs.existsSync(cargoPath)) {
90
+ try {
91
+ const content = fs.readFileSync(cargoPath, 'utf-8');
92
+ const depsSection = content.match(/\[dependencies\]([\s\S]*?)(?:\[|$)/);
93
+ if (depsSection) {
94
+ const lines = depsSection[1].split('\n');
95
+ for (const line of lines) {
96
+ const match = line.trim().match(/^([a-zA-Z0-9_-]+)\s*=\s*"([^"]+)"/);
97
+ if (match) {
98
+ components.push({
99
+ type: 'library',
100
+ name: match[1],
101
+ version: match[2],
102
+ purl: `pkg:cargo/${match[1]}@${match[2]}`,
103
+ scope: 'required',
104
+ });
105
+ }
106
+ }
107
+ }
108
+ } catch { /* skip */ }
109
+ }
110
+
111
+ // ── Detect licenses from lock files ─────────────────────────────────────
112
+ const licenses = this._detectLicenses(rootPath);
113
+
114
+ // ── Build CycloneDX BOM (CRA-enhanced) ──────────────────────────────────
115
+ const projectMeta = this.getProjectMetadata(rootPath);
116
+ const bom = {
117
+ bomFormat: 'CycloneDX',
118
+ specVersion: '1.5',
119
+ serialNumber: `urn:uuid:${this.uuid()}`,
120
+ version: 1,
121
+ metadata: {
122
+ timestamp: new Date().toISOString(),
123
+ tools: [{
124
+ vendor: 'praxis',
125
+ name: 'praxis',
126
+ version: '1.0.0',
127
+ }],
128
+ component: projectMeta,
129
+ // EU CRA: supplier identification
130
+ supplier: this._getSupplier(rootPath),
131
+ // EU CRA: lifecycle phase
132
+ lifecycles: [{ phase: 'build' }],
133
+ },
134
+ components: components.map((c, i) => {
135
+ const comp = {
136
+ 'bom-ref': `component-${i}`,
137
+ type: c.type,
138
+ name: c.name,
139
+ version: c.version,
140
+ purl: c.purl,
141
+ scope: c.scope,
142
+ };
143
+ // EU CRA: attach license if known
144
+ const lic = licenses[c.name];
145
+ if (lic) {
146
+ comp.licenses = [{ license: { id: lic } }];
147
+ }
148
+ return comp;
149
+ }),
150
+ // EU CRA: vulnerability disclosure info
151
+ vulnerabilities: [],
152
+ };
153
+
154
+ return bom;
155
+ }
156
+
157
+ /**
158
+ * Attach known vulnerabilities to the SBOM (CRA requirement).
159
+ */
160
+ attachVulnerabilities(bom, depVulns = []) {
161
+ bom.vulnerabilities = depVulns.map((v, i) => ({
162
+ 'bom-ref': `vuln-${i}`,
163
+ id: v.id || v.package || `VULN-${i}`,
164
+ source: { name: 'praxis' },
165
+ ratings: [{
166
+ severity: v.severity || 'unknown',
167
+ method: 'other',
168
+ }],
169
+ description: v.description || '',
170
+ affects: [{
171
+ ref: v.package || 'unknown',
172
+ }],
173
+ }));
174
+ return bom;
175
+ }
176
+
177
+ /**
178
+ * Generate SBOM and write to file.
179
+ */
180
+ generateToFile(rootPath, outputPath, format = 'cyclonedx') {
181
+ const bom = this.generate(rootPath);
182
+ const output = JSON.stringify(bom, null, 2);
183
+ fs.writeFileSync(outputPath, output);
184
+ return outputPath;
185
+ }
186
+
187
+ getProjectMetadata(rootPath) {
188
+ const pkgPath = path.join(rootPath, 'package.json');
189
+ try {
190
+ if (fs.existsSync(pkgPath)) {
191
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
192
+ return {
193
+ type: 'application',
194
+ name: pkg.name || path.basename(rootPath),
195
+ version: pkg.version || '0.0.0',
196
+ };
197
+ }
198
+ } catch { /* skip */ }
199
+ return {
200
+ type: 'application',
201
+ name: path.basename(rootPath),
202
+ version: '0.0.0',
203
+ };
204
+ }
205
+
206
+ /**
207
+ * EU CRA: Extract supplier info from package.json.
208
+ */
209
+ _getSupplier(rootPath) {
210
+ const pkgPath = path.join(rootPath, 'package.json');
211
+ try {
212
+ if (fs.existsSync(pkgPath)) {
213
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
214
+ const author = typeof pkg.author === 'string' ? pkg.author
215
+ : pkg.author?.name || pkg.author?.email || null;
216
+ if (author) {
217
+ return { name: author, url: [pkg.homepage || pkg.repository?.url || ''].filter(Boolean) };
218
+ }
219
+ }
220
+ } catch { /* skip */ }
221
+ return { name: 'Unknown' };
222
+ }
223
+
224
+ /**
225
+ * Detect licenses from node_modules (best-effort).
226
+ * Returns { packageName: 'MIT' | 'ISC' | ... }
227
+ */
228
+ _detectLicenses(rootPath) {
229
+ const licenses = {};
230
+ const nodeModules = path.join(rootPath, 'node_modules');
231
+ const pkgPath = path.join(rootPath, 'package.json');
232
+
233
+ if (!fs.existsSync(pkgPath)) return licenses;
234
+
235
+ try {
236
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
237
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
238
+
239
+ for (const name of Object.keys(allDeps)) {
240
+ const depPkgPath = path.join(nodeModules, name, 'package.json');
241
+ try {
242
+ if (fs.existsSync(depPkgPath)) {
243
+ const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
244
+ if (depPkg.license) {
245
+ licenses[name] = typeof depPkg.license === 'string'
246
+ ? depPkg.license
247
+ : depPkg.license.type || 'UNKNOWN';
248
+ }
249
+ }
250
+ } catch { /* skip */ }
251
+ }
252
+ } catch { /* skip */ }
253
+
254
+ return licenses;
255
+ }
256
+
257
+ uuid() {
258
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
259
+ const r = Math.random() * 16 | 0;
260
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
261
+ });
262
+ }
263
+ }
264
+
265
+ export default SBOMGenerator;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Enhanced Scoring Engine
3
+ * ========================
4
+ *
5
+ * Risk-based scoring with 8 categories, EPSS integration,
6
+ * KEV flagging, and historical trend tracking.
7
+ *
8
+ * Score = 100 - sum(category deductions)
9
+ * Each category has a weight and max deduction cap.
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { getComplianceSummary, getAgenticSummary, enrichAgenticRisk } from '../utils/compliance-map.js';
15
+ import { mapFindingToStandards, getStandardsSummary } from '../utils/standards/index.js';
16
+
17
+ // =============================================================================
18
+ // SCORING CONFIGURATION
19
+ // =============================================================================
20
+
21
+ // Weights aligned with OWASP Top 10 2025:
22
+ // A01 Broken Access Control (auth: 15), A02 Security Misconfiguration (config: 8),
23
+ // A03 Software Supply Chain Failures (supply-chain: 12, deps: 13),
24
+ // A05 Injection (injection: 15), A07 Auth Failures (secrets: 15),
25
+ // A10 Mishandling of Exceptional Conditions (→ injection category)
26
+ // + API Security (10), AI/LLM Security (12) — weights sum to 100
27
+ const CATEGORIES = {
28
+ secrets: { weight: 15, label: 'Secrets', deductions: { critical: 25, high: 15, medium: 5 } },
29
+ injection: { weight: 15, label: 'Code Vulnerabilities', deductions: { critical: 20, high: 10, medium: 3 } },
30
+ deps: { weight: 13, label: 'Dependencies', deductions: { critical: 20, high: 10, medium: 5, moderate: 5 } },
31
+ auth: { weight: 15, label: 'Auth & Access Control', deductions: { critical: 20, high: 10, medium: 3 } },
32
+ config: { weight: 8, label: 'Configuration', deductions: { critical: 15, high: 8, medium: 3 } },
33
+ 'supply-chain':{ weight: 12, label: 'Supply Chain', deductions: { critical: 15, high: 8, medium: 3 } },
34
+ api: { weight: 10, label: 'API Security', deductions: { critical: 15, high: 8, medium: 3 } },
35
+ llm: { weight: 12, label: 'AI/LLM Security', deductions: { critical: 15, high: 8, medium: 3 } },
36
+ };
37
+
38
+ // Fallback categories for findings that don't match a known category
39
+ const FALLBACK_CATEGORY_MAP = {
40
+ 'secret': 'secrets',
41
+ 'vulnerability': 'injection',
42
+ 'ssrf': 'injection', // OWASP 2025: SSRF merged into A01 Broken Access Control
43
+ 'history': 'secrets',
44
+ 'cicd': 'config',
45
+ 'mobile': 'injection',
46
+ 'privacy': 'config',
47
+ 'mcp': 'llm',
48
+ 'agentic': 'llm',
49
+ 'rag': 'llm',
50
+ 'vibe': 'injection', // Vibe coding findings → Code Vulnerabilities
51
+ 'exception': 'injection', // OWASP A10:2025 — Mishandling of Exceptional Conditions
52
+ 'agent-config': 'llm', // Agent config security → AI/LLM category
53
+ 'memory-poisoning': 'llm', // Memory poisoning → AI/LLM category
54
+ 'recon': null, // skip recon findings
55
+ };
56
+
57
+ const GRADES = [ { min: 90, letter: 'A', label: 'Ship it!', color: 'green' },
58
+ { min: 75, letter: 'B', label: 'Minor issues to review', color: 'cyan' },
59
+ { min: 60, letter: 'C', label: 'Fix before shipping', color: 'yellow' },
60
+ { min: 40, letter: 'D', label: 'Significant security risks', color: 'red' },
61
+ { min: 0, letter: 'F', label: 'Not safe to ship', color: 'red' },
62
+ ];
63
+
64
+ // =============================================================================
65
+ // SCORING ENGINE
66
+ // =============================================================================
67
+
68
+ export class ScoringEngine {
69
+ /**
70
+ * Compute the security score from agent findings + dependency vulnerabilities.
71
+ *
72
+ * @param {object[]} findings — Array of finding objects from agents
73
+ * @param {object[]} depVulns — Array of dependency CVE objects
74
+ * @returns {object} — { score, grade, categories, breakdown }
75
+ */
76
+ compute(findings = [], depVulns = []) {
77
+ const categoryResults = {};
78
+
79
+ // Initialize all categories
80
+ for (const [key, config] of Object.entries(CATEGORIES)) {
81
+ categoryResults[key] = {
82
+ label: config.label,
83
+ weight: config.weight,
84
+ counts: { critical: 0, high: 0, medium: 0, low: 0 },
85
+ deduction: 0,
86
+ maxDeduction: config.weight, // Cap at category weight
87
+ findings: [],
88
+ };
89
+ }
90
+
91
+ // ── Enrich findings with OWASP Agentic AI Top 10 metadata ──────────────
92
+ for (const finding of findings) {
93
+ enrichAgenticRisk(finding);
94
+ finding.standards = mapFindingToStandards(finding);
95
+ }
96
+
97
+ // ── Classify findings into categories ─────────────────────────────────────
98
+ for (const finding of findings) {
99
+ const cat = this.resolveCategory(finding.category);
100
+ if (!cat || !categoryResults[cat]) continue;
101
+
102
+ const sev = finding.severity || 'medium';
103
+ categoryResults[cat].counts[sev] = (categoryResults[cat].counts[sev] || 0) + 1;
104
+ categoryResults[cat].findings.push(finding);
105
+ }
106
+
107
+ // ── Add dependency vulnerabilities ────────────────────────────────────────
108
+ for (const vuln of depVulns) {
109
+ const sev = vuln.severity || 'medium';
110
+ categoryResults.deps.counts[sev] = (categoryResults.deps.counts[sev] || 0) + 1;
111
+ }
112
+
113
+ // ── Compute deductions per category (confidence-weighted) ─────────────────
114
+ const CONFIDENCE_MULTIPLIER = { high: 1.0, medium: 0.6, low: 0.3 };
115
+
116
+ for (const [key, config] of Object.entries(CATEGORIES)) {
117
+ const result = categoryResults[key];
118
+ let deduction = 0;
119
+
120
+ // Count-based deductions for deps (no per-finding confidence)
121
+ for (const [sev, pts] of Object.entries(config.deductions)) {
122
+ if (key === 'deps') {
123
+ deduction += (result.counts[sev] || 0) * pts;
124
+ }
125
+ }
126
+
127
+ // Per-finding confidence-weighted deductions for agent findings
128
+ if (key !== 'deps') {
129
+ for (const finding of result.findings) {
130
+ const sev = finding.severity || 'medium';
131
+ const pts = config.deductions[sev] || 0;
132
+ const confidence = finding.confidence || 'high';
133
+ const multiplier = CONFIDENCE_MULTIPLIER[confidence] || 1.0;
134
+ deduction += pts * multiplier;
135
+ }
136
+ }
137
+
138
+ result.deduction = Math.min(deduction, result.maxDeduction);
139
+ }
140
+
141
+ // ── Compute total score ───────────────────────────────────────────────────
142
+ const totalDeduction = Object.values(categoryResults).reduce(
143
+ (sum, r) => sum + r.deduction, 0
144
+ );
145
+ const score = Math.max(0, 100 - totalDeduction);
146
+ const grade = GRADES.find(g => score >= g.min);
147
+
148
+ // ── Compliance mapping ─────────────────────────────────────────────────
149
+ let compliance;
150
+ try {
151
+ compliance = getComplianceSummary(findings);
152
+ } catch {
153
+ compliance = null;
154
+ }
155
+
156
+ // ── OWASP Agentic AI Top 10 summary ──────────────────────────────────
157
+ let agenticSummary;
158
+ try {
159
+ agenticSummary = getAgenticSummary(findings);
160
+ } catch {
161
+ agenticSummary = null;
162
+ }
163
+
164
+ // ── AI security standards summary (OWASP LLM, ATLAS, NIST AI 600-1, ...)
165
+ let standardsSummary;
166
+ try {
167
+ standardsSummary = getStandardsSummary(findings);
168
+ } catch {
169
+ standardsSummary = null;
170
+ }
171
+
172
+ return {
173
+ score,
174
+ grade,
175
+ categories: categoryResults,
176
+ totalFindings: findings.length,
177
+ totalDepVulns: depVulns.length,
178
+ compliance,
179
+ agenticSummary,
180
+ standardsSummary,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Map a finding category to a scoring category.
186
+ */
187
+ resolveCategory(findingCategory) {
188
+ if (CATEGORIES[findingCategory]) return findingCategory;
189
+ if (FALLBACK_CATEGORY_MAP[findingCategory] !== undefined) {
190
+ return FALLBACK_CATEGORY_MAP[findingCategory];
191
+ }
192
+ return 'injection'; // default fallback
193
+ }
194
+
195
+ /**
196
+ * Save score to history file for trend tracking.
197
+ */
198
+ saveToHistory(rootPath, scoreResult, suppressions = null) {
199
+ const historyDir = path.join(rootPath, '.praxis');
200
+ const historyFile = path.join(historyDir, 'history.json');
201
+
202
+ try {
203
+ if (!fs.existsSync(historyDir)) {
204
+ fs.mkdirSync(historyDir, { recursive: true });
205
+ }
206
+
207
+ let history = [];
208
+ if (fs.existsSync(historyFile)) {
209
+ try {
210
+ history = JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
211
+ } catch { history = []; }
212
+ }
213
+
214
+ const entry = {
215
+ timestamp: new Date().toISOString(),
216
+ score: scoreResult.score,
217
+ grade: scoreResult.grade.letter,
218
+ totalFindings: scoreResult.totalFindings,
219
+ totalDepVulns: scoreResult.totalDepVulns,
220
+ categoryScores: Object.fromEntries(
221
+ Object.entries(scoreResult.categories).map(([k, v]) => [k, {
222
+ deduction: v.deduction,
223
+ counts: v.counts,
224
+ }])
225
+ ),
226
+ };
227
+ if (suppressions) entry.suppressions = suppressions;
228
+ history.push(entry);
229
+
230
+ // Keep last 100 entries
231
+ if (history.length > 100) history = history.slice(-100);
232
+
233
+ fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
234
+ } catch {
235
+ // Don't fail if history save fails
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Load score history for trend display.
241
+ */
242
+ loadHistory(rootPath) {
243
+ const historyFile = path.join(rootPath, '.praxis', 'history.json');
244
+ try {
245
+ if (fs.existsSync(historyFile)) {
246
+ return JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
247
+ }
248
+ } catch { /* ignore */ }
249
+ return [];
250
+ }
251
+
252
+ /**
253
+ * Get trend summary comparing current to last scan.
254
+ */
255
+ getTrend(rootPath, currentScore) {
256
+ const history = this.loadHistory(rootPath);
257
+ if (history.length < 2) return null;
258
+
259
+ const previous = history[history.length - 2];
260
+ const diff = currentScore - previous.score;
261
+
262
+ return {
263
+ previousScore: previous.score,
264
+ currentScore,
265
+ diff,
266
+ direction: diff > 0 ? 'improved' : diff < 0 ? 'regressed' : 'unchanged',
267
+ previousDate: previous.timestamp,
268
+ };
269
+ }
270
+ }
271
+
272
+ export { GRADES, CATEGORIES, FALLBACK_CATEGORY_MAP };
273
+ export default ScoringEngine;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * SSRFProber Agent
3
+ * =================
4
+ *
5
+ * Detects Server-Side Request Forgery vulnerabilities.
6
+ * The fastest-growing attack vector (452% surge in 2025).
7
+ *
8
+ * Checks: user input in URL construction, webhook validation,
9
+ * cloud metadata access, DNS rebinding, protocol smuggling.
10
+ */
11
+
12
+ import path from 'path';
13
+ import { BaseAgent } from './base-agent.js';
14
+
15
+ export const PATTERNS = [
16
+ {
17
+ rule: 'SSRF_USER_URL_FETCH',
18
+ title: 'SSRF: User Input in fetch()',
19
+ regex: /fetch\s*\(\s*(?:req\.|request\.|ctx\.|query|params|body|input|url|data)/g,
20
+ severity: 'critical',
21
+ cwe: 'CWE-918',
22
+ owasp: 'A10:2021',
23
+ description: 'User-controlled URL passed to fetch() enables SSRF. Validate against an allowlist.',
24
+ fix: 'Validate URL against allowlist: new URL(input).hostname must match allowed hosts',
25
+ },
26
+ {
27
+ rule: 'SSRF_USER_URL_AXIOS',
28
+ title: 'SSRF: User Input in axios/got/http',
29
+ regex: /(?:axios|got|http|https|request|superagent|node-fetch|undici)(?:\.get|\.post|\.put|\.delete|\.request|\s*\()\s*\(\s*(?:req\.|request\.|ctx\.|query|params|body|input|url|data)/g,
30
+ severity: 'critical',
31
+ cwe: 'CWE-918',
32
+ owasp: 'A10:2021',
33
+ description: 'User-supplied URL in HTTP client enables SSRF. Validate and restrict to public IPs.',
34
+ fix: 'Parse URL, block private IPs (127.0.0.1, 10.x, 172.16-31.x, 169.254.x), block file:// protocol',
35
+ },
36
+ {
37
+ rule: 'SSRF_URL_TEMPLATE',
38
+ title: 'SSRF: Template Literal in URL',
39
+ regex: /(?:fetch|axios|got|http\.get|https\.get)\s*\(\s*`[^`]*\$\{(?:req\.|request\.|ctx\.|query|params|body|input)/g,
40
+ severity: 'critical',
41
+ cwe: 'CWE-918',
42
+ owasp: 'A10:2021',
43
+ description: 'User input interpolated into URL for HTTP request enables SSRF.',
44
+ fix: 'Validate and sanitize the URL before making the request',
45
+ },
46
+ {
47
+ rule: 'SSRF_WEBHOOK_URL',
48
+ title: 'SSRF: Unvalidated Webhook URL',
49
+ regex: /webhook[_-]?url\s*[:=]\s*(?:req\.|request\.|ctx\.|body|query|params|input)/gi,
50
+ severity: 'high',
51
+ cwe: 'CWE-918',
52
+ owasp: 'A10:2021',
53
+ description: 'Accepting user-supplied webhook URLs without validation enables SSRF.',
54
+ fix: 'Validate webhook URL: must be HTTPS, public IP, not cloud metadata endpoint',
55
+ },
56
+ {
57
+ rule: 'SSRF_CLOUD_METADATA',
58
+ title: 'SSRF: Cloud Metadata Endpoint Access',
59
+ regex: /169\.254\.169\.254|metadata\.google\.internal|100\.100\.100\.200/g,
60
+ severity: 'critical',
61
+ cwe: 'CWE-918',
62
+ owasp: 'A10:2021',
63
+ description: 'Cloud metadata endpoint in code. If URL is user-controlled, this enables credential theft.',
64
+ fix: 'Block metadata IPs in URL validation. Use IMDSv2 on AWS (requires token header).',
65
+ },
66
+ {
67
+ rule: 'SSRF_INTERNAL_IP',
68
+ title: 'SSRF: Internal IP Pattern',
69
+ regex: /(?:127\.0\.0\.|0\.0\.0\.0|10\.\d+\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.)\d+/g,
70
+ severity: 'medium',
71
+ cwe: 'CWE-918',
72
+ owasp: 'A10:2021',
73
+ confidence: 'low',
74
+ description: 'Internal IP address in code. Verify it is not reachable via user-controlled URLs.',
75
+ fix: 'Block private IP ranges in URL validation for user-supplied URLs',
76
+ },
77
+ {
78
+ rule: 'SSRF_REDIRECT_FOLLOW',
79
+ title: 'SSRF: HTTP Client Follows Redirects',
80
+ regex: /(?:follow|maxRedirects|redirect)\s*:\s*(?:true|\d{2,})/g,
81
+ severity: 'medium',
82
+ cwe: 'CWE-918',
83
+ owasp: 'A10:2021',
84
+ confidence: 'low',
85
+ description: 'Following redirects can bypass SSRF protections (redirect to internal IP).',
86
+ fix: 'Disable redirect following or re-validate the redirect target URL',
87
+ },
88
+ {
89
+ rule: 'SSRF_PYTHON_REQUESTS',
90
+ title: 'SSRF: Python requests with User Input',
91
+ regex: /requests\.(?:get|post|put|delete|head|patch)\s*\(\s*(?:request\.|flask\.|data|args|form)/g,
92
+ severity: 'critical',
93
+ cwe: 'CWE-918',
94
+ owasp: 'A10:2021',
95
+ description: 'User-controlled URL in Python requests enables SSRF.',
96
+ fix: 'Validate URL scheme (https only), resolve DNS and check against private IP ranges',
97
+ },
98
+ {
99
+ rule: 'SSRF_IMAGE_PROXY',
100
+ title: 'SSRF: Image/Proxy URL from User Input',
101
+ regex: /(?:imageUrl|image_url|proxyUrl|proxy_url|avatarUrl|avatar_url|iconUrl|icon_url)\s*[:=]\s*(?:req\.|request\.|query|params|body)/gi,
102
+ severity: 'high',
103
+ cwe: 'CWE-918',
104
+ owasp: 'A10:2021',
105
+ description: 'Image/proxy URLs from user input are common SSRF vectors.',
106
+ fix: 'Validate URL, restrict to known CDN domains, or use an image proxy service',
107
+ },
108
+ ];
109
+
110
+ export class SSRFProber extends BaseAgent {
111
+ constructor() {
112
+ super('SSRFProber', 'Detect Server-Side Request Forgery vulnerabilities', 'ssrf');
113
+ }
114
+
115
+ async analyze(context) {
116
+ const { files } = context;
117
+ const codeFiles = files.filter(f => {
118
+ const ext = path.extname(f).toLowerCase();
119
+ return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.php', '.go'].includes(ext);
120
+ });
121
+
122
+ let findings = [];
123
+ for (const file of codeFiles) {
124
+ findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
125
+ }
126
+ return findings;
127
+ }
128
+ }
129
+
130
+ export default SSRFProber;