@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.
- package/LICENSE +1 -1
- package/README.md +64 -115
- package/dist/cli.mjs +23211 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +102 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +213 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +113 -0
- package/docs/THIRD-PARTY-NOTICES.md +64 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1044 -0
- package/src/config.ts +1036 -147
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +264 -7
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -174
- package/src/infrastructure/searxng-lifecycle.ts +0 -577
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- package/tsconfig.json +0 -40
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NVD (National Vulnerability Database) API Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface CVSSData {
|
|
6
|
+
readonly version?: string;
|
|
7
|
+
readonly vectorString?: string;
|
|
8
|
+
readonly baseScore?: number;
|
|
9
|
+
readonly baseSeverity?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CVSSMetricV31 {
|
|
13
|
+
readonly cvssData: CVSSData;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CVSSMetricV30 {
|
|
17
|
+
readonly cvssData: CVSSData;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WeaknessDescription {
|
|
21
|
+
readonly value?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Weakness {
|
|
25
|
+
readonly description?: WeaknessDescription[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Reference {
|
|
29
|
+
readonly url?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CPEMatch {
|
|
33
|
+
readonly criteria?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Node {
|
|
37
|
+
readonly cpeMatch?: CPEMatch[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Configuration {
|
|
41
|
+
readonly nodes?: Node[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface Description {
|
|
45
|
+
readonly value?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// CVSS v4.0 mirrors v3 (score + qualitative severity live in cvssData). NVD is actively adding it.
|
|
49
|
+
export interface CVSSMetricV40 {
|
|
50
|
+
readonly cvssData: CVSSData;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// CVSS v2 differs: cvssData carries the numeric baseScore/vectorString but NO qualitative rating —
|
|
54
|
+
// NVD exposes the v2 baseSeverity (LOW/MEDIUM/HIGH) at the METRIC level instead.
|
|
55
|
+
export interface CVSSMetricV2 {
|
|
56
|
+
readonly cvssData: CVSSData;
|
|
57
|
+
readonly baseSeverity?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Metrics {
|
|
61
|
+
readonly cvssMetricV40?: CVSSMetricV40[];
|
|
62
|
+
readonly cvssMetricV31?: CVSSMetricV31[];
|
|
63
|
+
readonly cvssMetricV30?: CVSSMetricV30[];
|
|
64
|
+
readonly cvssMetricV2?: CVSSMetricV2[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface CVE {
|
|
68
|
+
readonly id?: string;
|
|
69
|
+
readonly descriptions?: Description[];
|
|
70
|
+
readonly published?: string;
|
|
71
|
+
readonly lastModified?: string;
|
|
72
|
+
readonly metrics?: Metrics;
|
|
73
|
+
readonly weaknesses?: Weakness[];
|
|
74
|
+
readonly references?: Reference[];
|
|
75
|
+
readonly configurations?: Configuration[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface NVDEntry {
|
|
79
|
+
readonly cve: CVE;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface NVDApiResponse {
|
|
83
|
+
readonly vulnerabilities?: NVDEntry[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type Severity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
|
87
|
+
|
|
88
|
+
export interface SearchOptions {
|
|
89
|
+
readonly severity?: Severity;
|
|
90
|
+
readonly maxResults?: number;
|
|
91
|
+
readonly includeExploited?: boolean;
|
|
92
|
+
readonly cweId?: string;
|
|
93
|
+
readonly startDate?: string;
|
|
94
|
+
readonly endDate?: string;
|
|
95
|
+
readonly maxPages?: number;
|
|
96
|
+
// Caller cancellation; composed with the per-request timeout and used to
|
|
97
|
+
// short-circuit the rate-limit spacing, pagination, and retry backoff.
|
|
98
|
+
readonly signal?: AbortSignal;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface RetryOptions {
|
|
102
|
+
readonly maxRetries: number;
|
|
103
|
+
readonly initialDelay: number;
|
|
104
|
+
readonly maxDelay: number;
|
|
105
|
+
readonly signal?: AbortSignal;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Type guards
|
|
109
|
+
export function isWeaknessDescription(value: unknown): value is WeaknessDescription {
|
|
110
|
+
return (
|
|
111
|
+
typeof value === 'object' &&
|
|
112
|
+
value !== null &&
|
|
113
|
+
'value' in value
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isWeakness(value: unknown): value is Weakness {
|
|
118
|
+
return (
|
|
119
|
+
typeof value === 'object' &&
|
|
120
|
+
value !== null &&
|
|
121
|
+
'description' in value
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isReference(value: unknown): value is Reference {
|
|
126
|
+
return (
|
|
127
|
+
typeof value === 'object' &&
|
|
128
|
+
value !== null &&
|
|
129
|
+
'url' in value
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function isCPEMatch(value: unknown): value is CPEMatch {
|
|
134
|
+
return (
|
|
135
|
+
typeof value === 'object' &&
|
|
136
|
+
value !== null &&
|
|
137
|
+
'criteria' in value
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function isNode(value: unknown): value is Node {
|
|
142
|
+
return (
|
|
143
|
+
typeof value === 'object' &&
|
|
144
|
+
value !== null &&
|
|
145
|
+
'cpeMatch' in value
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function isConfiguration(value: unknown): value is Configuration {
|
|
150
|
+
return (
|
|
151
|
+
typeof value === 'object' &&
|
|
152
|
+
value !== null &&
|
|
153
|
+
'nodes' in value
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function isDescription(value: unknown): value is Description {
|
|
158
|
+
return (
|
|
159
|
+
typeof value === 'object' &&
|
|
160
|
+
value !== null &&
|
|
161
|
+
'value' in value
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function isNVDEntry(value: unknown): value is NVDEntry {
|
|
166
|
+
return (
|
|
167
|
+
typeof value === 'object' &&
|
|
168
|
+
value !== null &&
|
|
169
|
+
'cve' in value
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function isNVDApiResponse(value: unknown): value is NVDApiResponse {
|
|
174
|
+
return (
|
|
175
|
+
typeof value === 'object' &&
|
|
176
|
+
value !== null &&
|
|
177
|
+
'vulnerabilities' in value
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSV (Open Source Vulnerabilities) API Client
|
|
3
|
+
*
|
|
4
|
+
* Open Source Vulnerabilities
|
|
5
|
+
* API: https://api.osv.dev/v1/
|
|
6
|
+
* Free public API, no authentication required
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Vulnerability, OSVResult } from './types.ts';
|
|
10
|
+
import type { OsvVulnerability, OsvQueryRequest } from './osv-types.ts';
|
|
11
|
+
import { normalizeEcosystem } from './ecosystem.ts';
|
|
12
|
+
import { createTimeoutSignal, retryWithBackoff, isTransientError } from '../web-research/retry-utils.ts';
|
|
13
|
+
import { logger } from '../logger.ts';
|
|
14
|
+
import { OSV_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_INITIAL_DELAY_MS, DEFAULT_MAX_DELAY_MS } from '../constants.ts';
|
|
15
|
+
import { CircuitBreaker } from '../utils/circuit-breaker.ts';
|
|
16
|
+
import { metrics } from '../utils/metrics.ts';
|
|
17
|
+
import {
|
|
18
|
+
isOsvVulnerability,
|
|
19
|
+
isOsvQueryResponse,
|
|
20
|
+
mapOsvItemToVulnerability,
|
|
21
|
+
} from './osv-types.ts';
|
|
22
|
+
|
|
23
|
+
const osvCircuitBreaker = new CircuitBreaker({
|
|
24
|
+
failureThreshold: 5,
|
|
25
|
+
resetTimeoutMs: 10000,
|
|
26
|
+
name: 'OSV API',
|
|
27
|
+
isTransientError: isTransientError
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const OSV_BASE_URL = 'https://api.osv.dev/v1';
|
|
31
|
+
const DEFAULT_MAX_RESULTS = 20;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fetch with retry for resilience
|
|
35
|
+
*/
|
|
36
|
+
async function fetchWithRetry(
|
|
37
|
+
url: string,
|
|
38
|
+
options: RequestInit,
|
|
39
|
+
signal?: AbortSignal,
|
|
40
|
+
): Promise<Response> {
|
|
41
|
+
const endpoint = new URL(url).pathname;
|
|
42
|
+
return osvCircuitBreaker.execute(async () => {
|
|
43
|
+
return retryWithBackoff(
|
|
44
|
+
async () => {
|
|
45
|
+
const response = await fetch(url, options);
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText}`) as Error & { status?: number };
|
|
48
|
+
error.status = response.status;
|
|
49
|
+
metrics.increment('osv_errors_total', 1, { endpoint, status_code: response.status.toString() });
|
|
50
|
+
if (response.status === 429) {
|
|
51
|
+
metrics.increment('osv_ratelimit_hits_total', 1, { endpoint });
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
metrics.increment('osv_requests_total', 1, { endpoint, status: 'success' });
|
|
56
|
+
return response;
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
maxRetries: DEFAULT_MAX_RETRIES,
|
|
60
|
+
initialDelay: DEFAULT_INITIAL_DELAY_MS,
|
|
61
|
+
maxDelay: DEFAULT_MAX_DELAY_MS,
|
|
62
|
+
label: `OSV API: ${url}`,
|
|
63
|
+
signal,
|
|
64
|
+
isTransientError: (error) => {
|
|
65
|
+
const status = (error as Error & { status?: number })?.status;
|
|
66
|
+
if (typeof status === 'number') {
|
|
67
|
+
return status === 429 || status === 403 || status >= 500;
|
|
68
|
+
}
|
|
69
|
+
return isTransientError(error);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Search OSV for vulnerabilities
|
|
78
|
+
*
|
|
79
|
+
* @param terms - Package names, CVE IDs, or keywords
|
|
80
|
+
* @param options - Optional filters
|
|
81
|
+
* @returns OSV search result with vulnerabilities and optional error
|
|
82
|
+
*/
|
|
83
|
+
export async function searchOSV(
|
|
84
|
+
terms: string[],
|
|
85
|
+
options?: {
|
|
86
|
+
ecosystem?: string;
|
|
87
|
+
severity?: string;
|
|
88
|
+
maxResults?: number;
|
|
89
|
+
includeAffected?: boolean;
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
},
|
|
92
|
+
): Promise<OSVResult> {
|
|
93
|
+
const startTime = Date.now();
|
|
94
|
+
const maxResults: number = options?.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
95
|
+
const vulnerabilities: Vulnerability[] = [];
|
|
96
|
+
let error: string | undefined = undefined;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// Each term is fetched and settled independently so one failed term (a
|
|
100
|
+
// transient 5xx, a malformed body, an exhausted retry) never discards the
|
|
101
|
+
// vulnerabilities already gathered for the others. `skipped` flags a package
|
|
102
|
+
// term that needs the ecosystem parameter.
|
|
103
|
+
const fetchTerm = async (term: string): Promise<{ vulns: Vulnerability[]; skipped: boolean }> => {
|
|
104
|
+
const termUpper: string = term.toUpperCase();
|
|
105
|
+
const isIdLookup =
|
|
106
|
+
termUpper.startsWith('CVE-') || termUpper.startsWith('GHSA-') || termUpper.startsWith('OSV-');
|
|
107
|
+
let response: Response;
|
|
108
|
+
|
|
109
|
+
if (isIdLookup) {
|
|
110
|
+
const normalizedId: string = termUpper.startsWith('GHSA-')
|
|
111
|
+
? `GHSA-${term.slice(term.indexOf('-') + 1).toLowerCase()}`
|
|
112
|
+
: termUpper;
|
|
113
|
+
const url: string = `${OSV_BASE_URL}/vulns/${encodeURIComponent(normalizedId)}`;
|
|
114
|
+
response = await fetchWithRetry(url, {
|
|
115
|
+
headers: { 'User-Agent': 'pi-research/2.0', 'Accept': 'application/json' },
|
|
116
|
+
signal: createTimeoutSignal(OSV_TIMEOUT_MS, options?.signal),
|
|
117
|
+
}, options?.signal);
|
|
118
|
+
} else {
|
|
119
|
+
if (options?.ecosystem === undefined || options.ecosystem === '') {
|
|
120
|
+
return { vulns: [], skipped: true };
|
|
121
|
+
}
|
|
122
|
+
const body: OsvQueryRequest = { package: { name: term, ecosystem: normalizeEcosystem(options.ecosystem, 'osv') } };
|
|
123
|
+
response = await fetchWithRetry(`${OSV_BASE_URL}/query`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: {
|
|
126
|
+
'User-Agent': 'pi-research/2.0',
|
|
127
|
+
'Accept': 'application/json',
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify(body),
|
|
131
|
+
signal: createTimeoutSignal(OSV_TIMEOUT_MS, options?.signal),
|
|
132
|
+
}, options?.signal);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let data: unknown;
|
|
136
|
+
try {
|
|
137
|
+
data = await response.json();
|
|
138
|
+
} catch {
|
|
139
|
+
metrics.increment('osv_search_errors_total', 1, { error_type: 'malformed_json' });
|
|
140
|
+
throw new Error(`OSV returned a malformed JSON response for "${term}".`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let items: OsvVulnerability[];
|
|
144
|
+
if (isOsvVulnerability(data)) {
|
|
145
|
+
items = [data];
|
|
146
|
+
} else if (isOsvQueryResponse(data)) {
|
|
147
|
+
items = data.vulns ?? [];
|
|
148
|
+
} else {
|
|
149
|
+
logger.warn(`OSV returned unexpected format for "${term}"`);
|
|
150
|
+
return { vulns: [], skipped: false };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const endpoint = isIdLookup ? 'vulns_by_id' : 'query';
|
|
154
|
+
metrics.increment(items.length > 0 ? 'osv_cache_hits_total' : 'osv_cache_misses_total', 1, { term, endpoint });
|
|
155
|
+
|
|
156
|
+
const vulns: Vulnerability[] = [];
|
|
157
|
+
for (const item of items) {
|
|
158
|
+
// Withdrawn/retracted advisories are no longer valid vulnerabilities — skip them so they
|
|
159
|
+
// are not reported as active findings nor counted toward the researcher grounding gate.
|
|
160
|
+
if (item.withdrawn) continue;
|
|
161
|
+
const vuln: Vulnerability = mapOsvItemToVulnerability(item);
|
|
162
|
+
if (options?.severity !== undefined && options.severity !== '') {
|
|
163
|
+
// OSV/GHSA severity vocabulary is LOW/MODERATE/HIGH/CRITICAL. Accept the
|
|
164
|
+
// common "MEDIUM" synonym for MODERATE (GitHub maps it the same way at
|
|
165
|
+
// github-advisories.ts) — otherwise a severity:"medium" filter silently
|
|
166
|
+
// drops every OSV result.
|
|
167
|
+
// mapOsvItemToVulnerability canonicalizes severity to LOW/MEDIUM/HIGH/CRITICAL (both the
|
|
168
|
+
// OSV "MODERATE" and "MEDIUM" spellings map to 'MEDIUM'). Normalize the caller's filter
|
|
169
|
+
// into the SAME vocabulary — MODERATE→MEDIUM — before comparing. The previous direction
|
|
170
|
+
// (MEDIUM→MODERATE) compared against a value vuln.severity never holds, silently dropping
|
|
171
|
+
// every medium-severity OSV result (the most common filter value).
|
|
172
|
+
const want = options.severity.toUpperCase();
|
|
173
|
+
const normalized = want === 'MODERATE' ? 'MEDIUM' : want;
|
|
174
|
+
if (vuln.severity === 'UNKNOWN') {
|
|
175
|
+
// We could not rank this record. If it nonetheless carries a CVSS vector we
|
|
176
|
+
// don't score — mapOsvItemToVulnerability only computes CVSS v3, so a v4-only
|
|
177
|
+
// (increasingly common) or v2-only record stays UNKNOWN — it IS a scored
|
|
178
|
+
// vulnerability whose number we simply can't compute. Silently dropping it
|
|
179
|
+
// under a severity filter would hide a possible HIGH/CRITICAL, so surface it
|
|
180
|
+
// instead (the safe bias for security triage: a false-include beats a hidden
|
|
181
|
+
// finding). A record with NO severity signal at all stays filtered out so
|
|
182
|
+
// results are not flooded with genuinely unranked noise.
|
|
183
|
+
const hasUnscoredVector =
|
|
184
|
+
(Array.isArray(item.severity) && item.severity.length > 0) ||
|
|
185
|
+
(Array.isArray(item.affected) &&
|
|
186
|
+
item.affected.some((a) => Array.isArray(a.severity) && a.severity.length > 0));
|
|
187
|
+
if (!hasUnscoredVector) continue;
|
|
188
|
+
metrics.increment('osv_unscored_included_total', 1, { filter: normalized });
|
|
189
|
+
} else if (vuln.severity !== normalized) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
vulns.push(vuln);
|
|
194
|
+
}
|
|
195
|
+
return { vulns, skipped: false };
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const settled = await Promise.allSettled(terms.map(fetchTerm));
|
|
199
|
+
const allVulns: Vulnerability[] = [];
|
|
200
|
+
let skippedNoEcosystem = 0;
|
|
201
|
+
const failures: string[] = [];
|
|
202
|
+
settled.forEach((result, i) => {
|
|
203
|
+
if (result.status === 'fulfilled') {
|
|
204
|
+
allVulns.push(...result.value.vulns);
|
|
205
|
+
if (result.value.skipped) skippedNoEcosystem++;
|
|
206
|
+
} else {
|
|
207
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
208
|
+
// A 404 on an ID lookup means "no such vuln", not a real failure.
|
|
209
|
+
if (!reason.includes('HTTP 404')) failures.push(`${terms[i]} (${reason})`);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const uniqueVulns = new Map<string, Vulnerability>();
|
|
214
|
+
for (const vuln of allVulns) {
|
|
215
|
+
if (!uniqueVulns.has(vuln.id)) {
|
|
216
|
+
uniqueVulns.set(vuln.id, vuln);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
vulnerabilities.push(...Array.from(uniqueVulns.values()));
|
|
221
|
+
|
|
222
|
+
if (failures.length > 0 && failures.length === terms.length) {
|
|
223
|
+
throw new Error(`All OSV lookups failed: ${failures.join('; ')}`);
|
|
224
|
+
}
|
|
225
|
+
const notes: string[] = [];
|
|
226
|
+
if (failures.length > 0) notes.push(`Some OSV lookups failed: ${failures.join('; ')}`);
|
|
227
|
+
if (skippedNoEcosystem > 0) {
|
|
228
|
+
notes.push(`${skippedNoEcosystem} term(s) require the ecosystem parameter for OSV package search (e.g., ecosystem: "npm"). CVE/GHSA/OSV IDs work without it.`);
|
|
229
|
+
}
|
|
230
|
+
if (notes.length > 0) error = notes.join(' ');
|
|
231
|
+
|
|
232
|
+
} catch (err: unknown) {
|
|
233
|
+
error = err instanceof Error ? err.message : String(err);
|
|
234
|
+
metrics.increment('osv_search_errors_total', 1, { error_type: err instanceof Error ? err.name : 'unknown' });
|
|
235
|
+
} finally {
|
|
236
|
+
const duration = Date.now() - startTime;
|
|
237
|
+
metrics.observe('osv_search_duration_ms', duration, { has_error: error ? 'true' : 'false' });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
count: vulnerabilities.length,
|
|
242
|
+
vulnerabilities: vulnerabilities.slice(0, maxResults),
|
|
243
|
+
error,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSV (Open Source Vulnerabilities) API Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Vulnerability } from './types.ts';
|
|
6
|
+
import { scoreCvss3 } from './cvss.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OSV API vulnerability response (from GET /vulns/{id})
|
|
10
|
+
*/
|
|
11
|
+
export interface OsvVulnerability {
|
|
12
|
+
id: string;
|
|
13
|
+
summary?: string;
|
|
14
|
+
details?: string;
|
|
15
|
+
published?: string;
|
|
16
|
+
modified?: string;
|
|
17
|
+
withdrawn?: string;
|
|
18
|
+
aliases?: string[];
|
|
19
|
+
related?: string[];
|
|
20
|
+
affected?: OsvAffected[];
|
|
21
|
+
severity?: OsvSeverity[];
|
|
22
|
+
references?: OsvReference[];
|
|
23
|
+
database_specific?: OsvDatabaseSpecific;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* OSV severity entry
|
|
28
|
+
*/
|
|
29
|
+
export interface OsvSeverity {
|
|
30
|
+
type: string;
|
|
31
|
+
score: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* OSV database-specific metadata
|
|
36
|
+
*/
|
|
37
|
+
export interface OsvDatabaseSpecific {
|
|
38
|
+
severity?: string;
|
|
39
|
+
cwe?: Array<{ id: string } | string>;
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* OSV affected package entry
|
|
45
|
+
*/
|
|
46
|
+
export interface OsvAffected {
|
|
47
|
+
package?: OsvPackage;
|
|
48
|
+
ranges?: OsvRange[];
|
|
49
|
+
versions?: string[];
|
|
50
|
+
database_specific?: Record<string, unknown>;
|
|
51
|
+
severity?: OsvSeverity[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* OSV package information
|
|
56
|
+
*/
|
|
57
|
+
export interface OsvPackage {
|
|
58
|
+
name?: string;
|
|
59
|
+
ecosystem?: string;
|
|
60
|
+
purl?: string;
|
|
61
|
+
ecosystem_specific?: {
|
|
62
|
+
name?: string;
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
};
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* OSV version range information
|
|
70
|
+
*/
|
|
71
|
+
export interface OsvRange {
|
|
72
|
+
type: string;
|
|
73
|
+
repo?: string;
|
|
74
|
+
events: OsvRangeEvent[];
|
|
75
|
+
database_specific?: Record<string, unknown>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* OSV range event (introduced, fixed, last_affected, limits)
|
|
80
|
+
*/
|
|
81
|
+
export interface OsvRangeEvent {
|
|
82
|
+
introduced?: string;
|
|
83
|
+
fixed?: string;
|
|
84
|
+
last_affected?: string;
|
|
85
|
+
limit?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* OSV reference entry
|
|
90
|
+
*/
|
|
91
|
+
export interface OsvReference {
|
|
92
|
+
type?: string;
|
|
93
|
+
url?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* OSV query request body
|
|
98
|
+
*/
|
|
99
|
+
export interface OsvQueryRequest {
|
|
100
|
+
package: {
|
|
101
|
+
name: string;
|
|
102
|
+
ecosystem: string;
|
|
103
|
+
purl?: string;
|
|
104
|
+
};
|
|
105
|
+
version?: string;
|
|
106
|
+
commit?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* OSV query response (from POST /query)
|
|
111
|
+
*/
|
|
112
|
+
export interface OsvQueryResponse {
|
|
113
|
+
vulns?: OsvVulnerability[];
|
|
114
|
+
[key: string]: unknown;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Type guards
|
|
118
|
+
export function isOsvVulnerability(value: unknown): value is OsvVulnerability {
|
|
119
|
+
if (value === null || typeof value !== 'object') {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const obj = value as Record<string, unknown>;
|
|
123
|
+
return typeof obj['id'] === 'string';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isOsvQueryResponse(value: unknown): value is OsvQueryResponse {
|
|
127
|
+
if (value === null || typeof value !== 'object') {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const obj = value as Record<string, unknown>;
|
|
131
|
+
return Array.isArray(obj['vulns']);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Helper Functions
|
|
135
|
+
export function mapOsvItemToVulnerability(item: OsvVulnerability): Vulnerability {
|
|
136
|
+
const osvId: string = item.id;
|
|
137
|
+
const summary: string = item.summary ?? item.details ?? '';
|
|
138
|
+
const details: string = item.details ?? '';
|
|
139
|
+
const published: string | undefined = item.published;
|
|
140
|
+
const modified: string = item.modified ?? '';
|
|
141
|
+
|
|
142
|
+
let severityStr: string | undefined;
|
|
143
|
+
if (item.database_specific !== undefined &&
|
|
144
|
+
typeof item.database_specific.severity === 'string') {
|
|
145
|
+
severityStr = item.database_specific.severity;
|
|
146
|
+
}
|
|
147
|
+
let severity: string = mapOsvSeverity(severityStr);
|
|
148
|
+
|
|
149
|
+
// When OSV has no database_specific.severity (non-GHSA records: CVE/PyPA/Go/RustSec), the only
|
|
150
|
+
// severity signal is a CVSS vector in item.severity[]. Compute the v3 base score/severity from it
|
|
151
|
+
// rather than reporting UNKNOWN with no score.
|
|
152
|
+
let cvssScore: number | undefined;
|
|
153
|
+
let cvssVector: string | undefined;
|
|
154
|
+
if (severity === 'UNKNOWN' && Array.isArray(item.severity)) {
|
|
155
|
+
for (const sev of item.severity) {
|
|
156
|
+
if (sev && typeof sev.score === 'string' && /^CVSS_V3/i.test(sev.type ?? '')) {
|
|
157
|
+
const scored = scoreCvss3(sev.score);
|
|
158
|
+
if (scored) {
|
|
159
|
+
severity = scored.severity === 'NONE' ? 'UNKNOWN' : scored.severity;
|
|
160
|
+
cvssScore = scored.score;
|
|
161
|
+
cvssVector = sev.score;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const aliases: string[] = item.aliases ?? [];
|
|
169
|
+
const references: string[] = [...aliases];
|
|
170
|
+
|
|
171
|
+
if (item.references !== undefined) {
|
|
172
|
+
for (const ref of item.references) {
|
|
173
|
+
if (ref.url !== undefined && ref.url !== '') {
|
|
174
|
+
references.push(ref.url);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const affectedProducts: string[] = [];
|
|
180
|
+
const fixes: string[] = [];
|
|
181
|
+
|
|
182
|
+
if (item.affected !== undefined) {
|
|
183
|
+
for (const affectedEntry of item.affected) {
|
|
184
|
+
const pkg: OsvPackage | undefined = affectedEntry.package;
|
|
185
|
+
|
|
186
|
+
if (pkg !== undefined) {
|
|
187
|
+
const pkgName: string = pkg.name ??
|
|
188
|
+
(pkg.ecosystem_specific !== undefined && typeof pkg.ecosystem_specific.name === 'string'
|
|
189
|
+
? pkg.ecosystem_specific.name
|
|
190
|
+
: '');
|
|
191
|
+
|
|
192
|
+
if (pkgName !== '') {
|
|
193
|
+
affectedProducts.push(pkgName);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (affectedEntry.ranges !== undefined) {
|
|
197
|
+
for (const range of affectedEntry.ranges) {
|
|
198
|
+
// Guard against a malformed range missing `events` (upstream validation only
|
|
199
|
+
// checks the vuln `id`): an unguarded events.find() would throw a TypeError,
|
|
200
|
+
// fail the whole term in Promise.allSettled, and discard all its vulns.
|
|
201
|
+
const events: OsvRangeEvent[] = Array.isArray(range.events) ? range.events : [];
|
|
202
|
+
|
|
203
|
+
const introducedEvent: OsvRangeEvent | undefined = events.find(
|
|
204
|
+
(e: OsvRangeEvent) => e.introduced !== undefined,
|
|
205
|
+
);
|
|
206
|
+
const fixedEvent: OsvRangeEvent | undefined = events.find(
|
|
207
|
+
(e: OsvRangeEvent) => e.fixed !== undefined,
|
|
208
|
+
);
|
|
209
|
+
const lastAffectedEvent: OsvRangeEvent | undefined = events.find(
|
|
210
|
+
(e: OsvRangeEvent) => e.last_affected !== undefined,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const versionInfo: string[] = [];
|
|
214
|
+
|
|
215
|
+
if (introducedEvent?.introduced !== undefined) {
|
|
216
|
+
versionInfo.push(`introduced: ${introducedEvent.introduced}`);
|
|
217
|
+
}
|
|
218
|
+
if (fixedEvent?.fixed !== undefined) {
|
|
219
|
+
versionInfo.push(`fixed: ${fixedEvent.fixed}`);
|
|
220
|
+
}
|
|
221
|
+
if (lastAffectedEvent?.last_affected !== undefined) {
|
|
222
|
+
versionInfo.push(`last affected: ${lastAffectedEvent.last_affected}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (versionInfo.length > 0 && pkgName !== '') {
|
|
226
|
+
fixes.push(`${pkgName}: ${versionInfo.join(', ')}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const cwes: string[] = [];
|
|
235
|
+
if (item.database_specific !== undefined) {
|
|
236
|
+
if (item.database_specific.cwe !== undefined && Array.isArray(item.database_specific.cwe)) {
|
|
237
|
+
for (const cwe of item.database_specific.cwe) {
|
|
238
|
+
if (typeof cwe === 'string') {
|
|
239
|
+
cwes.push(cwe);
|
|
240
|
+
} else if (typeof cwe === 'object' && typeof (cwe as { id?: string }).id === 'string') {
|
|
241
|
+
cwes.push((cwe as { id: string }).id);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
id: osvId,
|
|
249
|
+
source: 'osv',
|
|
250
|
+
severity,
|
|
251
|
+
description: summary !== '' ? summary : details,
|
|
252
|
+
published,
|
|
253
|
+
modified,
|
|
254
|
+
cvssScore,
|
|
255
|
+
cvssVector,
|
|
256
|
+
cwes,
|
|
257
|
+
references,
|
|
258
|
+
affectedProducts,
|
|
259
|
+
fixes,
|
|
260
|
+
knownExploited: false,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function mapOsvSeverity(severity?: string): string {
|
|
265
|
+
if (severity === undefined || severity === '') {
|
|
266
|
+
return 'UNKNOWN';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const upper: string = severity.toUpperCase();
|
|
270
|
+
|
|
271
|
+
if (upper === 'CRITICAL') {
|
|
272
|
+
return 'CRITICAL';
|
|
273
|
+
}
|
|
274
|
+
if (upper === 'HIGH') {
|
|
275
|
+
return 'HIGH';
|
|
276
|
+
}
|
|
277
|
+
if (upper === 'MEDIUM' || upper === 'MODERATE') {
|
|
278
|
+
return 'MEDIUM';
|
|
279
|
+
}
|
|
280
|
+
if (upper === 'LOW') {
|
|
281
|
+
return 'LOW';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return 'UNKNOWN';
|
|
285
|
+
}
|