@lincoln504/pi-research 0.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/config/searxng/default-settings.yml +189 -0
  4. package/config/searxng/limiter.toml +34 -0
  5. package/config/tooling/eslint.config.js +73 -0
  6. package/config/tooling/vitest.config.integration.ts +17 -0
  7. package/config/tooling/vitest.config.ts +40 -0
  8. package/config/tooling/vitest.config.unit.ts +12 -0
  9. package/docs/ARCHITECTURE.md +109 -0
  10. package/docs/assets/README-banner.jpg +0 -0
  11. package/package.json +92 -0
  12. package/scripts/setup.js +174 -0
  13. package/src/config.ts +206 -0
  14. package/src/constants.ts +148 -0
  15. package/src/healthcheck/index.ts +309 -0
  16. package/src/index.ts +71 -0
  17. package/src/infrastructure/searxng-lifecycle.ts +577 -0
  18. package/src/infrastructure/searxng-manager.ts +1173 -0
  19. package/src/infrastructure/state-manager.ts +918 -0
  20. package/src/logger.ts +246 -0
  21. package/src/orchestration/deep-research-orchestrator.ts +848 -0
  22. package/src/orchestration/deep-research-reducer.ts +129 -0
  23. package/src/orchestration/deep-research-types.ts +54 -0
  24. package/src/orchestration/id-utils.ts +63 -0
  25. package/src/orchestration/researcher.ts +104 -0
  26. package/src/orchestration/session-context.ts +43 -0
  27. package/src/prompts/research-tool-usage.md +45 -0
  28. package/src/prompts/researcher.md +166 -0
  29. package/src/prompts/system-coordinator.md +24 -0
  30. package/src/prompts/system-lead-evaluator.md +225 -0
  31. package/src/security/cisa-kev.ts +242 -0
  32. package/src/security/github-advisories.ts +607 -0
  33. package/src/security/index.ts +406 -0
  34. package/src/security/nvd.ts +532 -0
  35. package/src/security/osv.ts +487 -0
  36. package/src/security/types.ts +138 -0
  37. package/src/stackexchange/cache.ts +111 -0
  38. package/src/stackexchange/index.ts +352 -0
  39. package/src/stackexchange/output/compact.ts +64 -0
  40. package/src/stackexchange/output/index.ts +7 -0
  41. package/src/stackexchange/output/json.ts +21 -0
  42. package/src/stackexchange/output/table.ts +123 -0
  43. package/src/stackexchange/queries.ts +168 -0
  44. package/src/stackexchange/rest-client.ts +127 -0
  45. package/src/stackexchange/types.ts +129 -0
  46. package/src/tool.ts +430 -0
  47. package/src/tools/grep.ts +335 -0
  48. package/src/tools/index.ts +77 -0
  49. package/src/tools/scrape.ts +451 -0
  50. package/src/tools/search.ts +188 -0
  51. package/src/tools/security.ts +292 -0
  52. package/src/tools/stackexchange.ts +101 -0
  53. package/src/tui/research-panel.ts +576 -0
  54. package/src/tui/searxng-status.ts +80 -0
  55. package/src/types/extension-context.ts +60 -0
  56. package/src/types/llm.ts +111 -0
  57. package/src/utils/inject-date.ts +29 -0
  58. package/src/utils/input-validation.ts +103 -0
  59. package/src/utils/json-utils.ts +262 -0
  60. package/src/utils/make-resource-loader.ts +43 -0
  61. package/src/utils/research-error.ts +205 -0
  62. package/src/utils/research-export.ts +199 -0
  63. package/src/utils/searxng-config.ts +94 -0
  64. package/src/utils/session-state.ts +295 -0
  65. package/src/utils/shared-links.ts +123 -0
  66. package/src/utils/shutdown-manager.ts +54 -0
  67. package/src/utils/text-utils.ts +83 -0
  68. package/src/utils/tool-usage-tracker.ts +135 -0
  69. package/src/web-research/dependencies.ts +20 -0
  70. package/src/web-research/retry-utils.ts +232 -0
  71. package/src/web-research/scrapers.ts +935 -0
  72. package/src/web-research/search.ts +235 -0
  73. package/src/web-research/types.ts +47 -0
  74. package/src/web-research/utils.ts +158 -0
  75. package/tsconfig.json +40 -0
@@ -0,0 +1,607 @@
1
+ /**
2
+ * GitHub Advisory Database API Client
3
+ *
4
+ * GitHub Security Advisories API
5
+ * API: https://api.github.com/advisories
6
+ * Free public API, rate limited (60/hr unauthenticated, 5000/hr authenticated)
7
+ *
8
+ * Rate limits are returned in headers:
9
+ * - X-RateLimit-Limit: Total requests allowed
10
+ * - X-RateLimit-Remaining: Requests remaining
11
+ * - X-RateLimit-Reset: Unix timestamp when limit resets
12
+ */
13
+
14
+ import type { Advisory, GitHubResult } from './types.ts';
15
+ import { logger } from '../logger.ts';
16
+ import { createTimeoutSignal, retryWithBackoff } from '../web-research/retry-utils.ts';
17
+
18
+ // ============================================================================
19
+ // Type Definitions for Fetch API
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Standard fetch API types for compatibility
24
+ */
25
+ type FetchType = (
26
+ _input: string | Request,
27
+ _init?: RequestInit,
28
+ ) => Promise<Response>;
29
+
30
+ declare const fetch: FetchType;
31
+
32
+ interface Headers {
33
+ append(_name: string, _value: string): void;
34
+ delete(_name: string): void;
35
+ get(_name: string): string | null;
36
+ has(_name: string): boolean;
37
+ set(_name: string, _value: string): void;
38
+ forEach(_callback: (_value: string, _name: string) => void): void;
39
+ }
40
+
41
+ interface Response {
42
+ readonly headers: Headers;
43
+ readonly ok: boolean;
44
+ readonly redirected: boolean;
45
+ readonly status: number;
46
+ readonly statusText: string;
47
+ readonly type: 'basic' | 'cors' | 'default' | 'error' | 'opaque' | 'opaqueredirect';
48
+ readonly url: string;
49
+ readonly body: ReadableStream<Uint8Array> | null;
50
+ readonly bodyUsed: boolean;
51
+ clone(): Response;
52
+ arrayBuffer(): Promise<ArrayBuffer>;
53
+ blob(): Promise<Blob>;
54
+ formData(): Promise<FormData>;
55
+ json(): Promise<unknown>;
56
+ text(): Promise<string>;
57
+ }
58
+
59
+ // ============================================================================
60
+ // Type Definitions for GitHub API Responses
61
+ // ============================================================================
62
+
63
+ /**
64
+ * GitHub Advisory Vulnerability Package
65
+ */
66
+ interface GitHubAdvisoryPackage {
67
+ readonly ecosystem?: string;
68
+ readonly name?: string;
69
+ }
70
+
71
+ /**
72
+ * GitHub Advisory Affected Entry
73
+ */
74
+ interface GitHubAdvisoryAffected {
75
+ readonly package?: GitHubAdvisoryPackage;
76
+ }
77
+
78
+ /**
79
+ * GitHub Advisory Vulnerability
80
+ */
81
+ interface GitHubAdvisoryVulnerability {
82
+ readonly package?: GitHubAdvisoryPackage;
83
+ readonly affected?: readonly GitHubAdvisoryAffected[];
84
+ }
85
+
86
+ /**
87
+ * Raw GitHub Advisory from API response
88
+ */
89
+ interface GitHubAdvisoryRaw {
90
+ readonly ghsa_id?: string;
91
+ readonly id?: string;
92
+ readonly summary?: string;
93
+ readonly description?: string;
94
+ readonly severity?: string;
95
+ readonly published_at?: string;
96
+ readonly updated_at?: string;
97
+ readonly cve_id?: string;
98
+ readonly html_url?: string;
99
+ readonly advisory_url?: string;
100
+ readonly vulnerabilities?: readonly GitHubAdvisoryVulnerability[];
101
+ readonly affected?: readonly GitHubAdvisoryAffected[];
102
+ }
103
+
104
+ /**
105
+ * GitHub API list response wrapper
106
+ */
107
+ interface GitHubAdvisoryListResponse {
108
+ readonly items?: readonly GitHubAdvisoryRaw[];
109
+ }
110
+
111
+
112
+ // ============================================================================
113
+ // Constants
114
+ // ============================================================================
115
+
116
+ const GITHUB_API_BASE = 'https://api.github.com';
117
+ const DEFAULT_MAX_RESULTS = 20;
118
+
119
+ // ============================================================================
120
+ // Type Guards
121
+ // ============================================================================
122
+
123
+ /**
124
+ * Type guard to check if value is a GitHubAdvisoryRaw
125
+ */
126
+ function isGitHubAdvisoryRaw(value: unknown): value is GitHubAdvisoryRaw {
127
+ if (typeof value !== 'object' || value === null) {
128
+ return false;
129
+ }
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * Type guard to check if value is an array
135
+ */
136
+ function isArray(value: unknown): value is readonly unknown[] {
137
+ return Array.isArray(value);
138
+ }
139
+
140
+ /**
141
+ * Type guard to check if value is a string
142
+ */
143
+ function isString(value: unknown): value is string {
144
+ return typeof value === 'string';
145
+ }
146
+
147
+ /**
148
+ * Type guard to check if value is an object (non-null, non-array)
149
+ */
150
+ function isObject(value: unknown): value is Record<string, unknown> {
151
+ return typeof value === 'object' && value !== null && !isArray(value);
152
+ }
153
+
154
+ /**
155
+ * Type guard for GitHubAdvisoryPackage
156
+ */
157
+ function isGitHubAdvisoryPackage(value: unknown): value is GitHubAdvisoryPackage {
158
+ if (!isObject(value)) {
159
+ return false;
160
+ }
161
+ return true;
162
+ }
163
+
164
+ /**
165
+ * Type guard for GitHubAdvisoryAffected
166
+ */
167
+ function isGitHubAdvisoryAffected(value: unknown): value is GitHubAdvisoryAffected {
168
+ if (!isObject(value)) {
169
+ return false;
170
+ }
171
+ return true;
172
+ }
173
+
174
+ /**
175
+ * Type guard for GitHubAdvisoryVulnerability
176
+ */
177
+ function isGitHubAdvisoryVulnerability(value: unknown): value is GitHubAdvisoryVulnerability {
178
+ if (!isObject(value)) {
179
+ return false;
180
+ }
181
+ return true;
182
+ }
183
+
184
+ /**
185
+ * Type guard to check if response is a list response with items
186
+ */
187
+ function isGitHubAdvisoryListResponse(value: unknown): value is GitHubAdvisoryListResponse {
188
+ if (!isObject(value)) {
189
+ return false;
190
+ }
191
+ return 'items' in value;
192
+ }
193
+
194
+ /**
195
+ * Type guard to check if response is an array of advisory objects
196
+ */
197
+ function isGitHubAdvisoryArray(value: unknown): value is GitHubAdvisoryRaw[] {
198
+ if (!isArray(value)) {
199
+ return false;
200
+ }
201
+ return value.every(isGitHubAdvisoryRaw);
202
+ }
203
+
204
+ /**
205
+ * Type guard to check if response is a single advisory object
206
+ */
207
+ function isSingleGitHubAdvisory(value: unknown): value is GitHubAdvisoryRaw {
208
+ if (!isGitHubAdvisoryRaw(value)) {
209
+ return false;
210
+ }
211
+ // Single advisory typically has ghsa_id
212
+ return 'ghsa_id' in value || 'summary' in value;
213
+ }
214
+
215
+ // ============================================================================
216
+ // Helper Functions
217
+ // ============================================================================
218
+
219
+ /**
220
+ * Extract affected packages from an advisory item
221
+ */
222
+ function extractAffectedPackages(item: GitHubAdvisoryRaw): string[] {
223
+ const affectedPackages: string[] = [];
224
+
225
+ // Try different possible structures for affected packages
226
+ // GitHub API response structure may vary between endpoints
227
+
228
+ // Check for vulnerabilities array
229
+ if (item.vulnerabilities !== undefined && isArray(item.vulnerabilities)) {
230
+ for (const vuln of item.vulnerabilities) {
231
+ if (!isGitHubAdvisoryVulnerability(vuln)) {
232
+ continue;
233
+ }
234
+
235
+ // Check for package in various possible locations
236
+ if (vuln.package !== undefined) {
237
+ if (isGitHubAdvisoryPackage(vuln.package)) {
238
+ const ecosystem = vuln.package.ecosystem ?? '';
239
+ const name = vuln.package.name;
240
+
241
+ if (isString(name) && name !== '') {
242
+ if (ecosystem !== '') {
243
+ affectedPackages.push(`${ecosystem}/${name}`);
244
+ } else {
245
+ affectedPackages.push(name);
246
+ }
247
+ }
248
+ }
249
+ } else if (vuln.affected !== undefined && isArray(vuln.affected)) {
250
+ // Alternative structure with 'affected' array
251
+ for (const aff of vuln.affected) {
252
+ if (isGitHubAdvisoryAffected(aff) && aff.package !== undefined) {
253
+ const pkg = aff.package;
254
+ if (isGitHubAdvisoryPackage(pkg)) {
255
+ const ecosystem = pkg.ecosystem ?? '';
256
+ const name = pkg.name;
257
+
258
+ if (isString(name) && name !== '') {
259
+ if (ecosystem !== '') {
260
+ affectedPackages.push(`${ecosystem}/${name}`);
261
+ } else {
262
+ affectedPackages.push(name);
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ // Check for direct 'affected' array at the top level
273
+ if (affectedPackages.length === 0 && item.affected !== undefined && isArray(item.affected)) {
274
+ for (const aff of item.affected) {
275
+ if (isGitHubAdvisoryAffected(aff) && aff.package !== undefined) {
276
+ const pkg = aff.package;
277
+ if (isGitHubAdvisoryPackage(pkg)) {
278
+ const ecosystem = pkg.ecosystem ?? '';
279
+ const name = pkg.name;
280
+
281
+ if (isString(name) && name !== '') {
282
+ if (ecosystem !== '') {
283
+ affectedPackages.push(`${ecosystem}/${name}`);
284
+ } else {
285
+ affectedPackages.push(name);
286
+ }
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ return affectedPackages;
294
+ }
295
+
296
+ /**
297
+ * Extract references from an advisory item
298
+ */
299
+ function extractReferences(item: GitHubAdvisoryRaw): string[] {
300
+ const references: string[] = [];
301
+
302
+ if (item.html_url !== undefined && item.html_url !== '') {
303
+ references.push(item.html_url);
304
+ }
305
+ if (item.advisory_url !== undefined && item.advisory_url !== '') {
306
+ references.push(item.advisory_url);
307
+ }
308
+
309
+ return references;
310
+ }
311
+
312
+ // ============================================================================
313
+ // Main API Functions
314
+ // ============================================================================
315
+
316
+ /**
317
+ * Search GitHub security advisories
318
+ *
319
+ * @param terms - Search terms (CVE IDs, GHSA IDs, package names)
320
+ * @param options - Optional filters
321
+ * @returns Promise<GitHubResult> containing matching advisories
322
+ */
323
+ export async function searchGitHubAdvisories(
324
+ terms: readonly string[],
325
+ options?: {
326
+ readonly ecosystem?: string; // npm, pip, maven, go, etc.
327
+ readonly severity?: string; // LOW, MODERATE, HIGH, CRITICAL
328
+ readonly maxResults?: number;
329
+ readonly repo?: string; // "owner/repo" format
330
+ },
331
+ ): Promise<GitHubResult> {
332
+ const maxResults = options?.maxResults ?? DEFAULT_MAX_RESULTS;
333
+ const advisories: Advisory[] = [];
334
+ let error: string | undefined = undefined;
335
+
336
+ try {
337
+ let allAdvisories: Advisory[] = [];
338
+
339
+ // If repo specified, search repo-specific advisories
340
+ if (options?.repo !== undefined && options.repo !== '') {
341
+ const repoParts = options.repo.split('/');
342
+
343
+ if (repoParts.length !== 2 || repoParts[0] === '' || repoParts[1] === '') {
344
+ throw new Error(`Invalid repo format: "${options.repo}". Expected "owner/name".`);
345
+ }
346
+
347
+ const [owner, name] = repoParts;
348
+ const url = `${GITHUB_API_BASE}/repos/${owner}/${name}/security-advisories?per_page=${maxResults}`;
349
+
350
+ const response = await retryWithBackoff(async (): Promise<Response> => {
351
+ const resp = await fetch(url, {
352
+ headers: {
353
+ 'User-Agent': 'pi-research/2.0',
354
+ 'Accept': 'application/vnd.github.v3+json',
355
+ },
356
+ signal: createTimeoutSignal(30000),
357
+ });
358
+
359
+ if (resp.ok === false) {
360
+ if (resp.status === 404) {
361
+ throw new Error(`Repository "${owner}/${name}" not found or no access to security advisories.`);
362
+ }
363
+ if (resp.status === 403) {
364
+ throw new Error('GitHub API rate limit exceeded (HTTP 403). Retrying with backoff...');
365
+ }
366
+ if (resp.status >= 500) {
367
+ throw new Error(`GitHub server error (HTTP ${resp.status}). Retrying with backoff...`);
368
+ }
369
+ throw new Error(`GitHub API error (${resp.status}): ${resp.statusText}`);
370
+ }
371
+
372
+ return resp;
373
+ }, {
374
+ maxRetries: 2,
375
+ initialDelay: 1000,
376
+ maxDelay: 5000,
377
+ });
378
+
379
+ const data = await response.json();
380
+ let repoAdvisories: readonly GitHubAdvisoryRaw[] = [];
381
+
382
+ if (isGitHubAdvisoryArray(data)) {
383
+ repoAdvisories = data;
384
+ } else if (isGitHubAdvisoryListResponse(data) && isArray(data.items)) {
385
+ // Filter to ensure only valid advisory objects
386
+ repoAdvisories = data.items.filter(isGitHubAdvisoryRaw);
387
+ } else if (isSingleGitHubAdvisory(data)) {
388
+ repoAdvisories = [data];
389
+ }
390
+
391
+ allAdvisories = repoAdvisories.map(mapGitHubAdvisory);
392
+ } else {
393
+ // Search global advisories.
394
+ // The GitHub advisories API supports cve_id and ghsa_id as query params,
395
+ // so use those for precise lookups; fall back to a general listing for
396
+ // keyword terms (client-side filtering is applied afterwards).
397
+ const termResults: Advisory[] = [];
398
+
399
+ for (const term of terms) {
400
+ const termUpper = term.toUpperCase();
401
+ let apiUrl: string;
402
+
403
+ if (termUpper.startsWith('CVE-')) {
404
+ // Use cve_id param for CVE lookups
405
+ apiUrl = `${GITHUB_API_BASE}/advisories?cve_id=${encodeURIComponent(termUpper)}&per_page=${maxResults}`;
406
+ } else if (termUpper.startsWith('GHSA-')) {
407
+ // Direct advisory lookup by GHSA ID
408
+ apiUrl = `${GITHUB_API_BASE}/advisories/${encodeURIComponent(term)}`;
409
+ } else {
410
+ // Keyword: fetch recent published advisories and filter client-side
411
+ apiUrl = `${GITHUB_API_BASE}/advisories?per_page=${maxResults}&state=published&direction=desc`;
412
+ }
413
+
414
+ const response = await retryWithBackoff(async (): Promise<Response> => {
415
+ const resp = await fetch(apiUrl, {
416
+ headers: {
417
+ 'User-Agent': 'pi-research/2.0',
418
+ 'Accept': 'application/vnd.github.v3+json',
419
+ },
420
+ signal: createTimeoutSignal(30000),
421
+ });
422
+
423
+ if (resp.ok === false) {
424
+ if (resp.status === 404) {
425
+ throw new Error('Advisory not found (HTTP 404)');
426
+ }
427
+ if (resp.status === 403) {
428
+ throw new Error('GitHub API rate limit exceeded (HTTP 403). Retrying with backoff...');
429
+ }
430
+ if (resp.status >= 500) {
431
+ throw new Error(`GitHub server error (HTTP ${resp.status}). Retrying with backoff...`);
432
+ }
433
+ throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
434
+ }
435
+
436
+ return resp;
437
+ }, {
438
+ maxRetries: 2,
439
+ initialDelay: 1000,
440
+ maxDelay: 5000,
441
+ });
442
+
443
+ const data = await response.json();
444
+ let items: readonly GitHubAdvisoryRaw[] = [];
445
+
446
+ // GHSA direct lookup returns an object; list endpoints return arrays
447
+ if (isGitHubAdvisoryArray(data)) {
448
+ items = data;
449
+ } else if (isSingleGitHubAdvisory(data)) {
450
+ items = [data];
451
+ } else if (isGitHubAdvisoryListResponse(data) && isArray(data.items)) {
452
+ items = data.items.filter(isGitHubAdvisoryRaw);
453
+ }
454
+
455
+ termResults.push(...items.map(mapGitHubAdvisory));
456
+ }
457
+
458
+ // Deduplicate by GHSA ID
459
+ const seen = new Set<string>();
460
+ for (const adv of termResults) {
461
+ if (!seen.has(adv.id)) {
462
+ seen.add(adv.id);
463
+ allAdvisories.push(adv);
464
+ }
465
+ }
466
+ }
467
+
468
+ // Filter by search terms if provided (OR logic: match any term)
469
+ if (terms.length > 0) {
470
+ allAdvisories = allAdvisories.filter((adv): boolean =>
471
+ terms.some((term): boolean => {
472
+ const t = term.toLowerCase();
473
+ const advId = adv.id.toLowerCase();
474
+ const advSummary = adv.summary.toLowerCase();
475
+ const advDescription = adv.description !== undefined
476
+ ? adv.description.toLowerCase()
477
+ : '';
478
+ const advCveId = adv.cveId !== undefined
479
+ ? adv.cveId.toLowerCase()
480
+ : '';
481
+
482
+ return (
483
+ advId.includes(t) ||
484
+ advCveId.includes(t) ||
485
+ advSummary.includes(t) ||
486
+ advDescription.includes(t)
487
+ );
488
+ }),
489
+ );
490
+ }
491
+
492
+ // Filter by severity if provided
493
+ // GitHub uses "MODERATE" where the rest of the industry uses "MEDIUM"
494
+ if (options?.severity !== undefined && options.severity !== '') {
495
+ const severity = options.severity.toUpperCase();
496
+ const githubSeverity = severity === 'MEDIUM' ? 'MODERATE' : severity;
497
+ allAdvisories = allAdvisories.filter((adv): boolean =>
498
+ adv.severity === githubSeverity,
499
+ );
500
+ }
501
+
502
+ advisories.push(...allAdvisories.slice(0, maxResults));
503
+
504
+ } catch (err: unknown) {
505
+ error = err instanceof Error ? err.message : String(err);
506
+ }
507
+
508
+ return {
509
+ count: advisories.length,
510
+ advisories,
511
+ error,
512
+ };
513
+ }
514
+
515
+ /**
516
+ * Get specific advisory by GHSA ID or CVE ID
517
+ *
518
+ * @param id - The GHSA ID or CVE ID to fetch
519
+ * @returns Promise resolving to Advisory or null if not found
520
+ */
521
+ export async function getAdvisoryById(id: string): Promise<Advisory | null> {
522
+ try {
523
+ if (id === '') {
524
+ return null;
525
+ }
526
+
527
+ const url = `${GITHUB_API_BASE}/advisories/${encodeURIComponent(id)}`;
528
+
529
+ const response = await retryWithBackoff(async (): Promise<Response> => {
530
+ const resp = await fetch(url, {
531
+ headers: {
532
+ 'User-Agent': 'pi-research/2.0',
533
+ 'Accept': 'application/vnd.github.v3+json',
534
+ },
535
+ signal: createTimeoutSignal(30000),
536
+ });
537
+
538
+ if (resp.ok === false) {
539
+ if (resp.status === 404) {
540
+ throw new Error('Advisory not found (HTTP 404)');
541
+ }
542
+ if (resp.status === 403) {
543
+ throw new Error('GitHub API rate limit exceeded (HTTP 403). Retrying with backoff...');
544
+ }
545
+ if (resp.status >= 500) {
546
+ throw new Error(`GitHub server error (HTTP ${resp.status}). Retrying with backoff...`);
547
+ }
548
+ throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
549
+ }
550
+
551
+ return resp;
552
+ }, {
553
+ maxRetries: 2,
554
+ initialDelay: 1000,
555
+ maxDelay: 5000,
556
+ });
557
+
558
+ const data = await response.json();
559
+
560
+ if (isGitHubAdvisoryRaw(data)) {
561
+ return mapGitHubAdvisory(data);
562
+ }
563
+
564
+ return null;
565
+ } catch (err: unknown) {
566
+ // Check if it's a 404 error (not found) - return null instead of throwing
567
+ if (err instanceof Error && err.message.includes('HTTP 404')) {
568
+ logger.warn(`[GitHub Advisories] Advisory ${id} not found`);
569
+ return null;
570
+ }
571
+ logger.error(`[GitHub Advisories] Error fetching advisory ${id}:`, err);
572
+ return null;
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Map GitHub advisory to our Advisory interface
578
+ *
579
+ * @param item - Raw GitHub advisory object
580
+ * @returns Mapped Advisory object
581
+ */
582
+ function mapGitHubAdvisory(item: GitHubAdvisoryRaw): Advisory {
583
+ const ghsaId = item.ghsa_id ?? item.id ?? '';
584
+ const summary = item.summary ?? '';
585
+ const description = item.description ?? '';
586
+ const severityRaw = item.severity ?? 'UNKNOWN';
587
+ const severity = severityRaw.toUpperCase();
588
+ const published = item.published_at ?? '';
589
+ const modified = item.updated_at ?? '';
590
+ const cveId = item.cve_id ?? '';
591
+
592
+ const affectedPackages = extractAffectedPackages(item);
593
+ const references = extractReferences(item);
594
+
595
+ return {
596
+ id: ghsaId,
597
+ source: 'github',
598
+ severity,
599
+ summary,
600
+ description,
601
+ published,
602
+ modified,
603
+ cveId,
604
+ references,
605
+ affectedPackages,
606
+ };
607
+ }