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,235 @@
1
+ /**
2
+ * Intel orchestrator.
3
+ *
4
+ * Public API:
5
+ * await runUpdate({ onProgress, sources, force }) → MergedReport
6
+ * loadMerged() → cached merged threat-intel.json
7
+ * isStale(maxAgeMs) → boolean
8
+ * listSources() → [{ name, tier, description, envKey? }]
9
+ *
10
+ * Cache layout under ~/.praxis/:
11
+ * intel/<source>.json per-source TTL'd raw payload
12
+ * threat-intel.json merged feed (replaces the legacy seed cache)
13
+ * intel-meta.json last update summary
14
+ */
15
+
16
+ import fs from 'fs';
17
+ import path from 'path';
18
+ import os from 'os';
19
+ import { fileURLToPath } from 'url';
20
+
21
+ import * as cache from './cache.js';
22
+ import { mergeIntel, serialize } from './merge.js';
23
+
24
+ import * as osv from './sources/osv.js';
25
+ import * as kev from './sources/kev.js';
26
+ import * as epss from './sources/epss.js';
27
+ import * as nvd from './sources/nvd.js';
28
+ import * as ghsa from './sources/ghsa.js';
29
+ import * as gitleaks from './sources/gitleaks.js';
30
+ import * as snyk from './sources/snyk.js';
31
+ import * as socket from './sources/socket.js';
32
+ import * as gitguardian from './sources/gitguardian.js';
33
+ import * as sonatype from './sources/sonatype.js';
34
+ import * as phylum from './sources/phylum.js';
35
+ import * as threatpack from './sources/threatpack.js';
36
+
37
+ const __filename = fileURLToPath(import.meta.url);
38
+ const __dirname = path.dirname(__filename);
39
+ const SEED_PATH = path.resolve(__dirname, '..', '..', 'data', 'threat-intel.json');
40
+
41
+ // Lazy — resolve at call time so HOME overrides (e.g. in tests) take effect.
42
+ const mergedPath = () => path.join(os.homedir(), '.praxis', 'threat-intel.json');
43
+ const metaPath = () => path.join(os.homedir(), '.praxis', 'intel-meta.json');
44
+
45
+ const ALL_SOURCES = [
46
+ osv, ghsa, kev, epss, nvd, gitleaks, threatpack, // free / core
47
+ snyk, socket, gitguardian, sonatype, phylum, // optional / paid
48
+ ];
49
+
50
+ export function listSources() {
51
+ return ALL_SOURCES.map(s => ({
52
+ name: s.name,
53
+ tier: s.tier,
54
+ description: s.description,
55
+ envKey: s.envKey || null,
56
+ }));
57
+ }
58
+
59
+ /**
60
+ * Run an update across all sources in parallel.
61
+ *
62
+ * @param {object} options
63
+ * @param {(evt: {source, status, message?}) => void} [options.onProgress]
64
+ * @param {string[]} [options.sources] limit to a subset
65
+ * @param {boolean} [options.force] ignore TTL on per-source caches
66
+ */
67
+ export async function runUpdate(options = {}) {
68
+ const { onProgress = () => {}, sources, force = false } = options;
69
+ const targets = sources
70
+ ? ALL_SOURCES.filter(s => sources.includes(s.name))
71
+ : ALL_SOURCES;
72
+
73
+ // First pass: parallel fetch of everything except NVD (NVD needs the
74
+ // CVE list from the others to know what to enrich).
75
+ const firstPass = targets.filter(s => s.name !== 'nvd');
76
+ const firstResults = await Promise.all(
77
+ firstPass.map(s => runOne(s, { force, onProgress }))
78
+ );
79
+
80
+ // Second pass: NVD enrichment, using CVEs we just collected.
81
+ let nvdResult = null;
82
+ if (targets.some(s => s.name === 'nvd')) {
83
+ const cveList = collectCves(firstResults);
84
+ nvdResult = await runOne(nvd, { force, onProgress, args: { cveList } });
85
+ }
86
+
87
+ const allResults = nvdResult ? [...firstResults, nvdResult] : firstResults;
88
+
89
+ // Merge into the unified feed. Start from the existing feed when present
90
+ // so subset updates (--only) don't wipe data owned by other sources.
91
+ let seed = loadSeed();
92
+ try {
93
+ const existing = loadMerged();
94
+ if (existing && typeof existing === 'object') seed = existing;
95
+ } catch { /* corrupt feed — rebuild from bundled seed */ }
96
+ const merged = mergeIntel(seed, allResults);
97
+ const out = serialize(merged);
98
+
99
+ ensureHomeDir();
100
+ fs.writeFileSync(mergedPath(), JSON.stringify(out, null, 2));
101
+
102
+ const meta = {
103
+ updatedAt: new Date().toISOString(),
104
+ version: out.version,
105
+ sources: out.sources,
106
+ };
107
+ fs.writeFileSync(metaPath(), JSON.stringify(meta, null, 2));
108
+
109
+ return { merged: out, meta, results: allResults };
110
+ }
111
+
112
+ async function runOne(source, { force, onProgress, args = {} }) {
113
+ onProgress({ source: source.name, status: 'start' });
114
+
115
+ // Cache hit?
116
+ const cached = cache.read(source.name);
117
+ if (!force && cached && cache.isFresh(cached)) {
118
+ onProgress({ source: source.name, status: 'cached' });
119
+ return {
120
+ name: source.name,
121
+ ok: true,
122
+ payload: cached.payload,
123
+ fetchedAt: cached.fetchedAt,
124
+ fromCache: true,
125
+ skipped: cached.payload?.skipped === true,
126
+ };
127
+ }
128
+
129
+ try {
130
+ const payload = await source.fetchAll(args);
131
+ if (payload && payload.skipped) {
132
+ // Optional source declined (env var unset, etc.) — record but don't fail.
133
+ cache.write(source.name, payload, source.TTL || 60_000);
134
+ onProgress({ source: source.name, status: 'skipped', message: payload.reason });
135
+ return {
136
+ name: source.name,
137
+ ok: true,
138
+ skipped: true,
139
+ payload,
140
+ fetchedAt: new Date().toISOString(),
141
+ };
142
+ }
143
+ cache.write(source.name, payload, source.TTL || 60_000);
144
+ onProgress({ source: source.name, status: 'ok' });
145
+ return {
146
+ name: source.name,
147
+ ok: true,
148
+ payload,
149
+ fetchedAt: new Date().toISOString(),
150
+ fromCache: false,
151
+ };
152
+ } catch (err) {
153
+ onProgress({ source: source.name, status: 'error', message: err.message });
154
+ // Fall back to whatever's in the cache, even if stale, so a transient
155
+ // outage doesn't blow away our data.
156
+ if (cached) {
157
+ return {
158
+ name: source.name,
159
+ ok: true,
160
+ payload: cached.payload,
161
+ fetchedAt: cached.fetchedAt,
162
+ fromCache: true,
163
+ stale: true,
164
+ error: err.message,
165
+ };
166
+ }
167
+ return {
168
+ name: source.name,
169
+ ok: false,
170
+ error: err.message,
171
+ };
172
+ }
173
+ }
174
+
175
+ function collectCves(results) {
176
+ const cves = new Set();
177
+ for (const r of results) {
178
+ if (!r.ok || r.skipped) continue;
179
+ const p = r.payload;
180
+ if (r.name === 'kev') {
181
+ for (const e of p.entries || []) cves.add(e.cve);
182
+ } else if (r.name === 'osv') {
183
+ for (const rec of p.records || []) {
184
+ for (const c of rec.cves || []) cves.add(c);
185
+ }
186
+ } else if (r.name === 'ghsa') {
187
+ for (const a of p.advisories || []) {
188
+ if (a.cveId) cves.add(a.cveId);
189
+ }
190
+ }
191
+ }
192
+ return [...cves];
193
+ }
194
+
195
+ function loadSeed() {
196
+ try {
197
+ return JSON.parse(fs.readFileSync(SEED_PATH, 'utf-8'));
198
+ } catch {
199
+ return { version: '1.0.0' };
200
+ }
201
+ }
202
+
203
+ function ensureHomeDir() {
204
+ const dir = path.dirname(mergedPath());
205
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
206
+ }
207
+
208
+ export function loadMerged() {
209
+ try {
210
+ const p = mergedPath();
211
+ if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'));
212
+ } catch { /* fall through */ }
213
+ return null;
214
+ }
215
+
216
+ export function loadMeta() {
217
+ try {
218
+ const p = metaPath();
219
+ if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'));
220
+ } catch { /* fall through */ }
221
+ return null;
222
+ }
223
+
224
+ export function isStale(maxAgeMs) {
225
+ const meta = loadMeta();
226
+ if (!meta || !meta.updatedAt) return true;
227
+ return Date.now() - new Date(meta.updatedAt).getTime() > maxAgeMs;
228
+ }
229
+
230
+ export const PATHS = {
231
+ get MERGED_PATH() { return mergedPath(); },
232
+ get META_PATH() { return metaPath(); },
233
+ SEED_PATH,
234
+ };
235
+ export const sources = { osv, ghsa, kev, epss, nvd, gitleaks, snyk, socket, gitguardian, sonatype, phylum };
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Merge per-source payloads into the unified threat-intel.json schema
3
+ * consumed by ThreatIntel and the agents.
4
+ *
5
+ * Schema additions on top of the existing seed:
6
+ * - cveAdvisories[] from OSV + GHSA + NVD
7
+ * - kevList[] from CISA KEV
8
+ * - epssScores{} cve → { score, percentile }
9
+ * - osvIndex{} ecosystem/package → [advisory]
10
+ * - ghsaIndex{} ecosystem/package → [advisory]
11
+ * - secretRules[] extra patterns from Gitleaks (+ optional GitGuardian)
12
+ * - sources{} per-source health: { fetchedAt, count, error?, skipped? }
13
+ */
14
+
15
+ export function mergeIntel(seedData, results) {
16
+ // results: array of { name, ok, payload, error, skipped, fetchedAt }
17
+ const merged = JSON.parse(JSON.stringify(seedData || {}));
18
+ merged.version = bumpVersion(merged.version || '1.0.0');
19
+ merged.updated = new Date().toISOString();
20
+ merged.sources = merged.sources || {};
21
+
22
+ // Reset only the collections owned by sources being freshly merged, so a
23
+ // subset update (--only) preserves data from the other sources.
24
+ const fresh = new Set(results.filter(r => r.ok && !r.skipped).map(r => r.name));
25
+ if (fresh.has('kev')) { merged.kevList = []; merged.kevDetails = {}; }
26
+ if (fresh.has('epss')) { merged.epssScores = {}; }
27
+ if (fresh.has('nvd')) { merged.nvdDetails = {}; }
28
+ if (fresh.has('osv')) merged.osvIndex = {};
29
+ if (fresh.has('ghsa')) merged.ghsaIndex = {};
30
+ if (fresh.has('gitleaks') || fresh.has('gitguardian')) merged.secretRules = [];
31
+
32
+ // Drop stale advisories from sources being refreshed; keep the rest and
33
+ // rebuild the dedup index from the survivors.
34
+ merged.cveAdvisories = (merged.cveAdvisories || []).filter(a => !fresh.has(a.source));
35
+ merged._cveAdvisorySeen = new Set(merged.cveAdvisories.map(a => `${a.cve}@${a.source}`));
36
+
37
+ for (const r of results) {
38
+ merged.sources[r.name] = {
39
+ ok: r.ok,
40
+ fetchedAt: r.fetchedAt || null,
41
+ skipped: r.skipped || false,
42
+ reason: r.skipped ? r.payload?.reason : undefined,
43
+ error: r.ok ? null : (r.error || null),
44
+ stats: r.ok && !r.skipped ? sourceStats(r) : null,
45
+ };
46
+
47
+ if (!r.ok || r.skipped) continue;
48
+
49
+ switch (r.name) {
50
+ case 'osv': mergeOsv(merged, r.payload); break;
51
+ case 'ghsa': mergeGhsa(merged, r.payload); break;
52
+ case 'kev': mergeKev(merged, r.payload); break;
53
+ case 'epss': mergeEpss(merged, r.payload); break;
54
+ case 'nvd': mergeNvd(merged, r.payload); break;
55
+ case 'gitleaks': mergeGitleaks(merged, r.payload); break;
56
+ case 'threatpack': mergeThreatpack(merged, r.payload); break;
57
+ case 'snyk': mergeSnyk(merged, r.payload); break;
58
+ case 'socket': mergeSocket(merged, r.payload); break;
59
+ case 'gitguardian': mergeGitguardian(merged, r.payload); break;
60
+ case 'sonatype': mergeSonatype(merged, r.payload); break;
61
+ case 'phylum': mergePhylum(merged, r.payload); break;
62
+ }
63
+ }
64
+
65
+ return merged;
66
+ }
67
+
68
+ function bumpVersion(v) {
69
+ const parts = String(v).split('.').map(Number);
70
+ while (parts.length < 3) parts.push(0);
71
+ parts[2] = parts[2] + 1;
72
+ return parts.join('.');
73
+ }
74
+
75
+ function sourceStats(r) {
76
+ const p = r.payload || {};
77
+ if (Array.isArray(p.records)) return { records: p.records.length };
78
+ if (Array.isArray(p.entries)) return { entries: p.entries.length };
79
+ if (Array.isArray(p.advisories)) return { advisories: p.advisories.length };
80
+ if (Array.isArray(p.detectors)) return { detectors: p.detectors.length };
81
+ if (Array.isArray(p.rules)) return { rules: p.rules.length };
82
+ if (Array.isArray(p.issues)) return { issues: p.issues.length };
83
+ if (typeof p.count === 'number') return { count: p.count };
84
+ if (p.scores) return { scores: Object.keys(p.scores).length };
85
+ if (p.details) return { details: Object.keys(p.details).length };
86
+ return {};
87
+ }
88
+
89
+ function mergeOsv(merged, payload) {
90
+ for (const r of payload.records || []) {
91
+ const key = `${r.ecosystem}/${r.package}`;
92
+ (merged.osvIndex[key] = merged.osvIndex[key] || []).push(r);
93
+ for (const cve of r.cves || []) {
94
+ addCveAdvisory(merged, {
95
+ cve, source: 'osv', summary: r.summary, severity: r.severity,
96
+ package: r.package, ecosystem: r.ecosystem, references: r.references,
97
+ });
98
+ }
99
+ }
100
+ }
101
+
102
+ function mergeGhsa(merged, payload) {
103
+ for (const adv of payload.advisories || []) {
104
+ for (const v of adv.vulnerabilities || []) {
105
+ if (!v.package) continue;
106
+ const key = `${v.ecosystem}/${v.package}`;
107
+ (merged.ghsaIndex[key] = merged.ghsaIndex[key] || []).push({
108
+ ghsaId: adv.ghsaId, cveId: adv.cveId,
109
+ severity: adv.severity, summary: adv.summary,
110
+ vulnerable_version_range: v.vulnerable_version_range,
111
+ first_patched_version: v.first_patched_version,
112
+ });
113
+ }
114
+ if (adv.cveId) {
115
+ addCveAdvisory(merged, {
116
+ cve: adv.cveId, source: 'ghsa', summary: adv.summary,
117
+ severity: adv.severity, references: adv.references, ghsaId: adv.ghsaId,
118
+ });
119
+ }
120
+ }
121
+ }
122
+
123
+ function mergeKev(merged, payload) {
124
+ merged.kevList = (payload.entries || []).map(e => e.cve);
125
+ merged.kevDetails = Object.fromEntries((payload.entries || []).map(e => [e.cve, e]));
126
+ merged.kevCatalogVersion = payload.catalogVersion;
127
+ }
128
+
129
+ function mergeEpss(merged, payload) {
130
+ merged.epssScores = payload.scores || {};
131
+ merged.epssModelDate = payload.modelDate;
132
+ }
133
+
134
+ function mergeNvd(merged, payload) {
135
+ merged.nvdDetails = payload.details || {};
136
+ }
137
+
138
+ function mergeGitleaks(merged, payload) {
139
+ for (const r of payload.rules || []) {
140
+ merged.secretRules.push({
141
+ id: r.id,
142
+ description: r.description,
143
+ regex: r.regex,
144
+ source: 'gitleaks',
145
+ severity: 'high',
146
+ });
147
+ }
148
+ }
149
+
150
+ function mergeThreatpack(merged, payload) {
151
+ // Threat-pack overlay: probe signatures + EAA additions + gateway allowlists.
152
+ // The corpus loader and agents read this section when it exists.
153
+ merged.threatPack = {
154
+ version: payload.version || '0.0.0',
155
+ fetchedRemote: Boolean(payload.fetchedRemote),
156
+ sourceUrl: payload.sourceUrl || null,
157
+ probes: payload.probes || [],
158
+ eaa: payload.eaa || [],
159
+ gateways: payload.gateways || [],
160
+ };
161
+ }
162
+
163
+ function mergeSnyk(merged, payload) {
164
+ for (const issue of payload.issues || []) {
165
+ for (const cve of issue.cves || []) {
166
+ addCveAdvisory(merged, {
167
+ cve, source: 'snyk', summary: issue.title,
168
+ severity: issue.severity,
169
+ });
170
+ }
171
+ }
172
+ }
173
+
174
+ function mergeSocket(merged, payload) {
175
+ merged.socketAdvisories = payload.advisories || [];
176
+ }
177
+
178
+ function mergeGitguardian(merged, payload) {
179
+ for (const d of payload.detectors || []) {
180
+ merged.secretRules.push({
181
+ id: d.id,
182
+ description: d.summary,
183
+ family: d.family,
184
+ source: 'gitguardian',
185
+ severity: d.severity,
186
+ });
187
+ }
188
+ }
189
+
190
+ function mergeSonatype(merged, payload) {
191
+ for (const adv of payload.advisories || []) {
192
+ if (!adv.cve) continue;
193
+ addCveAdvisory(merged, {
194
+ cve: adv.cve, source: 'sonatype', summary: adv.title,
195
+ severity: scoreToSeverity(adv.cvssScore),
196
+ package: adv.purl, references: adv.reference ? [adv.reference] : [],
197
+ });
198
+ }
199
+ }
200
+
201
+ function mergePhylum(merged, payload) {
202
+ merged.phylumAdvisories = payload.advisories || [];
203
+ }
204
+
205
+ function addCveAdvisory(merged, entry) {
206
+ // Dedup by (cve, source); keep all sources so we can show provenance.
207
+ const key = `${entry.cve}@${entry.source}`;
208
+ if (merged._cveAdvisorySeen?.has?.(key)) return;
209
+ merged._cveAdvisorySeen = merged._cveAdvisorySeen || new Set();
210
+ merged._cveAdvisorySeen.add(key);
211
+ merged.cveAdvisories.push(entry);
212
+ }
213
+
214
+ function scoreToSeverity(score) {
215
+ if (score == null) return 'medium';
216
+ if (score >= 9) return 'critical';
217
+ if (score >= 7) return 'high';
218
+ if (score >= 4) return 'medium';
219
+ return 'low';
220
+ }
221
+
222
+ /**
223
+ * Strip non-serializable internals before writing to disk.
224
+ */
225
+ export function serialize(merged) {
226
+ const out = { ...merged };
227
+ delete out._cveAdvisorySeen;
228
+ return out;
229
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * EPSS — Exploit Prediction Scoring System (FIRST.org).
3
+ * https://www.first.org/epss/
4
+ *
5
+ * Daily CSV. We download the gzipped current scores and parse into a map
6
+ * keyed by CVE → { score, percentile }. ~250k CVEs, ~10MB compressed.
7
+ *
8
+ * Score is 0..1, the probability the CVE will be exploited in the wild
9
+ * within 30 days. Used to sort findings by real-world risk.
10
+ */
11
+
12
+ import zlib from 'zlib';
13
+ import { promisify } from 'util';
14
+ import { safeFetch } from '../http.js';
15
+
16
+ const URL = 'https://epss.cyentia.com/epss_scores-current.csv.gz';
17
+ const TTL_MS = 24 * 60 * 60 * 1000; // 24h
18
+ const gunzip = promisify(zlib.gunzip);
19
+
20
+ export const name = 'epss';
21
+ export const tier = 'core';
22
+ export const description = 'EPSS — exploit-likelihood scores from FIRST.org';
23
+
24
+ export async function fetchAll() {
25
+ const res = await safeFetch(URL);
26
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
27
+
28
+ const buf = Buffer.from(await res.arrayBuffer());
29
+ const csv = (await gunzip(buf)).toString('utf-8');
30
+
31
+ const scores = {};
32
+ const lines = csv.split('\n');
33
+ let modelDate = null;
34
+
35
+ for (const line of lines) {
36
+ if (!line || line.startsWith('#')) {
37
+ // Header lines look like: #model_version:v2025.03.14
38
+ const m = line.match(/score_date:([0-9-]+)/);
39
+ if (m) modelDate = m[1];
40
+ continue;
41
+ }
42
+ if (line.startsWith('cve,')) continue; // column header
43
+ const [cve, score, percentile] = line.split(',');
44
+ if (!cve || !score) continue;
45
+ scores[cve] = {
46
+ score: parseFloat(score),
47
+ percentile: parseFloat(percentile),
48
+ };
49
+ }
50
+
51
+ return { modelDate, count: Object.keys(scores).length, scores };
52
+ }
53
+
54
+ export const TTL = TTL_MS;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * GitHub Security Advisories (GHSA).
3
+ * https://docs.github.com/en/rest/security-advisories/global-advisories
4
+ *
5
+ * REST endpoint, paginated, returns published advisories across all
6
+ * ecosystems. Auth optional (GITHUB_TOKEN raises rate limit from 60 to
7
+ * 5000 req/h). We pull the latest 1000 advisories — newer than KEV adds,
8
+ * captures advisories that may not yet be in OSV.
9
+ */
10
+
11
+ import { fetchJson, safeFetch } from '../http.js';
12
+
13
+ const BASE = 'https://api.github.com/advisories';
14
+ const PER_PAGE = 100;
15
+ const MAX_PAGES = 10;
16
+ const TTL_MS = 12 * 60 * 60 * 1000; // 12h
17
+
18
+ export const name = 'ghsa';
19
+ export const tier = 'core';
20
+ export const description = 'GitHub Security Advisories (global)';
21
+
22
+ export async function fetchAll() {
23
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
24
+ const headers = {
25
+ 'Accept': 'application/vnd.github+json',
26
+ 'X-GitHub-Api-Version': '2022-11-28',
27
+ };
28
+ if (token) headers.Authorization = `Bearer ${token}`;
29
+
30
+ const advisories = [];
31
+ let url = `${BASE}?per_page=${PER_PAGE}&type=reviewed&sort=published&direction=desc`;
32
+
33
+ for (let page = 0; page < MAX_PAGES && url; page++) {
34
+ const res = await safeFetch(url, { headers });
35
+ if (!res.ok) {
36
+ if (page === 0) throw new Error(`HTTP ${res.status} from GHSA`);
37
+ break;
38
+ }
39
+ const data = await res.json();
40
+ for (const adv of data) {
41
+ advisories.push(normalize(adv));
42
+ }
43
+ url = parseNextLink(res.headers.get('link')) || null;
44
+ }
45
+
46
+ return {
47
+ tokenUsed: !!token,
48
+ count: advisories.length,
49
+ advisories,
50
+ };
51
+ }
52
+
53
+ function normalize(adv) {
54
+ return {
55
+ ghsaId: adv.ghsa_id,
56
+ cveId: adv.cve_id,
57
+ summary: adv.summary,
58
+ severity: adv.severity, // critical / high / moderate / low
59
+ published: adv.published_at,
60
+ updated: adv.updated_at,
61
+ cwes: (adv.cwes || []).map(c => c.cwe_id),
62
+ references: (adv.references || []).slice(0, 5).map(r => r.url || r),
63
+ vulnerabilities: (adv.vulnerabilities || []).map(v => ({
64
+ ecosystem: v.package?.ecosystem,
65
+ package: v.package?.name,
66
+ vulnerable_version_range: v.vulnerable_version_range,
67
+ first_patched_version: v.first_patched_version,
68
+ })),
69
+ };
70
+ }
71
+
72
+ function parseNextLink(linkHeader) {
73
+ if (!linkHeader) return null;
74
+ for (const part of linkHeader.split(',')) {
75
+ const m = part.match(/<([^>]+)>;\s*rel="next"/);
76
+ if (m) return m[1];
77
+ }
78
+ return null;
79
+ }
80
+
81
+ export const TTL = TTL_MS;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * GitGuardian — paid.
3
+ * https://api.gitguardian.com/docs
4
+ *
5
+ * Requires GITGUARDIAN_API_KEY. Provides ~400 secret-detector definitions
6
+ * and a /v1/audit/detectors endpoint listing them. We pull detector
7
+ * definitions to enrich praxis's secret-scanning rule set.
8
+ */
9
+
10
+ import { fetchJson } from '../http.js';
11
+
12
+ const TTL_MS = 7 * 24 * 60 * 60 * 1000;
13
+
14
+ export const name = 'gitguardian';
15
+ export const tier = 'optional';
16
+ export const description = 'GitGuardian secret detector definitions (requires GITGUARDIAN_API_KEY)';
17
+ export const envKey = 'GITGUARDIAN_API_KEY';
18
+
19
+ export async function fetchAll() {
20
+ const key = process.env.GITGUARDIAN_API_KEY;
21
+ if (!key) return { skipped: true, reason: 'GITGUARDIAN_API_KEY not set' };
22
+
23
+ const headers = { Authorization: `Token ${key}` };
24
+ // GitGuardian's public detector list endpoint
25
+ const data = await fetchJson('https://api.gitguardian.com/v1/secret_detectors', { headers });
26
+
27
+ const detectors = (Array.isArray(data) ? data : data.results || []).map(d => ({
28
+ id: d.detector_name || d.id,
29
+ family: d.family,
30
+ summary: d.detector_group_name || d.name,
31
+ severity: d.severity || 'high',
32
+ }));
33
+
34
+ return {
35
+ count: detectors.length,
36
+ detectors,
37
+ };
38
+ }
39
+
40
+ export const TTL = TTL_MS;