kirograph 1.0.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +4 -2
  2. package/dist/architecture/manifest/python.js +2 -2
  3. package/dist/architecture/manifest/python.js.map +2 -2
  4. package/dist/bin/commands/help.js +3 -1
  5. package/dist/bin/commands/help.js.map +2 -2
  6. package/dist/bin/commands/memory.js +29 -9
  7. package/dist/bin/commands/memory.js.map +2 -2
  8. package/dist/bin/commands/reachability.js +7 -2
  9. package/dist/bin/commands/reachability.js.map +2 -2
  10. package/dist/bin/commands/vulns.js +16 -1
  11. package/dist/bin/commands/vulns.js.map +2 -2
  12. package/dist/bin/commands/wiki.js +59 -2
  13. package/dist/bin/commands/wiki.js.map +2 -2
  14. package/dist/bin/installer/steering.js +9 -0
  15. package/dist/bin/installer/steering.js.map +2 -2
  16. package/dist/bin/kirograph.js +1 -1
  17. package/dist/config.js +12 -0
  18. package/dist/config.js.map +2 -2
  19. package/dist/db/memory-schema.sql +2 -1
  20. package/dist/extraction/extractor.js +46 -41
  21. package/dist/extraction/extractor.js.map +2 -2
  22. package/dist/extraction/notebook.js +15 -7
  23. package/dist/extraction/notebook.js.map +2 -2
  24. package/dist/jev/client.js +99 -0
  25. package/dist/jev/client.js.map +7 -0
  26. package/dist/jev/types.js +17 -0
  27. package/dist/jev/types.js.map +7 -0
  28. package/dist/mcp/handlers/memory.js +15 -8
  29. package/dist/mcp/handlers/memory.js.map +2 -2
  30. package/dist/mcp/handlers/security.js +5 -3
  31. package/dist/mcp/handlers/security.js.map +2 -2
  32. package/dist/mcp/handlers/wiki.js +4 -2
  33. package/dist/mcp/handlers/wiki.js.map +2 -2
  34. package/dist/mcp/tools.js +3 -2
  35. package/dist/mcp/tools.js.map +2 -2
  36. package/dist/memory/database.js +17 -4
  37. package/dist/memory/database.js.map +2 -2
  38. package/dist/memory/index.js +31 -2
  39. package/dist/memory/index.js.map +3 -3
  40. package/dist/memory/relation-jev.js +59 -0
  41. package/dist/memory/relation-jev.js.map +7 -0
  42. package/dist/memory/schema.js +92 -0
  43. package/dist/memory/schema.js.map +7 -0
  44. package/dist/memory/types.js.map +1 -1
  45. package/dist/patterns/loader.js +2 -146
  46. package/dist/patterns/loader.js.map +3 -3
  47. package/dist/security/auth-detection-jev.js +47 -0
  48. package/dist/security/auth-detection-jev.js.map +7 -0
  49. package/dist/security/manifest/adapter.js +17 -3
  50. package/dist/security/manifest/adapter.js.map +3 -3
  51. package/dist/security/manifest/plugins/cargo.js +23 -0
  52. package/dist/security/manifest/plugins/cargo.js.map +2 -2
  53. package/dist/security/manifest/plugins/composer.js +12 -0
  54. package/dist/security/manifest/plugins/composer.js.map +2 -2
  55. package/dist/security/manifest/plugins/gradle.js +12 -0
  56. package/dist/security/manifest/plugins/gradle.js.map +2 -2
  57. package/dist/security/manifest/plugins/hex.js +12 -0
  58. package/dist/security/manifest/plugins/hex.js.map +2 -2
  59. package/dist/security/manifest/plugins/maven.js +101 -4
  60. package/dist/security/manifest/plugins/maven.js.map +2 -2
  61. package/dist/security/manifest/plugins/npm.js +117 -2
  62. package/dist/security/manifest/plugins/npm.js.map +2 -2
  63. package/dist/security/manifest/plugins/nuget.js +13 -0
  64. package/dist/security/manifest/plugins/nuget.js.map +2 -2
  65. package/dist/security/manifest/plugins/pubspec.js +12 -0
  66. package/dist/security/manifest/plugins/pubspec.js.map +2 -2
  67. package/dist/security/manifest/plugins/pyproject.js +12 -0
  68. package/dist/security/manifest/plugins/pyproject.js.map +2 -2
  69. package/dist/security/manifest/plugins/rubygems.js +12 -0
  70. package/dist/security/manifest/plugins/rubygems.js.map +2 -2
  71. package/dist/security/pipeline.js +1 -1
  72. package/dist/security/pipeline.js.map +2 -2
  73. package/dist/security/reachability.js +54 -5
  74. package/dist/security/reachability.js.map +3 -3
  75. package/dist/security/types.js.map +1 -1
  76. package/dist/security/vuln/client.js +45 -1
  77. package/dist/security/vuln/client.js.map +2 -2
  78. package/dist/security/vuln/osv-adapter.js +45 -1
  79. package/dist/security/vuln/osv-adapter.js.map +2 -2
  80. package/dist/shared/json-schema.js +136 -0
  81. package/dist/shared/json-schema.js.map +7 -0
  82. package/dist/shared/yaml.js +174 -0
  83. package/dist/shared/yaml.js.map +7 -0
  84. package/dist/wiki/contradiction-jev.js +49 -0
  85. package/dist/wiki/contradiction-jev.js.map +7 -0
  86. package/dist/wiki/index.js +16 -1
  87. package/dist/wiki/index.js.map +2 -2
  88. package/dist/wiki/ingest.js +2 -0
  89. package/dist/wiki/ingest.js.map +2 -2
  90. package/dist/wiki/links.js +53 -0
  91. package/dist/wiki/links.js.map +7 -0
  92. package/dist/wiki/lint.js +17 -7
  93. package/dist/wiki/lint.js.map +2 -2
  94. package/dist/wiki/rename.js +103 -0
  95. package/dist/wiki/rename.js.map +7 -0
  96. package/dist/wiki/typed-pages.js +67 -0
  97. package/dist/wiki/typed-pages.js.map +7 -0
  98. package/dist/wiki/types.js.map +1 -1
  99. package/package.json +1 -1
@@ -43,10 +43,11 @@ function computeRiskScore(cvss, epss, verdict, staleness) {
43
43
  return Math.min(raw * 10, 10);
44
44
  }
45
45
  class VulnerabilityDatabaseClient {
46
- constructor(adapters, db, timeoutMs = 3e4) {
46
+ constructor(adapters, db, timeoutMs = 3e4, projectRoot) {
47
47
  this.adapters = adapters;
48
48
  this.db = db;
49
49
  this.timeoutMs = timeoutMs;
50
+ this.projectRoot = projectRoot;
50
51
  }
51
52
  /**
52
53
  * Enrich all Dependency_Nodes with vulnerability data.
@@ -83,6 +84,7 @@ class VulnerabilityDatabaseClient {
83
84
  );
84
85
  }
85
86
  result.dependenciesChecked = queryableDeps.length;
87
+ await this.enrichDuplicateVersions(rawDb, queryableDeps, result);
86
88
  await this.enrichEpss(rawDb);
87
89
  await this.computeRiskScores(rawDb);
88
90
  return result;
@@ -190,6 +192,48 @@ class VulnerabilityDatabaseClient {
190
192
  }
191
193
  }
192
194
  }
195
+ /**
196
+ * Check every other distinct resolved version of a package that npm's
197
+ * nested node_modules layout may have installed alongside the one
198
+ * recorded on its Dependency_Node — e.g. `qs@6.16.0` at the top level and
199
+ * a separately-nested `qs@6.15.3` under `body-parser`. Only the recorded
200
+ * version gets queried by the normal enrichment pass above; a vulnerable
201
+ * copy at a different version would otherwise go undetected even though
202
+ * it's genuinely installed.
203
+ *
204
+ * Doesn't change what's stored as `resolved_version` — any vulnerability
205
+ * found against an alternate version is linked to the same
206
+ * Dependency_Node the same way `enrichAllBatch`/`enrichAllSequential` do.
207
+ * npm-only for now (see `findNpmDuplicateVersions`); best-effort — a
208
+ * missing or unparseable lock file just means this pass finds nothing.
209
+ */
210
+ async enrichDuplicateVersions(rawDb, deps, result) {
211
+ if (!this.projectRoot) return;
212
+ const npmDeps = deps.filter((d) => d.ecosystem === "npm");
213
+ if (npmDeps.length === 0) return;
214
+ let duplicates;
215
+ try {
216
+ const { findNpmDuplicateVersions } = await Promise.resolve().then(() => require("../manifest/plugins/npm.js"));
217
+ duplicates = findNpmDuplicateVersions(this.projectRoot);
218
+ } catch {
219
+ return;
220
+ }
221
+ if (duplicates.size === 0) return;
222
+ for (const dep of npmDeps) {
223
+ const versions = duplicates.get(dep.package_name);
224
+ if (!versions) continue;
225
+ const alreadyChecked = dep.resolved_version || dep.declared_constraint;
226
+ for (const version of versions) {
227
+ if (version === alreadyChecked) continue;
228
+ const cveRecords = await this.queryAdaptersForDependency(dep, version, result);
229
+ const uniqueCves = this.deduplicateByCveId(cveRecords);
230
+ for (const cve of uniqueCves) {
231
+ this.upsertVulnerabilityNode(rawDb, cve, dep.node_id);
232
+ result.vulnerabilitiesFound++;
233
+ }
234
+ }
235
+ }
236
+ }
193
237
  /**
194
238
  * Enrich a single Dependency_Node with vulnerability data.
195
239
  *
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/security/vuln/client.ts"],
4
- "sourcesContent": ["/**\n * VulnerabilityDatabaseClient\n *\n * Orchestrates vulnerability enrichment by querying configured databases,\n * merging and deduplicating results by CVE identifier, creating Vulnerability_Nodes\n * linked to affected Dependency_Nodes, and handling unreachable databases gracefully.\n *\n * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.7\n */\n\nimport type { GraphDatabase } from '../../db/database';\nimport type { Edge } from '../../types';\nimport type { CVERecord, EnrichmentResult } from '../types';\nimport type { VulnDatabaseAdapter, BatchQuery } from './types';\nimport { logError, logWarn } from '../../errors';\nimport { EpssClient } from './epss-client';\n\nconst BATCH_CHUNK_SIZE = 1000;\n\n/**\n * Compute a combined risk score (0.0\u201310.0) for a vulnerability.\n *\n * Formula:\n * risk = reachability_factor \u00D7 (0.4 \u00D7 cvss_normalized + 0.6 \u00D7 epss) \u00D7 staleness_bonus\n * scaled to 0\u201310: min(risk \u00D7 10, 10.0)\n *\n * @param cvss - CVSS base score (0.0\u201310.0), null treated as 5.0\n * @param epss - EPSS exploitation probability (0.0\u20131.0), null treated as 0.0\n * @param verdict - Reachability verdict or null/undefined\n * @param staleness - Dependency staleness score (0.0\u20131.0), null treated as 0.0\n */\nexport function computeRiskScore(\n cvss: number | null | undefined,\n epss: number | null | undefined,\n verdict: string | null | undefined,\n staleness: number | null | undefined,\n): number {\n const cvssNormalized = (cvss ?? 5.0) / 10.0;\n const epssValue = epss ?? 0.0;\n\n let reachabilityFactor: number;\n if (verdict === 'affected') {\n reachabilityFactor = 1.0;\n } else if (verdict === 'under_investigation') {\n reachabilityFactor = 0.5;\n } else if (verdict === 'not_affected') {\n reachabilityFactor = 0.1;\n } else {\n // no verdict\n reachabilityFactor = 0.3;\n }\n\n const stalenessBonus = 1.0 + ((staleness ?? 0) * 0.2);\n\n const raw = reachabilityFactor * (0.4 * cvssNormalized + 0.6 * epssValue) * stalenessBonus;\n return Math.min(raw * 10, 10.0);\n}\n\n/**\n * Dependency row from sec_dependencies table.\n */\ninterface DependencyRow {\n node_id: string;\n ecosystem: string;\n package_name: string;\n resolved_version: string | null;\n declared_constraint: string;\n}\n\nexport class VulnerabilityDatabaseClient {\n private readonly adapters: VulnDatabaseAdapter[];\n private readonly db: GraphDatabase;\n private readonly timeoutMs: number;\n\n constructor(\n adapters: VulnDatabaseAdapter[],\n db: GraphDatabase,\n timeoutMs: number = 30000,\n ) {\n this.adapters = adapters;\n this.db = db;\n this.timeoutMs = timeoutMs;\n }\n\n /**\n * Enrich all Dependency_Nodes with vulnerability data.\n *\n * Uses batch queries (up to 1000 per HTTP request) when the adapter supports\n * `queryBatch`. Falls back to sequential single queries per dependency if batch\n * is unavailable or fails. Results are deduplicated by CVE ID across adapters.\n */\n async enrichAll(): Promise<EnrichmentResult> {\n const rawDb = this.db.getRawDb();\n const result: EnrichmentResult = {\n vulnerabilitiesFound: 0,\n dependenciesChecked: 0,\n errors: [],\n staleNodes: [],\n };\n\n const depRows: DependencyRow[] = rawDb.all(\n `SELECT node_id, ecosystem, package_name, resolved_version, declared_constraint\n FROM sec_dependencies`,\n );\n\n // Filter out deps with no usable version\n const queryableDeps = depRows.filter(d => d.resolved_version || d.declared_constraint);\n\n for (const adapter of this.adapters) {\n if (adapter.queryBatch) {\n await this.enrichAllBatch(adapter as typeof adapter & Required<Pick<typeof adapter, 'queryBatch'>>, queryableDeps, result);\n } else {\n await this.enrichAllSequential(adapter, queryableDeps, result);\n }\n }\n\n // Record lastVulnCheck for all queried deps (do once, after all adapters)\n const now = Date.now();\n for (const dep of queryableDeps) {\n rawDb.run(\n `UPDATE sec_dependencies SET last_vuln_check = ? WHERE node_id = ?`,\n [now, dep.node_id],\n );\n }\n result.dependenciesChecked = queryableDeps.length;\n\n // Enrich stored vulnerabilities with EPSS scores\n await this.enrichEpss(rawDb);\n\n // Compute combined risk scores after all enrichment is done\n await this.computeRiskScores(rawDb);\n\n return result;\n }\n\n /**\n * Fetch EPSS scores for all stored vulnerabilities and update the database.\n */\n private async enrichEpss(rawDb: any): Promise<void> {\n try {\n const cveRows: Array<{ cve_id: string }> = rawDb.all(\n `SELECT cve_id FROM sec_vulnerabilities`,\n );\n\n if (cveRows.length === 0) {\n return;\n }\n\n const cveIds = cveRows.map(r => r.cve_id);\n const epssClient = new EpssClient();\n const scores = await epssClient.fetchScores(cveIds);\n\n if (scores.size === 0) {\n return;\n }\n\n const fetchedAt = Date.now();\n for (const [cveId, { score, percentile }] of scores) {\n rawDb.run(\n `UPDATE sec_vulnerabilities SET epss_score = ?, epss_percentile = ?, epss_fetched_at = ? WHERE cve_id = ?`,\n [score, percentile, fetchedAt, cveId],\n );\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n logWarn(`[sec:epss] EPSS enrichment failed (non-critical): ${msg}`);\n }\n }\n\n /**\n * Compute and persist combined risk scores for all stored vulnerabilities.\n * Called after EPSS enrichment so epss_score values are up to date.\n */\n private async computeRiskScores(rawDb: any): Promise<void> {\n const rows: Array<{\n node_id: string;\n severity_score: number | null;\n epss_score: number | null;\n verdict: string | null;\n staleness_score: number | null;\n }> = rawDb.all(`\n SELECT v.node_id, v.severity_score, v.epss_score,\n r.verdict,\n d.staleness_score\n FROM sec_vulnerabilities v\n LEFT JOIN edges e ON e.target = v.node_id AND e.kind = 'has_vulnerability'\n LEFT JOIN sec_dependencies d ON d.node_id = e.source\n LEFT JOIN sec_reachability r ON r.vulnerability_node_id = v.node_id\n `);\n for (const row of rows) {\n const score = computeRiskScore(row.severity_score, row.epss_score, row.verdict, row.staleness_score);\n rawDb.run(`UPDATE sec_vulnerabilities SET risk_score = ? WHERE node_id = ?`, [score, row.node_id]);\n }\n }\n\n /**\n * Batch enrichment path: groups deps into chunks of BATCH_CHUNK_SIZE and sends\n * one HTTP request per chunk. Falls back to sequential on batch failure.\n */\n private async enrichAllBatch(\n adapter: VulnDatabaseAdapter & Required<Pick<VulnDatabaseAdapter, 'queryBatch'>>,\n deps: DependencyRow[],\n result: EnrichmentResult,\n ): Promise<void> {\n const rawDb = this.db.getRawDb();\n\n for (let i = 0; i < deps.length; i += BATCH_CHUNK_SIZE) {\n const chunk = deps.slice(i, i + BATCH_CHUNK_SIZE);\n const queries: BatchQuery[] = chunk.map(dep => ({\n ecosystem: dep.ecosystem,\n packageName: dep.package_name,\n version: dep.resolved_version || dep.declared_constraint,\n }));\n\n let batchResults: Array<CVERecord[]>;\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this.timeoutMs);\n try {\n batchResults = await adapter.queryBatch(queries, controller.signal);\n } finally {\n clearTimeout(timeout);\n }\n } catch (error: unknown) {\n // Batch failed \u2014 fall back to sequential for this chunk\n const msg = error instanceof Error ? error.message : String(error);\n logWarn(`[sec:vuln] ${adapter.name} batch query failed (${msg}), falling back to sequential for chunk of ${chunk.length}`);\n await this.enrichAllSequential(adapter, chunk, result);\n continue;\n }\n\n for (let j = 0; j < chunk.length; j++) {\n const dep = chunk[j]!;\n const cveRecords = batchResults[j] ?? [];\n const uniqueCves = this.deduplicateByCveId(cveRecords);\n for (const cve of uniqueCves) {\n this.upsertVulnerabilityNode(rawDb, cve, dep.node_id);\n result.vulnerabilitiesFound++;\n }\n }\n }\n }\n\n /**\n * Sequential enrichment path: queries one dependency at a time.\n * Used when the adapter does not support queryBatch, or as a fallback.\n */\n private async enrichAllSequential(\n adapter: VulnDatabaseAdapter,\n deps: DependencyRow[],\n result: EnrichmentResult,\n ): Promise<void> {\n const rawDb = this.db.getRawDb();\n\n for (const dep of deps) {\n const version = dep.resolved_version || dep.declared_constraint;\n const cveRecords = await this.queryAdaptersForDependency(dep, version, result, [adapter]);\n const uniqueCves = this.deduplicateByCveId(cveRecords);\n for (const cve of uniqueCves) {\n this.upsertVulnerabilityNode(rawDb, cve, dep.node_id);\n result.vulnerabilitiesFound++;\n }\n }\n }\n\n /**\n * Enrich a single Dependency_Node with vulnerability data.\n *\n * @param dependencyNodeId - The node ID of the dependency to enrich\n * @returns Array of CVE records found for this dependency\n */\n async enrichOne(dependencyNodeId: string): Promise<CVERecord[]> {\n const rawDb = this.db.getRawDb();\n\n const dep: DependencyRow | undefined = rawDb.get(\n `SELECT node_id, ecosystem, package_name, resolved_version, declared_constraint\n FROM sec_dependencies WHERE node_id = ?`,\n [dependencyNodeId],\n );\n\n if (!dep) {\n return [];\n }\n\n const version = dep.resolved_version || dep.declared_constraint;\n if (!version) {\n return [];\n }\n\n const enrichResult: EnrichmentResult = {\n vulnerabilitiesFound: 0,\n dependenciesChecked: 0,\n errors: [],\n staleNodes: [],\n };\n\n const cveRecords = await this.queryAdaptersForDependency(\n dep,\n version,\n enrichResult,\n );\n\n // Deduplicate by CVE ID\n const uniqueCves = this.deduplicateByCveId(cveRecords);\n\n // Create Vulnerability_Nodes and edges\n for (const cve of uniqueCves) {\n this.upsertVulnerabilityNode(rawDb, cve, dep.node_id);\n }\n\n // Record lastVulnCheck timestamp\n const now = Date.now();\n rawDb.run(\n `UPDATE sec_dependencies SET last_vuln_check = ? WHERE node_id = ?`,\n [now, dep.node_id],\n );\n\n return uniqueCves;\n }\n\n /**\n * Query a set of adapters for a single dependency.\n * Handles timeouts and unreachable databases gracefully.\n * Defaults to all configured adapters when adapterList is omitted.\n */\n private async queryAdaptersForDependency(\n dep: DependencyRow,\n version: string,\n result: EnrichmentResult,\n adapterList: VulnDatabaseAdapter[] = this.adapters,\n ): Promise<CVERecord[]> {\n const allRecords: CVERecord[] = [];\n\n for (const adapter of adapterList) {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this.timeoutMs);\n\n try {\n const records = await adapter.query(\n dep.ecosystem,\n dep.package_name,\n version,\n controller.signal,\n );\n allRecords.push(...records);\n } finally {\n clearTimeout(timeout);\n }\n } catch (error: unknown) {\n // Handle unreachable database (Requirement 3.5)\n const errorMessage = error instanceof Error ? error.message : String(error);\n logError(\n `[sec:vuln] Failed to query ${adapter.name} for ${dep.package_name}@${version}: ${errorMessage}`,\n );\n\n result.errors.push({\n dependency: dep.package_name,\n database: adapter.name,\n error: errorMessage,\n });\n\n // Set vulnDataStale flag on the dependency node\n this.markDependencyStale(dep.node_id, result);\n }\n }\n\n return allRecords;\n }\n\n /**\n * Mark a dependency node as having stale vulnerability data.\n * Retains cached data but sets the stale flag with timestamp.\n */\n private markDependencyStale(nodeId: string, result: EnrichmentResult): void {\n const rawDb = this.db.getRawDb();\n const now = Date.now();\n\n rawDb.run(\n `UPDATE sec_dependencies\n SET vuln_data_stale = 1, vuln_data_stale_since = ?\n WHERE node_id = ?`,\n [now, nodeId],\n );\n\n if (!result.staleNodes.includes(nodeId)) {\n result.staleNodes.push(nodeId);\n }\n\n logWarn(\n `[sec:vuln] Vulnerability data marked stale for dependency node ${nodeId}`,\n );\n }\n\n /**\n * Deduplicate CVE records by their ID.\n * When the same CVE is reported by multiple databases, keep the first occurrence.\n */\n private deduplicateByCveId(records: CVERecord[]): CVERecord[] {\n const seen = new Map<string, CVERecord>();\n for (const record of records) {\n if (!seen.has(record.id)) {\n seen.set(record.id, record);\n }\n }\n return [...seen.values()];\n }\n\n /**\n * Create or update a Vulnerability_Node and link it to the affected dependency.\n *\n * - Creates a node in `nodes` table with kind='vulnerability', ID format `vuln:<cve_id>`\n * - Inserts a row in `sec_vulnerabilities` with CVE metadata\n * - Creates a `has_vulnerability` edge from the Dependency_Node to the Vulnerability_Node\n */\n private upsertVulnerabilityNode(\n rawDb: any,\n cve: CVERecord,\n dependencyNodeId: string,\n ): void {\n const vulnNodeId = `vuln:${cve.id}`;\n const now = Date.now();\n\n // Truncate summary to 500 characters (Requirement 3.3)\n const summary = cve.summary.length > 500\n ? cve.summary.slice(0, 500)\n : cve.summary;\n\n // Determine source database from the first adapter that returned this CVE\n // (in practice, the adapter name is not stored on CVERecord, so we use\n // the first configured adapter's name as the source)\n const sourceDatabase = this.adapters.length > 0 ? this.adapters[0].name : 'unknown';\n\n // Upsert into nodes table\n rawDb.run(\n `INSERT OR REPLACE INTO nodes\n (id, kind, name, qualified_name, file_path, language,\n start_line, end_line, start_column, end_column,\n is_exported, is_async, is_static, is_abstract, updated_at)\n VALUES (?, 'vulnerability', ?, ?, '', 'unknown', 0, 0, 0, 0, 0, 0, 0, 0, ?)`,\n [vulnNodeId, cve.id, cve.id, now],\n );\n\n // Upsert into sec_vulnerabilities table\n const affectedRangesJson = JSON.stringify(cve.affectedVersionRanges);\n rawDb.run(\n `INSERT OR REPLACE INTO sec_vulnerabilities\n (node_id, cve_id, severity_score, affected_ranges, fixed_version, summary, source_database)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n [\n vulnNodeId,\n cve.id,\n cve.severity,\n affectedRangesJson,\n cve.fixedVersion ?? null,\n summary,\n sourceDatabase,\n ],\n );\n\n // Create has_vulnerability edge from Dependency_Node to Vulnerability_Node\n const edge: Edge = {\n source: dependencyNodeId,\n target: vulnNodeId,\n kind: 'has_vulnerability',\n confidence: 'extracted',\n confidenceScore: 1.0,\n };\n this.db.insertEdge(edge);\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * VulnerabilityDatabaseClient\n *\n * Orchestrates vulnerability enrichment by querying configured databases,\n * merging and deduplicating results by CVE identifier, creating Vulnerability_Nodes\n * linked to affected Dependency_Nodes, and handling unreachable databases gracefully.\n *\n * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.7\n */\n\nimport type { GraphDatabase } from '../../db/database';\nimport type { Edge } from '../../types';\nimport type { CVERecord, EnrichmentResult } from '../types';\nimport type { VulnDatabaseAdapter, BatchQuery } from './types';\nimport { logError, logWarn } from '../../errors';\nimport { EpssClient } from './epss-client';\n\nconst BATCH_CHUNK_SIZE = 1000;\n\n/**\n * Compute a combined risk score (0.0\u201310.0) for a vulnerability.\n *\n * Formula:\n * risk = reachability_factor \u00D7 (0.4 \u00D7 cvss_normalized + 0.6 \u00D7 epss) \u00D7 staleness_bonus\n * scaled to 0\u201310: min(risk \u00D7 10, 10.0)\n *\n * @param cvss - CVSS base score (0.0\u201310.0), null treated as 5.0\n * @param epss - EPSS exploitation probability (0.0\u20131.0), null treated as 0.0\n * @param verdict - Reachability verdict or null/undefined\n * @param staleness - Dependency staleness score (0.0\u20131.0), null treated as 0.0\n */\nexport function computeRiskScore(\n cvss: number | null | undefined,\n epss: number | null | undefined,\n verdict: string | null | undefined,\n staleness: number | null | undefined,\n): number {\n const cvssNormalized = (cvss ?? 5.0) / 10.0;\n const epssValue = epss ?? 0.0;\n\n let reachabilityFactor: number;\n if (verdict === 'affected') {\n reachabilityFactor = 1.0;\n } else if (verdict === 'under_investigation') {\n reachabilityFactor = 0.5;\n } else if (verdict === 'not_affected') {\n reachabilityFactor = 0.1;\n } else {\n // no verdict\n reachabilityFactor = 0.3;\n }\n\n const stalenessBonus = 1.0 + ((staleness ?? 0) * 0.2);\n\n const raw = reachabilityFactor * (0.4 * cvssNormalized + 0.6 * epssValue) * stalenessBonus;\n return Math.min(raw * 10, 10.0);\n}\n\n/**\n * Dependency row from sec_dependencies table.\n */\ninterface DependencyRow {\n node_id: string;\n ecosystem: string;\n package_name: string;\n resolved_version: string | null;\n declared_constraint: string;\n}\n\nexport class VulnerabilityDatabaseClient {\n private readonly adapters: VulnDatabaseAdapter[];\n private readonly db: GraphDatabase;\n private readonly timeoutMs: number;\n private readonly projectRoot?: string;\n\n constructor(\n adapters: VulnDatabaseAdapter[],\n db: GraphDatabase,\n timeoutMs: number = 30000,\n projectRoot?: string,\n ) {\n this.adapters = adapters;\n this.db = db;\n this.timeoutMs = timeoutMs;\n this.projectRoot = projectRoot;\n }\n\n /**\n * Enrich all Dependency_Nodes with vulnerability data.\n *\n * Uses batch queries (up to 1000 per HTTP request) when the adapter supports\n * `queryBatch`. Falls back to sequential single queries per dependency if batch\n * is unavailable or fails. Results are deduplicated by CVE ID across adapters.\n */\n async enrichAll(): Promise<EnrichmentResult> {\n const rawDb = this.db.getRawDb();\n const result: EnrichmentResult = {\n vulnerabilitiesFound: 0,\n dependenciesChecked: 0,\n errors: [],\n staleNodes: [],\n };\n\n const depRows: DependencyRow[] = rawDb.all(\n `SELECT node_id, ecosystem, package_name, resolved_version, declared_constraint\n FROM sec_dependencies`,\n );\n\n // Filter out deps with no usable version\n const queryableDeps = depRows.filter(d => d.resolved_version || d.declared_constraint);\n\n for (const adapter of this.adapters) {\n if (adapter.queryBatch) {\n await this.enrichAllBatch(adapter as typeof adapter & Required<Pick<typeof adapter, 'queryBatch'>>, queryableDeps, result);\n } else {\n await this.enrichAllSequential(adapter, queryableDeps, result);\n }\n }\n\n // Record lastVulnCheck for all queried deps (do once, after all adapters)\n const now = Date.now();\n for (const dep of queryableDeps) {\n rawDb.run(\n `UPDATE sec_dependencies SET last_vuln_check = ? WHERE node_id = ?`,\n [now, dep.node_id],\n );\n }\n result.dependenciesChecked = queryableDeps.length;\n\n // A package name can resolve to more than one distinct version in the\n // same tree (npm's nested node_modules layout); the query above only\n // checked whichever single version got recorded on the Dependency_Node.\n // Check the other installed copies too, without changing what's stored.\n await this.enrichDuplicateVersions(rawDb, queryableDeps, result);\n\n // Enrich stored vulnerabilities with EPSS scores\n await this.enrichEpss(rawDb);\n\n // Compute combined risk scores after all enrichment is done\n await this.computeRiskScores(rawDb);\n\n return result;\n }\n\n /**\n * Fetch EPSS scores for all stored vulnerabilities and update the database.\n */\n private async enrichEpss(rawDb: any): Promise<void> {\n try {\n const cveRows: Array<{ cve_id: string }> = rawDb.all(\n `SELECT cve_id FROM sec_vulnerabilities`,\n );\n\n if (cveRows.length === 0) {\n return;\n }\n\n const cveIds = cveRows.map(r => r.cve_id);\n const epssClient = new EpssClient();\n const scores = await epssClient.fetchScores(cveIds);\n\n if (scores.size === 0) {\n return;\n }\n\n const fetchedAt = Date.now();\n for (const [cveId, { score, percentile }] of scores) {\n rawDb.run(\n `UPDATE sec_vulnerabilities SET epss_score = ?, epss_percentile = ?, epss_fetched_at = ? WHERE cve_id = ?`,\n [score, percentile, fetchedAt, cveId],\n );\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n logWarn(`[sec:epss] EPSS enrichment failed (non-critical): ${msg}`);\n }\n }\n\n /**\n * Compute and persist combined risk scores for all stored vulnerabilities.\n * Called after EPSS enrichment so epss_score values are up to date.\n */\n private async computeRiskScores(rawDb: any): Promise<void> {\n const rows: Array<{\n node_id: string;\n severity_score: number | null;\n epss_score: number | null;\n verdict: string | null;\n staleness_score: number | null;\n }> = rawDb.all(`\n SELECT v.node_id, v.severity_score, v.epss_score,\n r.verdict,\n d.staleness_score\n FROM sec_vulnerabilities v\n LEFT JOIN edges e ON e.target = v.node_id AND e.kind = 'has_vulnerability'\n LEFT JOIN sec_dependencies d ON d.node_id = e.source\n LEFT JOIN sec_reachability r ON r.vulnerability_node_id = v.node_id\n `);\n for (const row of rows) {\n const score = computeRiskScore(row.severity_score, row.epss_score, row.verdict, row.staleness_score);\n rawDb.run(`UPDATE sec_vulnerabilities SET risk_score = ? WHERE node_id = ?`, [score, row.node_id]);\n }\n }\n\n /**\n * Batch enrichment path: groups deps into chunks of BATCH_CHUNK_SIZE and sends\n * one HTTP request per chunk. Falls back to sequential on batch failure.\n */\n private async enrichAllBatch(\n adapter: VulnDatabaseAdapter & Required<Pick<VulnDatabaseAdapter, 'queryBatch'>>,\n deps: DependencyRow[],\n result: EnrichmentResult,\n ): Promise<void> {\n const rawDb = this.db.getRawDb();\n\n for (let i = 0; i < deps.length; i += BATCH_CHUNK_SIZE) {\n const chunk = deps.slice(i, i + BATCH_CHUNK_SIZE);\n const queries: BatchQuery[] = chunk.map(dep => ({\n ecosystem: dep.ecosystem,\n packageName: dep.package_name,\n version: dep.resolved_version || dep.declared_constraint,\n }));\n\n let batchResults: Array<CVERecord[]>;\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this.timeoutMs);\n try {\n batchResults = await adapter.queryBatch(queries, controller.signal);\n } finally {\n clearTimeout(timeout);\n }\n } catch (error: unknown) {\n // Batch failed \u2014 fall back to sequential for this chunk\n const msg = error instanceof Error ? error.message : String(error);\n logWarn(`[sec:vuln] ${adapter.name} batch query failed (${msg}), falling back to sequential for chunk of ${chunk.length}`);\n await this.enrichAllSequential(adapter, chunk, result);\n continue;\n }\n\n for (let j = 0; j < chunk.length; j++) {\n const dep = chunk[j]!;\n const cveRecords = batchResults[j] ?? [];\n const uniqueCves = this.deduplicateByCveId(cveRecords);\n for (const cve of uniqueCves) {\n this.upsertVulnerabilityNode(rawDb, cve, dep.node_id);\n result.vulnerabilitiesFound++;\n }\n }\n }\n }\n\n /**\n * Sequential enrichment path: queries one dependency at a time.\n * Used when the adapter does not support queryBatch, or as a fallback.\n */\n private async enrichAllSequential(\n adapter: VulnDatabaseAdapter,\n deps: DependencyRow[],\n result: EnrichmentResult,\n ): Promise<void> {\n const rawDb = this.db.getRawDb();\n\n for (const dep of deps) {\n const version = dep.resolved_version || dep.declared_constraint;\n const cveRecords = await this.queryAdaptersForDependency(dep, version, result, [adapter]);\n const uniqueCves = this.deduplicateByCveId(cveRecords);\n for (const cve of uniqueCves) {\n this.upsertVulnerabilityNode(rawDb, cve, dep.node_id);\n result.vulnerabilitiesFound++;\n }\n }\n }\n\n /**\n * Check every other distinct resolved version of a package that npm's\n * nested node_modules layout may have installed alongside the one\n * recorded on its Dependency_Node \u2014 e.g. `qs@6.16.0` at the top level and\n * a separately-nested `qs@6.15.3` under `body-parser`. Only the recorded\n * version gets queried by the normal enrichment pass above; a vulnerable\n * copy at a different version would otherwise go undetected even though\n * it's genuinely installed.\n *\n * Doesn't change what's stored as `resolved_version` \u2014 any vulnerability\n * found against an alternate version is linked to the same\n * Dependency_Node the same way `enrichAllBatch`/`enrichAllSequential` do.\n * npm-only for now (see `findNpmDuplicateVersions`); best-effort \u2014 a\n * missing or unparseable lock file just means this pass finds nothing.\n */\n private async enrichDuplicateVersions(\n rawDb: any,\n deps: DependencyRow[],\n result: EnrichmentResult,\n ): Promise<void> {\n if (!this.projectRoot) return;\n const npmDeps = deps.filter(d => d.ecosystem === 'npm');\n if (npmDeps.length === 0) return;\n\n let duplicates: Map<string, Set<string>>;\n try {\n const { findNpmDuplicateVersions } = await Promise.resolve().then(() => require('../manifest/plugins/npm.js'));\n duplicates = findNpmDuplicateVersions(this.projectRoot);\n } catch {\n return;\n }\n if (duplicates.size === 0) return;\n\n for (const dep of npmDeps) {\n const versions = duplicates.get(dep.package_name);\n if (!versions) continue;\n\n const alreadyChecked = dep.resolved_version || dep.declared_constraint;\n for (const version of versions) {\n if (version === alreadyChecked) continue;\n\n const cveRecords = await this.queryAdaptersForDependency(dep, version, result);\n const uniqueCves = this.deduplicateByCveId(cveRecords);\n for (const cve of uniqueCves) {\n this.upsertVulnerabilityNode(rawDb, cve, dep.node_id);\n result.vulnerabilitiesFound++;\n }\n }\n }\n }\n\n /**\n * Enrich a single Dependency_Node with vulnerability data.\n *\n * @param dependencyNodeId - The node ID of the dependency to enrich\n * @returns Array of CVE records found for this dependency\n */\n async enrichOne(dependencyNodeId: string): Promise<CVERecord[]> {\n const rawDb = this.db.getRawDb();\n\n const dep: DependencyRow | undefined = rawDb.get(\n `SELECT node_id, ecosystem, package_name, resolved_version, declared_constraint\n FROM sec_dependencies WHERE node_id = ?`,\n [dependencyNodeId],\n );\n\n if (!dep) {\n return [];\n }\n\n const version = dep.resolved_version || dep.declared_constraint;\n if (!version) {\n return [];\n }\n\n const enrichResult: EnrichmentResult = {\n vulnerabilitiesFound: 0,\n dependenciesChecked: 0,\n errors: [],\n staleNodes: [],\n };\n\n const cveRecords = await this.queryAdaptersForDependency(\n dep,\n version,\n enrichResult,\n );\n\n // Deduplicate by CVE ID\n const uniqueCves = this.deduplicateByCveId(cveRecords);\n\n // Create Vulnerability_Nodes and edges\n for (const cve of uniqueCves) {\n this.upsertVulnerabilityNode(rawDb, cve, dep.node_id);\n }\n\n // Record lastVulnCheck timestamp\n const now = Date.now();\n rawDb.run(\n `UPDATE sec_dependencies SET last_vuln_check = ? WHERE node_id = ?`,\n [now, dep.node_id],\n );\n\n return uniqueCves;\n }\n\n /**\n * Query a set of adapters for a single dependency.\n * Handles timeouts and unreachable databases gracefully.\n * Defaults to all configured adapters when adapterList is omitted.\n */\n private async queryAdaptersForDependency(\n dep: DependencyRow,\n version: string,\n result: EnrichmentResult,\n adapterList: VulnDatabaseAdapter[] = this.adapters,\n ): Promise<CVERecord[]> {\n const allRecords: CVERecord[] = [];\n\n for (const adapter of adapterList) {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), this.timeoutMs);\n\n try {\n const records = await adapter.query(\n dep.ecosystem,\n dep.package_name,\n version,\n controller.signal,\n );\n allRecords.push(...records);\n } finally {\n clearTimeout(timeout);\n }\n } catch (error: unknown) {\n // Handle unreachable database (Requirement 3.5)\n const errorMessage = error instanceof Error ? error.message : String(error);\n logError(\n `[sec:vuln] Failed to query ${adapter.name} for ${dep.package_name}@${version}: ${errorMessage}`,\n );\n\n result.errors.push({\n dependency: dep.package_name,\n database: adapter.name,\n error: errorMessage,\n });\n\n // Set vulnDataStale flag on the dependency node\n this.markDependencyStale(dep.node_id, result);\n }\n }\n\n return allRecords;\n }\n\n /**\n * Mark a dependency node as having stale vulnerability data.\n * Retains cached data but sets the stale flag with timestamp.\n */\n private markDependencyStale(nodeId: string, result: EnrichmentResult): void {\n const rawDb = this.db.getRawDb();\n const now = Date.now();\n\n rawDb.run(\n `UPDATE sec_dependencies\n SET vuln_data_stale = 1, vuln_data_stale_since = ?\n WHERE node_id = ?`,\n [now, nodeId],\n );\n\n if (!result.staleNodes.includes(nodeId)) {\n result.staleNodes.push(nodeId);\n }\n\n logWarn(\n `[sec:vuln] Vulnerability data marked stale for dependency node ${nodeId}`,\n );\n }\n\n /**\n * Deduplicate CVE records by their ID.\n * When the same CVE is reported by multiple databases, keep the first occurrence.\n */\n private deduplicateByCveId(records: CVERecord[]): CVERecord[] {\n const seen = new Map<string, CVERecord>();\n for (const record of records) {\n if (!seen.has(record.id)) {\n seen.set(record.id, record);\n }\n }\n return [...seen.values()];\n }\n\n /**\n * Create or update a Vulnerability_Node and link it to the affected dependency.\n *\n * - Creates a node in `nodes` table with kind='vulnerability', ID format `vuln:<cve_id>`\n * - Inserts a row in `sec_vulnerabilities` with CVE metadata\n * - Creates a `has_vulnerability` edge from the Dependency_Node to the Vulnerability_Node\n */\n private upsertVulnerabilityNode(\n rawDb: any,\n cve: CVERecord,\n dependencyNodeId: string,\n ): void {\n const vulnNodeId = `vuln:${cve.id}`;\n const now = Date.now();\n\n // Truncate summary to 500 characters (Requirement 3.3)\n const summary = cve.summary.length > 500\n ? cve.summary.slice(0, 500)\n : cve.summary;\n\n // Determine source database from the first adapter that returned this CVE\n // (in practice, the adapter name is not stored on CVERecord, so we use\n // the first configured adapter's name as the source)\n const sourceDatabase = this.adapters.length > 0 ? this.adapters[0].name : 'unknown';\n\n // Upsert into nodes table\n rawDb.run(\n `INSERT OR REPLACE INTO nodes\n (id, kind, name, qualified_name, file_path, language,\n start_line, end_line, start_column, end_column,\n is_exported, is_async, is_static, is_abstract, updated_at)\n VALUES (?, 'vulnerability', ?, ?, '', 'unknown', 0, 0, 0, 0, 0, 0, 0, 0, ?)`,\n [vulnNodeId, cve.id, cve.id, now],\n );\n\n // Upsert into sec_vulnerabilities table\n const affectedRangesJson = JSON.stringify(cve.affectedVersionRanges);\n rawDb.run(\n `INSERT OR REPLACE INTO sec_vulnerabilities\n (node_id, cve_id, severity_score, affected_ranges, fixed_version, summary, source_database)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n [\n vulnNodeId,\n cve.id,\n cve.severity,\n affectedRangesJson,\n cve.fixedVersion ?? null,\n summary,\n sourceDatabase,\n ],\n );\n\n // Create has_vulnerability edge from Dependency_Node to Vulnerability_Node\n const edge: Edge = {\n source: dependencyNodeId,\n target: vulnNodeId,\n kind: 'has_vulnerability',\n confidence: 'extracted',\n confidenceScore: 1.0,\n };\n this.db.insertEdge(edge);\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -49,8 +49,10 @@ const ECOSYSTEM_MAP = {
49
49
  };
50
50
  const OSV_API_URL = "https://api.osv.dev/v1/query";
51
51
  const OSV_BATCH_API_URL = "https://api.osv.dev/v1/querybatch";
52
+ const OSV_VULN_DETAIL_URL = "https://api.osv.dev/v1/vulns";
52
53
  const DEFAULT_TIMEOUT_MS = 3e4;
53
54
  const OSV_BATCH_MAX_QUERIES = 1e3;
55
+ const OSV_DETAIL_FETCH_CONCURRENCY = 10;
54
56
  const MAX_SUMMARY_LENGTH = 500;
55
57
  function parseCvssScore(scoreStr) {
56
58
  if (!scoreStr) return null;
@@ -120,6 +122,7 @@ class OsvAdapter {
120
122
  this.name = "OSV";
121
123
  this.apiUrl = options?.apiUrl ?? OSV_API_URL;
122
124
  this.batchApiUrl = options?.batchApiUrl ?? OSV_BATCH_API_URL;
125
+ this.vulnDetailUrl = options?.vulnDetailUrl ?? OSV_VULN_DETAIL_URL;
123
126
  this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
124
127
  }
125
128
  /**
@@ -180,14 +183,55 @@ class OsvAdapter {
180
183
  } finally {
181
184
  clearTimeout(timeoutId);
182
185
  }
186
+ const uniqueIds = /* @__PURE__ */ new Set();
187
+ for (const result of batchResults) {
188
+ for (const vuln of result.vulns ?? []) {
189
+ uniqueIds.add(vuln.id);
190
+ }
191
+ }
192
+ const detailsById = await this.fetchVulnDetails([...uniqueIds], combinedSignal);
183
193
  return queries.map((_, i) => {
184
194
  const osvIdx = indexMap[i];
185
195
  if (osvIdx === null) return [];
186
196
  const resultEntry = batchResults[osvIdx];
187
197
  if (!resultEntry) return [];
188
- return this.parseResponse({ vulns: resultEntry.vulns });
198
+ const fullVulns = (resultEntry.vulns ?? []).map((stub) => detailsById.get(stub.id) ?? stub);
199
+ return this.parseResponse({ vulns: fullVulns });
189
200
  });
190
201
  }
202
+ /**
203
+ * Fetch full vulnerability records for a set of IDs via OSV's per-ID detail
204
+ * endpoint (the batch endpoint doesn't include this data). Runs with
205
+ * bounded concurrency; a failure on one ID is logged and that ID falls
206
+ * back to its stub (still usable, just without severity/EPSS-relevant data)
207
+ * rather than failing the whole batch.
208
+ */
209
+ async fetchVulnDetails(ids, signal) {
210
+ const results = /* @__PURE__ */ new Map();
211
+ if (ids.length === 0) return results;
212
+ let cursor = 0;
213
+ const worker = async () => {
214
+ while (cursor < ids.length) {
215
+ const id = ids[cursor++];
216
+ try {
217
+ const response = await fetch(`${this.vulnDetailUrl}/${encodeURIComponent(id)}`, { signal });
218
+ if (!response.ok) {
219
+ (0, import_errors2.logWarn)(`OSV: failed to fetch details for ${id}: HTTP ${response.status}`);
220
+ continue;
221
+ }
222
+ const vuln = await response.json();
223
+ results.set(id, vuln);
224
+ } catch (error) {
225
+ if (isAbortError(error)) return;
226
+ const msg = error instanceof Error ? error.message : String(error);
227
+ (0, import_errors2.logWarn)(`OSV: failed to fetch details for ${id}: ${msg}`);
228
+ }
229
+ }
230
+ };
231
+ const workerCount = Math.min(OSV_DETAIL_FETCH_CONCURRENCY, ids.length);
232
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
233
+ return results;
234
+ }
191
235
  async query(ecosystem, packageName, version, signal) {
192
236
  const osvEcosystem = ECOSYSTEM_MAP[ecosystem.toLowerCase()];
193
237
  if (!osvEcosystem) {
@@ -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 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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",
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 OSV_VULN_DETAIL_URL = 'https://api.osv.dev/v1/vulns';\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst OSV_BATCH_MAX_QUERIES = 1000;\nconst OSV_DETAIL_FETCH_CONCURRENCY = 10;\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 vulnDetailUrl: string;\n private readonly timeoutMs: number;\n\n constructor(options?: { apiUrl?: string; batchApiUrl?: string; vulnDetailUrl?: string; timeoutMs?: number }) {\n this.apiUrl = options?.apiUrl ?? OSV_API_URL;\n this.batchApiUrl = options?.batchApiUrl ?? OSV_BATCH_API_URL;\n this.vulnDetailUrl = options?.vulnDetailUrl ?? OSV_VULN_DETAIL_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 // OSV's batch endpoint only ever returns { id, modified } stubs \u2014 no\n // severity, affected ranges, summary, or fixed version. Fetch full\n // details for every unique vulnerability ID found, deduplicated since\n // many packages in the same batch often share the same advisory.\n const uniqueIds = new Set<string>();\n for (const result of batchResults) {\n for (const vuln of result.vulns ?? []) {\n uniqueIds.add(vuln.id);\n }\n }\n const detailsById = await this.fetchVulnDetails([...uniqueIds], combinedSignal);\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 const fullVulns = (resultEntry.vulns ?? []).map(stub => detailsById.get(stub.id) ?? stub);\n return this.parseResponse({ vulns: fullVulns });\n });\n }\n\n /**\n * Fetch full vulnerability records for a set of IDs via OSV's per-ID detail\n * endpoint (the batch endpoint doesn't include this data). Runs with\n * bounded concurrency; a failure on one ID is logged and that ID falls\n * back to its stub (still usable, just without severity/EPSS-relevant data)\n * rather than failing the whole batch.\n */\n private async fetchVulnDetails(\n ids: string[],\n signal: AbortSignal,\n ): Promise<Map<string, OsvVulnerability>> {\n const results = new Map<string, OsvVulnerability>();\n if (ids.length === 0) return results;\n\n let cursor = 0;\n const worker = async (): Promise<void> => {\n while (cursor < ids.length) {\n const id = ids[cursor++];\n try {\n const response = await fetch(`${this.vulnDetailUrl}/${encodeURIComponent(id)}`, { signal });\n if (!response.ok) {\n logWarn(`OSV: failed to fetch details for ${id}: HTTP ${response.status}`);\n continue;\n }\n const vuln = (await response.json()) as OsvVulnerability;\n results.set(id, vuln);\n } catch (error: unknown) {\n if (isAbortError(error)) return;\n const msg = error instanceof Error ? error.message : String(error);\n logWarn(`OSV: failed to fetch details for ${id}: ${msg}`);\n }\n }\n };\n\n const workerCount = Math.min(OSV_DETAIL_FETCH_CONCURRENCY, ids.length);\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n return results;\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
  }
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var json_schema_exports = {};
30
+ __export(json_schema_exports, {
31
+ SchemaLoadError: () => SchemaLoadError,
32
+ loadJsonSchema: () => loadJsonSchema,
33
+ validateAgainstSchema: () => validateAgainstSchema
34
+ });
35
+ module.exports = __toCommonJS(json_schema_exports);
36
+ var fs = __toESM(require("fs"));
37
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
38
+ function typeOf(value) {
39
+ if (value === null) return "null";
40
+ if (Array.isArray(value)) return "array";
41
+ return typeof value;
42
+ }
43
+ function describe(value) {
44
+ if (typeof value === "string") return JSON.stringify(value);
45
+ if (value === void 0) return "undefined";
46
+ return JSON.stringify(value);
47
+ }
48
+ function validateAgainstSchema(data, schema, pathPrefix = "") {
49
+ const errors = [];
50
+ const label = pathPrefix || "(root)";
51
+ if (schema.type) {
52
+ const actual = typeOf(data);
53
+ const matches = schema.type === "integer" ? actual === "number" && Number.isInteger(data) : actual === schema.type;
54
+ if (!matches) {
55
+ errors.push(`${label}: expected type ${schema.type}, got ${actual}`);
56
+ return errors;
57
+ }
58
+ }
59
+ if (schema.enum && !schema.enum.some((v) => v === data)) {
60
+ errors.push(`${label}: expected one of ${schema.enum.map(describe).join(", ")}, got ${describe(data)}`);
61
+ }
62
+ if (typeof data === "string") {
63
+ if (schema.pattern && !new RegExp(schema.pattern).test(data)) {
64
+ errors.push(`${label}: does not match pattern ${schema.pattern}`);
65
+ }
66
+ if (schema.minLength !== void 0 && data.length < schema.minLength) {
67
+ errors.push(`${label}: shorter than minLength ${schema.minLength}`);
68
+ }
69
+ if (schema.maxLength !== void 0 && data.length > schema.maxLength) {
70
+ errors.push(`${label}: longer than maxLength ${schema.maxLength}`);
71
+ }
72
+ if (schema.format === "date" && !DATE_RE.test(data)) {
73
+ errors.push(`${label}: expected format date (YYYY-MM-DD), got ${describe(data)}`);
74
+ }
75
+ }
76
+ if (typeof data === "number") {
77
+ if (schema.minimum !== void 0 && data < schema.minimum) {
78
+ errors.push(`${label}: below minimum ${schema.minimum}`);
79
+ }
80
+ if (schema.maximum !== void 0 && data > schema.maximum) {
81
+ errors.push(`${label}: above maximum ${schema.maximum}`);
82
+ }
83
+ }
84
+ if (schema.properties && typeOf(data) === "object") {
85
+ const obj = data;
86
+ for (const key of schema.required ?? []) {
87
+ if (!(key in obj) || obj[key] === void 0) {
88
+ errors.push(`${pathPrefix ? pathPrefix + "." : ""}${key}: missing required field`);
89
+ }
90
+ }
91
+ for (const [key, value] of Object.entries(obj)) {
92
+ const childPath = pathPrefix ? `${pathPrefix}.${key}` : key;
93
+ const propSchema = schema.properties[key];
94
+ if (!propSchema) {
95
+ if (schema.additionalProperties === false) {
96
+ errors.push(`${childPath}: unknown field, not allowed by schema`);
97
+ }
98
+ continue;
99
+ }
100
+ errors.push(...validateAgainstSchema(value, propSchema, childPath));
101
+ }
102
+ }
103
+ if (schema.items && Array.isArray(data)) {
104
+ data.forEach((item, i) => {
105
+ errors.push(...validateAgainstSchema(item, schema.items, `${pathPrefix}[${i}]`));
106
+ });
107
+ }
108
+ return errors;
109
+ }
110
+ class SchemaLoadError extends Error {
111
+ constructor(message) {
112
+ super(message);
113
+ this.name = "SchemaLoadError";
114
+ }
115
+ }
116
+ function loadJsonSchema(schemaPath) {
117
+ if (!fs.existsSync(schemaPath)) return null;
118
+ let raw;
119
+ try {
120
+ raw = fs.readFileSync(schemaPath, "utf8");
121
+ } catch (err) {
122
+ throw new SchemaLoadError(`Failed to read schema "${schemaPath}": ${err instanceof Error ? err.message : String(err)}`);
123
+ }
124
+ try {
125
+ return JSON.parse(raw);
126
+ } catch (err) {
127
+ throw new SchemaLoadError(`Schema "${schemaPath}" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
128
+ }
129
+ }
130
+ // Annotate the CommonJS export names for ESM import in node:
131
+ 0 && (module.exports = {
132
+ SchemaLoadError,
133
+ loadJsonSchema,
134
+ validateAgainstSchema
135
+ });
136
+ //# sourceMappingURL=json-schema.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/shared/json-schema.ts"],
4
+ "sourcesContent": ["/**\n * KiroGraph \u2014 Minimal JSON Schema subset validator\n *\n * No runtime dependency (ajv, etc.) \u2014 this validates the subset of JSON\n * Schema actually needed for opt-in structured-field validation in the\n * memory (memorySchemaValidation) and wiki (wikiTypedPages) modules:\n * type, enum, required, properties (recursive), items (arrays),\n * additionalProperties, pattern, min/maxLength, minimum/maximum, and a\n * basic `format: \"date\"` check. Unknown schema keywords (title,\n * description, $schema, x-* extensions, etc.) are ignored rather than\n * rejected, so a schema authored for a fuller validator still loads here.\n */\n\nimport * as fs from 'fs';\n\nexport interface JsonSchema {\n type?: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null';\n enum?: unknown[];\n required?: string[];\n properties?: Record<string, JsonSchema>;\n items?: JsonSchema;\n additionalProperties?: boolean;\n pattern?: string;\n minLength?: number;\n maxLength?: number;\n minimum?: number;\n maximum?: number;\n format?: string;\n [key: string]: unknown;\n}\n\nconst DATE_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nfunction typeOf(value: unknown): string {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'array';\n return typeof value;\n}\n\nfunction describe(value: unknown): string {\n if (typeof value === 'string') return JSON.stringify(value);\n if (value === undefined) return 'undefined';\n return JSON.stringify(value);\n}\n\n/**\n * Validate `data` against `schema`, returning one human-readable message per\n * violation (empty array = valid). `pathPrefix` is prepended to field paths\n * for nested calls \u2014 pass '' at the top level.\n */\nexport function validateAgainstSchema(data: unknown, schema: JsonSchema, pathPrefix = ''): string[] {\n const errors: string[] = [];\n const label = pathPrefix || '(root)';\n\n if (schema.type) {\n const actual = typeOf(data);\n const matches = schema.type === 'integer'\n ? actual === 'number' && Number.isInteger(data)\n : actual === schema.type;\n if (!matches) {\n errors.push(`${label}: expected type ${schema.type}, got ${actual}`);\n return errors; // further checks would be meaningless against the wrong type\n }\n }\n\n if (schema.enum && !schema.enum.some(v => v === data)) {\n errors.push(`${label}: expected one of ${schema.enum.map(describe).join(', ')}, got ${describe(data)}`);\n }\n\n if (typeof data === 'string') {\n if (schema.pattern && !new RegExp(schema.pattern).test(data)) {\n errors.push(`${label}: does not match pattern ${schema.pattern}`);\n }\n if (schema.minLength !== undefined && data.length < schema.minLength) {\n errors.push(`${label}: shorter than minLength ${schema.minLength}`);\n }\n if (schema.maxLength !== undefined && data.length > schema.maxLength) {\n errors.push(`${label}: longer than maxLength ${schema.maxLength}`);\n }\n if (schema.format === 'date' && !DATE_RE.test(data)) {\n errors.push(`${label}: expected format date (YYYY-MM-DD), got ${describe(data)}`);\n }\n }\n\n if (typeof data === 'number') {\n if (schema.minimum !== undefined && data < schema.minimum) {\n errors.push(`${label}: below minimum ${schema.minimum}`);\n }\n if (schema.maximum !== undefined && data > schema.maximum) {\n errors.push(`${label}: above maximum ${schema.maximum}`);\n }\n }\n\n if (schema.properties && typeOf(data) === 'object') {\n const obj = data as Record<string, unknown>;\n for (const key of schema.required ?? []) {\n if (!(key in obj) || obj[key] === undefined) {\n errors.push(`${pathPrefix ? pathPrefix + '.' : ''}${key}: missing required field`);\n }\n }\n for (const [key, value] of Object.entries(obj)) {\n const childPath = pathPrefix ? `${pathPrefix}.${key}` : key;\n const propSchema = schema.properties[key];\n if (!propSchema) {\n if (schema.additionalProperties === false) {\n errors.push(`${childPath}: unknown field, not allowed by schema`);\n }\n continue;\n }\n errors.push(...validateAgainstSchema(value, propSchema, childPath));\n }\n }\n\n if (schema.items && Array.isArray(data)) {\n data.forEach((item, i) => {\n errors.push(...validateAgainstSchema(item, schema.items as JsonSchema, `${pathPrefix}[${i}]`));\n });\n }\n\n return errors;\n}\n\nexport class SchemaLoadError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SchemaLoadError';\n }\n}\n\n/**\n * Load a `.schema.json` file from `schemaPath`. Returns null when the file\n * doesn't exist \u2014 schemas are opt-in per kind/type, so \"no schema for this\n * kind\" is a normal, silent no-op rather than an error. Throws\n * SchemaLoadError when the file exists but isn't valid JSON, since that's\n * an authoring mistake worth surfacing.\n */\nexport function loadJsonSchema(schemaPath: string): JsonSchema | null {\n if (!fs.existsSync(schemaPath)) return null;\n let raw: string;\n try {\n raw = fs.readFileSync(schemaPath, 'utf8');\n } catch (err) {\n throw new SchemaLoadError(`Failed to read schema \"${schemaPath}\": ${err instanceof Error ? err.message : String(err)}`);\n }\n try {\n return JSON.parse(raw) as JsonSchema;\n } catch (err) {\n throw new SchemaLoadError(`Schema \"${schemaPath}\" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }