@lincoln504/pi-research 0.1.12 → 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 (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +64 -115
  3. package/dist/cli.mjs +23211 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +102 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +213 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +113 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +64 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1044 -0
  30. package/src/config.ts +1036 -147
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +264 -7
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -174
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -577
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. package/tsconfig.json +0 -40
@@ -7,10 +7,20 @@
7
7
  */
8
8
 
9
9
  import type { Vulnerability, CisaKevResult } from './types.ts';
10
- import { createTimeoutSignal } from '../web-research/retry-utils.ts';
10
+ import { createTimeoutSignal, retryWithBackoff, isTransientError } from '../web-research/retry-utils.ts';
11
+ import { CircuitBreaker } from '../utils/circuit-breaker.ts';
12
+ import { metrics } from '../utils/metrics.ts';
11
13
 
12
14
  const CISA_KEV_URL = 'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json';
13
15
 
16
+ // Circuit breaker to prevent cascading failures
17
+ const cisaCircuitBreaker = new CircuitBreaker({
18
+ failureThreshold: 5,
19
+ resetTimeoutMs: 10000,
20
+ name: 'CISA KEV API',
21
+ isTransientError: isTransientError
22
+ });
23
+
14
24
  // ============================================================================
15
25
  // Type Definitions for CISA KEV API
16
26
  // ============================================================================
@@ -102,6 +112,42 @@ function extractCisaKevItems(data: unknown): readonly CisaKevItem[] {
102
112
  // Main API Functions
103
113
  // ============================================================================
104
114
 
115
+ /**
116
+ * Fetch with retry and circuit breaker for resilience
117
+ */
118
+ async function fetchWithRetryImpl(url: string, options: RequestInit, signal?: AbortSignal): Promise<Response> {
119
+ const endpoint = 'cisa_kev_feed';
120
+ return cisaCircuitBreaker.execute(async () => {
121
+ return retryWithBackoff(
122
+ async () => {
123
+ const response = await fetch(url, options);
124
+ if (!response.ok) {
125
+ const error = new Error(`HTTP ${response.status}: ${response.statusText}`) as Error & { status?: number };
126
+ error.status = response.status;
127
+ metrics.increment('cisa_kev_errors_total', 1, { endpoint, status_code: response.status.toString() });
128
+ throw error;
129
+ }
130
+ metrics.increment('cisa_kev_requests_total', 1, { endpoint, status: 'success' });
131
+ return response;
132
+ },
133
+ {
134
+ maxRetries: 3,
135
+ initialDelay: 1000,
136
+ maxDelay: 10000,
137
+ label: 'CISA KEV API',
138
+ signal,
139
+ isTransientError: (error) => {
140
+ const status = (error as Error & { status?: number })?.status;
141
+ if (typeof status === 'number') {
142
+ return status === 429 || status === 403 || status >= 500;
143
+ }
144
+ return isTransientError(error);
145
+ },
146
+ },
147
+ );
148
+ });
149
+ }
150
+
105
151
  /**
106
152
  * Fetch and search CISA KEV catalog
107
153
  *
@@ -115,29 +161,36 @@ export async function searchCisaKev(
115
161
  readonly vendor?: string;
116
162
  readonly product?: string;
117
163
  readonly maxResults?: number;
164
+ readonly signal?: AbortSignal;
118
165
  },
119
166
  ): Promise<CisaKevResult> {
167
+ const startTime = Date.now();
120
168
  const maxResults = options?.maxResults ?? 100;
121
169
  const vulnerabilities: Vulnerability[] = [];
122
170
  let error: string | undefined = undefined;
123
171
 
124
172
  try {
125
- const response: Response = await fetch(CISA_KEV_URL, {
173
+ const response: Response = await fetchWithRetryImpl(CISA_KEV_URL, {
126
174
  headers: {
127
175
  'User-Agent': 'pi-research/2.0',
128
176
  'Accept': 'application/json',
129
177
  },
130
- signal: createTimeoutSignal(30000), // 30s timeout
131
- });
178
+ signal: createTimeoutSignal(30000, options?.signal), // 30s timeout, composed with caller cancel
179
+ }, options?.signal);
132
180
 
133
- if (!response.ok) {
134
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
181
+ let data: unknown;
182
+ try {
183
+ data = await response.json();
184
+ } catch {
185
+ metrics.increment('cisa_kev_errors_total', 1, { error_type: 'malformed_json' });
186
+ throw new Error('CISA KEV feed returned a malformed JSON response.');
135
187
  }
136
188
 
137
- const data: unknown = await response.json();
138
-
139
189
  // CISA KEV format is an array of vulnerability objects or nested in a response object
140
190
  const cisaData = extractCisaKevItems(data);
191
+
192
+ // Track cache metrics
193
+ metrics.increment(cisaData.length > 0 ? 'cisa_kev_cache_hits_total' : 'cisa_kev_cache_misses_total', 1);
141
194
 
142
195
  // Map CISA format to our Vulnerability interface
143
196
  for (const item of cisaData) {
@@ -170,6 +223,7 @@ export async function searchCisaKev(
170
223
  }
171
224
 
172
225
  // Sort by due date (most urgent first)
226
+ // FIX (#24): Guard against NaN from invalid dates to ensure stable sort
173
227
  vulnerabilities.sort((a: Vulnerability, b: Vulnerability): number => {
174
228
  if (a.dueDate === undefined) {
175
229
  return 1;
@@ -177,11 +231,20 @@ export async function searchCisaKev(
177
231
  if (b.dueDate === undefined) {
178
232
  return -1;
179
233
  }
180
- return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
234
+ const aTime = new Date(a.dueDate).getTime();
235
+ const bTime = new Date(b.dueDate).getTime();
236
+ // Treat invalid dates as Infinity (sorted to the end)
237
+ const aSafe = Number.isFinite(aTime) ? aTime : Infinity;
238
+ const bSafe = Number.isFinite(bTime) ? bTime : Infinity;
239
+ return aSafe - bSafe;
181
240
  });
182
241
 
183
242
  } catch (err: unknown) {
184
243
  error = err instanceof Error ? err.message : String(err);
244
+ metrics.increment('cisa_kev_search_errors_total', 1, { error_type: err instanceof Error ? err.name : 'unknown' });
245
+ } finally {
246
+ const duration = Date.now() - startTime;
247
+ metrics.observe('cisa_kev_fetch_duration_ms', duration, { has_error: error ? 'true' : 'false' });
185
248
  }
186
249
 
187
250
  return {
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Minimal CVSS v3.0/v3.1 base-score calculator.
3
+ *
4
+ * Used to derive a qualitative severity + numeric base score from a CVSS vector STRING (OSV
5
+ * `severity[]` entries carry a vector, not a number). Implements the official base-score equations,
6
+ * which are IDENTICAL across v3.0 and v3.1 — there is no version branch in Base scoring. (The
7
+ * `× 0.9731`/exponent-13 changed-scope variant belongs only to the v3.1 Environmental metric group,
8
+ * not to Base; applying it here would mis-band high-impact changed-scope vectors, e.g. reporting
9
+ * the changed-scope vector AV:P/AC:H/PR:H/UI:N/S:C/C:H/I:H/A:H as 7.0/HIGH instead of 6.9/MEDIUM.)
10
+ * Returns null for a vector it
11
+ * cannot parse, a non-v3 vector, or an incomplete base vector — callers then fall back to UNKNOWN
12
+ * rather than guess.
13
+ *
14
+ * Only the BASE metric group is computed (temporal/environmental are ignored), which is what a
15
+ * severity label reflects. Verified against the CVSS v3.1 specification examples (9.8, 7.8, 6.1, 5.9)
16
+ * and the changed-scope case AV:P/AC:H/PR:H/UI:N/S:C/C:H/I:H/A:H → 6.9/MEDIUM (the shorthand
17
+ * S:C/C:H/I:H/A:H alone, with default AV:N/AC:L/PR:N/UI:N, actually scores 10.0/CRITICAL).
18
+ */
19
+
20
+ const AV: Readonly<Record<string, number>> = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 };
21
+ const AC: Readonly<Record<string, number>> = { L: 0.77, H: 0.44 };
22
+ const UI: Readonly<Record<string, number>> = { N: 0.85, R: 0.62 };
23
+ // Privileges Required weight depends on Scope (higher when the attack crosses a scope boundary).
24
+ const PR_UNCHANGED: Readonly<Record<string, number>> = { N: 0.85, L: 0.62, H: 0.27 };
25
+ const PR_CHANGED: Readonly<Record<string, number>> = { N: 0.85, L: 0.68, H: 0.5 };
26
+ const IMPACT: Readonly<Record<string, number>> = { H: 0.56, L: 0.22, N: 0 };
27
+
28
+ /** CVSS v3.1 Roundup: the smallest number to one decimal place that is >= input (float-safe). */
29
+ function roundUp1(input: number): number {
30
+ const intInput = Math.round(input * 100000);
31
+ if (intInput % 10000 === 0) return intInput / 100000;
32
+ return (Math.floor(intInput / 10000) + 1) / 10;
33
+ }
34
+
35
+ /** Map a CVSS base score (0.0–10.0) to its qualitative severity rating (CVSS v3 bands). */
36
+ export function severityFromScore(score: number): string {
37
+ if (score <= 0) return 'NONE';
38
+ if (score < 4.0) return 'LOW';
39
+ if (score < 7.0) return 'MEDIUM';
40
+ if (score < 9.0) return 'HIGH';
41
+ return 'CRITICAL';
42
+ }
43
+
44
+ /**
45
+ * Compute the CVSS v3.0/v3.1 base score + severity from a vector string, or null if it is not a
46
+ * parseable, complete v3 base vector.
47
+ */
48
+ export function scoreCvss3(vector: string): { score: number; severity: string } | null {
49
+ // Accept only complete v3.0 / v3.1 base vectors. The Base formula is identical across both.
50
+ if (typeof vector !== 'string' || !/^CVSS:3\.[01]\//i.test(vector)) return null;
51
+
52
+ const metrics: Record<string, string> = {};
53
+ for (const part of vector.split('/').slice(1)) {
54
+ const [k, v] = part.split(':');
55
+ if (k && v) metrics[k.toUpperCase()] = v.toUpperCase();
56
+ }
57
+
58
+ const scope = metrics['S'];
59
+ const av = AV[metrics['AV'] ?? ''];
60
+ const ac = AC[metrics['AC'] ?? ''];
61
+ const ui = UI[metrics['UI'] ?? ''];
62
+ const pr = (scope === 'C' ? PR_CHANGED : PR_UNCHANGED)[metrics['PR'] ?? ''];
63
+ const c = IMPACT[metrics['C'] ?? ''];
64
+ const i = IMPACT[metrics['I'] ?? ''];
65
+ const a = IMPACT[metrics['A'] ?? ''];
66
+
67
+ if ((scope !== 'U' && scope !== 'C') || [av, ac, ui, pr, c, i, a].some(x => x === undefined)) {
68
+ return null; // incomplete / invalid base vector
69
+ }
70
+
71
+ const iss = 1 - (1 - c!) * (1 - i!) * (1 - a!);
72
+ // The CVSS v3.0 and v3.1 BASE impact formulas are identical for both scopes — the `× 0.9731`
73
+ // multiplier / exponent-13 variant belongs ONLY to the v3.1 Environmental (ModifiedImpact)
74
+ // metric group, never to Base scoring. Applying it here mis-scored high-impact changed-scope
75
+ // vectors across a severity band (e.g. S:C/C:H/I:H/A:H → 7.0/HIGH instead of the correct
76
+ // 6.9/MEDIUM). There is no version branch in Base scoring.
77
+ const impact = scope === 'U'
78
+ ? 6.42 * iss
79
+ : 7.52 * (iss - 0.029) - 3.25 * Math.pow(iss - 0.02, 15);
80
+ const exploitability = 8.22 * av! * ac! * pr! * ui!;
81
+
82
+ let score: number;
83
+ if (impact <= 0) {
84
+ score = 0;
85
+ } else if (scope === 'U') {
86
+ score = roundUp1(Math.min(impact + exploitability, 10));
87
+ } else {
88
+ score = roundUp1(Math.min(1.08 * (impact + exploitability), 10));
89
+ }
90
+
91
+ return { score, severity: severityFromScore(score) };
92
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Ecosystem-name normalization for the vulnerability databases.
3
+ *
4
+ * GitHub's Advisory Database and OSV use DIFFERENT identifiers for the same package ecosystem
5
+ * (e.g. GitHub wants `pip`/`rust`/`go`, OSV wants `PyPI`/`crates.io`/`Go`). The security tool
6
+ * accepts one user-facing `ecosystem` value, so a name that works for one database would be
7
+ * rejected outright by the other (GitHub 422 / OSV 400) — a guaranteed, silent lookup failure
8
+ * for common ecosystems like Python and Rust. This maps a user-friendly value onto the exact
9
+ * identifier each database expects. Unknown values pass through unchanged (prior behavior), so
10
+ * this can only fix a mismatch, never introduce one.
11
+ */
12
+
13
+ /** Canonical ecosystem key → per-database identifier. Keys are matched case-insensitively. */
14
+ const ECOSYSTEM_ALIASES: Record<string, { github: string; osv: string }> = {
15
+ npm: { github: 'npm', osv: 'npm' },
16
+ node: { github: 'npm', osv: 'npm' },
17
+ pip: { github: 'pip', osv: 'PyPI' },
18
+ pypi: { github: 'pip', osv: 'PyPI' },
19
+ python: { github: 'pip', osv: 'PyPI' },
20
+ maven: { github: 'maven', osv: 'Maven' },
21
+ go: { github: 'go', osv: 'Go' },
22
+ golang: { github: 'go', osv: 'Go' },
23
+ cargo: { github: 'rust', osv: 'crates.io' },
24
+ rust: { github: 'rust', osv: 'crates.io' },
25
+ 'crates.io': { github: 'rust', osv: 'crates.io' },
26
+ nuget: { github: 'nuget', osv: 'NuGet' },
27
+ rubygems: { github: 'rubygems', osv: 'RubyGems' },
28
+ gem: { github: 'rubygems', osv: 'RubyGems' },
29
+ gems: { github: 'rubygems', osv: 'RubyGems' },
30
+ composer: { github: 'composer', osv: 'Packagist' },
31
+ packagist: { github: 'composer', osv: 'Packagist' },
32
+ php: { github: 'composer', osv: 'Packagist' },
33
+ pub: { github: 'pub', osv: 'Pub' },
34
+ dart: { github: 'pub', osv: 'Pub' },
35
+ };
36
+
37
+ /**
38
+ * Map a user-supplied ecosystem name to the identifier the given database expects.
39
+ * Returns the input verbatim (trimmed) when it isn't a known alias.
40
+ */
41
+ export function normalizeEcosystem(value: string, target: 'github' | 'osv'): string {
42
+ const key = value.trim().toLowerCase();
43
+ const entry = ECOSYSTEM_ALIASES[key];
44
+ return entry ? entry[target] : value.trim();
45
+ }