kirograph 1.0.0 → 1.1.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 (84) hide show
  1. package/README.md +3 -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 +18 -8
  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 +56 -0
  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 +4 -0
  18. package/dist/config.js.map +2 -2
  19. package/dist/extraction/extractor.js +46 -41
  20. package/dist/extraction/extractor.js.map +2 -2
  21. package/dist/extraction/notebook.js +15 -7
  22. package/dist/extraction/notebook.js.map +2 -2
  23. package/dist/jev/client.js +99 -0
  24. package/dist/jev/client.js.map +7 -0
  25. package/dist/jev/types.js +17 -0
  26. package/dist/jev/types.js.map +7 -0
  27. package/dist/mcp/handlers/memory.js +14 -8
  28. package/dist/mcp/handlers/memory.js.map +2 -2
  29. package/dist/mcp/handlers/security.js +5 -3
  30. package/dist/mcp/handlers/security.js.map +2 -2
  31. package/dist/mcp/tools.js +1 -1
  32. package/dist/mcp/tools.js.map +1 -1
  33. package/dist/memory/index.js +13 -1
  34. package/dist/memory/index.js.map +3 -3
  35. package/dist/memory/relation-jev.js +59 -0
  36. package/dist/memory/relation-jev.js.map +7 -0
  37. package/dist/memory/schema.js +57 -0
  38. package/dist/memory/schema.js.map +7 -0
  39. package/dist/security/auth-detection-jev.js +47 -0
  40. package/dist/security/auth-detection-jev.js.map +7 -0
  41. package/dist/security/manifest/adapter.js +17 -3
  42. package/dist/security/manifest/adapter.js.map +3 -3
  43. package/dist/security/manifest/plugins/cargo.js +23 -0
  44. package/dist/security/manifest/plugins/cargo.js.map +2 -2
  45. package/dist/security/manifest/plugins/composer.js +12 -0
  46. package/dist/security/manifest/plugins/composer.js.map +2 -2
  47. package/dist/security/manifest/plugins/gradle.js +12 -0
  48. package/dist/security/manifest/plugins/gradle.js.map +2 -2
  49. package/dist/security/manifest/plugins/hex.js +12 -0
  50. package/dist/security/manifest/plugins/hex.js.map +2 -2
  51. package/dist/security/manifest/plugins/maven.js +68 -1
  52. package/dist/security/manifest/plugins/maven.js.map +2 -2
  53. package/dist/security/manifest/plugins/npm.js +86 -0
  54. package/dist/security/manifest/plugins/npm.js.map +2 -2
  55. package/dist/security/manifest/plugins/nuget.js +13 -0
  56. package/dist/security/manifest/plugins/nuget.js.map +2 -2
  57. package/dist/security/manifest/plugins/pubspec.js +12 -0
  58. package/dist/security/manifest/plugins/pubspec.js.map +2 -2
  59. package/dist/security/manifest/plugins/pyproject.js +12 -0
  60. package/dist/security/manifest/plugins/pyproject.js.map +2 -2
  61. package/dist/security/manifest/plugins/rubygems.js +12 -0
  62. package/dist/security/manifest/plugins/rubygems.js.map +2 -2
  63. package/dist/security/pipeline.js +1 -1
  64. package/dist/security/pipeline.js.map +2 -2
  65. package/dist/security/reachability.js +54 -5
  66. package/dist/security/reachability.js.map +3 -3
  67. package/dist/security/types.js.map +1 -1
  68. package/dist/security/vuln/client.js +45 -1
  69. package/dist/security/vuln/client.js.map +2 -2
  70. package/dist/security/vuln/osv-adapter.js +45 -1
  71. package/dist/security/vuln/osv-adapter.js.map +2 -2
  72. package/dist/wiki/contradiction-jev.js +49 -0
  73. package/dist/wiki/contradiction-jev.js.map +7 -0
  74. package/dist/wiki/index.js +13 -0
  75. package/dist/wiki/index.js.map +2 -2
  76. package/dist/wiki/ingest.js +2 -0
  77. package/dist/wiki/ingest.js.map +2 -2
  78. package/dist/wiki/links.js +53 -0
  79. package/dist/wiki/links.js.map +7 -0
  80. package/dist/wiki/lint.js +3 -6
  81. package/dist/wiki/lint.js.map +2 -2
  82. package/dist/wiki/rename.js +103 -0
  83. package/dist/wiki/rename.js.map +7 -0
  84. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/security/manifest/plugins/rubygems.ts"],
4
- "sourcesContent": ["/**\n * RubyGems Version Extraction Plugin for KiroGraph-Sec\n *\n * Parses Gemfile manifests to extract gem declarations with version constraints,\n * scopes (derived from `group` blocks), and resolved versions from Gemfile.lock.\n *\n * Handles:\n * - `gem 'name', '~> 1.0'` \u2014 single version constraint\n * - `gem 'name', '>= 1.0', '< 2.0'` \u2014 multiple constraints (joined with ', ')\n * - `gem 'name'` \u2014 no version constraint\n * - `group :development, :test do ... end` blocks \u2192 'development' scope\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\n/**\n * Resolved version map: gem name \u2192 resolved version string.\n * Built from Gemfile.lock.\n */\ntype ResolvedVersionMap = Map<string, string>;\n\n/**\n * Parse a Gemfile manifest and extract gem declarations with version constraints,\n * scopes, and resolved versions from Gemfile.lock.\n *\n * @param manifestPath - Absolute path to the Gemfile\n * @param projectRoot - Absolute path to the project root directory\n * @returns Array of parsed dependencies with version and scope information\n */\nexport async function parseRubygemsManifest(\n manifestPath: string,\n projectRoot: string,\n): Promise<ParsedDependency[]> {\n const manifestDir = path.dirname(manifestPath);\n const relativeManifest = path.relative(projectRoot, manifestPath).replace(/\\\\/g, '/');\n\n // Read the Gemfile content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:rubygems] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Attempt to load resolved versions from Gemfile.lock\n const resolvedVersions = loadResolvedVersions(manifestDir);\n\n // Attempt to extract license from a gemspec in the same directory\n const license = loadGemspecLicense(manifestDir);\n\n // Extract dependencies from the Gemfile\n const dependencies: ParsedDependency[] = [];\n const lines = content.split('\\n');\n\n // Track current group scope \u2014 null means top-level (production)\n let currentScope: ParsedDependency['scope'] = 'production';\n let groupDepth = 0; // tracks nesting depth inside a group block\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n\n // Detect start of a group block: `group :test, :development do`\n const groupMatch = trimmed.match(/^group\\s+(.+?)\\s+do\\s*(?:#.*)?$/);\n if (groupMatch) {\n groupDepth++;\n if (groupDepth === 1) {\n currentScope = resolveGroupScope(groupMatch[1]);\n }\n continue;\n }\n\n // Detect `end` \u2014 only reset scope when we exit the outermost group block\n if (trimmed === 'end' || trimmed.startsWith('end ') || trimmed.startsWith('end#')) {\n if (groupDepth > 0) {\n groupDepth--;\n if (groupDepth === 0) {\n currentScope = 'production';\n }\n }\n continue;\n }\n\n // Match gem declarations\n const dep = parseGemLine(trimmed, relativeManifest);\n if (dep) {\n const resolvedVersion = resolvedVersions.get(dep.name);\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion,\n scope: currentScope,\n ecosystem: 'rubygems',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n\n return dependencies;\n}\n\n/**\n * A raw dependency entry extracted from a Gemfile line.\n */\ninterface RawDependency {\n name: string;\n constraint: string;\n}\n\n/**\n * Parse a single `gem` declaration line from a Gemfile.\n *\n * Handles:\n * - `gem 'name'`\n * - `gem 'name', '~> 1.0'`\n * - `gem \"name\", \"~> 1.0\"`\n * - `gem 'name', '>= 1.0', '< 2.0'` (multiple constraints joined with ', ')\n */\nfunction parseGemLine(line: string, relativeManifest: string): RawDependency | null {\n // Strip inline comments\n const withoutComment = line.replace(/\\s*#.*$/, '');\n\n // Match: gem 'name' or gem \"name\" followed by optional version strings\n const gemMatch = withoutComment.match(/^gem\\s+(['\"])([^'\"]+)\\1(.*)?$/);\n if (!gemMatch) return null;\n\n const name = gemMatch[2].trim();\n const rest = (gemMatch[3] || '').trim();\n\n if (!isValidGemName(name)) {\n logWarn(`[sec:rubygems] Invalid gem name \"${name}\" in ${relativeManifest} \u2014 skipping`);\n return null;\n }\n\n // No version constraint\n if (rest === '' || rest.startsWith(':') || rest.startsWith(',\\s*:')) {\n return { name, constraint: '*' };\n }\n\n // Extract all string literals from the rest of the line that look like version constraints\n const versionConstraints: string[] = [];\n const versionPattern = /['\"]([^'\"]*)['\"]/g;\n let versionMatch: RegExpExecArray | null;\n while ((versionMatch = versionPattern.exec(rest)) !== null) {\n const candidate = versionMatch[1].trim();\n // Only capture strings that look like version constraints (start with ~>, >=, <=, !=, >, <, =, or a digit)\n if (/^(~>|>=|<=|!=|>|<|=|\\d)/.test(candidate)) {\n versionConstraints.push(candidate);\n }\n }\n\n if (versionConstraints.length === 0) {\n return { name, constraint: '*' };\n }\n\n return { name, constraint: versionConstraints.join(', ') };\n}\n\n/**\n * Determine the dependency scope from a group specifier string.\n * Any group containing :test or :development maps to 'development'.\n *\n * @param groupSpec - The group specifier string, e.g., `:development, :test`\n */\nfunction resolveGroupScope(groupSpec: string): ParsedDependency['scope'] {\n if (/:test\\b/.test(groupSpec) || /:development\\b/.test(groupSpec)) {\n return 'development';\n }\n return 'production';\n}\n\n/**\n * Validate that a gem name follows RubyGems naming conventions.\n */\nfunction isValidGemName(name: string): boolean {\n if (!name || typeof name !== 'string') return false;\n if (name.length > 128) return false;\n return /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name);\n}\n\n/**\n * Attempt to load resolved versions from Gemfile.lock in the given directory.\n * Returns an empty map if Gemfile.lock is not available.\n *\n * Gemfile.lock GEM section format:\n * ```\n * GEM\n * remote: https://rubygems.org/\n * specs:\n * rails (7.0.4)\n * rake (13.0.6)\n * ```\n */\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n const lockPath = path.join(manifestDir, 'Gemfile.lock');\n if (!fs.existsSync(lockPath)) {\n return map;\n }\n\n try {\n const lockContent = fs.readFileSync(lockPath, 'utf8');\n extractFromGemfileLock(lockContent, map);\n } catch (err) {\n logWarn(`[sec:rubygems] Failed to parse Gemfile.lock: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n return map;\n}\n\n/**\n * Extract resolved versions from Gemfile.lock content.\n *\n * The GEM section contains a `specs:` subsection with lines of the form:\n * ` gemname (1.2.3)` (4-space indent for direct deps, 6-space for sub-deps).\n * We capture all entries under `specs:`.\n */\nfunction extractFromGemfileLock(content: string, map: ResolvedVersionMap): void {\n const lines = content.split('\\n');\n let inGemSection = false;\n let inSpecsSection = false;\n\n for (const line of lines) {\n // Detect top-level section headers (no indent)\n if (/^\\S/.test(line)) {\n inGemSection = line.trimEnd() === 'GEM';\n inSpecsSection = false;\n continue;\n }\n\n if (!inGemSection) continue;\n\n // Detect ` specs:` subsection (2-space indent)\n if (/^ specs:\\s*$/.test(line)) {\n inSpecsSection = true;\n continue;\n }\n\n if (!inSpecsSection) continue;\n\n // Parse gem entries: ` gemname (1.2.3)` \u2014 4+ spaces indent\n const specMatch = line.match(/^ ([a-zA-Z0-9][a-zA-Z0-9_.-]*)\\s+\\(([^)]+)\\)\\s*$/);\n if (specMatch) {\n const name = specMatch[1];\n const version = specMatch[2].trim();\n if (!map.has(name)) {\n map.set(name, version);\n }\n }\n }\n}\n\n/**\n * Attempt to load the license from a .gemspec file in the given directory.\n * Looks for: s.license = 'MIT' or spec.license = \"MIT\"\n */\nfunction loadGemspecLicense(dir: string): string | undefined {\n try {\n const entries = fs.readdirSync(dir);\n const gemspecFile = entries.find(f => f.endsWith('.gemspec'));\n if (!gemspecFile) return undefined;\n const content = fs.readFileSync(path.join(dir, gemspecFile), 'utf8');\n const match = content.match(/\\.license\\s*=\\s*['\"]([^'\"]+)['\"]/);\n if (match && match[1].trim() !== '') {\n return match[1].trim();\n }\n } catch {\n // best-effort \u2014 ignore errors\n }\n return undefined;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * RubyGems Version Extraction Plugin for KiroGraph-Sec\n *\n * Parses Gemfile manifests to extract gem declarations with version constraints,\n * scopes (derived from `group` blocks), and resolved versions from Gemfile.lock.\n *\n * Handles:\n * - `gem 'name', '~> 1.0'` \u2014 single version constraint\n * - `gem 'name', '>= 1.0', '< 2.0'` \u2014 multiple constraints (joined with ', ')\n * - `gem 'name'` \u2014 no version constraint\n * - `group :development, :test do ... end` blocks \u2192 'development' scope\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\n/**\n * Resolved version map: gem name \u2192 resolved version string.\n * Built from Gemfile.lock.\n */\ntype ResolvedVersionMap = Map<string, string>;\n\n/**\n * Parse a Gemfile manifest and extract gem declarations with version constraints,\n * scopes, and resolved versions from Gemfile.lock.\n *\n * @param manifestPath - Absolute path to the Gemfile\n * @param projectRoot - Absolute path to the project root directory\n * @returns Array of parsed dependencies with version and scope information\n */\nexport async function parseRubygemsManifest(\n manifestPath: string,\n projectRoot: string,\n): Promise<ParsedDependency[]> {\n const manifestDir = path.dirname(manifestPath);\n const relativeManifest = path.relative(projectRoot, manifestPath).replace(/\\\\/g, '/');\n\n // Read the Gemfile content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:rubygems] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Attempt to load resolved versions from Gemfile.lock\n const resolvedVersions = loadResolvedVersions(manifestDir);\n\n // Attempt to extract license from a gemspec in the same directory\n const license = loadGemspecLicense(manifestDir);\n\n // Extract dependencies from the Gemfile\n const dependencies: ParsedDependency[] = [];\n const lines = content.split('\\n');\n\n // Track current group scope \u2014 null means top-level (production)\n let currentScope: ParsedDependency['scope'] = 'production';\n let groupDepth = 0; // tracks nesting depth inside a group block\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n\n // Detect start of a group block: `group :test, :development do`\n const groupMatch = trimmed.match(/^group\\s+(.+?)\\s+do\\s*(?:#.*)?$/);\n if (groupMatch) {\n groupDepth++;\n if (groupDepth === 1) {\n currentScope = resolveGroupScope(groupMatch[1]);\n }\n continue;\n }\n\n // Detect `end` \u2014 only reset scope when we exit the outermost group block\n if (trimmed === 'end' || trimmed.startsWith('end ') || trimmed.startsWith('end#')) {\n if (groupDepth > 0) {\n groupDepth--;\n if (groupDepth === 0) {\n currentScope = 'production';\n }\n }\n continue;\n }\n\n // Match gem declarations\n const dep = parseGemLine(trimmed, relativeManifest);\n if (dep) {\n const resolvedVersion = resolvedVersions.get(dep.name);\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion,\n scope: currentScope,\n ecosystem: 'rubygems',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n\n // Transitive-only gems (present in Gemfile.lock but never declared in the\n // Gemfile) would otherwise never become a Dependency_Node and would\n // silently skip vulnerability scanning. resolvedVersions already has every\n // gem Gemfile.lock resolved, direct or transitive.\n const directNames = new Set(dependencies.map(d => d.name));\n for (const [name, version] of resolvedVersions) {\n if (directNames.has(name)) continue;\n dependencies.push({\n name,\n declaredConstraint: version,\n resolvedVersion: version,\n scope: 'production',\n ecosystem: 'rubygems',\n sourceManifest: relativeManifest,\n });\n }\n\n return dependencies;\n}\n\n/**\n * A raw dependency entry extracted from a Gemfile line.\n */\ninterface RawDependency {\n name: string;\n constraint: string;\n}\n\n/**\n * Parse a single `gem` declaration line from a Gemfile.\n *\n * Handles:\n * - `gem 'name'`\n * - `gem 'name', '~> 1.0'`\n * - `gem \"name\", \"~> 1.0\"`\n * - `gem 'name', '>= 1.0', '< 2.0'` (multiple constraints joined with ', ')\n */\nfunction parseGemLine(line: string, relativeManifest: string): RawDependency | null {\n // Strip inline comments\n const withoutComment = line.replace(/\\s*#.*$/, '');\n\n // Match: gem 'name' or gem \"name\" followed by optional version strings\n const gemMatch = withoutComment.match(/^gem\\s+(['\"])([^'\"]+)\\1(.*)?$/);\n if (!gemMatch) return null;\n\n const name = gemMatch[2].trim();\n const rest = (gemMatch[3] || '').trim();\n\n if (!isValidGemName(name)) {\n logWarn(`[sec:rubygems] Invalid gem name \"${name}\" in ${relativeManifest} \u2014 skipping`);\n return null;\n }\n\n // No version constraint\n if (rest === '' || rest.startsWith(':') || rest.startsWith(',\\s*:')) {\n return { name, constraint: '*' };\n }\n\n // Extract all string literals from the rest of the line that look like version constraints\n const versionConstraints: string[] = [];\n const versionPattern = /['\"]([^'\"]*)['\"]/g;\n let versionMatch: RegExpExecArray | null;\n while ((versionMatch = versionPattern.exec(rest)) !== null) {\n const candidate = versionMatch[1].trim();\n // Only capture strings that look like version constraints (start with ~>, >=, <=, !=, >, <, =, or a digit)\n if (/^(~>|>=|<=|!=|>|<|=|\\d)/.test(candidate)) {\n versionConstraints.push(candidate);\n }\n }\n\n if (versionConstraints.length === 0) {\n return { name, constraint: '*' };\n }\n\n return { name, constraint: versionConstraints.join(', ') };\n}\n\n/**\n * Determine the dependency scope from a group specifier string.\n * Any group containing :test or :development maps to 'development'.\n *\n * @param groupSpec - The group specifier string, e.g., `:development, :test`\n */\nfunction resolveGroupScope(groupSpec: string): ParsedDependency['scope'] {\n if (/:test\\b/.test(groupSpec) || /:development\\b/.test(groupSpec)) {\n return 'development';\n }\n return 'production';\n}\n\n/**\n * Validate that a gem name follows RubyGems naming conventions.\n */\nfunction isValidGemName(name: string): boolean {\n if (!name || typeof name !== 'string') return false;\n if (name.length > 128) return false;\n return /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name);\n}\n\n/**\n * Attempt to load resolved versions from Gemfile.lock in the given directory.\n * Returns an empty map if Gemfile.lock is not available.\n *\n * Gemfile.lock GEM section format:\n * ```\n * GEM\n * remote: https://rubygems.org/\n * specs:\n * rails (7.0.4)\n * rake (13.0.6)\n * ```\n */\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n const lockPath = path.join(manifestDir, 'Gemfile.lock');\n if (!fs.existsSync(lockPath)) {\n return map;\n }\n\n try {\n const lockContent = fs.readFileSync(lockPath, 'utf8');\n extractFromGemfileLock(lockContent, map);\n } catch (err) {\n logWarn(`[sec:rubygems] Failed to parse Gemfile.lock: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n return map;\n}\n\n/**\n * Extract resolved versions from Gemfile.lock content.\n *\n * The GEM section contains a `specs:` subsection with lines of the form:\n * ` gemname (1.2.3)` (4-space indent for direct deps, 6-space for sub-deps).\n * We capture all entries under `specs:`.\n */\nfunction extractFromGemfileLock(content: string, map: ResolvedVersionMap): void {\n const lines = content.split('\\n');\n let inGemSection = false;\n let inSpecsSection = false;\n\n for (const line of lines) {\n // Detect top-level section headers (no indent)\n if (/^\\S/.test(line)) {\n inGemSection = line.trimEnd() === 'GEM';\n inSpecsSection = false;\n continue;\n }\n\n if (!inGemSection) continue;\n\n // Detect ` specs:` subsection (2-space indent)\n if (/^ specs:\\s*$/.test(line)) {\n inSpecsSection = true;\n continue;\n }\n\n if (!inSpecsSection) continue;\n\n // Parse gem entries: ` gemname (1.2.3)` \u2014 4+ spaces indent\n const specMatch = line.match(/^ ([a-zA-Z0-9][a-zA-Z0-9_.-]*)\\s+\\(([^)]+)\\)\\s*$/);\n if (specMatch) {\n const name = specMatch[1];\n const version = specMatch[2].trim();\n if (!map.has(name)) {\n map.set(name, version);\n }\n }\n }\n}\n\n/**\n * Attempt to load the license from a .gemspec file in the given directory.\n * Looks for: s.license = 'MIT' or spec.license = \"MIT\"\n */\nfunction loadGemspecLicense(dir: string): string | undefined {\n try {\n const entries = fs.readdirSync(dir);\n const gemspecFile = entries.find(f => f.endsWith('.gemspec'));\n if (!gemspecFile) return undefined;\n const content = fs.readFileSync(path.join(dir, gemspecFile), 'utf8');\n const match = content.match(/\\.license\\s*=\\s*['\"]([^'\"]+)['\"]/);\n if (match && match[1].trim() !== '') {\n return match[1].trim();\n }\n } catch {\n // best-effort \u2014 ignore errors\n }\n return undefined;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -68,7 +68,7 @@ class SecurityPipeline {
68
68
  if (this.config.securityAutoEnrich) {
69
69
  onProgress?.("vulnerability-enrichment", 2, 4);
70
70
  const adapters = this.createVulnAdapters();
71
- const vulnClient = new import_client.VulnerabilityDatabaseClient(adapters, this.db);
71
+ const vulnClient = new import_client.VulnerabilityDatabaseClient(adapters, this.db, void 0, this.projectRoot);
72
72
  const enrichResult = await vulnClient.enrichAll();
73
73
  result.vulnerabilitiesFound = enrichResult.vulnerabilitiesFound;
74
74
  onProgress?.("vulnerability-enrichment", 3, 4);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/security/pipeline.ts"],
4
- "sourcesContent": ["/**\n * SecurityPipeline \u2014 Orchestrates the security analysis phases.\n *\n * Phases (in order):\n * 1. Manifest parsing \u2014 discover and parse dependency manifests\n * 2. Dependency integration \u2014 link dependencies to code symbols\n * 3. Vulnerability enrichment \u2014 query databases for CVEs (if securityAutoEnrich)\n * 4. Reachability analysis \u2014 determine if vulnerable code is reachable\n *\n * Reports progress via callback and returns a SecurityResult with counts and duration.\n *\n * Requirements: 9.3, 9.5\n */\n\nimport type { GraphDatabase } from '../db/database';\nimport type { KiroGraphConfig } from '../config';\nimport type { SecurityResult } from './types';\nimport { ManifestParser } from './manifest/parser';\nimport { DependencyGraphIntegrator } from './integrator';\nimport { VulnerabilityDatabaseClient } from './vuln/client';\nimport { OsvAdapter } from './vuln/osv-adapter';\nimport { ReachabilityAnalyzer } from './reachability';\nimport { logWarn } from '../errors';\n\n/**\n * SecurityPipeline orchestrates the full security analysis workflow.\n */\nexport class SecurityPipeline {\n private readonly db: GraphDatabase;\n private readonly config: KiroGraphConfig;\n private readonly projectRoot: string;\n\n constructor(db: GraphDatabase, config: KiroGraphConfig, projectRoot: string) {\n this.db = db;\n this.config = config;\n this.projectRoot = projectRoot;\n }\n\n /**\n * Run the full security analysis pipeline.\n *\n * @param onProgress - Optional callback reporting (phase, current, total)\n * @returns SecurityResult with counts and duration\n */\n async run(\n onProgress?: (phase: string, current: number, total: number) => void,\n ): Promise<SecurityResult> {\n const start = Date.now();\n\n const result: SecurityResult = {\n manifestsDiscovered: 0,\n dependenciesCreated: 0,\n vulnerabilitiesFound: 0,\n affectedCount: 0,\n notAffectedCount: 0,\n underInvestigationCount: 0,\n duration: 0,\n };\n\n // \u2500\u2500 Phase 1: Manifest Parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.('manifest-parsing', 0, 4);\n\n const parser = new ManifestParser(this.db, this.projectRoot, {\n include: this.config.include,\n exclude: this.config.exclude,\n });\n const parseResult = await parser.parseAll();\n\n result.manifestsDiscovered = parseResult.manifestsParsed;\n result.dependenciesCreated = parseResult.dependenciesCreated;\n\n onProgress?.('manifest-parsing', 1, 4);\n\n // \u2500\u2500 Phase 2: Dependency Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.('dependency-integration', 1, 4);\n\n const integrator = new DependencyGraphIntegrator(this.db, this.projectRoot);\n await integrator.integrate();\n await integrator.resolveTransitives(10);\n await integrator.cleanup();\n\n onProgress?.('dependency-integration', 2, 4);\n\n // \u2500\u2500 Phase 3: Vulnerability Enrichment (conditional) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (this.config.securityAutoEnrich) {\n onProgress?.('vulnerability-enrichment', 2, 4);\n\n const adapters = this.createVulnAdapters();\n const vulnClient = new VulnerabilityDatabaseClient(adapters, this.db);\n const enrichResult = await vulnClient.enrichAll();\n\n result.vulnerabilitiesFound = enrichResult.vulnerabilitiesFound;\n\n onProgress?.('vulnerability-enrichment', 3, 4);\n } else {\n onProgress?.('vulnerability-enrichment', 3, 4);\n }\n\n // \u2500\u2500 Phase 4: Reachability Analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.('reachability-analysis', 3, 4);\n\n const reachability = new ReachabilityAnalyzer(this.db, this.config);\n const reachResults = await reachability.analyzeAll();\n\n // Count verdicts\n for (const [, reachResult] of reachResults) {\n switch (reachResult.verdict) {\n case 'affected':\n result.affectedCount++;\n break;\n case 'not_affected':\n result.notAffectedCount++;\n break;\n case 'under_investigation':\n result.underInvestigationCount++;\n break;\n }\n }\n\n // Generate impact summaries for affected vulnerabilities\n for (const [vulnNodeId, reachResult] of reachResults) {\n if (reachResult.verdict === 'affected') {\n await reachability.getImpactSummary(vulnNodeId);\n }\n }\n\n onProgress?.('reachability-analysis', 4, 4);\n\n result.duration = Date.now() - start;\n return result;\n }\n\n /**\n * Create vulnerability database adapters based on config.\n */\n private createVulnAdapters(): import('./vuln/types').VulnDatabaseAdapter[] {\n const adapters: import('./vuln/types').VulnDatabaseAdapter[] = [];\n\n for (const dbName of this.config.securityDatabases) {\n switch (dbName.toUpperCase()) {\n case 'OSV':\n adapters.push(new OsvAdapter());\n break;\n default:\n logWarn(`[sec:pipeline] Unknown vulnerability database \"${dbName}\", skipping`);\n break;\n }\n }\n\n // Fallback to OSV if no valid adapters configured\n if (adapters.length === 0) {\n adapters.push(new OsvAdapter());\n }\n\n return adapters;\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,oBAA+B;AAC/B,wBAA0C;AAC1C,oBAA4C;AAC5C,yBAA2B;AAC3B,0BAAqC;AACrC,oBAAwB;AAKjB,MAAM,iBAAiB;AAAA,EAK5B,YAAY,IAAmB,QAAyB,aAAqB;AAC3E,SAAK,KAAK;AACV,SAAK,SAAS;AACd,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,IACJ,YACyB;AACzB,UAAM,QAAQ,KAAK,IAAI;AAEvB,UAAM,SAAyB;AAAA,MAC7B,qBAAqB;AAAA,MACrB,qBAAqB;AAAA,MACrB,sBAAsB;AAAA,MACtB,eAAe;AAAA,MACf,kBAAkB;AAAA,MAClB,yBAAyB;AAAA,MACzB,UAAU;AAAA,IACZ;AAGA,iBAAa,oBAAoB,GAAG,CAAC;AAErC,UAAM,SAAS,IAAI,6BAAe,KAAK,IAAI,KAAK,aAAa;AAAA,MAC3D,SAAS,KAAK,OAAO;AAAA,MACrB,SAAS,KAAK,OAAO;AAAA,IACvB,CAAC;AACD,UAAM,cAAc,MAAM,OAAO,SAAS;AAE1C,WAAO,sBAAsB,YAAY;AACzC,WAAO,sBAAsB,YAAY;AAEzC,iBAAa,oBAAoB,GAAG,CAAC;AAGrC,iBAAa,0BAA0B,GAAG,CAAC;AAE3C,UAAM,aAAa,IAAI,4CAA0B,KAAK,IAAI,KAAK,WAAW;AAC1E,UAAM,WAAW,UAAU;AAC3B,UAAM,WAAW,mBAAmB,EAAE;AACtC,UAAM,WAAW,QAAQ;AAEzB,iBAAa,0BAA0B,GAAG,CAAC;AAG3C,QAAI,KAAK,OAAO,oBAAoB;AAClC,mBAAa,4BAA4B,GAAG,CAAC;AAE7C,YAAM,WAAW,KAAK,mBAAmB;AACzC,YAAM,aAAa,IAAI,0CAA4B,UAAU,KAAK,EAAE;AACpE,YAAM,eAAe,MAAM,WAAW,UAAU;AAEhD,aAAO,uBAAuB,aAAa;AAE3C,mBAAa,4BAA4B,GAAG,CAAC;AAAA,IAC/C,OAAO;AACL,mBAAa,4BAA4B,GAAG,CAAC;AAAA,IAC/C;AAGA,iBAAa,yBAAyB,GAAG,CAAC;AAE1C,UAAM,eAAe,IAAI,yCAAqB,KAAK,IAAI,KAAK,MAAM;AAClE,UAAM,eAAe,MAAM,aAAa,WAAW;AAGnD,eAAW,CAAC,EAAE,WAAW,KAAK,cAAc;AAC1C,cAAQ,YAAY,SAAS;AAAA,QAC3B,KAAK;AACH,iBAAO;AACP;AAAA,QACF,KAAK;AACH,iBAAO;AACP;AAAA,QACF,KAAK;AACH,iBAAO;AACP;AAAA,MACJ;AAAA,IACF;AAGA,eAAW,CAAC,YAAY,WAAW,KAAK,cAAc;AACpD,UAAI,YAAY,YAAY,YAAY;AACtC,cAAM,aAAa,iBAAiB,UAAU;AAAA,MAChD;AAAA,IACF;AAEA,iBAAa,yBAAyB,GAAG,CAAC;AAE1C,WAAO,WAAW,KAAK,IAAI,IAAI;AAC/B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,qBAAmE;AACzE,UAAM,WAAyD,CAAC;AAEhE,eAAW,UAAU,KAAK,OAAO,mBAAmB;AAClD,cAAQ,OAAO,YAAY,GAAG;AAAA,QAC5B,KAAK;AACH,mBAAS,KAAK,IAAI,8BAAW,CAAC;AAC9B;AAAA,QACF;AACE,qCAAQ,kDAAkD,MAAM,aAAa;AAC7E;AAAA,MACJ;AAAA,IACF;AAGA,QAAI,SAAS,WAAW,GAAG;AACzB,eAAS,KAAK,IAAI,8BAAW,CAAC;AAAA,IAChC;AAEA,WAAO;AAAA,EACT;AACF;",
4
+ "sourcesContent": ["/**\n * SecurityPipeline \u2014 Orchestrates the security analysis phases.\n *\n * Phases (in order):\n * 1. Manifest parsing \u2014 discover and parse dependency manifests\n * 2. Dependency integration \u2014 link dependencies to code symbols\n * 3. Vulnerability enrichment \u2014 query databases for CVEs (if securityAutoEnrich)\n * 4. Reachability analysis \u2014 determine if vulnerable code is reachable\n *\n * Reports progress via callback and returns a SecurityResult with counts and duration.\n *\n * Requirements: 9.3, 9.5\n */\n\nimport type { GraphDatabase } from '../db/database';\nimport type { KiroGraphConfig } from '../config';\nimport type { SecurityResult } from './types';\nimport { ManifestParser } from './manifest/parser';\nimport { DependencyGraphIntegrator } from './integrator';\nimport { VulnerabilityDatabaseClient } from './vuln/client';\nimport { OsvAdapter } from './vuln/osv-adapter';\nimport { ReachabilityAnalyzer } from './reachability';\nimport { logWarn } from '../errors';\n\n/**\n * SecurityPipeline orchestrates the full security analysis workflow.\n */\nexport class SecurityPipeline {\n private readonly db: GraphDatabase;\n private readonly config: KiroGraphConfig;\n private readonly projectRoot: string;\n\n constructor(db: GraphDatabase, config: KiroGraphConfig, projectRoot: string) {\n this.db = db;\n this.config = config;\n this.projectRoot = projectRoot;\n }\n\n /**\n * Run the full security analysis pipeline.\n *\n * @param onProgress - Optional callback reporting (phase, current, total)\n * @returns SecurityResult with counts and duration\n */\n async run(\n onProgress?: (phase: string, current: number, total: number) => void,\n ): Promise<SecurityResult> {\n const start = Date.now();\n\n const result: SecurityResult = {\n manifestsDiscovered: 0,\n dependenciesCreated: 0,\n vulnerabilitiesFound: 0,\n affectedCount: 0,\n notAffectedCount: 0,\n underInvestigationCount: 0,\n duration: 0,\n };\n\n // \u2500\u2500 Phase 1: Manifest Parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.('manifest-parsing', 0, 4);\n\n const parser = new ManifestParser(this.db, this.projectRoot, {\n include: this.config.include,\n exclude: this.config.exclude,\n });\n const parseResult = await parser.parseAll();\n\n result.manifestsDiscovered = parseResult.manifestsParsed;\n result.dependenciesCreated = parseResult.dependenciesCreated;\n\n onProgress?.('manifest-parsing', 1, 4);\n\n // \u2500\u2500 Phase 2: Dependency Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.('dependency-integration', 1, 4);\n\n const integrator = new DependencyGraphIntegrator(this.db, this.projectRoot);\n await integrator.integrate();\n await integrator.resolveTransitives(10);\n await integrator.cleanup();\n\n onProgress?.('dependency-integration', 2, 4);\n\n // \u2500\u2500 Phase 3: Vulnerability Enrichment (conditional) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (this.config.securityAutoEnrich) {\n onProgress?.('vulnerability-enrichment', 2, 4);\n\n const adapters = this.createVulnAdapters();\n const vulnClient = new VulnerabilityDatabaseClient(adapters, this.db, undefined, this.projectRoot);\n const enrichResult = await vulnClient.enrichAll();\n\n result.vulnerabilitiesFound = enrichResult.vulnerabilitiesFound;\n\n onProgress?.('vulnerability-enrichment', 3, 4);\n } else {\n onProgress?.('vulnerability-enrichment', 3, 4);\n }\n\n // \u2500\u2500 Phase 4: Reachability Analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.('reachability-analysis', 3, 4);\n\n const reachability = new ReachabilityAnalyzer(this.db, this.config);\n const reachResults = await reachability.analyzeAll();\n\n // Count verdicts\n for (const [, reachResult] of reachResults) {\n switch (reachResult.verdict) {\n case 'affected':\n result.affectedCount++;\n break;\n case 'not_affected':\n result.notAffectedCount++;\n break;\n case 'under_investigation':\n result.underInvestigationCount++;\n break;\n }\n }\n\n // Generate impact summaries for affected vulnerabilities\n for (const [vulnNodeId, reachResult] of reachResults) {\n if (reachResult.verdict === 'affected') {\n await reachability.getImpactSummary(vulnNodeId);\n }\n }\n\n onProgress?.('reachability-analysis', 4, 4);\n\n result.duration = Date.now() - start;\n return result;\n }\n\n /**\n * Create vulnerability database adapters based on config.\n */\n private createVulnAdapters(): import('./vuln/types').VulnDatabaseAdapter[] {\n const adapters: import('./vuln/types').VulnDatabaseAdapter[] = [];\n\n for (const dbName of this.config.securityDatabases) {\n switch (dbName.toUpperCase()) {\n case 'OSV':\n adapters.push(new OsvAdapter());\n break;\n default:\n logWarn(`[sec:pipeline] Unknown vulnerability database \"${dbName}\", skipping`);\n break;\n }\n }\n\n // Fallback to OSV if no valid adapters configured\n if (adapters.length === 0) {\n adapters.push(new OsvAdapter());\n }\n\n return adapters;\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,oBAA+B;AAC/B,wBAA0C;AAC1C,oBAA4C;AAC5C,yBAA2B;AAC3B,0BAAqC;AACrC,oBAAwB;AAKjB,MAAM,iBAAiB;AAAA,EAK5B,YAAY,IAAmB,QAAyB,aAAqB;AAC3E,SAAK,KAAK;AACV,SAAK,SAAS;AACd,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,IACJ,YACyB;AACzB,UAAM,QAAQ,KAAK,IAAI;AAEvB,UAAM,SAAyB;AAAA,MAC7B,qBAAqB;AAAA,MACrB,qBAAqB;AAAA,MACrB,sBAAsB;AAAA,MACtB,eAAe;AAAA,MACf,kBAAkB;AAAA,MAClB,yBAAyB;AAAA,MACzB,UAAU;AAAA,IACZ;AAGA,iBAAa,oBAAoB,GAAG,CAAC;AAErC,UAAM,SAAS,IAAI,6BAAe,KAAK,IAAI,KAAK,aAAa;AAAA,MAC3D,SAAS,KAAK,OAAO;AAAA,MACrB,SAAS,KAAK,OAAO;AAAA,IACvB,CAAC;AACD,UAAM,cAAc,MAAM,OAAO,SAAS;AAE1C,WAAO,sBAAsB,YAAY;AACzC,WAAO,sBAAsB,YAAY;AAEzC,iBAAa,oBAAoB,GAAG,CAAC;AAGrC,iBAAa,0BAA0B,GAAG,CAAC;AAE3C,UAAM,aAAa,IAAI,4CAA0B,KAAK,IAAI,KAAK,WAAW;AAC1E,UAAM,WAAW,UAAU;AAC3B,UAAM,WAAW,mBAAmB,EAAE;AACtC,UAAM,WAAW,QAAQ;AAEzB,iBAAa,0BAA0B,GAAG,CAAC;AAG3C,QAAI,KAAK,OAAO,oBAAoB;AAClC,mBAAa,4BAA4B,GAAG,CAAC;AAE7C,YAAM,WAAW,KAAK,mBAAmB;AACzC,YAAM,aAAa,IAAI,0CAA4B,UAAU,KAAK,IAAI,QAAW,KAAK,WAAW;AACjG,YAAM,eAAe,MAAM,WAAW,UAAU;AAEhD,aAAO,uBAAuB,aAAa;AAE3C,mBAAa,4BAA4B,GAAG,CAAC;AAAA,IAC/C,OAAO;AACL,mBAAa,4BAA4B,GAAG,CAAC;AAAA,IAC/C;AAGA,iBAAa,yBAAyB,GAAG,CAAC;AAE1C,UAAM,eAAe,IAAI,yCAAqB,KAAK,IAAI,KAAK,MAAM;AAClE,UAAM,eAAe,MAAM,aAAa,WAAW;AAGnD,eAAW,CAAC,EAAE,WAAW,KAAK,cAAc;AAC1C,cAAQ,YAAY,SAAS;AAAA,QAC3B,KAAK;AACH,iBAAO;AACP;AAAA,QACF,KAAK;AACH,iBAAO;AACP;AAAA,QACF,KAAK;AACH,iBAAO;AACP;AAAA,MACJ;AAAA,IACF;AAGA,eAAW,CAAC,YAAY,WAAW,KAAK,cAAc;AACpD,UAAI,YAAY,YAAY,YAAY;AACtC,cAAM,aAAa,iBAAiB,UAAU;AAAA,MAChD;AAAA,IACF;AAEA,iBAAa,yBAAyB,GAAG,CAAC;AAE1C,WAAO,WAAW,KAAK,IAAI,IAAI;AAC/B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,qBAAmE;AACzE,UAAM,WAAyD,CAAC;AAEhE,eAAW,UAAU,KAAK,OAAO,mBAAmB;AAClD,cAAQ,OAAO,YAAY,GAAG;AAAA,QAC5B,KAAK;AACH,mBAAS,KAAK,IAAI,8BAAW,CAAC;AAC9B;AAAA,QACF;AACE,qCAAQ,kDAAkD,MAAM,aAAa;AAC7E;AAAA,MACJ;AAAA,IACF;AAGA,QAAI,SAAS,WAAW,GAAG;AACzB,eAAS,KAAK,IAAI,8BAAW,CAAC;AAAA,IAChC;AAEA,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -18,10 +18,12 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var reachability_exports = {};
20
20
  __export(reachability_exports, {
21
- ReachabilityAnalyzer: () => ReachabilityAnalyzer
21
+ ReachabilityAnalyzer: () => ReachabilityAnalyzer,
22
+ explainReachability: () => explainReachability
22
23
  });
23
24
  module.exports = __toCommonJS(reachability_exports);
24
25
  const TRAVERSAL_EDGE_KINDS = ["calls", "imports", "references"];
26
+ const TRAVERSAL_EDGE_KINDS_SQL = TRAVERSAL_EDGE_KINDS.map((k) => `'${k}'`).join(",");
25
27
  const MAX_UNRESOLVED_SYMBOLS = 50;
26
28
  const MAX_DISTINCT_PATHS = 100;
27
29
  class ReachabilityAnalyzer {
@@ -46,26 +48,47 @@ class ReachabilityAnalyzer {
46
48
  [vulnerabilityNodeId]
47
49
  );
48
50
  if (!depEdge) {
49
- return {
51
+ const result2 = {
50
52
  verdict: "under_investigation",
53
+ reason: "no_dependency_link",
51
54
  paths: [],
52
55
  unresolvedSymbols: [],
53
56
  reachingEntryPointCount: 0
54
57
  };
58
+ this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result2);
59
+ return result2;
55
60
  }
56
61
  const dependencyNodeId = depEdge.source;
62
+ const incomingEdgeCount = rawDb.get(
63
+ `SELECT COUNT(*) as c FROM edges WHERE target = ? AND kind IN (${TRAVERSAL_EDGE_KINDS_SQL})`,
64
+ [dependencyNodeId]
65
+ );
66
+ if (incomingEdgeCount.c === 0) {
67
+ const result2 = {
68
+ verdict: "under_investigation",
69
+ reason: "no_call_graph_signal",
70
+ paths: [],
71
+ unresolvedSymbols: [],
72
+ reachingEntryPointCount: 0
73
+ };
74
+ this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result2);
75
+ return result2;
76
+ }
57
77
  const entryPoints = rawDb.all(
58
78
  `SELECT id FROM nodes WHERE kind = 'route'
59
79
  UNION
60
80
  SELECT id FROM nodes WHERE kind = 'function' AND is_exported = 1`
61
81
  );
62
82
  if (entryPoints.length === 0) {
63
- return {
83
+ const result2 = {
64
84
  verdict: "not_affected",
85
+ reason: "no_entry_points",
65
86
  paths: [],
66
87
  unresolvedSymbols: [],
67
88
  reachingEntryPointCount: 0
68
89
  };
90
+ this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result2);
91
+ return result2;
69
92
  }
70
93
  const reachingPaths = [];
71
94
  const entryPointIds = new Set(entryPoints.map((ep) => ep.id));
@@ -82,15 +105,20 @@ class ReachabilityAnalyzer {
82
105
  }
83
106
  const cappedUnresolved = unresolvedSymbols.slice(0, MAX_UNRESOLVED_SYMBOLS);
84
107
  let verdict;
108
+ let reason;
85
109
  if (reachingPaths.length > 0) {
86
110
  verdict = "affected";
111
+ reason = "path_found";
87
112
  } else if (cappedUnresolved.length > 0) {
88
113
  verdict = "under_investigation";
114
+ reason = "unresolved_imports";
89
115
  } else {
90
116
  verdict = "not_affected";
117
+ reason = "no_path_found";
91
118
  }
92
119
  const result = {
93
120
  verdict,
121
+ reason,
94
122
  paths: reachingPaths,
95
123
  unresolvedSymbols: verdict === "under_investigation" ? cappedUnresolved : [],
96
124
  reachingEntryPointCount: reachingPaths.length
@@ -210,7 +238,7 @@ class ReachabilityAnalyzer {
210
238
  const unresolvedSymbols = /* @__PURE__ */ new Set();
211
239
  const visited = /* @__PURE__ */ new Set();
212
240
  const parentMap = /* @__PURE__ */ new Map();
213
- const edgeKinds = TRAVERSAL_EDGE_KINDS.map((k) => `'${k}'`).join(",");
241
+ const edgeKinds = TRAVERSAL_EDGE_KINDS_SQL;
214
242
  const queue = [dependencyNodeId];
215
243
  visited.add(dependencyNodeId);
216
244
  while (queue.length > 0) {
@@ -285,8 +313,29 @@ class ReachabilityAnalyzer {
285
313
  );
286
314
  }
287
315
  }
316
+ function explainReachability(result) {
317
+ switch (result.reason) {
318
+ case "path_found":
319
+ return `Reachable: ${result.reachingEntryPointCount} entry point${result.reachingEntryPointCount === 1 ? "" : "s"} have a call/import/reference path to this dependency (see paths below).`;
320
+ case "no_dependency_link":
321
+ return `This vulnerability isn't linked to any dependency node in the graph, so reachability can't be evaluated at all. This usually means the vulnerability was registered manually (e.g. via "kirograph vulns --add") against a package that isn't currently indexed.`;
322
+ case "no_call_graph_signal":
323
+ return `Nothing in the indexed code explicitly imports, calls, or references this dependency \u2014 it has zero incoming edges in the call graph. This is common for dependencies a framework wires in via classpath scanning or reflection rather than an explicit reference (e.g. an embedded servlet container like Tomcat, Netty, or Jetty). The call graph has no signal about it either way, so "not reachable" can't be concluded.`;
324
+ case "unresolved_imports":
325
+ return `No call/import/reference path was found from any entry point, but the backward traversal hit ${result.unresolvedSymbols.length} unresolved import${result.unresolvedSymbols.length === 1 ? "" : "s"} along the way (dynamic dispatch, reflection, or unindexed code \u2014 see below). The graph is incomplete near this dependency, so "not reachable" can't be concluded with confidence.`;
326
+ case "no_entry_points":
327
+ return `No entry points (HTTP routes or exported functions) were found anywhere in the indexed project, so there's nowhere to start a reachability search from. This can mean the project genuinely has none (a library, not a server), or that entry-point detection missed them for this codebase.`;
328
+ case "no_path_found":
329
+ return `No call/import/reference path was found from any entry point to this dependency, and the traversal completed without hitting any unresolved imports \u2014 a high-confidence result.`;
330
+ default: {
331
+ const _exhaustive = result.reason;
332
+ return _exhaustive;
333
+ }
334
+ }
335
+ }
288
336
  // Annotate the CommonJS export names for ESM import in node:
289
337
  0 && (module.exports = {
290
- ReachabilityAnalyzer
338
+ ReachabilityAnalyzer,
339
+ explainReachability
291
340
  });
292
341
  //# sourceMappingURL=reachability.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/security/reachability.ts"],
4
- "sourcesContent": ["/**\n * KiroGraph-Sec Reachability Analyzer\n *\n * Traverses the knowledge graph from application entry points to determine\n * whether vulnerable dependencies are reachable through actual code paths.\n * Produces reachability verdicts and impact summaries enriched with\n * architectural layer context.\n *\n * Requirements: 4.1, 4.2, 4.3, 4.4, 4.6, 5.1, 5.2, 5.3, 5.4, 5.5\n */\n\nimport type { GraphDatabase } from '../db/database';\nimport type { KiroGraphConfig } from '../config';\nimport type {\n ReachabilityVerdict,\n ReachabilityPath,\n ReachabilityResult,\n ImpactSummary,\n} from './types';\n\n/** Edge kinds used for reachability traversal */\nconst TRAVERSAL_EDGE_KINDS = ['calls', 'imports', 'references'] as const;\n\n/** Maximum unresolved symbols to report (for the under_investigation verdict) */\nconst MAX_UNRESOLVED_SYMBOLS = 50;\n\n/** Maximum distinct paths to report in impact summary */\nconst MAX_DISTINCT_PATHS = 100;\n\n/**\n * ReachabilityAnalyzer traverses the knowledge graph from entry points\n * toward affected dependency nodes to determine reachability verdicts.\n */\nexport class ReachabilityAnalyzer {\n private readonly db: GraphDatabase;\n private readonly config: KiroGraphConfig;\n\n constructor(db: GraphDatabase, config: KiroGraphConfig) {\n this.db = db;\n this.config = config;\n }\n\n /**\n * Analyze reachability for a single Vulnerability_Node.\n *\n * 1. Find the Dependency_Node linked via `has_vulnerability` edge\n * 2. Find all Entry_Points in the graph\n * 3. Reverse BFS from the dependency node through incoming edges toward entry points\n * 4. Assign verdict based on path existence and unresolved imports\n *\n * Requirements: 4.1, 4.2, 4.3, 4.4, 4.6\n */\n async analyze(vulnerabilityNodeId: string): Promise<ReachabilityResult> {\n const rawDb = this.db.getRawDb();\n\n // Step 1: Find the Dependency_Node linked to this vulnerability\n const depEdge = rawDb.get(\n `SELECT source FROM edges WHERE target = ? AND kind = 'has_vulnerability'`,\n [vulnerabilityNodeId],\n );\n\n if (!depEdge) {\n // No dependency linked \u2014 cannot determine reachability\n return {\n verdict: 'under_investigation',\n paths: [],\n unresolvedSymbols: [],\n reachingEntryPointCount: 0,\n };\n }\n\n const dependencyNodeId: string = depEdge.source;\n\n // Step 2: Find all Entry_Points\n // Entry points are: nodes with kind='route' OR nodes with kind='function' that are exported\n const entryPoints: Array<{ id: string }> = rawDb.all(\n `SELECT id FROM nodes WHERE kind = 'route'\n UNION\n SELECT id FROM nodes WHERE kind = 'function' AND is_exported = 1`,\n );\n\n if (entryPoints.length === 0) {\n // No entry points \u2014 cannot determine reachability\n return {\n verdict: 'not_affected',\n paths: [],\n unresolvedSymbols: [],\n reachingEntryPointCount: 0,\n };\n }\n\n // Step 3: Reverse BFS from the dependency node through INCOMING edges\n // to find which entry points can reach it. This is O(V+E) per vulnerability\n // instead of O(entryPoints \u00D7 (V+E)) for forward BFS from each entry point.\n const reachingPaths: ReachabilityPath[] = [];\n const entryPointIds = new Set(entryPoints.map(ep => ep.id));\n const { reached, unresolvedSymbols } = this.reverseBfsToEntryPoints(\n rawDb,\n dependencyNodeId,\n entryPointIds,\n );\n for (const { entryPointId, path } of reached) {\n reachingPaths.push({\n entryPoint: entryPointId,\n path,\n });\n }\n\n // Step 4: Assign verdict.\n // - affected: at least one entry point reaches the dependency.\n // - under_investigation: no path found, but the backward traversal hit\n // unresolved imports (dead-end import nodes). The graph is incomplete\n // near the dependency (dynamic dispatch, reflection, unindexed code),\n // so \"not reachable\" cannot be concluded \u2014 treat with caution.\n // - not_affected: no path found and no unresolved imports \u2014 strong signal.\n const cappedUnresolved = unresolvedSymbols.slice(0, MAX_UNRESOLVED_SYMBOLS);\n let verdict: ReachabilityVerdict;\n if (reachingPaths.length > 0) {\n verdict = 'affected';\n } else if (cappedUnresolved.length > 0) {\n verdict = 'under_investigation';\n } else {\n verdict = 'not_affected';\n }\n\n const result: ReachabilityResult = {\n verdict,\n paths: reachingPaths,\n unresolvedSymbols: verdict === 'under_investigation' ? cappedUnresolved : [],\n reachingEntryPointCount: reachingPaths.length,\n };\n\n // Store result in sec_reachability table\n this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result);\n\n return result;\n }\n\n /**\n * Produce impact summary for an affected vulnerability.\n *\n * Returns null if the vulnerability is not affected.\n * Reads layer assignments from `arch_file_layers` table.\n *\n * Requirements: 5.1, 5.2, 5.3, 5.4, 5.5\n */\n async getImpactSummary(vulnerabilityNodeId: string): Promise<ImpactSummary | null> {\n const rawDb = this.db.getRawDb();\n\n // Check if the vulnerability has verdict 'affected'\n const reachRow = rawDb.get(\n `SELECT verdict, paths, reaching_entry_point_count FROM sec_reachability WHERE vulnerability_node_id = ?`,\n [vulnerabilityNodeId],\n );\n\n if (!reachRow || reachRow.verdict !== 'affected') {\n return null;\n }\n\n const paths: ReachabilityPath[] = reachRow.paths ? JSON.parse(reachRow.paths) : [];\n\n if (paths.length === 0) {\n return null;\n }\n\n // Collect all node IDs on reachable paths\n const allNodeIds = new Set<string>();\n const affectedEntryPoints: string[] = [];\n\n for (const p of paths) {\n affectedEntryPoints.push(p.entryPoint);\n for (const nodeId of p.path) {\n allNodeIds.add(nodeId);\n }\n }\n\n // Identify architectural layers on reachable paths\n const affectedLayers = new Set<string>();\n\n if (this.config.enableArchitecture) {\n // Get file paths for all nodes on reachable paths\n const nodeIds = Array.from(allNodeIds);\n if (nodeIds.length > 0) {\n const placeholders = nodeIds.map(() => '?').join(',');\n const nodeFiles: Array<{ id: string; file_path: string }> = rawDb.all(\n `SELECT id, file_path FROM nodes WHERE id IN (${placeholders})`,\n nodeIds,\n );\n\n // Get layer assignments for those files from arch_file_layers\n const filePaths = [...new Set(nodeFiles.map(n => n.file_path).filter(Boolean))];\n if (filePaths.length > 0) {\n const filePlaceholders = filePaths.map(() => '?').join(',');\n const layerRows: Array<{ file_path: string; layer_id: string }> = rawDb.all(\n `SELECT file_path, layer_id FROM arch_file_layers WHERE file_path IN (${filePlaceholders})`,\n filePaths,\n );\n\n const fileToLayer = new Map<string, string>();\n for (const row of layerRows) {\n fileToLayer.set(row.file_path, row.layer_id);\n }\n\n // Assign layers to nodes; classify as \"unclassified\" if no layer assigned\n for (const nf of nodeFiles) {\n if (nf.file_path) {\n const layer = fileToLayer.get(nf.file_path);\n affectedLayers.add(layer ?? 'unclassified');\n }\n }\n }\n }\n }\n // When enableArchitecture is false, omit layer classification (Requirement 5.4)\n\n // Count distinct paths (capped at MAX_DISTINCT_PATHS)\n // Two paths are distinct if they differ by at least one intermediate symbol\n const distinctPathSet = new Set<string>();\n for (const p of paths) {\n // Use the full path as a key for distinctness\n const pathKey = p.path.join('\u2192');\n distinctPathSet.add(pathKey);\n if (distinctPathSet.size >= MAX_DISTINCT_PATHS) break;\n }\n\n const summary: ImpactSummary = {\n affectedLayers: Array.from(affectedLayers),\n affectedEntryPoints,\n distinctPathCount: Math.min(distinctPathSet.size, MAX_DISTINCT_PATHS),\n };\n\n // Store impact summary in sec_impact table\n this.storeImpactSummary(rawDb, vulnerabilityNodeId, summary);\n\n return summary;\n }\n\n /**\n * Analyze all Vulnerability_Nodes in the graph.\n *\n * Iterates all vulnerability nodes and calls analyze() for each.\n */\n async analyzeAll(): Promise<Map<string, ReachabilityResult>> {\n const rawDb = this.db.getRawDb();\n const results = new Map<string, ReachabilityResult>();\n\n const vulnRows: Array<{ node_id: string }> = rawDb.all(\n `SELECT node_id FROM sec_vulnerabilities`,\n );\n\n for (const row of vulnRows) {\n const result = await this.analyze(row.node_id);\n results.set(row.node_id, result);\n }\n\n return results;\n }\n\n /**\n * Reverse BFS: start from the dependency node and walk BACKWARDS through\n * incoming edges to find which entry points can reach it.\n *\n * This is O(V+E) total (one traversal) regardless of how many entry points exist,\n * compared to O(entryPoints \u00D7 (V+E)) for forward BFS from each entry point.\n *\n * While traversing, collects \"unresolved imports\" \u2014 import-kind nodes with no\n * outgoing traversal edges. These are dead ends in the forward graph: the\n * traversal reached them but couldn't follow them to a definition (dynamic\n * dispatch, reflection, unindexed code). When no entry point reaches the\n * dependency but such imports were encountered on the backward frontier,\n * \"not reachable\" cannot be concluded and the caller reports\n * under_investigation instead of not_affected.\n *\n * Returns the entry points that have a path to the dependency (with their\n * paths) and the set of unresolved import node IDs encountered.\n */\n private reverseBfsToEntryPoints(\n rawDb: any,\n dependencyNodeId: string,\n entryPointIds: Set<string>,\n ): {\n reached: Array<{ entryPointId: string; path: string[] }>;\n unresolvedSymbols: string[];\n } {\n const reached: Array<{ entryPointId: string; path: string[] }> = [];\n const unresolvedSymbols = new Set<string>();\n const visited = new Set<string>();\n const parentMap = new Map<string, string>(); // child \u2192 parent (for path reconstruction)\n\n const edgeKinds = TRAVERSAL_EDGE_KINDS.map(k => `'${k}'`).join(',');\n const queue: string[] = [dependencyNodeId];\n visited.add(dependencyNodeId);\n\n while (queue.length > 0) {\n const currentId = queue.shift()!;\n\n // Check if we reached an entry point\n if (currentId !== dependencyNodeId && entryPointIds.has(currentId)) {\n // Reconstruct path from entry point to dependency\n const path: string[] = [];\n let node: string | undefined = currentId;\n while (node !== undefined) {\n path.push(node);\n node = parentMap.get(node);\n }\n // path is [entryPoint, ..., dependencyNode] \u2014 already correct direction\n reached.push({ entryPointId: currentId, path });\n // Keep traversing \u2014 other entry points may reach this one transitively\n // (e.g., main() \u2192 authMiddleware() \u2192 dep \u2014 both should be reported)\n }\n\n // Walk INCOMING edges (reverse direction: who calls/imports currentId?)\n const inEdges: Array<{ source: string }> = rawDb.all(\n `SELECT source FROM edges WHERE target = ? AND kind IN (${edgeKinds})`,\n [currentId],\n );\n\n // An import node reached via incoming edges that has no outgoing traversal\n // edges is an unresolved import \u2014 a dead end that leaves reachability\n // inconclusive. Detect it here so the verdict can fall back to\n // under_investigation rather than a false not_affected.\n if (currentId !== dependencyNodeId) {\n const outCount: { c: number } = rawDb.get(\n `SELECT COUNT(*) AS c FROM edges WHERE source = ? AND kind IN (${edgeKinds})`,\n [currentId],\n );\n if (outCount.c === 0) {\n const nodeRow = rawDb.get(`SELECT kind FROM nodes WHERE id = ?`, [currentId]);\n if (nodeRow && nodeRow.kind === 'import') {\n unresolvedSymbols.add(currentId);\n }\n }\n }\n\n for (const edge of inEdges) {\n if (visited.has(edge.source)) continue;\n visited.add(edge.source);\n parentMap.set(edge.source, currentId);\n queue.push(edge.source);\n }\n }\n\n return { reached, unresolvedSymbols: Array.from(unresolvedSymbols) };\n }\n\n /**\n * Store reachability result in sec_reachability table.\n */\n private storeReachabilityResult(\n rawDb: any,\n vulnerabilityNodeId: string,\n result: ReachabilityResult,\n ): void {\n rawDb.run(\n `INSERT OR REPLACE INTO sec_reachability\n (vulnerability_node_id, verdict, paths, unresolved_symbols, reaching_entry_point_count, analyzed_at)\n VALUES (?, ?, ?, ?, ?, ?)`,\n [\n vulnerabilityNodeId,\n result.verdict,\n result.paths.length > 0 ? JSON.stringify(result.paths) : null,\n result.unresolvedSymbols.length > 0 ? JSON.stringify(result.unresolvedSymbols) : null,\n result.reachingEntryPointCount,\n Date.now(),\n ],\n );\n }\n\n /**\n * Store impact summary in sec_impact table.\n */\n private storeImpactSummary(\n rawDb: any,\n vulnerabilityNodeId: string,\n summary: ImpactSummary,\n ): void {\n rawDb.run(\n `INSERT OR REPLACE INTO sec_impact\n (vulnerability_node_id, affected_layers, affected_entry_points, distinct_path_count, analyzed_at)\n VALUES (?, ?, ?, ?, ?)`,\n [\n vulnerabilityNodeId,\n JSON.stringify(summary.affectedLayers),\n JSON.stringify(summary.affectedEntryPoints),\n summary.distinctPathCount,\n Date.now(),\n ],\n );\n }\n}\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["/**\n * KiroGraph-Sec Reachability Analyzer\n *\n * Traverses the knowledge graph from application entry points to determine\n * whether vulnerable dependencies are reachable through actual code paths.\n * Produces reachability verdicts and impact summaries enriched with\n * architectural layer context.\n *\n * Requirements: 4.1, 4.2, 4.3, 4.4, 4.6, 5.1, 5.2, 5.3, 5.4, 5.5\n */\n\nimport type { GraphDatabase } from '../db/database';\nimport type { KiroGraphConfig } from '../config';\nimport type {\n ReachabilityVerdict,\n ReachabilityReason,\n ReachabilityPath,\n ReachabilityResult,\n ImpactSummary,\n} from './types';\n\n/** Edge kinds used for reachability traversal */\nconst TRAVERSAL_EDGE_KINDS = ['calls', 'imports', 'references'] as const;\nconst TRAVERSAL_EDGE_KINDS_SQL = TRAVERSAL_EDGE_KINDS.map(k => `'${k}'`).join(',');\n\n/** Maximum unresolved symbols to report (for the under_investigation verdict) */\nconst MAX_UNRESOLVED_SYMBOLS = 50;\n\n/** Maximum distinct paths to report in impact summary */\nconst MAX_DISTINCT_PATHS = 100;\n\n/**\n * ReachabilityAnalyzer traverses the knowledge graph from entry points\n * toward affected dependency nodes to determine reachability verdicts.\n */\nexport class ReachabilityAnalyzer {\n private readonly db: GraphDatabase;\n private readonly config: KiroGraphConfig;\n\n constructor(db: GraphDatabase, config: KiroGraphConfig) {\n this.db = db;\n this.config = config;\n }\n\n /**\n * Analyze reachability for a single Vulnerability_Node.\n *\n * 1. Find the Dependency_Node linked via `has_vulnerability` edge\n * 2. Find all Entry_Points in the graph\n * 3. Reverse BFS from the dependency node through incoming edges toward entry points\n * 4. Assign verdict based on path existence and unresolved imports\n *\n * Requirements: 4.1, 4.2, 4.3, 4.4, 4.6\n */\n async analyze(vulnerabilityNodeId: string): Promise<ReachabilityResult> {\n const rawDb = this.db.getRawDb();\n\n // Step 1: Find the Dependency_Node linked to this vulnerability\n const depEdge = rawDb.get(\n `SELECT source FROM edges WHERE target = ? AND kind = 'has_vulnerability'`,\n [vulnerabilityNodeId],\n );\n\n if (!depEdge) {\n // No dependency linked \u2014 cannot determine reachability\n const result: ReachabilityResult = {\n verdict: 'under_investigation',\n reason: 'no_dependency_link',\n paths: [],\n unresolvedSymbols: [],\n reachingEntryPointCount: 0,\n };\n this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result);\n return result;\n }\n\n const dependencyNodeId: string = depEdge.source;\n\n // Step 1b: If nothing in the indexed code ever calls, imports, or\n // references this dependency directly, the reverse BFS below is a\n // guaranteed no-op regardless of the rest of the graph \u2014 it always\n // reports \"no path, no unresolved imports\" and falls through to\n // not_affected. That's a false negative for dependencies a framework\n // wires in via classpath scanning/reflection rather than an explicit\n // reference \u2014 embedded servlet containers (Tomcat, Netty, Jetty,\n // Undertow) are the common case: they sit in the request path of every\n // route, but application code never calls into them by name. The call\n // graph has no signal about this dependency at all, so \"not reachable\"\n // cannot be concluded \u2014 report under_investigation instead.\n const incomingEdgeCount: { c: number } = rawDb.get(\n `SELECT COUNT(*) as c FROM edges WHERE target = ? AND kind IN (${TRAVERSAL_EDGE_KINDS_SQL})`,\n [dependencyNodeId],\n );\n if (incomingEdgeCount.c === 0) {\n const result: ReachabilityResult = {\n verdict: 'under_investigation',\n reason: 'no_call_graph_signal',\n paths: [],\n unresolvedSymbols: [],\n reachingEntryPointCount: 0,\n };\n this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result);\n return result;\n }\n\n // Step 2: Find all Entry_Points\n // Entry points are: nodes with kind='route' OR nodes with kind='function' that are exported\n const entryPoints: Array<{ id: string }> = rawDb.all(\n `SELECT id FROM nodes WHERE kind = 'route'\n UNION\n SELECT id FROM nodes WHERE kind = 'function' AND is_exported = 1`,\n );\n\n if (entryPoints.length === 0) {\n // No entry points \u2014 cannot determine reachability\n const result: ReachabilityResult = {\n verdict: 'not_affected',\n reason: 'no_entry_points',\n paths: [],\n unresolvedSymbols: [],\n reachingEntryPointCount: 0,\n };\n this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result);\n return result;\n }\n\n // Step 3: Reverse BFS from the dependency node through INCOMING edges\n // to find which entry points can reach it. This is O(V+E) per vulnerability\n // instead of O(entryPoints \u00D7 (V+E)) for forward BFS from each entry point.\n const reachingPaths: ReachabilityPath[] = [];\n const entryPointIds = new Set(entryPoints.map(ep => ep.id));\n const { reached, unresolvedSymbols } = this.reverseBfsToEntryPoints(\n rawDb,\n dependencyNodeId,\n entryPointIds,\n );\n for (const { entryPointId, path } of reached) {\n reachingPaths.push({\n entryPoint: entryPointId,\n path,\n });\n }\n\n // Step 4: Assign verdict.\n // - affected: at least one entry point reaches the dependency.\n // - under_investigation: no path found, but the backward traversal hit\n // unresolved imports (dead-end import nodes). The graph is incomplete\n // near the dependency (dynamic dispatch, reflection, unindexed code),\n // so \"not reachable\" cannot be concluded \u2014 treat with caution.\n // - not_affected: no path found and no unresolved imports \u2014 strong signal.\n const cappedUnresolved = unresolvedSymbols.slice(0, MAX_UNRESOLVED_SYMBOLS);\n let verdict: ReachabilityVerdict;\n let reason: ReachabilityReason;\n if (reachingPaths.length > 0) {\n verdict = 'affected';\n reason = 'path_found';\n } else if (cappedUnresolved.length > 0) {\n verdict = 'under_investigation';\n reason = 'unresolved_imports';\n } else {\n verdict = 'not_affected';\n reason = 'no_path_found';\n }\n\n const result: ReachabilityResult = {\n verdict,\n reason,\n paths: reachingPaths,\n unresolvedSymbols: verdict === 'under_investigation' ? cappedUnresolved : [],\n reachingEntryPointCount: reachingPaths.length,\n };\n\n // Store result in sec_reachability table\n this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result);\n\n return result;\n }\n\n /**\n * Produce impact summary for an affected vulnerability.\n *\n * Returns null if the vulnerability is not affected.\n * Reads layer assignments from `arch_file_layers` table.\n *\n * Requirements: 5.1, 5.2, 5.3, 5.4, 5.5\n */\n async getImpactSummary(vulnerabilityNodeId: string): Promise<ImpactSummary | null> {\n const rawDb = this.db.getRawDb();\n\n // Check if the vulnerability has verdict 'affected'\n const reachRow = rawDb.get(\n `SELECT verdict, paths, reaching_entry_point_count FROM sec_reachability WHERE vulnerability_node_id = ?`,\n [vulnerabilityNodeId],\n );\n\n if (!reachRow || reachRow.verdict !== 'affected') {\n return null;\n }\n\n const paths: ReachabilityPath[] = reachRow.paths ? JSON.parse(reachRow.paths) : [];\n\n if (paths.length === 0) {\n return null;\n }\n\n // Collect all node IDs on reachable paths\n const allNodeIds = new Set<string>();\n const affectedEntryPoints: string[] = [];\n\n for (const p of paths) {\n affectedEntryPoints.push(p.entryPoint);\n for (const nodeId of p.path) {\n allNodeIds.add(nodeId);\n }\n }\n\n // Identify architectural layers on reachable paths\n const affectedLayers = new Set<string>();\n\n if (this.config.enableArchitecture) {\n // Get file paths for all nodes on reachable paths\n const nodeIds = Array.from(allNodeIds);\n if (nodeIds.length > 0) {\n const placeholders = nodeIds.map(() => '?').join(',');\n const nodeFiles: Array<{ id: string; file_path: string }> = rawDb.all(\n `SELECT id, file_path FROM nodes WHERE id IN (${placeholders})`,\n nodeIds,\n );\n\n // Get layer assignments for those files from arch_file_layers\n const filePaths = [...new Set(nodeFiles.map(n => n.file_path).filter(Boolean))];\n if (filePaths.length > 0) {\n const filePlaceholders = filePaths.map(() => '?').join(',');\n const layerRows: Array<{ file_path: string; layer_id: string }> = rawDb.all(\n `SELECT file_path, layer_id FROM arch_file_layers WHERE file_path IN (${filePlaceholders})`,\n filePaths,\n );\n\n const fileToLayer = new Map<string, string>();\n for (const row of layerRows) {\n fileToLayer.set(row.file_path, row.layer_id);\n }\n\n // Assign layers to nodes; classify as \"unclassified\" if no layer assigned\n for (const nf of nodeFiles) {\n if (nf.file_path) {\n const layer = fileToLayer.get(nf.file_path);\n affectedLayers.add(layer ?? 'unclassified');\n }\n }\n }\n }\n }\n // When enableArchitecture is false, omit layer classification (Requirement 5.4)\n\n // Count distinct paths (capped at MAX_DISTINCT_PATHS)\n // Two paths are distinct if they differ by at least one intermediate symbol\n const distinctPathSet = new Set<string>();\n for (const p of paths) {\n // Use the full path as a key for distinctness\n const pathKey = p.path.join('\u2192');\n distinctPathSet.add(pathKey);\n if (distinctPathSet.size >= MAX_DISTINCT_PATHS) break;\n }\n\n const summary: ImpactSummary = {\n affectedLayers: Array.from(affectedLayers),\n affectedEntryPoints,\n distinctPathCount: Math.min(distinctPathSet.size, MAX_DISTINCT_PATHS),\n };\n\n // Store impact summary in sec_impact table\n this.storeImpactSummary(rawDb, vulnerabilityNodeId, summary);\n\n return summary;\n }\n\n /**\n * Analyze all Vulnerability_Nodes in the graph.\n *\n * Iterates all vulnerability nodes and calls analyze() for each.\n */\n async analyzeAll(): Promise<Map<string, ReachabilityResult>> {\n const rawDb = this.db.getRawDb();\n const results = new Map<string, ReachabilityResult>();\n\n const vulnRows: Array<{ node_id: string }> = rawDb.all(\n `SELECT node_id FROM sec_vulnerabilities`,\n );\n\n for (const row of vulnRows) {\n const result = await this.analyze(row.node_id);\n results.set(row.node_id, result);\n }\n\n return results;\n }\n\n /**\n * Reverse BFS: start from the dependency node and walk BACKWARDS through\n * incoming edges to find which entry points can reach it.\n *\n * This is O(V+E) total (one traversal) regardless of how many entry points exist,\n * compared to O(entryPoints \u00D7 (V+E)) for forward BFS from each entry point.\n *\n * While traversing, collects \"unresolved imports\" \u2014 import-kind nodes with no\n * outgoing traversal edges. These are dead ends in the forward graph: the\n * traversal reached them but couldn't follow them to a definition (dynamic\n * dispatch, reflection, unindexed code). When no entry point reaches the\n * dependency but such imports were encountered on the backward frontier,\n * \"not reachable\" cannot be concluded and the caller reports\n * under_investigation instead of not_affected.\n *\n * Returns the entry points that have a path to the dependency (with their\n * paths) and the set of unresolved import node IDs encountered.\n */\n private reverseBfsToEntryPoints(\n rawDb: any,\n dependencyNodeId: string,\n entryPointIds: Set<string>,\n ): {\n reached: Array<{ entryPointId: string; path: string[] }>;\n unresolvedSymbols: string[];\n } {\n const reached: Array<{ entryPointId: string; path: string[] }> = [];\n const unresolvedSymbols = new Set<string>();\n const visited = new Set<string>();\n const parentMap = new Map<string, string>(); // child \u2192 parent (for path reconstruction)\n\n const edgeKinds = TRAVERSAL_EDGE_KINDS_SQL;\n const queue: string[] = [dependencyNodeId];\n visited.add(dependencyNodeId);\n\n while (queue.length > 0) {\n const currentId = queue.shift()!;\n\n // Check if we reached an entry point\n if (currentId !== dependencyNodeId && entryPointIds.has(currentId)) {\n // Reconstruct path from entry point to dependency\n const path: string[] = [];\n let node: string | undefined = currentId;\n while (node !== undefined) {\n path.push(node);\n node = parentMap.get(node);\n }\n // path is [entryPoint, ..., dependencyNode] \u2014 already correct direction\n reached.push({ entryPointId: currentId, path });\n // Keep traversing \u2014 other entry points may reach this one transitively\n // (e.g., main() \u2192 authMiddleware() \u2192 dep \u2014 both should be reported)\n }\n\n // Walk INCOMING edges (reverse direction: who calls/imports currentId?)\n const inEdges: Array<{ source: string }> = rawDb.all(\n `SELECT source FROM edges WHERE target = ? AND kind IN (${edgeKinds})`,\n [currentId],\n );\n\n // An import node reached via incoming edges that has no outgoing traversal\n // edges is an unresolved import \u2014 a dead end that leaves reachability\n // inconclusive. Detect it here so the verdict can fall back to\n // under_investigation rather than a false not_affected.\n if (currentId !== dependencyNodeId) {\n const outCount: { c: number } = rawDb.get(\n `SELECT COUNT(*) AS c FROM edges WHERE source = ? AND kind IN (${edgeKinds})`,\n [currentId],\n );\n if (outCount.c === 0) {\n const nodeRow = rawDb.get(`SELECT kind FROM nodes WHERE id = ?`, [currentId]);\n if (nodeRow && nodeRow.kind === 'import') {\n unresolvedSymbols.add(currentId);\n }\n }\n }\n\n for (const edge of inEdges) {\n if (visited.has(edge.source)) continue;\n visited.add(edge.source);\n parentMap.set(edge.source, currentId);\n queue.push(edge.source);\n }\n }\n\n return { reached, unresolvedSymbols: Array.from(unresolvedSymbols) };\n }\n\n /**\n * Store reachability result in sec_reachability table.\n */\n private storeReachabilityResult(\n rawDb: any,\n vulnerabilityNodeId: string,\n result: ReachabilityResult,\n ): void {\n rawDb.run(\n `INSERT OR REPLACE INTO sec_reachability\n (vulnerability_node_id, verdict, paths, unresolved_symbols, reaching_entry_point_count, analyzed_at)\n VALUES (?, ?, ?, ?, ?, ?)`,\n [\n vulnerabilityNodeId,\n result.verdict,\n result.paths.length > 0 ? JSON.stringify(result.paths) : null,\n result.unresolvedSymbols.length > 0 ? JSON.stringify(result.unresolvedSymbols) : null,\n result.reachingEntryPointCount,\n Date.now(),\n ],\n );\n }\n\n /**\n * Store impact summary in sec_impact table.\n */\n private storeImpactSummary(\n rawDb: any,\n vulnerabilityNodeId: string,\n summary: ImpactSummary,\n ): void {\n rawDb.run(\n `INSERT OR REPLACE INTO sec_impact\n (vulnerability_node_id, affected_layers, affected_entry_points, distinct_path_count, analyzed_at)\n VALUES (?, ?, ?, ?, ?)`,\n [\n vulnerabilityNodeId,\n JSON.stringify(summary.affectedLayers),\n JSON.stringify(summary.affectedEntryPoints),\n summary.distinctPathCount,\n Date.now(),\n ],\n );\n }\n}\n\n/**\n * Plain-English explanation of a reachability result, for `--explain` on\n * `kirograph reachability` and the `kirograph_reachability` MCP tool.\n * Pure function of the result \u2014 no DB access, safe to call on a result\n * that was just computed or re-derived from one already in hand.\n */\nexport function explainReachability(result: ReachabilityResult): string {\n switch (result.reason) {\n case 'path_found':\n return `Reachable: ${result.reachingEntryPointCount} entry point${result.reachingEntryPointCount === 1 ? '' : 's'} have a call/import/reference path to this dependency (see paths below).`;\n case 'no_dependency_link':\n return `This vulnerability isn't linked to any dependency node in the graph, so reachability can't be evaluated at all. This usually means the vulnerability was registered manually (e.g. via \"kirograph vulns --add\") against a package that isn't currently indexed.`;\n case 'no_call_graph_signal':\n return `Nothing in the indexed code explicitly imports, calls, or references this dependency \u2014 it has zero incoming edges in the call graph. This is common for dependencies a framework wires in via classpath scanning or reflection rather than an explicit reference (e.g. an embedded servlet container like Tomcat, Netty, or Jetty). The call graph has no signal about it either way, so \"not reachable\" can't be concluded.`;\n case 'unresolved_imports':\n return `No call/import/reference path was found from any entry point, but the backward traversal hit ${result.unresolvedSymbols.length} unresolved import${result.unresolvedSymbols.length === 1 ? '' : 's'} along the way (dynamic dispatch, reflection, or unindexed code \u2014 see below). The graph is incomplete near this dependency, so \"not reachable\" can't be concluded with confidence.`;\n case 'no_entry_points':\n return `No entry points (HTTP routes or exported functions) were found anywhere in the indexed project, so there's nowhere to start a reachability search from. This can mean the project genuinely has none (a library, not a server), or that entry-point detection missed them for this codebase.`;\n case 'no_path_found':\n return `No call/import/reference path was found from any entry point to this dependency, and the traversal completed without hitting any unresolved imports \u2014 a high-confidence result.`;\n default: {\n const _exhaustive: never = result.reason;\n return _exhaustive;\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["result"]
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/security/types.ts"],
4
- "sourcesContent": ["/**\n * KiroGraph-Sec Type Definitions\n *\n * Interfaces for the security analysis module including manifest parsing,\n * vulnerability detection, reachability analysis, and CycloneDX export.\n */\n\n// \u2500\u2500 Manifest Parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ParsedDependency {\n name: string;\n declaredConstraint: string;\n resolvedVersion?: string; // from lock file\n scope: 'production' | 'development' | 'optional';\n ecosystem: string;\n sourceManifest: string; // relative path to declaring manifest\n license?: string; // SPDX expression or license name, if available in the manifest\n}\n\nexport interface ManifestParseResult {\n dependenciesCreated: number;\n manifestsParsed: number;\n warnings: Array<{ file: string; line?: number; message: string }>;\n errors: Array<{ file: string; message: string }>;\n}\n\n// \u2500\u2500 Dependency Graph Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface IntegrationResult {\n importsEdgesCreated: number;\n referencesEdgesCreated: number;\n}\n\nexport interface TransitiveResult {\n dependsOnEdgesCreated: number;\n incompleteNodes: string[]; // nodes marked with transitive_status=incomplete\n}\n\nexport interface CleanupResult {\n nodesRemoved: number;\n edgesRemoved: number;\n}\n\n// \u2500\u2500 Vulnerability Database \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CVERecord {\n id: string; // e.g. \"CVE-2023-12345\"\n severity: number | null; // CVSS base score (0.0\u201310.0), or null when unknown\n affectedVersionRanges: VersionRange[];\n fixedVersion?: string;\n summary: string;\n}\n\nexport interface VersionRange {\n introduced?: string;\n fixed?: string;\n lastAffected?: string;\n}\n\nexport interface EnrichmentResult {\n vulnerabilitiesFound: number;\n dependenciesChecked: number;\n errors: Array<{ dependency: string; database: string; error: string }>;\n staleNodes: string[]; // nodes where vulnDataStale was set\n}\n\n// \u2500\u2500 Reachability Analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type ReachabilityVerdict = 'affected' | 'not_affected' | 'under_investigation';\n\nexport interface ReachabilityPath {\n entryPoint: string; // node ID of the entry point\n path: string[]; // ordered list of node IDs from entry to dependency\n}\n\nexport interface ReachabilityResult {\n verdict: ReachabilityVerdict;\n paths: ReachabilityPath[];\n unresolvedSymbols: string[]; // up to 50, for under_investigation\n reachingEntryPointCount: number;\n}\n\nexport interface ImpactSummary {\n affectedLayers: string[]; // e.g. ['api', 'service']\n affectedEntryPoints: string[]; // node IDs of reaching entry points\n distinctPathCount: number; // capped at 100\n}\n\n// \u2500\u2500 Security Pipeline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface SecurityResult {\n manifestsDiscovered: number;\n dependenciesCreated: number;\n vulnerabilitiesFound: number;\n affectedCount: number;\n notAffectedCount: number;\n underInvestigationCount: number;\n duration: number;\n}\n\n// \u2500\u2500 CycloneDX SBOM Export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface SBOMMetadata {\n toolName: string;\n toolVersion: string;\n timestamp: string; // ISO 8601 UTC\n projectName: string;\n}\n\nexport interface CycloneDXSBOM {\n bomFormat: 'CycloneDX';\n specVersion: '1.5';\n version: number;\n metadata: Record<string, unknown>;\n components: CycloneDXComponent[];\n dependencies: CycloneDXDependencyEntry[];\n}\n\nexport interface CycloneDXComponent {\n type: 'library';\n name: string;\n version: string;\n purl: string;\n scope?: 'required' | 'optional';\n properties?: Array<{ name: string; value: string }>;\n}\n\nexport interface CycloneDXDependencyEntry {\n ref: string;\n dependsOn?: string[];\n}\n\n// \u2500\u2500 CycloneDX VEX Export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CycloneDXVEX {\n bomFormat: 'CycloneDX';\n specVersion: '1.5';\n version: number;\n metadata: Record<string, unknown>;\n vulnerabilities: CycloneDXVulnerability[];\n}\n\nexport interface CycloneDXVulnerability {\n id: string;\n source?: { name: string; url?: string };\n ratings?: Array<{ score: number; severity?: string; method?: string }>;\n analysis?: {\n state: 'affected' | 'not_affected' | 'under_investigation';\n justification?: string;\n detail?: string;\n };\n affects?: Array<{ ref: string }>;\n}\n"],
4
+ "sourcesContent": ["/**\n * KiroGraph-Sec Type Definitions\n *\n * Interfaces for the security analysis module including manifest parsing,\n * vulnerability detection, reachability analysis, and CycloneDX export.\n */\n\n// \u2500\u2500 Manifest Parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ParsedDependency {\n name: string;\n declaredConstraint: string;\n resolvedVersion?: string; // from lock file\n scope: 'production' | 'development' | 'optional';\n ecosystem: string;\n sourceManifest: string; // relative path to declaring manifest\n license?: string; // SPDX expression or license name, if available in the manifest\n}\n\nexport interface ManifestParseResult {\n dependenciesCreated: number;\n manifestsParsed: number;\n warnings: Array<{ file: string; line?: number; message: string }>;\n errors: Array<{ file: string; message: string }>;\n}\n\n// \u2500\u2500 Dependency Graph Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface IntegrationResult {\n importsEdgesCreated: number;\n referencesEdgesCreated: number;\n}\n\nexport interface TransitiveResult {\n dependsOnEdgesCreated: number;\n incompleteNodes: string[]; // nodes marked with transitive_status=incomplete\n}\n\nexport interface CleanupResult {\n nodesRemoved: number;\n edgesRemoved: number;\n}\n\n// \u2500\u2500 Vulnerability Database \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CVERecord {\n id: string; // e.g. \"CVE-2023-12345\"\n severity: number | null; // CVSS base score (0.0\u201310.0), or null when unknown\n affectedVersionRanges: VersionRange[];\n fixedVersion?: string;\n summary: string;\n}\n\nexport interface VersionRange {\n introduced?: string;\n fixed?: string;\n lastAffected?: string;\n}\n\nexport interface EnrichmentResult {\n vulnerabilitiesFound: number;\n dependenciesChecked: number;\n errors: Array<{ dependency: string; database: string; error: string }>;\n staleNodes: string[]; // nodes where vulnDataStale was set\n}\n\n// \u2500\u2500 Reachability Analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type ReachabilityVerdict = 'affected' | 'not_affected' | 'under_investigation';\n\n/**\n * Which specific code path in ReachabilityAnalyzer.analyze() produced the\n * verdict \u2014 the machine-readable form of `--explain`'s plain-English text.\n */\nexport type ReachabilityReason =\n | 'path_found' // affected: at least one entry point reaches the dependency\n | 'no_dependency_link' // under_investigation: the vulnerability isn't linked to any dependency node\n | 'no_call_graph_signal' // under_investigation: the dependency has zero incoming calls/imports/references edges anywhere\n | 'unresolved_imports' // under_investigation: the backward traversal hit dead-end import nodes\n | 'no_entry_points' // not_affected: the project has no routes or exported functions at all\n | 'no_path_found'; // not_affected: traversal completed, no path to any entry point, no unresolved imports\n\nexport interface ReachabilityPath {\n entryPoint: string; // node ID of the entry point\n path: string[]; // ordered list of node IDs from entry to dependency\n}\n\nexport interface ReachabilityResult {\n verdict: ReachabilityVerdict;\n reason: ReachabilityReason;\n paths: ReachabilityPath[];\n unresolvedSymbols: string[]; // up to 50, for under_investigation\n reachingEntryPointCount: number;\n}\n\nexport interface ImpactSummary {\n affectedLayers: string[]; // e.g. ['api', 'service']\n affectedEntryPoints: string[]; // node IDs of reaching entry points\n distinctPathCount: number; // capped at 100\n}\n\n// \u2500\u2500 Security Pipeline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface SecurityResult {\n manifestsDiscovered: number;\n dependenciesCreated: number;\n vulnerabilitiesFound: number;\n affectedCount: number;\n notAffectedCount: number;\n underInvestigationCount: number;\n duration: number;\n}\n\n// \u2500\u2500 CycloneDX SBOM Export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface SBOMMetadata {\n toolName: string;\n toolVersion: string;\n timestamp: string; // ISO 8601 UTC\n projectName: string;\n}\n\nexport interface CycloneDXSBOM {\n bomFormat: 'CycloneDX';\n specVersion: '1.5';\n version: number;\n metadata: Record<string, unknown>;\n components: CycloneDXComponent[];\n dependencies: CycloneDXDependencyEntry[];\n}\n\nexport interface CycloneDXComponent {\n type: 'library';\n name: string;\n version: string;\n purl: string;\n scope?: 'required' | 'optional';\n properties?: Array<{ name: string; value: string }>;\n}\n\nexport interface CycloneDXDependencyEntry {\n ref: string;\n dependsOn?: string[];\n}\n\n// \u2500\u2500 CycloneDX VEX Export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CycloneDXVEX {\n bomFormat: 'CycloneDX';\n specVersion: '1.5';\n version: number;\n metadata: Record<string, unknown>;\n vulnerabilities: CycloneDXVulnerability[];\n}\n\nexport interface CycloneDXVulnerability {\n id: string;\n source?: { name: string; url?: string };\n ratings?: Array<{ score: number; severity?: string; method?: string }>;\n analysis?: {\n state: 'affected' | 'not_affected' | 'under_investigation';\n justification?: string;\n detail?: string;\n };\n affects?: Array<{ ref: string }>;\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;AAAA;AAAA;",
6
6
  "names": []
7
7
  }
@@ -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
  }