kirograph 0.28.1 → 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/dist/architecture/index.js +4 -1
- package/dist/architecture/index.js.map +2 -2
- package/dist/architecture/manifest/index.js +13 -2
- package/dist/architecture/manifest/index.js.map +2 -2
- package/dist/bin/commands/install.js +2 -1
- package/dist/bin/commands/install.js.map +2 -2
- package/dist/bin/commands/status.js +2 -1
- package/dist/bin/commands/status.js.map +2 -2
- package/dist/bin/commands/uninit.js +2 -1
- package/dist/bin/commands/uninit.js.map +2 -2
- package/dist/bin/commands/vulns.js +10 -5
- package/dist/bin/commands/vulns.js.map +2 -2
- package/dist/bin/installer/common.js +7 -1
- package/dist/bin/installer/common.js.map +2 -2
- package/dist/bin/installer/detect.js +10 -0
- package/dist/bin/installer/detect.js.map +2 -2
- package/dist/bin/installer/hooks.js +13 -8
- package/dist/bin/installer/hooks.js.map +2 -2
- package/dist/bin/installer/targets/antigravity.js +1 -1
- package/dist/bin/installer/targets/antigravity.js.map +2 -2
- package/dist/bin/installer/targets/claude.js +1 -1
- package/dist/bin/installer/targets/claude.js.map +2 -2
- package/dist/bin/installer/targets/codex.js +1 -1
- package/dist/bin/installer/targets/codex.js.map +2 -2
- package/dist/bin/installer/targets/commandcode.js +84 -0
- package/dist/bin/installer/targets/commandcode.js.map +7 -0
- package/dist/bin/installer/targets/copilot.js +1 -1
- package/dist/bin/installer/targets/copilot.js.map +2 -2
- package/dist/bin/installer/targets/cursor.js +2 -2
- package/dist/bin/installer/targets/cursor.js.map +2 -2
- package/dist/bin/installer/targets/devin.js +1 -1
- package/dist/bin/installer/targets/devin.js.map +2 -2
- package/dist/bin/installer/targets/gemini-cli.js +1 -1
- package/dist/bin/installer/targets/gemini-cli.js.map +2 -2
- package/dist/bin/installer/targets/index.js +10 -0
- package/dist/bin/installer/targets/index.js.map +2 -2
- package/dist/bin/installer/targets/opencode.js +1 -1
- package/dist/bin/installer/targets/opencode.js.map +2 -2
- package/dist/bin/installer/targets/windsurf.js +1 -1
- package/dist/bin/installer/targets/windsurf.js.map +2 -2
- package/dist/bin/kirograph.js +1 -1
- package/dist/bin/kirograph.js.map +1 -1
- package/dist/config.js +21 -1
- package/dist/config.js.map +2 -2
- package/dist/core/pipeline.js +18 -6
- package/dist/core/pipeline.js.map +2 -2
- package/dist/extraction/extractor.js +58 -1
- package/dist/extraction/extractor.js.map +2 -2
- package/dist/extraction/grammars.js +38 -3
- package/dist/extraction/grammars.js.map +2 -2
- package/dist/extraction/wasm/tree-sitter-bash.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-c.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-csharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-css.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-dart.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-elisp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-elixir.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-elm.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-embedded_template.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-embeddedtemplate.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-html.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-json.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-kotlin.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-objc.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-ocaml.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-php.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-ql.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-rescript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-rust.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-solidity.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-swift.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-systemrdl.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-tlaplus.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-toml.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-vue.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-yaml.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-zig.wasm +0 -0
- package/dist/frameworks/index.js +68 -7
- package/dist/frameworks/index.js.map +2 -2
- package/dist/security/manifest/adapter.js +17 -2
- package/dist/security/manifest/adapter.js.map +2 -2
- package/dist/security/manifest/parser.js +2 -2
- package/dist/security/manifest/parser.js.map +2 -2
- package/dist/security/pipeline.js +4 -1
- package/dist/security/pipeline.js.map +2 -2
- package/dist/security/reachability.js +61 -45
- package/dist/security/reachability.js.map +3 -3
- package/dist/security/types.js.map +1 -1
- package/dist/security/vuln/epss-client.js +1 -1
- package/dist/security/vuln/epss-client.js.map +1 -1
- package/dist/security/vuln/osv-adapter.js +99 -31
- package/dist/security/vuln/osv-adapter.js.map +2 -2
- package/dist/sync/index.js +9 -2
- package/dist/sync/index.js.map +2 -2
- package/dist/vectors/libsql-index.js +287 -0
- package/dist/vectors/libsql-index.js.map +7 -0
- package/dist/vectors/turso-index.js +307 -0
- package/dist/vectors/turso-index.js.map +7 -0
- package/package.json +3 -2
|
@@ -34,7 +34,7 @@ class ReachabilityAnalyzer {
|
|
|
34
34
|
*
|
|
35
35
|
* 1. Find the Dependency_Node linked via `has_vulnerability` edge
|
|
36
36
|
* 2. Find all Entry_Points in the graph
|
|
37
|
-
* 3. BFS from
|
|
37
|
+
* 3. Reverse BFS from the dependency node through incoming edges toward entry points
|
|
38
38
|
* 4. Assign verdict based on path existence and unresolved imports
|
|
39
39
|
*
|
|
40
40
|
* Requirements: 4.1, 4.2, 4.3, 4.4, 4.6
|
|
@@ -68,24 +68,23 @@ class ReachabilityAnalyzer {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
const reachingPaths = [];
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
71
|
+
const entryPointIds = new Set(entryPoints.map((ep) => ep.id));
|
|
72
|
+
const { reached, unresolvedSymbols } = this.reverseBfsToEntryPoints(
|
|
73
|
+
rawDb,
|
|
74
|
+
dependencyNodeId,
|
|
75
|
+
entryPointIds
|
|
76
|
+
);
|
|
77
|
+
for (const { entryPointId, path } of reached) {
|
|
78
|
+
reachingPaths.push({
|
|
79
|
+
entryPoint: entryPointId,
|
|
80
|
+
path
|
|
81
|
+
});
|
|
83
82
|
}
|
|
84
|
-
const
|
|
83
|
+
const cappedUnresolved = unresolvedSymbols.slice(0, MAX_UNRESOLVED_SYMBOLS);
|
|
85
84
|
let verdict;
|
|
86
85
|
if (reachingPaths.length > 0) {
|
|
87
86
|
verdict = "affected";
|
|
88
|
-
} else if (
|
|
87
|
+
} else if (cappedUnresolved.length > 0) {
|
|
89
88
|
verdict = "under_investigation";
|
|
90
89
|
} else {
|
|
91
90
|
verdict = "not_affected";
|
|
@@ -93,7 +92,7 @@ class ReachabilityAnalyzer {
|
|
|
93
92
|
const result = {
|
|
94
93
|
verdict,
|
|
95
94
|
paths: reachingPaths,
|
|
96
|
-
unresolvedSymbols: verdict === "under_investigation" ?
|
|
95
|
+
unresolvedSymbols: verdict === "under_investigation" ? cappedUnresolved : [],
|
|
97
96
|
reachingEntryPointCount: reachingPaths.length
|
|
98
97
|
};
|
|
99
98
|
this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result);
|
|
@@ -189,49 +188,66 @@ class ReachabilityAnalyzer {
|
|
|
189
188
|
return results;
|
|
190
189
|
}
|
|
191
190
|
/**
|
|
192
|
-
* BFS from
|
|
191
|
+
* Reverse BFS: start from the dependency node and walk BACKWARDS through
|
|
192
|
+
* incoming edges to find which entry points can reach it.
|
|
193
|
+
*
|
|
194
|
+
* This is O(V+E) total (one traversal) regardless of how many entry points exist,
|
|
195
|
+
* compared to O(entryPoints × (V+E)) for forward BFS from each entry point.
|
|
193
196
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
+
* While traversing, collects "unresolved imports" — import-kind nodes with no
|
|
198
|
+
* outgoing traversal edges. These are dead ends in the forward graph: the
|
|
199
|
+
* traversal reached them but couldn't follow them to a definition (dynamic
|
|
200
|
+
* dispatch, reflection, unindexed code). When no entry point reaches the
|
|
201
|
+
* dependency but such imports were encountered on the backward frontier,
|
|
202
|
+
* "not reachable" cannot be concluded and the caller reports
|
|
203
|
+
* under_investigation instead of not_affected.
|
|
197
204
|
*
|
|
198
|
-
* Returns the
|
|
205
|
+
* Returns the entry points that have a path to the dependency (with their
|
|
206
|
+
* paths) and the set of unresolved import node IDs encountered.
|
|
199
207
|
*/
|
|
200
|
-
|
|
208
|
+
reverseBfsToEntryPoints(rawDb, dependencyNodeId, entryPointIds) {
|
|
209
|
+
const reached = [];
|
|
210
|
+
const unresolvedSymbols = /* @__PURE__ */ new Set();
|
|
201
211
|
const visited = /* @__PURE__ */ new Set();
|
|
202
|
-
const
|
|
203
|
-
const queue = [[entryPointId, [entryPointId]]];
|
|
204
|
-
visited.add(entryPointId);
|
|
212
|
+
const parentMap = /* @__PURE__ */ new Map();
|
|
205
213
|
const edgeKinds = TRAVERSAL_EDGE_KINDS.map((k) => `'${k}'`).join(",");
|
|
214
|
+
const queue = [dependencyNodeId];
|
|
215
|
+
visited.add(dependencyNodeId);
|
|
206
216
|
while (queue.length > 0) {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
217
|
+
const currentId = queue.shift();
|
|
218
|
+
if (currentId !== dependencyNodeId && entryPointIds.has(currentId)) {
|
|
219
|
+
const path = [];
|
|
220
|
+
let node = currentId;
|
|
221
|
+
while (node !== void 0) {
|
|
222
|
+
path.push(node);
|
|
223
|
+
node = parentMap.get(node);
|
|
224
|
+
}
|
|
225
|
+
reached.push({ entryPointId: currentId, path });
|
|
226
|
+
}
|
|
227
|
+
const inEdges = rawDb.all(
|
|
228
|
+
`SELECT source FROM edges WHERE target = ? AND kind IN (${edgeKinds})`,
|
|
210
229
|
[currentId]
|
|
211
230
|
);
|
|
212
|
-
if (
|
|
213
|
-
const
|
|
214
|
-
`SELECT
|
|
231
|
+
if (currentId !== dependencyNodeId) {
|
|
232
|
+
const outCount = rawDb.get(
|
|
233
|
+
`SELECT COUNT(*) AS c FROM edges WHERE source = ? AND kind IN (${edgeKinds})`,
|
|
215
234
|
[currentId]
|
|
216
235
|
);
|
|
217
|
-
if (
|
|
218
|
-
|
|
236
|
+
if (outCount.c === 0) {
|
|
237
|
+
const nodeRow = rawDb.get(`SELECT kind FROM nodes WHERE id = ?`, [currentId]);
|
|
238
|
+
if (nodeRow && nodeRow.kind === "import") {
|
|
239
|
+
unresolvedSymbols.add(currentId);
|
|
240
|
+
}
|
|
219
241
|
}
|
|
220
242
|
}
|
|
221
|
-
for (const edge of
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
unresolvedSymbols
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
if (visited.has(targetId)) continue;
|
|
230
|
-
visited.add(targetId);
|
|
231
|
-
queue.push([targetId, [...currentPath, targetId]]);
|
|
243
|
+
for (const edge of inEdges) {
|
|
244
|
+
if (visited.has(edge.source)) continue;
|
|
245
|
+
visited.add(edge.source);
|
|
246
|
+
parentMap.set(edge.source, currentId);
|
|
247
|
+
queue.push(edge.source);
|
|
232
248
|
}
|
|
233
249
|
}
|
|
234
|
-
return {
|
|
250
|
+
return { reached, unresolvedSymbols: Array.from(unresolvedSymbols) };
|
|
235
251
|
}
|
|
236
252
|
/**
|
|
237
253
|
* Store reachability result in sec_reachability table.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/security/reachability.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * KiroGraph-Sec Reachability Analyzer\n *\n * Traverses the knowledge graph from application entry points to determine\n * whether vulnerable dependencies are reachable through actual code paths.\n * Produces reachability verdicts and impact summaries enriched with\n * architectural layer context.\n *\n * Requirements: 4.1, 4.2, 4.3, 4.4, 4.6, 5.1, 5.2, 5.3, 5.4, 5.5\n */\n\nimport type { GraphDatabase } from '../db/database';\nimport type { KiroGraphConfig } from '../config';\nimport type {\n ReachabilityVerdict,\n ReachabilityPath,\n ReachabilityResult,\n ImpactSummary,\n} from './types';\n\n/** Edge kinds used for reachability traversal */\nconst TRAVERSAL_EDGE_KINDS = ['calls', 'imports', 'references'] as const;\n\n/** Maximum unresolved symbols to report */\nconst MAX_UNRESOLVED_SYMBOLS = 50;\n\n/** Maximum distinct paths to report in impact summary */\nconst MAX_DISTINCT_PATHS = 100;\n\n/**\n * ReachabilityAnalyzer traverses the knowledge graph from entry points\n * toward affected dependency nodes to determine reachability verdicts.\n */\nexport class ReachabilityAnalyzer {\n private readonly db: GraphDatabase;\n private readonly config: KiroGraphConfig;\n\n constructor(db: GraphDatabase, config: KiroGraphConfig) {\n this.db = db;\n this.config = config;\n }\n\n /**\n * Analyze reachability for a single Vulnerability_Node.\n *\n * 1. Find the Dependency_Node linked via `has_vulnerability` edge\n * 2. Find all Entry_Points in the graph\n * 3. BFS from each entry point toward the dependency node\n * 4. Assign verdict based on path existence and unresolved imports\n *\n * Requirements: 4.1, 4.2, 4.3, 4.4, 4.6\n */\n async analyze(vulnerabilityNodeId: string): Promise<ReachabilityResult> {\n const rawDb = this.db.getRawDb();\n\n // Step 1: Find the Dependency_Node linked to this vulnerability\n const depEdge = rawDb.get(\n `SELECT source FROM edges WHERE target = ? AND kind = 'has_vulnerability'`,\n [vulnerabilityNodeId],\n );\n\n if (!depEdge) {\n // No dependency linked \u2014 cannot determine reachability\n return {\n verdict: 'under_investigation',\n paths: [],\n unresolvedSymbols: [],\n reachingEntryPointCount: 0,\n };\n }\n\n const dependencyNodeId: string = depEdge.source;\n\n // Step 2: Find all Entry_Points\n // Entry points are: nodes with kind='route' OR nodes with kind='function' that are exported\n const entryPoints: Array<{ id: string }> = rawDb.all(\n `SELECT id FROM nodes WHERE kind = 'route'\n UNION\n SELECT id FROM nodes WHERE kind = 'function' AND is_exported = 1`,\n );\n\n if (entryPoints.length === 0) {\n // No entry points \u2014 cannot determine reachability\n return {\n verdict: 'not_affected',\n paths: [],\n unresolvedSymbols: [],\n reachingEntryPointCount: 0,\n };\n }\n\n // Step 3: BFS from each entry point toward the dependency node\n const reachingPaths: ReachabilityPath[] = [];\n const allUnresolvedSymbols = new Set<string>();\n\n for (const ep of entryPoints) {\n const result = this.bfsFromEntryPoint(rawDb, ep.id, dependencyNodeId);\n\n if (result.path) {\n reachingPaths.push({\n entryPoint: ep.id,\n path: result.path,\n });\n }\n\n for (const sym of result.unresolvedSymbols) {\n allUnresolvedSymbols.add(sym);\n }\n }\n\n // Step 4: Assign verdict\n const unresolvedSymbols = Array.from(allUnresolvedSymbols).slice(0, MAX_UNRESOLVED_SYMBOLS);\n\n let verdict: ReachabilityVerdict;\n if (reachingPaths.length > 0) {\n verdict = 'affected';\n } else if (allUnresolvedSymbols.size > 0) {\n verdict = 'under_investigation';\n } else {\n verdict = 'not_affected';\n }\n\n const result: ReachabilityResult = {\n verdict,\n paths: reachingPaths,\n unresolvedSymbols: verdict === 'under_investigation' ? unresolvedSymbols : [],\n reachingEntryPointCount: reachingPaths.length,\n };\n\n // Store result in sec_reachability table\n this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result);\n\n return result;\n }\n\n /**\n * Produce impact summary for an affected vulnerability.\n *\n * Returns null if the vulnerability is not affected.\n * Reads layer assignments from `arch_file_layers` table.\n *\n * Requirements: 5.1, 5.2, 5.3, 5.4, 5.5\n */\n async getImpactSummary(vulnerabilityNodeId: string): Promise<ImpactSummary | null> {\n const rawDb = this.db.getRawDb();\n\n // Check if the vulnerability has verdict 'affected'\n const reachRow = rawDb.get(\n `SELECT verdict, paths, reaching_entry_point_count FROM sec_reachability WHERE vulnerability_node_id = ?`,\n [vulnerabilityNodeId],\n );\n\n if (!reachRow || reachRow.verdict !== 'affected') {\n return null;\n }\n\n const paths: ReachabilityPath[] = reachRow.paths ? JSON.parse(reachRow.paths) : [];\n\n if (paths.length === 0) {\n return null;\n }\n\n // Collect all node IDs on reachable paths\n const allNodeIds = new Set<string>();\n const affectedEntryPoints: string[] = [];\n\n for (const p of paths) {\n affectedEntryPoints.push(p.entryPoint);\n for (const nodeId of p.path) {\n allNodeIds.add(nodeId);\n }\n }\n\n // Identify architectural layers on reachable paths\n const affectedLayers = new Set<string>();\n\n if (this.config.enableArchitecture) {\n // Get file paths for all nodes on reachable paths\n const nodeIds = Array.from(allNodeIds);\n if (nodeIds.length > 0) {\n const placeholders = nodeIds.map(() => '?').join(',');\n const nodeFiles: Array<{ id: string; file_path: string }> = rawDb.all(\n `SELECT id, file_path FROM nodes WHERE id IN (${placeholders})`,\n nodeIds,\n );\n\n // Get layer assignments for those files from arch_file_layers\n const filePaths = [...new Set(nodeFiles.map(n => n.file_path).filter(Boolean))];\n if (filePaths.length > 0) {\n const filePlaceholders = filePaths.map(() => '?').join(',');\n const layerRows: Array<{ file_path: string; layer_id: string }> = rawDb.all(\n `SELECT file_path, layer_id FROM arch_file_layers WHERE file_path IN (${filePlaceholders})`,\n filePaths,\n );\n\n const fileToLayer = new Map<string, string>();\n for (const row of layerRows) {\n fileToLayer.set(row.file_path, row.layer_id);\n }\n\n // Assign layers to nodes; classify as \"unclassified\" if no layer assigned\n for (const nf of nodeFiles) {\n if (nf.file_path) {\n const layer = fileToLayer.get(nf.file_path);\n affectedLayers.add(layer ?? 'unclassified');\n }\n }\n }\n }\n }\n // When enableArchitecture is false, omit layer classification (Requirement 5.4)\n\n // Count distinct paths (capped at MAX_DISTINCT_PATHS)\n // Two paths are distinct if they differ by at least one intermediate symbol\n const distinctPathSet = new Set<string>();\n for (const p of paths) {\n // Use the full path as a key for distinctness\n const pathKey = p.path.join('\u2192');\n distinctPathSet.add(pathKey);\n if (distinctPathSet.size >= MAX_DISTINCT_PATHS) break;\n }\n\n const summary: ImpactSummary = {\n affectedLayers: Array.from(affectedLayers),\n affectedEntryPoints,\n distinctPathCount: Math.min(distinctPathSet.size, MAX_DISTINCT_PATHS),\n };\n\n // Store impact summary in sec_impact table\n this.storeImpactSummary(rawDb, vulnerabilityNodeId, summary);\n\n return summary;\n }\n\n /**\n * Analyze all Vulnerability_Nodes in the graph.\n *\n * Iterates all vulnerability nodes and calls analyze() for each.\n */\n async analyzeAll(): Promise<Map<string, ReachabilityResult>> {\n const rawDb = this.db.getRawDb();\n const results = new Map<string, ReachabilityResult>();\n\n const vulnRows: Array<{ node_id: string }> = rawDb.all(\n `SELECT node_id FROM sec_vulnerabilities`,\n );\n\n for (const row of vulnRows) {\n const result = await this.analyze(row.node_id);\n results.set(row.node_id, result);\n }\n\n return results;\n }\n\n /**\n * BFS from a single entry point toward the dependency node.\n *\n * Uses outgoing edges of types: calls, imports, references.\n * Visits each node at most once (handles cycles).\n * Tracks unresolved imports encountered during traversal.\n *\n * Returns the shortest path if found, plus any unresolved symbols encountered.\n */\n private bfsFromEntryPoint(\n rawDb: any,\n entryPointId: string,\n dependencyNodeId: string,\n ): { path: string[] | null; unresolvedSymbols: string[] } {\n const visited = new Set<string>();\n const unresolvedSymbols: string[] = [];\n\n // BFS queue: each entry is [nodeId, path from entry point]\n const queue: Array<[string, string[]]> = [[entryPointId, [entryPointId]]];\n visited.add(entryPointId);\n\n // Prepare edge kind filter\n const edgeKinds = TRAVERSAL_EDGE_KINDS.map(k => `'${k}'`).join(',');\n\n while (queue.length > 0) {\n const [currentId, currentPath] = queue.shift()!;\n\n // Get outgoing edges from current node (calls, imports, references)\n const outEdges: Array<{ target: string; kind: string }> = rawDb.all(\n `SELECT target, kind FROM edges WHERE source = ? AND kind IN (${edgeKinds})`,\n [currentId],\n );\n\n // Check if current node is an unresolved import\n // An unresolved import is a node with kind='import' that has no outgoing edges\n if (outEdges.length === 0) {\n const nodeRow = rawDb.get(\n `SELECT kind FROM nodes WHERE id = ?`,\n [currentId],\n );\n if (nodeRow && nodeRow.kind === 'import') {\n unresolvedSymbols.push(currentId);\n }\n }\n\n for (const edge of outEdges) {\n const targetId = edge.target;\n\n // Check if we reached the dependency node\n if (targetId === dependencyNodeId) {\n return {\n path: [...currentPath, targetId],\n unresolvedSymbols,\n };\n }\n\n // Skip already visited nodes (cycle handling)\n if (visited.has(targetId)) continue;\n visited.add(targetId);\n\n queue.push([targetId, [...currentPath, targetId]]);\n }\n }\n\n // No path found from this entry point\n return { path: null, unresolvedSymbols };\n }\n\n /**\n * Store reachability result in sec_reachability table.\n */\n private storeReachabilityResult(\n rawDb: any,\n vulnerabilityNodeId: string,\n result: ReachabilityResult,\n ): void {\n rawDb.run(\n `INSERT OR REPLACE INTO sec_reachability\n (vulnerability_node_id, verdict, paths, unresolved_symbols, reaching_entry_point_count, analyzed_at)\n VALUES (?, ?, ?, ?, ?, ?)`,\n [\n vulnerabilityNodeId,\n result.verdict,\n result.paths.length > 0 ? JSON.stringify(result.paths) : null,\n result.unresolvedSymbols.length > 0 ? JSON.stringify(result.unresolvedSymbols) : null,\n result.reachingEntryPointCount,\n Date.now(),\n ],\n );\n }\n\n /**\n * Store impact summary in sec_impact table.\n */\n private storeImpactSummary(\n rawDb: any,\n vulnerabilityNodeId: string,\n summary: ImpactSummary,\n ): void {\n rawDb.run(\n `INSERT OR REPLACE INTO sec_impact\n (vulnerability_node_id, affected_layers, affected_entry_points, distinct_path_count, analyzed_at)\n VALUES (?, ?, ?, ?, ?)`,\n [\n vulnerabilityNodeId,\n JSON.stringify(summary.affectedLayers),\n JSON.stringify(summary.affectedEntryPoints),\n summary.distinctPathCount,\n Date.now(),\n ],\n );\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBA,MAAM,uBAAuB,CAAC,SAAS,WAAW,YAAY;AAG9D,MAAM,yBAAyB;AAG/B,MAAM,qBAAqB;AAMpB,MAAM,qBAAqB;AAAA,EAIhC,YAAY,IAAmB,QAAyB;AACtD,SAAK,KAAK;AACV,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,QAAQ,qBAA0D;AACtE,UAAM,QAAQ,KAAK,GAAG,SAAS;AAG/B,UAAM,UAAU,MAAM;AAAA,MACpB;AAAA,MACA,CAAC,mBAAmB;AAAA,IACtB;AAEA,QAAI,CAAC,SAAS;AAEZ,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO,CAAC;AAAA,QACR,mBAAmB,CAAC;AAAA,QACpB,yBAAyB;AAAA,MAC3B;AAAA,IACF;AAEA,UAAM,mBAA2B,QAAQ;AAIzC,UAAM,cAAqC,MAAM;AAAA,MAC/C;AAAA;AAAA;AAAA,IAGF;AAEA,QAAI,YAAY,WAAW,GAAG;AAE5B,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO,CAAC;AAAA,QACR,mBAAmB,CAAC;AAAA,QACpB,yBAAyB;AAAA,MAC3B;AAAA,IACF;
|
|
6
|
-
"names": [
|
|
4
|
+
"sourcesContent": ["/**\n * KiroGraph-Sec Reachability Analyzer\n *\n * Traverses the knowledge graph from application entry points to determine\n * whether vulnerable dependencies are reachable through actual code paths.\n * Produces reachability verdicts and impact summaries enriched with\n * architectural layer context.\n *\n * Requirements: 4.1, 4.2, 4.3, 4.4, 4.6, 5.1, 5.2, 5.3, 5.4, 5.5\n */\n\nimport type { GraphDatabase } from '../db/database';\nimport type { KiroGraphConfig } from '../config';\nimport type {\n ReachabilityVerdict,\n ReachabilityPath,\n ReachabilityResult,\n ImpactSummary,\n} from './types';\n\n/** Edge kinds used for reachability traversal */\nconst TRAVERSAL_EDGE_KINDS = ['calls', 'imports', 'references'] as const;\n\n/** Maximum unresolved symbols to report (for the under_investigation verdict) */\nconst MAX_UNRESOLVED_SYMBOLS = 50;\n\n/** Maximum distinct paths to report in impact summary */\nconst MAX_DISTINCT_PATHS = 100;\n\n/**\n * ReachabilityAnalyzer traverses the knowledge graph from entry points\n * toward affected dependency nodes to determine reachability verdicts.\n */\nexport class ReachabilityAnalyzer {\n private readonly db: GraphDatabase;\n private readonly config: KiroGraphConfig;\n\n constructor(db: GraphDatabase, config: KiroGraphConfig) {\n this.db = db;\n this.config = config;\n }\n\n /**\n * Analyze reachability for a single Vulnerability_Node.\n *\n * 1. Find the Dependency_Node linked via `has_vulnerability` edge\n * 2. Find all Entry_Points in the graph\n * 3. Reverse BFS from the dependency node through incoming edges toward entry points\n * 4. Assign verdict based on path existence and unresolved imports\n *\n * Requirements: 4.1, 4.2, 4.3, 4.4, 4.6\n */\n async analyze(vulnerabilityNodeId: string): Promise<ReachabilityResult> {\n const rawDb = this.db.getRawDb();\n\n // Step 1: Find the Dependency_Node linked to this vulnerability\n const depEdge = rawDb.get(\n `SELECT source FROM edges WHERE target = ? AND kind = 'has_vulnerability'`,\n [vulnerabilityNodeId],\n );\n\n if (!depEdge) {\n // No dependency linked \u2014 cannot determine reachability\n return {\n verdict: 'under_investigation',\n paths: [],\n unresolvedSymbols: [],\n reachingEntryPointCount: 0,\n };\n }\n\n const dependencyNodeId: string = depEdge.source;\n\n // Step 2: Find all Entry_Points\n // Entry points are: nodes with kind='route' OR nodes with kind='function' that are exported\n const entryPoints: Array<{ id: string }> = rawDb.all(\n `SELECT id FROM nodes WHERE kind = 'route'\n UNION\n SELECT id FROM nodes WHERE kind = 'function' AND is_exported = 1`,\n );\n\n if (entryPoints.length === 0) {\n // No entry points \u2014 cannot determine reachability\n return {\n verdict: 'not_affected',\n paths: [],\n unresolvedSymbols: [],\n reachingEntryPointCount: 0,\n };\n }\n\n // Step 3: Reverse BFS from the dependency node through INCOMING edges\n // to find which entry points can reach it. This is O(V+E) per vulnerability\n // instead of O(entryPoints \u00D7 (V+E)) for forward BFS from each entry point.\n const reachingPaths: ReachabilityPath[] = [];\n const entryPointIds = new Set(entryPoints.map(ep => ep.id));\n const { reached, unresolvedSymbols } = this.reverseBfsToEntryPoints(\n rawDb,\n dependencyNodeId,\n entryPointIds,\n );\n for (const { entryPointId, path } of reached) {\n reachingPaths.push({\n entryPoint: entryPointId,\n path,\n });\n }\n\n // Step 4: Assign verdict.\n // - affected: at least one entry point reaches the dependency.\n // - under_investigation: no path found, but the backward traversal hit\n // unresolved imports (dead-end import nodes). The graph is incomplete\n // near the dependency (dynamic dispatch, reflection, unindexed code),\n // so \"not reachable\" cannot be concluded \u2014 treat with caution.\n // - not_affected: no path found and no unresolved imports \u2014 strong signal.\n const cappedUnresolved = unresolvedSymbols.slice(0, MAX_UNRESOLVED_SYMBOLS);\n let verdict: ReachabilityVerdict;\n if (reachingPaths.length > 0) {\n verdict = 'affected';\n } else if (cappedUnresolved.length > 0) {\n verdict = 'under_investigation';\n } else {\n verdict = 'not_affected';\n }\n\n const result: ReachabilityResult = {\n verdict,\n paths: reachingPaths,\n unresolvedSymbols: verdict === 'under_investigation' ? cappedUnresolved : [],\n reachingEntryPointCount: reachingPaths.length,\n };\n\n // Store result in sec_reachability table\n this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result);\n\n return result;\n }\n\n /**\n * Produce impact summary for an affected vulnerability.\n *\n * Returns null if the vulnerability is not affected.\n * Reads layer assignments from `arch_file_layers` table.\n *\n * Requirements: 5.1, 5.2, 5.3, 5.4, 5.5\n */\n async getImpactSummary(vulnerabilityNodeId: string): Promise<ImpactSummary | null> {\n const rawDb = this.db.getRawDb();\n\n // Check if the vulnerability has verdict 'affected'\n const reachRow = rawDb.get(\n `SELECT verdict, paths, reaching_entry_point_count FROM sec_reachability WHERE vulnerability_node_id = ?`,\n [vulnerabilityNodeId],\n );\n\n if (!reachRow || reachRow.verdict !== 'affected') {\n return null;\n }\n\n const paths: ReachabilityPath[] = reachRow.paths ? JSON.parse(reachRow.paths) : [];\n\n if (paths.length === 0) {\n return null;\n }\n\n // Collect all node IDs on reachable paths\n const allNodeIds = new Set<string>();\n const affectedEntryPoints: string[] = [];\n\n for (const p of paths) {\n affectedEntryPoints.push(p.entryPoint);\n for (const nodeId of p.path) {\n allNodeIds.add(nodeId);\n }\n }\n\n // Identify architectural layers on reachable paths\n const affectedLayers = new Set<string>();\n\n if (this.config.enableArchitecture) {\n // Get file paths for all nodes on reachable paths\n const nodeIds = Array.from(allNodeIds);\n if (nodeIds.length > 0) {\n const placeholders = nodeIds.map(() => '?').join(',');\n const nodeFiles: Array<{ id: string; file_path: string }> = rawDb.all(\n `SELECT id, file_path FROM nodes WHERE id IN (${placeholders})`,\n nodeIds,\n );\n\n // Get layer assignments for those files from arch_file_layers\n const filePaths = [...new Set(nodeFiles.map(n => n.file_path).filter(Boolean))];\n if (filePaths.length > 0) {\n const filePlaceholders = filePaths.map(() => '?').join(',');\n const layerRows: Array<{ file_path: string; layer_id: string }> = rawDb.all(\n `SELECT file_path, layer_id FROM arch_file_layers WHERE file_path IN (${filePlaceholders})`,\n filePaths,\n );\n\n const fileToLayer = new Map<string, string>();\n for (const row of layerRows) {\n fileToLayer.set(row.file_path, row.layer_id);\n }\n\n // Assign layers to nodes; classify as \"unclassified\" if no layer assigned\n for (const nf of nodeFiles) {\n if (nf.file_path) {\n const layer = fileToLayer.get(nf.file_path);\n affectedLayers.add(layer ?? 'unclassified');\n }\n }\n }\n }\n }\n // When enableArchitecture is false, omit layer classification (Requirement 5.4)\n\n // Count distinct paths (capped at MAX_DISTINCT_PATHS)\n // Two paths are distinct if they differ by at least one intermediate symbol\n const distinctPathSet = new Set<string>();\n for (const p of paths) {\n // Use the full path as a key for distinctness\n const pathKey = p.path.join('\u2192');\n distinctPathSet.add(pathKey);\n if (distinctPathSet.size >= MAX_DISTINCT_PATHS) break;\n }\n\n const summary: ImpactSummary = {\n affectedLayers: Array.from(affectedLayers),\n affectedEntryPoints,\n distinctPathCount: Math.min(distinctPathSet.size, MAX_DISTINCT_PATHS),\n };\n\n // Store impact summary in sec_impact table\n this.storeImpactSummary(rawDb, vulnerabilityNodeId, summary);\n\n return summary;\n }\n\n /**\n * Analyze all Vulnerability_Nodes in the graph.\n *\n * Iterates all vulnerability nodes and calls analyze() for each.\n */\n async analyzeAll(): Promise<Map<string, ReachabilityResult>> {\n const rawDb = this.db.getRawDb();\n const results = new Map<string, ReachabilityResult>();\n\n const vulnRows: Array<{ node_id: string }> = rawDb.all(\n `SELECT node_id FROM sec_vulnerabilities`,\n );\n\n for (const row of vulnRows) {\n const result = await this.analyze(row.node_id);\n results.set(row.node_id, result);\n }\n\n return results;\n }\n\n /**\n * Reverse BFS: start from the dependency node and walk BACKWARDS through\n * incoming edges to find which entry points can reach it.\n *\n * This is O(V+E) total (one traversal) regardless of how many entry points exist,\n * compared to O(entryPoints \u00D7 (V+E)) for forward BFS from each entry point.\n *\n * While traversing, collects \"unresolved imports\" \u2014 import-kind nodes with no\n * outgoing traversal edges. These are dead ends in the forward graph: the\n * traversal reached them but couldn't follow them to a definition (dynamic\n * dispatch, reflection, unindexed code). When no entry point reaches the\n * dependency but such imports were encountered on the backward frontier,\n * \"not reachable\" cannot be concluded and the caller reports\n * under_investigation instead of not_affected.\n *\n * Returns the entry points that have a path to the dependency (with their\n * paths) and the set of unresolved import node IDs encountered.\n */\n private reverseBfsToEntryPoints(\n rawDb: any,\n dependencyNodeId: string,\n entryPointIds: Set<string>,\n ): {\n reached: Array<{ entryPointId: string; path: string[] }>;\n unresolvedSymbols: string[];\n } {\n const reached: Array<{ entryPointId: string; path: string[] }> = [];\n const unresolvedSymbols = new Set<string>();\n const visited = new Set<string>();\n const parentMap = new Map<string, string>(); // child \u2192 parent (for path reconstruction)\n\n const edgeKinds = TRAVERSAL_EDGE_KINDS.map(k => `'${k}'`).join(',');\n const queue: string[] = [dependencyNodeId];\n visited.add(dependencyNodeId);\n\n while (queue.length > 0) {\n const currentId = queue.shift()!;\n\n // Check if we reached an entry point\n if (currentId !== dependencyNodeId && entryPointIds.has(currentId)) {\n // Reconstruct path from entry point to dependency\n const path: string[] = [];\n let node: string | undefined = currentId;\n while (node !== undefined) {\n path.push(node);\n node = parentMap.get(node);\n }\n // path is [entryPoint, ..., dependencyNode] \u2014 already correct direction\n reached.push({ entryPointId: currentId, path });\n // Keep traversing \u2014 other entry points may reach this one transitively\n // (e.g., main() \u2192 authMiddleware() \u2192 dep \u2014 both should be reported)\n }\n\n // Walk INCOMING edges (reverse direction: who calls/imports currentId?)\n const inEdges: Array<{ source: string }> = rawDb.all(\n `SELECT source FROM edges WHERE target = ? AND kind IN (${edgeKinds})`,\n [currentId],\n );\n\n // An import node reached via incoming edges that has no outgoing traversal\n // edges is an unresolved import \u2014 a dead end that leaves reachability\n // inconclusive. Detect it here so the verdict can fall back to\n // under_investigation rather than a false not_affected.\n if (currentId !== dependencyNodeId) {\n const outCount: { c: number } = rawDb.get(\n `SELECT COUNT(*) AS c FROM edges WHERE source = ? AND kind IN (${edgeKinds})`,\n [currentId],\n );\n if (outCount.c === 0) {\n const nodeRow = rawDb.get(`SELECT kind FROM nodes WHERE id = ?`, [currentId]);\n if (nodeRow && nodeRow.kind === 'import') {\n unresolvedSymbols.add(currentId);\n }\n }\n }\n\n for (const edge of inEdges) {\n if (visited.has(edge.source)) continue;\n visited.add(edge.source);\n parentMap.set(edge.source, currentId);\n queue.push(edge.source);\n }\n }\n\n return { reached, unresolvedSymbols: Array.from(unresolvedSymbols) };\n }\n\n /**\n * Store reachability result in sec_reachability table.\n */\n private storeReachabilityResult(\n rawDb: any,\n vulnerabilityNodeId: string,\n result: ReachabilityResult,\n ): void {\n rawDb.run(\n `INSERT OR REPLACE INTO sec_reachability\n (vulnerability_node_id, verdict, paths, unresolved_symbols, reaching_entry_point_count, analyzed_at)\n VALUES (?, ?, ?, ?, ?, ?)`,\n [\n vulnerabilityNodeId,\n result.verdict,\n result.paths.length > 0 ? JSON.stringify(result.paths) : null,\n result.unresolvedSymbols.length > 0 ? JSON.stringify(result.unresolvedSymbols) : null,\n result.reachingEntryPointCount,\n Date.now(),\n ],\n );\n }\n\n /**\n * Store impact summary in sec_impact table.\n */\n private storeImpactSummary(\n rawDb: any,\n vulnerabilityNodeId: string,\n summary: ImpactSummary,\n ): void {\n rawDb.run(\n `INSERT OR REPLACE INTO sec_impact\n (vulnerability_node_id, affected_layers, affected_entry_points, distinct_path_count, analyzed_at)\n VALUES (?, ?, ?, ?, ?)`,\n [\n vulnerabilityNodeId,\n JSON.stringify(summary.affectedLayers),\n JSON.stringify(summary.affectedEntryPoints),\n summary.distinctPathCount,\n Date.now(),\n ],\n );\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/security/types.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * KiroGraph-Sec Type Definitions\n *\n * Interfaces for the security analysis module including manifest parsing,\n * vulnerability detection, reachability analysis, and CycloneDX export.\n */\n\n// \u2500\u2500 Manifest Parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ParsedDependency {\n name: string;\n declaredConstraint: string;\n resolvedVersion?: string; // from lock file\n scope: 'production' | 'development' | 'optional';\n ecosystem: string;\n sourceManifest: string; // relative path to declaring manifest\n license?: string; // SPDX expression or license name, if available in the manifest\n}\n\nexport interface ManifestParseResult {\n dependenciesCreated: number;\n manifestsParsed: number;\n warnings: Array<{ file: string; line?: number; message: string }>;\n errors: Array<{ file: string; message: string }>;\n}\n\n// \u2500\u2500 Dependency Graph Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface IntegrationResult {\n importsEdgesCreated: number;\n referencesEdgesCreated: number;\n}\n\nexport interface TransitiveResult {\n dependsOnEdgesCreated: number;\n incompleteNodes: string[]; // nodes marked with transitive_status=incomplete\n}\n\nexport interface CleanupResult {\n nodesRemoved: number;\n edgesRemoved: number;\n}\n\n// \u2500\u2500 Vulnerability Database \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CVERecord {\n id: string; // e.g. \"CVE-2023-12345\"\n severity: number; // CVSS
|
|
4
|
+
"sourcesContent": ["/**\n * KiroGraph-Sec Type Definitions\n *\n * Interfaces for the security analysis module including manifest parsing,\n * vulnerability detection, reachability analysis, and CycloneDX export.\n */\n\n// \u2500\u2500 Manifest Parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ParsedDependency {\n name: string;\n declaredConstraint: string;\n resolvedVersion?: string; // from lock file\n scope: 'production' | 'development' | 'optional';\n ecosystem: string;\n sourceManifest: string; // relative path to declaring manifest\n license?: string; // SPDX expression or license name, if available in the manifest\n}\n\nexport interface ManifestParseResult {\n dependenciesCreated: number;\n manifestsParsed: number;\n warnings: Array<{ file: string; line?: number; message: string }>;\n errors: Array<{ file: string; message: string }>;\n}\n\n// \u2500\u2500 Dependency Graph Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface IntegrationResult {\n importsEdgesCreated: number;\n referencesEdgesCreated: number;\n}\n\nexport interface TransitiveResult {\n dependsOnEdgesCreated: number;\n incompleteNodes: string[]; // nodes marked with transitive_status=incomplete\n}\n\nexport interface CleanupResult {\n nodesRemoved: number;\n edgesRemoved: number;\n}\n\n// \u2500\u2500 Vulnerability Database \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CVERecord {\n id: string; // e.g. \"CVE-2023-12345\"\n severity: number | null; // CVSS base score (0.0\u201310.0), or null when unknown\n affectedVersionRanges: VersionRange[];\n fixedVersion?: string;\n summary: string;\n}\n\nexport interface VersionRange {\n introduced?: string;\n fixed?: string;\n lastAffected?: string;\n}\n\nexport interface EnrichmentResult {\n vulnerabilitiesFound: number;\n dependenciesChecked: number;\n errors: Array<{ dependency: string; database: string; error: string }>;\n staleNodes: string[]; // nodes where vulnDataStale was set\n}\n\n// \u2500\u2500 Reachability Analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type ReachabilityVerdict = 'affected' | 'not_affected' | 'under_investigation';\n\nexport interface ReachabilityPath {\n entryPoint: string; // node ID of the entry point\n path: string[]; // ordered list of node IDs from entry to dependency\n}\n\nexport interface ReachabilityResult {\n verdict: ReachabilityVerdict;\n paths: ReachabilityPath[];\n unresolvedSymbols: string[]; // up to 50, for under_investigation\n reachingEntryPointCount: number;\n}\n\nexport interface ImpactSummary {\n affectedLayers: string[]; // e.g. ['api', 'service']\n affectedEntryPoints: string[]; // node IDs of reaching entry points\n distinctPathCount: number; // capped at 100\n}\n\n// \u2500\u2500 Security Pipeline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface SecurityResult {\n manifestsDiscovered: number;\n dependenciesCreated: number;\n vulnerabilitiesFound: number;\n affectedCount: number;\n notAffectedCount: number;\n underInvestigationCount: number;\n duration: number;\n}\n\n// \u2500\u2500 CycloneDX SBOM Export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface SBOMMetadata {\n toolName: string;\n toolVersion: string;\n timestamp: string; // ISO 8601 UTC\n projectName: string;\n}\n\nexport interface CycloneDXSBOM {\n bomFormat: 'CycloneDX';\n specVersion: '1.5';\n version: number;\n metadata: Record<string, unknown>;\n components: CycloneDXComponent[];\n dependencies: CycloneDXDependencyEntry[];\n}\n\nexport interface CycloneDXComponent {\n type: 'library';\n name: string;\n version: string;\n purl: string;\n scope?: 'required' | 'optional';\n properties?: Array<{ name: string; value: string }>;\n}\n\nexport interface CycloneDXDependencyEntry {\n ref: string;\n dependsOn?: string[];\n}\n\n// \u2500\u2500 CycloneDX VEX Export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CycloneDXVEX {\n bomFormat: 'CycloneDX';\n specVersion: '1.5';\n version: number;\n metadata: Record<string, unknown>;\n vulnerabilities: CycloneDXVulnerability[];\n}\n\nexport interface CycloneDXVulnerability {\n id: string;\n source?: { name: string; url?: string };\n ratings?: Array<{ score: number; severity?: string; method?: string }>;\n analysis?: {\n state: 'affected' | 'not_affected' | 'under_investigation';\n justification?: string;\n detail?: string;\n };\n affects?: Array<{ ref: string }>;\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;AAAA;AAAA;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -23,7 +23,7 @@ __export(epss_client_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(epss_client_exports);
|
|
24
24
|
var import_errors = require("../../errors");
|
|
25
25
|
const EPSS_API_URL = "https://api.first.org/data/v1/epss";
|
|
26
|
-
const CHUNK_SIZE =
|
|
26
|
+
const CHUNK_SIZE = 100;
|
|
27
27
|
const TIMEOUT_MS = 3e4;
|
|
28
28
|
class EpssClient {
|
|
29
29
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/security/vuln/epss-client.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * EPSS (Exploit Prediction Scoring System) Client\n *\n * Fetches exploit prediction scores from the FIRST.org EPSS API for CVE IDs.\n * EPSS is non-critical: on error, an empty map is returned so the caller can\n * continue without scores.\n *\n * API: https://api.first.org/data/v1/epss\n */\n\nimport { logWarn, logError } from '../../errors';\n\nconst EPSS_API_URL = 'https://api.first.org/data/v1/epss';\nconst CHUNK_SIZE =
|
|
4
|
+
"sourcesContent": ["/**\n * EPSS (Exploit Prediction Scoring System) Client\n *\n * Fetches exploit prediction scores from the FIRST.org EPSS API for CVE IDs.\n * EPSS is non-critical: on error, an empty map is returned so the caller can\n * continue without scores.\n *\n * API: https://api.first.org/data/v1/epss\n */\n\nimport { logWarn, logError } from '../../errors';\n\nconst EPSS_API_URL = 'https://api.first.org/data/v1/epss';\nconst CHUNK_SIZE = 100; // Keep URL under 2048-char limit (100 CVEs \u00D7 ~16 chars each)\nconst TIMEOUT_MS = 30000;\n\ninterface EpssApiResponse {\n status: string;\n status_code: number;\n data: Array<{\n cve: string;\n epss: string;\n percentile: string;\n date: string;\n }>;\n}\n\nexport class EpssClient {\n /**\n * Fetch EPSS scores for a list of CVE IDs.\n *\n * CVEs are batched into chunks of 500 per request. Missing CVEs simply do\n * not appear in the response and are omitted from the returned map.\n *\n * @param cveIds - List of CVE identifiers (e.g. [\"CVE-2021-44228\", ...])\n * @returns Map from CVE ID to { score, percentile } (floats in [0, 1])\n */\n async fetchScores(cveIds: string[]): Promise<Map<string, { score: number; percentile: number }>> {\n const result = new Map<string, { score: number; percentile: number }>();\n\n if (cveIds.length === 0) {\n return result;\n }\n\n for (let i = 0; i < cveIds.length; i += CHUNK_SIZE) {\n const chunk = cveIds.slice(i, i + CHUNK_SIZE);\n try {\n const chunkResult = await this.fetchChunk(chunk);\n for (const [cve, scores] of chunkResult) {\n result.set(cve, scores);\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n logWarn(`[sec:epss] Failed to fetch EPSS scores for chunk of ${chunk.length} CVEs: ${msg}`);\n // EPSS is non-critical \u2014 continue with partial results\n }\n }\n\n return result;\n }\n\n private async fetchChunk(\n cveIds: string[],\n ): Promise<Map<string, { score: number; percentile: number }>> {\n const result = new Map<string, { score: number; percentile: number }>();\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);\n\n try {\n const cveParam = cveIds.join(',');\n const url = `${EPSS_API_URL}?cve=${encodeURIComponent(cveParam)}`;\n\n const response = await fetch(url, { signal: controller.signal });\n\n if (!response.ok) {\n logWarn(`[sec:epss] EPSS API returned HTTP ${response.status}`);\n return result;\n }\n\n const json = (await response.json()) as EpssApiResponse;\n\n if (json.status !== 'OK' || !Array.isArray(json.data)) {\n logWarn(`[sec:epss] Unexpected EPSS API response structure`);\n return result;\n }\n\n for (const entry of json.data) {\n const score = parseFloat(entry.epss);\n const percentile = parseFloat(entry.percentile);\n if (!isNaN(score) && !isNaN(percentile)) {\n result.set(entry.cve, { score, percentile });\n }\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n logError(`[sec:epss] Error fetching EPSS chunk: ${msg}`);\n throw error;\n } finally {\n clearTimeout(timeout);\n }\n\n return result;\n }\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,oBAAkC;AAElC,MAAM,eAAe;AACrB,MAAM,aAAa;AACnB,MAAM,aAAa;AAaZ,MAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUtB,MAAM,YAAY,QAA+E;AAC/F,UAAM,SAAS,oBAAI,IAAmD;AAEtE,QAAI,OAAO,WAAW,GAAG;AACvB,aAAO;AAAA,IACT;AAEA,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,YAAY;AAClD,YAAM,QAAQ,OAAO,MAAM,GAAG,IAAI,UAAU;AAC5C,UAAI;AACF,cAAM,cAAc,MAAM,KAAK,WAAW,KAAK;AAC/C,mBAAW,CAAC,KAAK,MAAM,KAAK,aAAa;AACvC,iBAAO,IAAI,KAAK,MAAM;AAAA,QACxB;AAAA,MACF,SAAS,OAAgB;AACvB,cAAM,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACjE,mCAAQ,uDAAuD,MAAM,MAAM,UAAU,GAAG,EAAE;AAAA,MAE5F;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,WACZ,QAC6D;AAC7D,UAAM,SAAS,oBAAI,IAAmD;AAEtE,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,UAAU;AAE/D,QAAI;AACF,YAAM,WAAW,OAAO,KAAK,GAAG;AAChC,YAAM,MAAM,GAAG,YAAY,QAAQ,mBAAmB,QAAQ,CAAC;AAE/D,YAAM,WAAW,MAAM,MAAM,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC;AAE/D,UAAI,CAAC,SAAS,IAAI;AAChB,mCAAQ,qCAAqC,SAAS,MAAM,EAAE;AAC9D,eAAO;AAAA,MACT;AAEA,YAAM,OAAQ,MAAM,SAAS,KAAK;AAElC,UAAI,KAAK,WAAW,QAAQ,CAAC,MAAM,QAAQ,KAAK,IAAI,GAAG;AACrD,mCAAQ,mDAAmD;AAC3D,eAAO;AAAA,MACT;AAEA,iBAAW,SAAS,KAAK,MAAM;AAC7B,cAAM,QAAQ,WAAW,MAAM,IAAI;AACnC,cAAM,aAAa,WAAW,MAAM,UAAU;AAC9C,YAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,UAAU,GAAG;AACvC,iBAAO,IAAI,MAAM,KAAK,EAAE,OAAO,WAAW,CAAC;AAAA,QAC7C;AAAA,MACF;AAAA,IACF,SAAS,OAAgB;AACvB,YAAM,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACjE,kCAAS,yCAAyC,GAAG,EAAE;AACvD,YAAM;AAAA,IACR,UAAE;AACA,mBAAa,OAAO;AAAA,IACtB;AAEA,WAAO;AAAA,EACT;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -18,7 +18,10 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var osv_adapter_exports = {};
|
|
20
20
|
__export(osv_adapter_exports, {
|
|
21
|
-
OsvAdapter: () => OsvAdapter
|
|
21
|
+
OsvAdapter: () => OsvAdapter,
|
|
22
|
+
computeCvssV3BaseScore: () => computeCvssV3BaseScore,
|
|
23
|
+
parseCvssScore: () => parseCvssScore,
|
|
24
|
+
qualitativeToScore: () => qualitativeToScore
|
|
22
25
|
});
|
|
23
26
|
module.exports = __toCommonJS(osv_adapter_exports);
|
|
24
27
|
var import_errors = require("../errors");
|
|
@@ -28,10 +31,14 @@ const ECOSYSTEM_MAP = {
|
|
|
28
31
|
maven: "Maven",
|
|
29
32
|
go: "Go",
|
|
30
33
|
pypi: "PyPI",
|
|
34
|
+
python: "PyPI",
|
|
35
|
+
// manifest parser sends 'python' for requirements.txt
|
|
31
36
|
cargo: "crates.io",
|
|
32
37
|
pyproject: "PyPI",
|
|
33
38
|
// pyproject.toml (Poetry/Hatch/PDM/PEP 621)
|
|
34
39
|
nuget: "NuGet",
|
|
40
|
+
csproj: "NuGet",
|
|
41
|
+
// manifest parser sends 'csproj' for .csproj files
|
|
35
42
|
gradle: "Maven",
|
|
36
43
|
// Gradle projects use Maven Central
|
|
37
44
|
rubygems: "RubyGems",
|
|
@@ -45,6 +52,69 @@ const OSV_BATCH_API_URL = "https://api.osv.dev/v1/querybatch";
|
|
|
45
52
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
46
53
|
const OSV_BATCH_MAX_QUERIES = 1e3;
|
|
47
54
|
const MAX_SUMMARY_LENGTH = 500;
|
|
55
|
+
function parseCvssScore(scoreStr) {
|
|
56
|
+
if (!scoreStr) return null;
|
|
57
|
+
const trimmed = scoreStr.trim();
|
|
58
|
+
const num = parseFloat(trimmed);
|
|
59
|
+
if (!isNaN(num) && String(num) === trimmed && num >= 0 && num <= 10) {
|
|
60
|
+
return num;
|
|
61
|
+
}
|
|
62
|
+
if (/^CVSS:3\.[01]\//i.test(trimmed)) {
|
|
63
|
+
return computeCvssV3BaseScore(trimmed);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function cvssRoundUp(value) {
|
|
68
|
+
const intInput = Math.round(value * 1e5);
|
|
69
|
+
if (intInput % 1e4 === 0) return intInput / 1e5;
|
|
70
|
+
return (Math.floor(intInput / 1e4) + 1) / 10;
|
|
71
|
+
}
|
|
72
|
+
function computeCvssV3BaseScore(vector) {
|
|
73
|
+
const metrics = {};
|
|
74
|
+
for (const part of vector.split("/")) {
|
|
75
|
+
const [k, v] = part.split(":");
|
|
76
|
+
if (k && v) metrics[k.toUpperCase()] = v.toUpperCase();
|
|
77
|
+
}
|
|
78
|
+
const AV = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 }[metrics.AV];
|
|
79
|
+
const AC = { L: 0.77, H: 0.44 }[metrics.AC];
|
|
80
|
+
const UI = { N: 0.85, R: 0.62 }[metrics.UI];
|
|
81
|
+
const scopeChanged = metrics.S === "C";
|
|
82
|
+
const PR = (() => {
|
|
83
|
+
if (metrics.PR === "N") return 0.85;
|
|
84
|
+
if (metrics.PR === "L") return scopeChanged ? 0.68 : 0.62;
|
|
85
|
+
if (metrics.PR === "H") return scopeChanged ? 0.5 : 0.27;
|
|
86
|
+
return void 0;
|
|
87
|
+
})();
|
|
88
|
+
const impactVal = { H: 0.56, L: 0.22, N: 0 };
|
|
89
|
+
const C = impactVal[metrics.C];
|
|
90
|
+
const I = impactVal[metrics.I];
|
|
91
|
+
const A = impactVal[metrics.A];
|
|
92
|
+
if ([AV, AC, UI, PR, C, I, A].some((v) => v === void 0)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const iscBase = 1 - (1 - C) * (1 - I) * (1 - A);
|
|
96
|
+
const impact = scopeChanged ? 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase - 0.02, 15) : 6.42 * iscBase;
|
|
97
|
+
const exploitability = 8.22 * AV * AC * PR * UI;
|
|
98
|
+
if (impact <= 0) return 0;
|
|
99
|
+
const raw = scopeChanged ? Math.min(1.08 * (impact + exploitability), 10) : Math.min(impact + exploitability, 10);
|
|
100
|
+
return cvssRoundUp(raw);
|
|
101
|
+
}
|
|
102
|
+
function qualitativeToScore(label) {
|
|
103
|
+
if (!label) return null;
|
|
104
|
+
switch (label.trim().toUpperCase()) {
|
|
105
|
+
case "CRITICAL":
|
|
106
|
+
return 9.8;
|
|
107
|
+
case "HIGH":
|
|
108
|
+
return 7.5;
|
|
109
|
+
case "MODERATE":
|
|
110
|
+
case "MEDIUM":
|
|
111
|
+
return 5.5;
|
|
112
|
+
case "LOW":
|
|
113
|
+
return 3.1;
|
|
114
|
+
default:
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
48
118
|
class OsvAdapter {
|
|
49
119
|
constructor(options) {
|
|
50
120
|
this.name = "OSV";
|
|
@@ -210,40 +280,35 @@ class OsvAdapter {
|
|
|
210
280
|
return vuln.id;
|
|
211
281
|
}
|
|
212
282
|
/**
|
|
213
|
-
* Extract CVSS
|
|
214
|
-
*
|
|
283
|
+
* Extract a CVSS base score (0.0–10.0) for the vulnerability.
|
|
284
|
+
*
|
|
285
|
+
* OSV encodes severity as an array of `{ type, score }`. For CVSS_V3/CVSS_V4
|
|
286
|
+
* the `score` field is a *vector string* (e.g. "CVSS:3.1/AV:N/AC:L/...") — NOT
|
|
287
|
+
* a plain number — so the base score must be computed from the vector.
|
|
288
|
+
*
|
|
289
|
+
* Resolution order:
|
|
290
|
+
* 1. CVSS_V3 vector → computed base score
|
|
291
|
+
* 2. CVSS_V4 vector → computed base score
|
|
292
|
+
* 3. Any severity entry that happens to be a plain number
|
|
293
|
+
* 4. Coarse qualitative `database_specific.severity` (GHSA) → representative score
|
|
294
|
+
*
|
|
295
|
+
* Returns null when no severity data is available (so callers can distinguish
|
|
296
|
+
* "unknown" from a genuine 0.0 score).
|
|
215
297
|
*/
|
|
216
298
|
extractSeverity(vuln) {
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const score = this.parseCvssScore(sev.score);
|
|
223
|
-
if (score !== null) {
|
|
224
|
-
return score;
|
|
225
|
-
}
|
|
299
|
+
if (vuln.severity && vuln.severity.length > 0) {
|
|
300
|
+
const byType = (type) => vuln.severity.filter((s) => s.type === type);
|
|
301
|
+
for (const sev of [...byType("CVSS_V3"), ...byType("CVSS_V4")]) {
|
|
302
|
+
const score = parseCvssScore(sev.score);
|
|
303
|
+
if (score !== null) return score;
|
|
226
304
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (score !== null) {
|
|
231
|
-
return score;
|
|
305
|
+
for (const sev of vuln.severity) {
|
|
306
|
+
const score = parseCvssScore(sev.score);
|
|
307
|
+
if (score !== null) return score;
|
|
232
308
|
}
|
|
233
309
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Parse a CVSS vector string or numeric score.
|
|
238
|
-
* CVSS vectors look like: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
|
|
239
|
-
* The base score is not directly in the vector — we extract it if it's a plain number,
|
|
240
|
-
* or return null if we can't parse it.
|
|
241
|
-
*/
|
|
242
|
-
parseCvssScore(scoreStr) {
|
|
243
|
-
const num = parseFloat(scoreStr);
|
|
244
|
-
if (!isNaN(num) && num >= 0 && num <= 10) {
|
|
245
|
-
return num;
|
|
246
|
-
}
|
|
310
|
+
const qualitative = qualitativeToScore(vuln.database_specific?.severity);
|
|
311
|
+
if (qualitative !== null) return qualitative;
|
|
247
312
|
return null;
|
|
248
313
|
}
|
|
249
314
|
/**
|
|
@@ -316,6 +381,9 @@ function isAbortError(error) {
|
|
|
316
381
|
}
|
|
317
382
|
// Annotate the CommonJS export names for ESM import in node:
|
|
318
383
|
0 && (module.exports = {
|
|
319
|
-
OsvAdapter
|
|
384
|
+
OsvAdapter,
|
|
385
|
+
computeCvssV3BaseScore,
|
|
386
|
+
parseCvssScore,
|
|
387
|
+
qualitativeToScore
|
|
320
388
|
});
|
|
321
389
|
//# sourceMappingURL=osv-adapter.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/security/vuln/osv-adapter.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * OSV API Adapter\n *\n * Queries the OSV (Open Source Vulnerabilities) database for known CVEs\n * affecting project dependencies. Implements the VulnDatabaseAdapter interface.\n */\n\nimport { CVERecord, VersionRange } from '../types';\nimport type { VulnDatabaseAdapter, BatchQuery } from './types';\nimport { VulnDatabaseError } from '../errors';\nimport { logWarn, logError } from '../../errors';\n\nexport type { VulnDatabaseAdapter, BatchQuery };\n\n// \u2500\u2500 Ecosystem Mapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ECOSYSTEM_MAP: Record<string, string> = {\n npm: 'npm',\n maven: 'Maven',\n go: 'Go',\n pypi: 'PyPI',\n cargo: 'crates.io',\n pyproject: 'PyPI', // pyproject.toml (Poetry/Hatch/PDM/PEP 621)\n nuget: 'NuGet',\n gradle: 'Maven', // Gradle projects use Maven Central\n rubygems: 'RubyGems',\n composer: 'Packagist',\n swift: 'SwiftURL',\n pub: 'Pub',\n hex: 'Hex',\n};\n\n// \u2500\u2500 OSV Request/Response Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface OsvSeverity {\n type: string;\n score: string;\n}\n\ninterface OsvAffectedRange {\n type: string;\n events: Array<{ introduced?: string; fixed?: string; last_affected?: string }>;\n}\n\ninterface OsvAffected {\n package?: { name?: string; ecosystem?: string };\n ranges?: OsvAffectedRange[];\n versions?: string[];\n}\n\ninterface OsvVulnerability {\n id: string;\n aliases?: string[];\n summary?: string;\n details?: string;\n severity?: OsvSeverity[];\n affected?: OsvAffected[];\n}\n\ninterface OsvQueryResponse {\n vulns?: OsvVulnerability[];\n}\n\ninterface OsvBatchQueryItem {\n package: { name: string; ecosystem: string };\n version: string;\n}\n\ninterface OsvBatchResponse {\n results: Array<{ vulns?: OsvVulnerability[] }>;\n}\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst OSV_API_URL = 'https://api.osv.dev/v1/query';\nconst OSV_BATCH_API_URL = 'https://api.osv.dev/v1/querybatch';\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst OSV_BATCH_MAX_QUERIES = 1000;\nconst MAX_SUMMARY_LENGTH = 500;\n\n// \u2500\u2500 OsvAdapter Implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class OsvAdapter implements VulnDatabaseAdapter {\n public readonly name = 'OSV';\n\n private readonly apiUrl: string;\n private readonly batchApiUrl: string;\n private readonly timeoutMs: number;\n\n constructor(options?: { apiUrl?: string; batchApiUrl?: string; timeoutMs?: number }) {\n this.apiUrl = options?.apiUrl ?? OSV_API_URL;\n this.batchApiUrl = options?.batchApiUrl ?? OSV_BATCH_API_URL;\n this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n }\n\n /**\n * Batch query up to OSV_BATCH_MAX_QUERIES packages in a single HTTP request.\n * Results are returned in the same order as the input queries.\n * Queries for unsupported ecosystems produce empty results without an HTTP call.\n */\n async queryBatch(\n queries: BatchQuery[],\n signal?: AbortSignal,\n ): Promise<Array<CVERecord[]>> {\n if (queries.length === 0) return [];\n\n // Map each query to its OSV ecosystem \u2014 track indices of unsupported ones\n const osvQueries: OsvBatchQueryItem[] = [];\n const indexMap: Array<number | null> = []; // index into osvQueries, or null if unsupported\n\n for (const q of queries) {\n const osvEcosystem = ECOSYSTEM_MAP[q.ecosystem.toLowerCase()];\n if (!osvEcosystem) {\n logWarn(`OSV adapter: unsupported ecosystem \"${q.ecosystem}\", skipping batch entry`);\n indexMap.push(null);\n } else {\n indexMap.push(osvQueries.length);\n osvQueries.push({\n package: { name: q.packageName, ecosystem: osvEcosystem },\n version: q.version,\n });\n }\n }\n\n if (osvQueries.length === 0) {\n return queries.map(() => []);\n }\n\n const timeoutController = new AbortController();\n const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);\n const combinedSignal = signal\n ? combineAbortSignals(signal, timeoutController.signal)\n : timeoutController.signal;\n\n let batchResults: Array<{ vulns?: OsvVulnerability[] }>;\n\n try {\n const response = await fetch(this.batchApiUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ queries: osvQueries }),\n signal: combinedSignal,\n });\n\n if (!response.ok) {\n throw new VulnDatabaseError(\n `OSV batch API returned HTTP ${response.status}: ${response.statusText}`,\n 'OSV',\n response.status,\n );\n }\n\n const data = (await response.json()) as OsvBatchResponse;\n batchResults = data.results ?? [];\n } catch (error: unknown) {\n if (error instanceof VulnDatabaseError) throw error;\n if (isAbortError(error)) {\n const msg = timeoutController.signal.aborted\n ? `OSV batch query timed out after ${this.timeoutMs}ms (${osvQueries.length} packages)`\n : `OSV batch query aborted`;\n logError(msg);\n throw new VulnDatabaseError(msg, 'OSV');\n }\n const msg = error instanceof Error ? error.message : String(error);\n logError(`OSV batch query failed: ${msg}`);\n throw new VulnDatabaseError(`Network error in OSV batch query: ${msg}`, 'OSV');\n } finally {\n clearTimeout(timeoutId);\n }\n\n // Rebuild full results array aligned with input queries\n return queries.map((_, i) => {\n const osvIdx = indexMap[i];\n if (osvIdx === null) return [];\n const resultEntry = batchResults[osvIdx];\n if (!resultEntry) return [];\n return this.parseResponse({ vulns: resultEntry.vulns });\n });\n }\n\n async query(\n ecosystem: string,\n packageName: string,\n version: string,\n signal?: AbortSignal,\n ): Promise<CVERecord[]> {\n const osvEcosystem = ECOSYSTEM_MAP[ecosystem.toLowerCase()];\n if (!osvEcosystem) {\n logWarn(`OSV adapter: unsupported ecosystem \"${ecosystem}\", skipping query`);\n return [];\n }\n\n const body = JSON.stringify({\n package: {\n name: packageName,\n ecosystem: osvEcosystem,\n },\n version,\n });\n\n // Create a timeout abort signal and combine with any external signal\n const timeoutController = new AbortController();\n const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);\n\n const combinedSignal = signal\n ? combineAbortSignals(signal, timeoutController.signal)\n : timeoutController.signal;\n\n try {\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n signal: combinedSignal,\n });\n\n if (!response.ok) {\n throw new VulnDatabaseError(\n `OSV API returned HTTP ${response.status}: ${response.statusText}`,\n 'OSV',\n response.status,\n );\n }\n\n const data = (await response.json()) as OsvQueryResponse;\n return this.parseResponse(data);\n } catch (error: unknown) {\n if (error instanceof VulnDatabaseError) {\n throw error;\n }\n\n if (isAbortError(error)) {\n const isTimeout = timeoutController.signal.aborted;\n const message = isTimeout\n ? `OSV query timed out after ${this.timeoutMs}ms for ${packageName}@${version}`\n : `OSV query aborted for ${packageName}@${version}`;\n logError(message);\n throw new VulnDatabaseError(message, 'OSV');\n }\n\n const errorMessage = error instanceof Error ? error.message : String(error);\n logError(`OSV query failed for ${packageName}@${version}: ${errorMessage}`);\n throw new VulnDatabaseError(\n `Network error querying OSV for ${packageName}@${version}: ${errorMessage}`,\n 'OSV',\n );\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Parse the OSV API response into CVERecord objects.\n */\n private parseResponse(data: OsvQueryResponse): CVERecord[] {\n if (!data.vulns || data.vulns.length === 0) {\n return [];\n }\n\n const records: CVERecord[] = [];\n\n for (const vuln of data.vulns) {\n const cveId = this.extractCveId(vuln);\n if (!cveId) {\n // Skip vulnerabilities without a CVE identifier\n continue;\n }\n\n const severity = this.extractSeverity(vuln);\n const { ranges, fixedVersion } = this.extractAffectedRanges(vuln);\n const summary = this.extractSummary(vuln);\n\n records.push({\n id: cveId,\n severity,\n affectedVersionRanges: ranges,\n fixedVersion,\n summary,\n });\n }\n\n return records;\n }\n\n /**\n * Extract CVE ID from the vulnerability's aliases or use the OSV ID.\n * Prefers CVE-* identifiers from the aliases array.\n */\n private extractCveId(vuln: OsvVulnerability): string | undefined {\n // Look for a CVE alias first\n if (vuln.aliases) {\n const cveAlias = vuln.aliases.find((alias) => alias.startsWith('CVE-'));\n if (cveAlias) {\n return cveAlias;\n }\n }\n\n // Fall back to the OSV ID (e.g., GHSA-xxxx-xxxx-xxxx)\n return vuln.id;\n }\n\n /**\n * Extract CVSS v3.1 base score from the severity array.\n * Returns 0 if no CVSS score is available.\n */\n private extractSeverity(vuln: OsvVulnerability): number {\n if (!vuln.severity || vuln.severity.length === 0) {\n return 0;\n }\n\n for (const sev of vuln.severity) {\n if (sev.type === 'CVSS_V3') {\n const score = this.parseCvssScore(sev.score);\n if (score !== null) {\n return score;\n }\n }\n }\n\n // Try any severity type as fallback\n for (const sev of vuln.severity) {\n const score = this.parseCvssScore(sev.score);\n if (score !== null) {\n return score;\n }\n }\n\n return 0;\n }\n\n /**\n * Parse a CVSS vector string or numeric score.\n * CVSS vectors look like: \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\"\n * The base score is not directly in the vector \u2014 we extract it if it's a plain number,\n * or return null if we can't parse it.\n */\n private parseCvssScore(scoreStr: string): number | null {\n // If it's a plain number\n const num = parseFloat(scoreStr);\n if (!isNaN(num) && num >= 0 && num <= 10) {\n return num;\n }\n\n // CVSS vector strings don't contain the base score directly.\n // OSV sometimes provides the score as a plain number in the score field.\n return null;\n }\n\n /**\n * Extract affected version ranges and the first fixed version from the vulnerability.\n */\n private extractAffectedRanges(vuln: OsvVulnerability): {\n ranges: VersionRange[];\n fixedVersion?: string;\n } {\n const ranges: VersionRange[] = [];\n let fixedVersion: string | undefined;\n\n if (!vuln.affected) {\n return { ranges, fixedVersion };\n }\n\n for (const affected of vuln.affected) {\n if (!affected.ranges) {\n continue;\n }\n\n for (const range of affected.ranges) {\n if (!range.events || range.events.length === 0) {\n continue;\n }\n\n const versionRange: VersionRange = {};\n\n for (const event of range.events) {\n if (event.introduced) {\n versionRange.introduced = event.introduced;\n }\n if (event.fixed) {\n versionRange.fixed = event.fixed;\n // Capture the first fixed version we encounter\n if (!fixedVersion) {\n fixedVersion = event.fixed;\n }\n }\n if (event.last_affected) {\n versionRange.lastAffected = event.last_affected;\n }\n }\n\n // Only add ranges that have at least one meaningful field\n if (versionRange.introduced || versionRange.fixed || versionRange.lastAffected) {\n ranges.push(versionRange);\n }\n }\n }\n\n return { ranges, fixedVersion };\n }\n\n /**\n * Extract and truncate the summary from the vulnerability.\n * Prefers the summary field, falls back to details.\n */\n private extractSummary(vuln: OsvVulnerability): string {\n const text = vuln.summary || vuln.details || '';\n if (text.length <= MAX_SUMMARY_LENGTH) {\n return text;\n }\n return text.slice(0, MAX_SUMMARY_LENGTH - 3) + '...';\n }\n}\n\n// \u2500\u2500 Utility Functions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Combine multiple AbortSignals into one that aborts when any of them aborts.\n */\nfunction combineAbortSignals(...signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort(signal.reason);\n return controller.signal;\n }\n\n signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });\n }\n\n return controller.signal;\n}\n\n/**\n * Check if an error is an abort/timeout error.\n */\nfunction isAbortError(error: unknown): boolean {\n if (error instanceof Error) {\n return error.name === 'AbortError' || error.name === 'TimeoutError';\n }\n return false;\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,oBAAkC;AAClC,IAAAA,iBAAkC;AAMlC,MAAM,gBAAwC;AAAA,EAC5C,KAAK;AAAA,EACL,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,OAAO;AAAA,EACP,WAAW;AAAA;AAAA,EACX,OAAO;AAAA,EACP,QAAQ;AAAA;AAAA,EACR,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KAAK;AAAA,EACL,KAAK;AACP;
|
|
4
|
+
"sourcesContent": ["/**\n * OSV API Adapter\n *\n * Queries the OSV (Open Source Vulnerabilities) database for known CVEs\n * affecting project dependencies. Implements the VulnDatabaseAdapter interface.\n */\n\nimport { CVERecord, VersionRange } from '../types';\nimport type { VulnDatabaseAdapter, BatchQuery } from './types';\nimport { VulnDatabaseError } from '../errors';\nimport { logWarn, logError } from '../../errors';\n\nexport type { VulnDatabaseAdapter, BatchQuery };\n\n// \u2500\u2500 Ecosystem Mapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ECOSYSTEM_MAP: Record<string, string> = {\n npm: 'npm',\n maven: 'Maven',\n go: 'Go',\n pypi: 'PyPI',\n python: 'PyPI', // manifest parser sends 'python' for requirements.txt\n cargo: 'crates.io',\n pyproject: 'PyPI', // pyproject.toml (Poetry/Hatch/PDM/PEP 621)\n nuget: 'NuGet',\n csproj: 'NuGet', // manifest parser sends 'csproj' for .csproj files\n gradle: 'Maven', // Gradle projects use Maven Central\n rubygems: 'RubyGems',\n composer: 'Packagist',\n swift: 'SwiftURL',\n pub: 'Pub',\n hex: 'Hex',\n};\n\n// \u2500\u2500 OSV Request/Response Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface OsvSeverity {\n type: string;\n score: string;\n}\n\ninterface OsvAffectedRange {\n type: string;\n events: Array<{ introduced?: string; fixed?: string; last_affected?: string }>;\n}\n\ninterface OsvAffected {\n package?: { name?: string; ecosystem?: string };\n ranges?: OsvAffectedRange[];\n versions?: string[];\n}\n\ninterface OsvVulnerability {\n id: string;\n aliases?: string[];\n summary?: string;\n details?: string;\n severity?: OsvSeverity[];\n affected?: OsvAffected[];\n /** OSV per-database extras. GHSA records expose a coarse qualitative severity here. */\n database_specific?: { severity?: string;[key: string]: unknown };\n}\n\ninterface OsvQueryResponse {\n vulns?: OsvVulnerability[];\n}\n\ninterface OsvBatchQueryItem {\n package: { name: string; ecosystem: string };\n version: string;\n}\n\ninterface OsvBatchResponse {\n results: Array<{ vulns?: OsvVulnerability[] }>;\n}\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst OSV_API_URL = 'https://api.osv.dev/v1/query';\nconst OSV_BATCH_API_URL = 'https://api.osv.dev/v1/querybatch';\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst OSV_BATCH_MAX_QUERIES = 1000;\nconst MAX_SUMMARY_LENGTH = 500;\n\n// \u2500\u2500 CVSS parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Parse an OSV severity `score` field into a CVSS base score (0.0\u201310.0).\n *\n * The field is either a plain number (rare) or a CVSS vector string such as\n * \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\". For v3.0/3.1 vectors we compute\n * the base score from the metrics per the FIRST CVSS specification. Returns null\n * when the value can't be interpreted.\n */\nexport function parseCvssScore(scoreStr: string | undefined): number | null {\n if (!scoreStr) return null;\n\n const trimmed = scoreStr.trim();\n\n // Plain numeric score.\n const num = parseFloat(trimmed);\n if (!isNaN(num) && String(num) === trimmed && num >= 0 && num <= 10) {\n return num;\n }\n\n // CVSS vector string.\n if (/^CVSS:3\\.[01]\\//i.test(trimmed)) {\n return computeCvssV3BaseScore(trimmed);\n }\n\n return null;\n}\n\n/** Round up to one decimal place, per the CVSS spec's roundup() function. */\nfunction cvssRoundUp(value: number): number {\n const intInput = Math.round(value * 100000);\n if (intInput % 10000 === 0) return intInput / 100000;\n return (Math.floor(intInput / 10000) + 1) / 10;\n}\n\n/**\n * Compute the CVSS v3.0/3.1 base score from a vector string.\n * Implements the base-score equations from the FIRST CVSS v3.1 specification.\n * Returns null if required base metrics are missing.\n */\nexport function computeCvssV3BaseScore(vector: string): number | null {\n const metrics: Record<string, string> = {};\n for (const part of vector.split('/')) {\n const [k, v] = part.split(':');\n if (k && v) metrics[k.toUpperCase()] = v.toUpperCase();\n }\n\n // Required base metrics.\n const AV = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 }[metrics.AV];\n const AC = { L: 0.77, H: 0.44 }[metrics.AC];\n const UI = { N: 0.85, R: 0.62 }[metrics.UI];\n const scopeChanged = metrics.S === 'C';\n // Privileges Required depends on Scope.\n const PR = ((): number | undefined => {\n if (metrics.PR === 'N') return 0.85;\n if (metrics.PR === 'L') return scopeChanged ? 0.68 : 0.62;\n if (metrics.PR === 'H') return scopeChanged ? 0.5 : 0.27;\n return undefined;\n })();\n const impactVal = { H: 0.56, L: 0.22, N: 0.0 };\n const C = impactVal[metrics.C as keyof typeof impactVal];\n const I = impactVal[metrics.I as keyof typeof impactVal];\n const A = impactVal[metrics.A as keyof typeof impactVal];\n\n if ([AV, AC, UI, PR, C, I, A].some(v => v === undefined)) {\n return null;\n }\n\n const iscBase = 1 - (1 - C!) * (1 - I!) * (1 - A!);\n const impact = scopeChanged\n ? 7.52 * (iscBase - 0.029) - 3.25 * Math.pow(iscBase - 0.02, 15)\n : 6.42 * iscBase;\n const exploitability = 8.22 * AV! * AC! * PR! * UI!;\n\n if (impact <= 0) return 0.0;\n\n const raw = scopeChanged\n ? Math.min(1.08 * (impact + exploitability), 10)\n : Math.min(impact + exploitability, 10);\n\n return cvssRoundUp(raw);\n}\n\n/**\n * Map a coarse qualitative severity label (GHSA `database_specific.severity`,\n * or a CVSS qualitative rating) to a representative numeric score. Used only as\n * a fallback when no CVSS vector is available. Returns null for unknown labels.\n */\nexport function qualitativeToScore(label: string | undefined): number | null {\n if (!label) return null;\n switch (label.trim().toUpperCase()) {\n case 'CRITICAL': return 9.8;\n case 'HIGH': return 7.5;\n case 'MODERATE':\n case 'MEDIUM': return 5.5;\n case 'LOW': return 3.1;\n default: return null;\n }\n}\n\n// \u2500\u2500 OsvAdapter Implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class OsvAdapter implements VulnDatabaseAdapter {\n public readonly name = 'OSV';\n\n private readonly apiUrl: string;\n private readonly batchApiUrl: string;\n private readonly timeoutMs: number;\n\n constructor(options?: { apiUrl?: string; batchApiUrl?: string; timeoutMs?: number }) {\n this.apiUrl = options?.apiUrl ?? OSV_API_URL;\n this.batchApiUrl = options?.batchApiUrl ?? OSV_BATCH_API_URL;\n this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n }\n\n /**\n * Batch query up to OSV_BATCH_MAX_QUERIES packages in a single HTTP request.\n * Results are returned in the same order as the input queries.\n * Queries for unsupported ecosystems produce empty results without an HTTP call.\n */\n async queryBatch(\n queries: BatchQuery[],\n signal?: AbortSignal,\n ): Promise<Array<CVERecord[]>> {\n if (queries.length === 0) return [];\n\n // Map each query to its OSV ecosystem \u2014 track indices of unsupported ones\n const osvQueries: OsvBatchQueryItem[] = [];\n const indexMap: Array<number | null> = []; // index into osvQueries, or null if unsupported\n\n for (const q of queries) {\n const osvEcosystem = ECOSYSTEM_MAP[q.ecosystem.toLowerCase()];\n if (!osvEcosystem) {\n logWarn(`OSV adapter: unsupported ecosystem \"${q.ecosystem}\", skipping batch entry`);\n indexMap.push(null);\n } else {\n indexMap.push(osvQueries.length);\n osvQueries.push({\n package: { name: q.packageName, ecosystem: osvEcosystem },\n version: q.version,\n });\n }\n }\n\n if (osvQueries.length === 0) {\n return queries.map(() => []);\n }\n\n const timeoutController = new AbortController();\n const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);\n const combinedSignal = signal\n ? combineAbortSignals(signal, timeoutController.signal)\n : timeoutController.signal;\n\n let batchResults: Array<{ vulns?: OsvVulnerability[] }>;\n\n try {\n const response = await fetch(this.batchApiUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ queries: osvQueries }),\n signal: combinedSignal,\n });\n\n if (!response.ok) {\n throw new VulnDatabaseError(\n `OSV batch API returned HTTP ${response.status}: ${response.statusText}`,\n 'OSV',\n response.status,\n );\n }\n\n const data = (await response.json()) as OsvBatchResponse;\n batchResults = data.results ?? [];\n } catch (error: unknown) {\n if (error instanceof VulnDatabaseError) throw error;\n if (isAbortError(error)) {\n const msg = timeoutController.signal.aborted\n ? `OSV batch query timed out after ${this.timeoutMs}ms (${osvQueries.length} packages)`\n : `OSV batch query aborted`;\n logError(msg);\n throw new VulnDatabaseError(msg, 'OSV');\n }\n const msg = error instanceof Error ? error.message : String(error);\n logError(`OSV batch query failed: ${msg}`);\n throw new VulnDatabaseError(`Network error in OSV batch query: ${msg}`, 'OSV');\n } finally {\n clearTimeout(timeoutId);\n }\n\n // Rebuild full results array aligned with input queries\n return queries.map((_, i) => {\n const osvIdx = indexMap[i];\n if (osvIdx === null) return [];\n const resultEntry = batchResults[osvIdx];\n if (!resultEntry) return [];\n return this.parseResponse({ vulns: resultEntry.vulns });\n });\n }\n\n async query(\n ecosystem: string,\n packageName: string,\n version: string,\n signal?: AbortSignal,\n ): Promise<CVERecord[]> {\n const osvEcosystem = ECOSYSTEM_MAP[ecosystem.toLowerCase()];\n if (!osvEcosystem) {\n logWarn(`OSV adapter: unsupported ecosystem \"${ecosystem}\", skipping query`);\n return [];\n }\n\n const body = JSON.stringify({\n package: {\n name: packageName,\n ecosystem: osvEcosystem,\n },\n version,\n });\n\n // Create a timeout abort signal and combine with any external signal\n const timeoutController = new AbortController();\n const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);\n\n const combinedSignal = signal\n ? combineAbortSignals(signal, timeoutController.signal)\n : timeoutController.signal;\n\n try {\n const response = await fetch(this.apiUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n signal: combinedSignal,\n });\n\n if (!response.ok) {\n throw new VulnDatabaseError(\n `OSV API returned HTTP ${response.status}: ${response.statusText}`,\n 'OSV',\n response.status,\n );\n }\n\n const data = (await response.json()) as OsvQueryResponse;\n return this.parseResponse(data);\n } catch (error: unknown) {\n if (error instanceof VulnDatabaseError) {\n throw error;\n }\n\n if (isAbortError(error)) {\n const isTimeout = timeoutController.signal.aborted;\n const message = isTimeout\n ? `OSV query timed out after ${this.timeoutMs}ms for ${packageName}@${version}`\n : `OSV query aborted for ${packageName}@${version}`;\n logError(message);\n throw new VulnDatabaseError(message, 'OSV');\n }\n\n const errorMessage = error instanceof Error ? error.message : String(error);\n logError(`OSV query failed for ${packageName}@${version}: ${errorMessage}`);\n throw new VulnDatabaseError(\n `Network error querying OSV for ${packageName}@${version}: ${errorMessage}`,\n 'OSV',\n );\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Parse the OSV API response into CVERecord objects.\n */\n private parseResponse(data: OsvQueryResponse): CVERecord[] {\n if (!data.vulns || data.vulns.length === 0) {\n return [];\n }\n\n const records: CVERecord[] = [];\n\n for (const vuln of data.vulns) {\n const cveId = this.extractCveId(vuln);\n if (!cveId) {\n // Skip vulnerabilities without a CVE identifier\n continue;\n }\n\n const severity = this.extractSeverity(vuln);\n const { ranges, fixedVersion } = this.extractAffectedRanges(vuln);\n const summary = this.extractSummary(vuln);\n\n records.push({\n id: cveId,\n severity,\n affectedVersionRanges: ranges,\n fixedVersion,\n summary,\n });\n }\n\n return records;\n }\n\n /**\n * Extract CVE ID from the vulnerability's aliases or use the OSV ID.\n * Prefers CVE-* identifiers from the aliases array.\n */\n private extractCveId(vuln: OsvVulnerability): string | undefined {\n // Look for a CVE alias first\n if (vuln.aliases) {\n const cveAlias = vuln.aliases.find((alias) => alias.startsWith('CVE-'));\n if (cveAlias) {\n return cveAlias;\n }\n }\n\n // Fall back to the OSV ID (e.g., GHSA-xxxx-xxxx-xxxx)\n return vuln.id;\n }\n\n /**\n * Extract a CVSS base score (0.0\u201310.0) for the vulnerability.\n *\n * OSV encodes severity as an array of `{ type, score }`. For CVSS_V3/CVSS_V4\n * the `score` field is a *vector string* (e.g. \"CVSS:3.1/AV:N/AC:L/...\") \u2014 NOT\n * a plain number \u2014 so the base score must be computed from the vector.\n *\n * Resolution order:\n * 1. CVSS_V3 vector \u2192 computed base score\n * 2. CVSS_V4 vector \u2192 computed base score\n * 3. Any severity entry that happens to be a plain number\n * 4. Coarse qualitative `database_specific.severity` (GHSA) \u2192 representative score\n *\n * Returns null when no severity data is available (so callers can distinguish\n * \"unknown\" from a genuine 0.0 score).\n */\n private extractSeverity(vuln: OsvVulnerability): number | null {\n if (vuln.severity && vuln.severity.length > 0) {\n // Prefer CVSS v3, then v4, then any parseable entry.\n const byType = (type: string) =>\n vuln.severity!.filter(s => s.type === type);\n\n for (const sev of [...byType('CVSS_V3'), ...byType('CVSS_V4')]) {\n const score = parseCvssScore(sev.score);\n if (score !== null) return score;\n }\n for (const sev of vuln.severity) {\n const score = parseCvssScore(sev.score);\n if (score !== null) return score;\n }\n }\n\n // Fallback: GHSA advisories expose a coarse qualitative severity here even\n // when a CVSS vector is absent.\n const qualitative = qualitativeToScore(vuln.database_specific?.severity);\n if (qualitative !== null) return qualitative;\n\n return null;\n }\n\n /**\n * Extract affected version ranges and the first fixed version from the vulnerability.\n */\n private extractAffectedRanges(vuln: OsvVulnerability): {\n ranges: VersionRange[];\n fixedVersion?: string;\n } {\n const ranges: VersionRange[] = [];\n let fixedVersion: string | undefined;\n\n if (!vuln.affected) {\n return { ranges, fixedVersion };\n }\n\n for (const affected of vuln.affected) {\n if (!affected.ranges) {\n continue;\n }\n\n for (const range of affected.ranges) {\n if (!range.events || range.events.length === 0) {\n continue;\n }\n\n const versionRange: VersionRange = {};\n\n for (const event of range.events) {\n if (event.introduced) {\n versionRange.introduced = event.introduced;\n }\n if (event.fixed) {\n versionRange.fixed = event.fixed;\n // Capture the first fixed version we encounter\n if (!fixedVersion) {\n fixedVersion = event.fixed;\n }\n }\n if (event.last_affected) {\n versionRange.lastAffected = event.last_affected;\n }\n }\n\n // Only add ranges that have at least one meaningful field\n if (versionRange.introduced || versionRange.fixed || versionRange.lastAffected) {\n ranges.push(versionRange);\n }\n }\n }\n\n return { ranges, fixedVersion };\n }\n\n /**\n * Extract and truncate the summary from the vulnerability.\n * Prefers the summary field, falls back to details.\n */\n private extractSummary(vuln: OsvVulnerability): string {\n const text = vuln.summary || vuln.details || '';\n if (text.length <= MAX_SUMMARY_LENGTH) {\n return text;\n }\n return text.slice(0, MAX_SUMMARY_LENGTH - 3) + '...';\n }\n}\n\n// \u2500\u2500 Utility Functions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Combine multiple AbortSignals into one that aborts when any of them aborts.\n */\nfunction combineAbortSignals(...signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort(signal.reason);\n return controller.signal;\n }\n\n signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });\n }\n\n return controller.signal;\n}\n\n/**\n * Check if an error is an abort/timeout error.\n */\nfunction isAbortError(error: unknown): boolean {\n if (error instanceof Error) {\n return error.name === 'AbortError' || error.name === 'TimeoutError';\n }\n return false;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["import_errors", "msg"]
|
|
7
7
|
}
|