kirograph 0.28.1 → 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 (177) hide show
  1. package/README.md +3 -2
  2. package/dist/architecture/index.js +4 -1
  3. package/dist/architecture/index.js.map +2 -2
  4. package/dist/architecture/manifest/index.js +13 -2
  5. package/dist/architecture/manifest/index.js.map +2 -2
  6. package/dist/architecture/manifest/python.js +2 -2
  7. package/dist/architecture/manifest/python.js.map +2 -2
  8. package/dist/bin/commands/help.js +3 -1
  9. package/dist/bin/commands/help.js.map +2 -2
  10. package/dist/bin/commands/install.js +2 -1
  11. package/dist/bin/commands/install.js.map +2 -2
  12. package/dist/bin/commands/memory.js +18 -8
  13. package/dist/bin/commands/memory.js.map +2 -2
  14. package/dist/bin/commands/reachability.js +7 -2
  15. package/dist/bin/commands/reachability.js.map +2 -2
  16. package/dist/bin/commands/status.js +2 -1
  17. package/dist/bin/commands/status.js.map +2 -2
  18. package/dist/bin/commands/uninit.js +2 -1
  19. package/dist/bin/commands/uninit.js.map +2 -2
  20. package/dist/bin/commands/vulns.js +26 -6
  21. package/dist/bin/commands/vulns.js.map +2 -2
  22. package/dist/bin/commands/wiki.js +56 -0
  23. package/dist/bin/commands/wiki.js.map +2 -2
  24. package/dist/bin/installer/common.js +7 -1
  25. package/dist/bin/installer/common.js.map +2 -2
  26. package/dist/bin/installer/detect.js +10 -0
  27. package/dist/bin/installer/detect.js.map +2 -2
  28. package/dist/bin/installer/hooks.js +13 -8
  29. package/dist/bin/installer/hooks.js.map +2 -2
  30. package/dist/bin/installer/steering.js +9 -0
  31. package/dist/bin/installer/steering.js.map +2 -2
  32. package/dist/bin/installer/targets/antigravity.js +1 -1
  33. package/dist/bin/installer/targets/antigravity.js.map +2 -2
  34. package/dist/bin/installer/targets/claude.js +1 -1
  35. package/dist/bin/installer/targets/claude.js.map +2 -2
  36. package/dist/bin/installer/targets/codex.js +1 -1
  37. package/dist/bin/installer/targets/codex.js.map +2 -2
  38. package/dist/bin/installer/targets/commandcode.js +84 -0
  39. package/dist/bin/installer/targets/commandcode.js.map +7 -0
  40. package/dist/bin/installer/targets/copilot.js +1 -1
  41. package/dist/bin/installer/targets/copilot.js.map +2 -2
  42. package/dist/bin/installer/targets/cursor.js +2 -2
  43. package/dist/bin/installer/targets/cursor.js.map +2 -2
  44. package/dist/bin/installer/targets/devin.js +1 -1
  45. package/dist/bin/installer/targets/devin.js.map +2 -2
  46. package/dist/bin/installer/targets/gemini-cli.js +1 -1
  47. package/dist/bin/installer/targets/gemini-cli.js.map +2 -2
  48. package/dist/bin/installer/targets/index.js +10 -0
  49. package/dist/bin/installer/targets/index.js.map +2 -2
  50. package/dist/bin/installer/targets/opencode.js +1 -1
  51. package/dist/bin/installer/targets/opencode.js.map +2 -2
  52. package/dist/bin/installer/targets/windsurf.js +1 -1
  53. package/dist/bin/installer/targets/windsurf.js.map +2 -2
  54. package/dist/bin/kirograph.js +1 -1
  55. package/dist/bin/kirograph.js.map +1 -1
  56. package/dist/config.js +25 -1
  57. package/dist/config.js.map +2 -2
  58. package/dist/core/pipeline.js +18 -6
  59. package/dist/core/pipeline.js.map +2 -2
  60. package/dist/extraction/extractor.js +88 -26
  61. package/dist/extraction/extractor.js.map +2 -2
  62. package/dist/extraction/grammars.js +38 -3
  63. package/dist/extraction/grammars.js.map +2 -2
  64. package/dist/extraction/notebook.js +15 -7
  65. package/dist/extraction/notebook.js.map +2 -2
  66. package/dist/extraction/wasm/tree-sitter-bash.wasm +0 -0
  67. package/dist/extraction/wasm/tree-sitter-c.wasm +0 -0
  68. package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  69. package/dist/extraction/wasm/tree-sitter-cpp.wasm +0 -0
  70. package/dist/extraction/wasm/tree-sitter-csharp.wasm +0 -0
  71. package/dist/extraction/wasm/tree-sitter-css.wasm +0 -0
  72. package/dist/extraction/wasm/tree-sitter-dart.wasm +0 -0
  73. package/dist/extraction/wasm/tree-sitter-elisp.wasm +0 -0
  74. package/dist/extraction/wasm/tree-sitter-elixir.wasm +0 -0
  75. package/dist/extraction/wasm/tree-sitter-elm.wasm +0 -0
  76. package/dist/extraction/wasm/tree-sitter-embedded_template.wasm +0 -0
  77. package/dist/extraction/wasm/tree-sitter-embeddedtemplate.wasm +0 -0
  78. package/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
  79. package/dist/extraction/wasm/tree-sitter-html.wasm +0 -0
  80. package/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
  81. package/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
  82. package/dist/extraction/wasm/tree-sitter-json.wasm +0 -0
  83. package/dist/extraction/wasm/tree-sitter-kotlin.wasm +0 -0
  84. package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
  85. package/dist/extraction/wasm/tree-sitter-objc.wasm +0 -0
  86. package/dist/extraction/wasm/tree-sitter-ocaml.wasm +0 -0
  87. package/dist/extraction/wasm/tree-sitter-php.wasm +0 -0
  88. package/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
  89. package/dist/extraction/wasm/tree-sitter-ql.wasm +0 -0
  90. package/dist/extraction/wasm/tree-sitter-rescript.wasm +0 -0
  91. package/dist/extraction/wasm/tree-sitter-ruby.wasm +0 -0
  92. package/dist/extraction/wasm/tree-sitter-rust.wasm +0 -0
  93. package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
  94. package/dist/extraction/wasm/tree-sitter-solidity.wasm +0 -0
  95. package/dist/extraction/wasm/tree-sitter-swift.wasm +0 -0
  96. package/dist/extraction/wasm/tree-sitter-systemrdl.wasm +0 -0
  97. package/dist/extraction/wasm/tree-sitter-tlaplus.wasm +0 -0
  98. package/dist/extraction/wasm/tree-sitter-toml.wasm +0 -0
  99. package/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
  100. package/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
  101. package/dist/extraction/wasm/tree-sitter-vue.wasm +0 -0
  102. package/dist/extraction/wasm/tree-sitter-yaml.wasm +0 -0
  103. package/dist/extraction/wasm/tree-sitter-zig.wasm +0 -0
  104. package/dist/frameworks/index.js +68 -7
  105. package/dist/frameworks/index.js.map +2 -2
  106. package/dist/jev/client.js +99 -0
  107. package/dist/jev/client.js.map +7 -0
  108. package/dist/jev/types.js +17 -0
  109. package/dist/jev/types.js.map +7 -0
  110. package/dist/mcp/handlers/memory.js +14 -8
  111. package/dist/mcp/handlers/memory.js.map +2 -2
  112. package/dist/mcp/handlers/security.js +5 -3
  113. package/dist/mcp/handlers/security.js.map +2 -2
  114. package/dist/mcp/tools.js +1 -1
  115. package/dist/mcp/tools.js.map +1 -1
  116. package/dist/memory/index.js +13 -1
  117. package/dist/memory/index.js.map +3 -3
  118. package/dist/memory/relation-jev.js +59 -0
  119. package/dist/memory/relation-jev.js.map +7 -0
  120. package/dist/memory/schema.js +57 -0
  121. package/dist/memory/schema.js.map +7 -0
  122. package/dist/security/auth-detection-jev.js +47 -0
  123. package/dist/security/auth-detection-jev.js.map +7 -0
  124. package/dist/security/manifest/adapter.js +34 -5
  125. package/dist/security/manifest/adapter.js.map +3 -3
  126. package/dist/security/manifest/parser.js +2 -2
  127. package/dist/security/manifest/parser.js.map +2 -2
  128. package/dist/security/manifest/plugins/cargo.js +23 -0
  129. package/dist/security/manifest/plugins/cargo.js.map +2 -2
  130. package/dist/security/manifest/plugins/composer.js +12 -0
  131. package/dist/security/manifest/plugins/composer.js.map +2 -2
  132. package/dist/security/manifest/plugins/gradle.js +12 -0
  133. package/dist/security/manifest/plugins/gradle.js.map +2 -2
  134. package/dist/security/manifest/plugins/hex.js +12 -0
  135. package/dist/security/manifest/plugins/hex.js.map +2 -2
  136. package/dist/security/manifest/plugins/maven.js +68 -1
  137. package/dist/security/manifest/plugins/maven.js.map +2 -2
  138. package/dist/security/manifest/plugins/npm.js +86 -0
  139. package/dist/security/manifest/plugins/npm.js.map +2 -2
  140. package/dist/security/manifest/plugins/nuget.js +13 -0
  141. package/dist/security/manifest/plugins/nuget.js.map +2 -2
  142. package/dist/security/manifest/plugins/pubspec.js +12 -0
  143. package/dist/security/manifest/plugins/pubspec.js.map +2 -2
  144. package/dist/security/manifest/plugins/pyproject.js +12 -0
  145. package/dist/security/manifest/plugins/pyproject.js.map +2 -2
  146. package/dist/security/manifest/plugins/rubygems.js +12 -0
  147. package/dist/security/manifest/plugins/rubygems.js.map +2 -2
  148. package/dist/security/pipeline.js +5 -2
  149. package/dist/security/pipeline.js.map +2 -2
  150. package/dist/security/reachability.js +115 -50
  151. package/dist/security/reachability.js.map +2 -2
  152. package/dist/security/types.js.map +1 -1
  153. package/dist/security/vuln/client.js +45 -1
  154. package/dist/security/vuln/client.js.map +2 -2
  155. package/dist/security/vuln/epss-client.js +1 -1
  156. package/dist/security/vuln/epss-client.js.map +1 -1
  157. package/dist/security/vuln/osv-adapter.js +144 -32
  158. package/dist/security/vuln/osv-adapter.js.map +2 -2
  159. package/dist/sync/index.js +9 -2
  160. package/dist/sync/index.js.map +2 -2
  161. package/dist/vectors/libsql-index.js +287 -0
  162. package/dist/vectors/libsql-index.js.map +7 -0
  163. package/dist/vectors/turso-index.js +307 -0
  164. package/dist/vectors/turso-index.js.map +7 -0
  165. package/dist/wiki/contradiction-jev.js +49 -0
  166. package/dist/wiki/contradiction-jev.js.map +7 -0
  167. package/dist/wiki/index.js +13 -0
  168. package/dist/wiki/index.js.map +2 -2
  169. package/dist/wiki/ingest.js +2 -0
  170. package/dist/wiki/ingest.js.map +2 -2
  171. package/dist/wiki/links.js +53 -0
  172. package/dist/wiki/links.js.map +7 -0
  173. package/dist/wiki/lint.js +3 -6
  174. package/dist/wiki/lint.js.map +2 -2
  175. package/dist/wiki/rename.js +103 -0
  176. package/dist/wiki/rename.js.map +7 -0
  177. package/package.json +3 -2
@@ -89,6 +89,18 @@ async function parsePyprojectManifest(manifestPath, projectRoot) {
89
89
  });
90
90
  }
91
91
  }
92
+ const directNames = new Set(dependencies.map((d) => d.name.toLowerCase()));
93
+ for (const [name, version] of resolvedVersions) {
94
+ if (directNames.has(name)) continue;
95
+ dependencies.push({
96
+ name,
97
+ declaredConstraint: version,
98
+ resolvedVersion: version,
99
+ scope: "production",
100
+ ecosystem: "pypi",
101
+ sourceManifest: relativeManifest
102
+ });
103
+ }
92
104
  return dependencies;
93
105
  }
94
106
  function extractPep621Dependencies(content, relativeManifest) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/security/manifest/plugins/pyproject.ts"],
4
- "sourcesContent": ["/**\n * pyproject.toml Version Extraction Plugin for KiroGraph-Sec\n *\n * Handles modern Python packaging: PEP 621 (standard), Poetry, Hatch, PDM, Flit.\n * pyproject.toml is the canonical manifest for virtually all Python projects\n * created after 2021 \u2014 requirements.txt is only used for pinned deployment deps.\n *\n * Supports:\n * - PEP 621 [project] dependencies = [\"package>=1.0\"]\n * - Poetry [tool.poetry.dependencies] { package = \"^1.0\" }\n * - PDM [tool.pdm.dev-dependencies] { dev = [\"package>=1.0\"] }\n * - Hatch [tool.hatch.envs.default.dependencies] = [\"package>=1.0\"]\n * Lock files: poetry.lock, pdm.lock, uv.lock (all share the same version extraction)\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\ntype ResolvedVersionMap = Map<string, string>;\n\n// \u2500\u2500 Entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 async function parsePyprojectManifest(\n manifestPath: string,\n projectRoot: string,\n): Promise<ParsedDependency[]> {\n const relativeManifest = path.relative(projectRoot, manifestPath).replace(/\\\\/g, '/');\n\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:pyproject] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n const manifestDir = path.dirname(manifestPath);\n const resolvedVersions = loadResolvedVersions(manifestDir);\n const license = extractPyprojectLicense(content);\n const dependencies: ParsedDependency[] = [];\n\n // \u2500\u2500 PEP 621 [project] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 const pep621Deps = extractPep621Dependencies(content, relativeManifest);\n for (const dep of pep621Deps) {\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion: resolvedVersions.get(dep.name.toLowerCase()),\n scope: dep.scope,\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n\n // \u2500\u2500 Poetry [tool.poetry.dependencies] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 if (dependencies.length === 0 || hasPoetrySection(content)) {\n const poetryDeps = extractPoetryDependencies(content, relativeManifest);\n for (const dep of poetryDeps) {\n if (!dependencies.find(d => d.name === dep.name)) {\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion: resolvedVersions.get(dep.name.toLowerCase()),\n scope: dep.scope,\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n }\n\n // \u2500\u2500 PDM [tool.pdm.dev-dependencies] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 const pdmDeps = extractPdmDependencies(content, relativeManifest);\n for (const dep of pdmDeps) {\n if (!dependencies.find(d => d.name === dep.name)) {\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion: resolvedVersions.get(dep.name.toLowerCase()),\n scope: dep.scope,\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n\n return dependencies;\n}\n\n// \u2500\u2500 PEP 621 extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface RawDep { name: string; constraint: string; scope: ParsedDependency['scope'] }\n\nfunction extractPep621Dependencies(content: string, relativeManifest: string): RawDep[] {\n const deps: RawDep[] = [];\n\n // [project] dependencies = [\"package>=1.0\", ...]\n const depsBlock = extractTomlArray(content, /^\\[project\\]/m, 'dependencies');\n for (const spec of depsBlock) {\n const dep = parsePep508Spec(spec, relativeManifest);\n if (dep) deps.push({ ...dep, scope: 'production' });\n }\n\n // [project.optional-dependencies] group = [\"package>=1.0\", ...]\n const optSection = extractSection(content, /^\\[project\\.optional-dependencies\\]/m);\n if (optSection) {\n for (const m of optSection.matchAll(/^\\s*([a-zA-Z0-9_-]+)\\s*=\\s*\\[([^\\]]*)\\]/gm)) {\n const groupName = m[1].toLowerCase();\n const isDevGroup = /^(dev|test|lint|docs|ci|build|check|debug)/.test(groupName);\n const items = parseTomlStringArray(m[2]);\n for (const spec of items) {\n const dep = parsePep508Spec(spec, relativeManifest);\n if (dep) deps.push({ ...dep, scope: isDevGroup ? 'development' : 'optional' });\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 Poetry extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction hasPoetrySection(content: string): boolean {\n return /^\\[tool\\.poetry/m.test(content);\n}\n\nfunction extractPoetryDependencies(content: string, relativeManifest: string): RawDep[] {\n const deps: RawDep[] = [];\n\n const sections: Array<{ pattern: RegExp; scope: ParsedDependency['scope'] }> = [\n { pattern: /^\\[tool\\.poetry\\.dependencies\\]/m, scope: 'production' },\n { pattern: /^\\[tool\\.poetry\\.dev-dependencies\\]/m, scope: 'development' },\n ];\n\n // Also handle [tool.poetry.group.*.dependencies]\n for (const m of content.matchAll(/^\\[tool\\.poetry\\.group\\.([a-zA-Z0-9_-]+)\\.dependencies\\]/gm)) {\n const groupName = m[1].toLowerCase();\n const scope: ParsedDependency['scope'] = /^(dev|test|lint|docs|ci)/.test(groupName) ? 'development' : 'optional';\n sections.push({ pattern: new RegExp(`^\\\\[tool\\\\.poetry\\\\.group\\\\.${m[1]}\\\\.dependencies\\\\]`, 'm'), scope });\n }\n\n for (const { pattern, scope } of sections) {\n const section = extractSection(content, pattern);\n if (!section) continue;\n\n for (const line of section.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('[')) break;\n\n // name = \"^1.0\" or name = { version = \"^1.0\", ... }\n const simpleMatch = trimmed.match(/^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)\\s*=\\s*\"([^\"]+)\"/);\n if (simpleMatch) {\n const name = simpleMatch[1];\n if (name === 'python') continue; // skip python version constraint\n deps.push({ name: normalizePypiName(name), constraint: simpleMatch[3], scope });\n continue;\n }\n\n // name = { version = \"^1.0\", ... }\n const tableMatch = trimmed.match(/^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)\\s*=\\s*\\{/);\n if (tableMatch) {\n const name = tableMatch[1];\n if (name === 'python') continue;\n const versionMatch = trimmed.match(/version\\s*=\\s*\"([^\"]+)\"/);\n if (versionMatch) {\n deps.push({ name: normalizePypiName(name), constraint: versionMatch[1], scope });\n } else {\n // path/git dep without version \u2014 record with wildcard\n deps.push({ name: normalizePypiName(name), constraint: '*', scope });\n }\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 PDM extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction extractPdmDependencies(content: string, relativeManifest: string): RawDep[] {\n const deps: RawDep[] = [];\n\n // [tool.pdm.dev-dependencies] groups with arrays\n const section = extractSection(content, /^\\[tool\\.pdm\\.dev-dependencies\\]/m);\n if (section) {\n for (const m of section.matchAll(/^\\s*([a-zA-Z0-9_-]+)\\s*=\\s*\\[([^\\]]*)\\]/gm)) {\n const items = parseTomlStringArray(m[2]);\n for (const spec of items) {\n const dep = parsePep508Spec(spec, relativeManifest);\n if (dep) deps.push({ ...dep, scope: 'development' });\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 PEP 508 spec parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction parsePep508Spec(spec: string, _relativeManifest: string): { name: string; constraint: string } | null {\n // PEP 508: name [extras] [version_spec] [; marker]\n // Strip markers (semicolon and beyond), extras ([...])\n const withoutMarker = spec.split(';')[0].trim();\n const withoutExtras = withoutMarker.replace(/\\[[^\\]]*\\]/, '').trim();\n\n const match = withoutExtras.match(/^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)\\s*(.*)/);\n if (!match) return null;\n\n const name = normalizePypiName(match[1]);\n const constraint = match[3].trim() || '*';\n return { name, constraint };\n}\n\n// \u2500\u2500 Lock file loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n // Try poetry.lock, pdm.lock, uv.lock in order\n const candidates = ['poetry.lock', 'pdm.lock', 'uv.lock'];\n for (const lockFile of candidates) {\n const lockPath = path.join(manifestDir, lockFile);\n if (!fs.existsSync(lockPath)) continue;\n try {\n const content = fs.readFileSync(lockPath, 'utf8');\n if (lockFile === 'poetry.lock') extractFromPoetryLock(content, map);\n else extractFromPdmUvLock(content, map);\n break; // only use the first lock file found\n } catch (err) {\n logWarn(`[sec:pyproject] Failed to parse ${lockFile}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return map;\n}\n\nfunction extractFromPoetryLock(content: string, map: ResolvedVersionMap): void {\n // [[package]] blocks with name and version fields\n for (const m of content.matchAll(/\\[\\[package\\]\\][^[]*?name\\s*=\\s*\"([^\"]+)\"[^[]*?version\\s*=\\s*\"([^\"]+)\"/gs)) {\n map.set(m[1].toLowerCase(), m[2]);\n }\n}\n\nfunction extractFromPdmUvLock(content: string, map: ResolvedVersionMap): void {\n // [[package]] or [[distribution]] blocks (pdm/uv use slightly different headers)\n for (const m of content.matchAll(/\\[\\[(?:package|distribution)\\]\\][^[]*?name\\s*=\\s*\"([^\"]+)\"[^[]*?version\\s*=\\s*\"([^\"]+)\"/gs)) {\n map.set(m[1].toLowerCase(), m[2]);\n }\n}\n\n// \u2500\u2500 TOML helpers (no library dependency) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Extract the content after a section header until the next header. */\nfunction extractSection(content: string, headerPattern: RegExp): string | null {\n const match = headerPattern.exec(content);\n if (!match) return null;\n const start = match.index + match[0].length;\n const remaining = content.slice(start);\n const nextSection = remaining.match(/^\\[/m);\n return nextSection ? remaining.slice(0, nextSection.index) : remaining;\n}\n\n/** Extract a TOML array value by key from a section. */\nfunction extractTomlArray(content: string, sectionPattern: RegExp, key: string): string[] {\n const section = extractSection(content, sectionPattern);\n if (!section) return [];\n\n // Match key = [\"...\", \"...\", ...] \u2014 may span multiple lines\n const keyPattern = new RegExp(`^${key}\\\\s*=\\\\s*\\\\[([^\\\\]]*(?:\\\\][^\\\\[\\\\]]*\\\\[)*[^\\\\]]*)\\\\]`, 'ms');\n const m = keyPattern.exec(section);\n if (!m) return [];\n return parseTomlStringArray(m[1]);\n}\n\n/** Parse a TOML string array body (contents inside [...]) into individual strings. */\nfunction parseTomlStringArray(body: string): string[] {\n const results: string[] = [];\n for (const m of body.matchAll(/\"([^\"]+)\"/g)) {\n results.push(m[1].trim());\n }\n for (const m of body.matchAll(/'([^']+)'/g)) {\n results.push(m[1].trim());\n }\n return results.filter(Boolean);\n}\n\n/** Normalize PyPI package name: lowercase, replace - and _ with - (canonical form). */\nfunction normalizePypiName(name: string): string {\n return name.toLowerCase().replace(/[-_.]+/g, '-');\n}\n\n/**\n * Extract the license from a pyproject.toml file.\n *\n * Handles:\n * - PEP 621 [project] license = {text = \"MIT\"} or license = \"MIT\"\n * - Poetry [tool.poetry] license = \"MIT\"\n */\nfunction extractPyprojectLicense(content: string): string | undefined {\n // PEP 621: license = {text = \"MIT\"} or license = {file = \"LICENSE\"}\n const pep621Section = extractSection(content, /^\\[project\\]/m);\n if (pep621Section) {\n // license = {text = \"MIT\"}\n const tableTextMatch = pep621Section.match(/^license\\s*=\\s*\\{[^}]*text\\s*=\\s*[\"']([^\"']+)[\"'][^}]*\\}/m);\n if (tableTextMatch && tableTextMatch[1].trim() !== '') {\n return tableTextMatch[1].trim();\n }\n // license = \"MIT\"\n const stringMatch = pep621Section.match(/^license\\s*=\\s*[\"']([^\"']+)[\"']/m);\n if (stringMatch && stringMatch[1].trim() !== '') {\n return stringMatch[1].trim();\n }\n }\n\n // Poetry: [tool.poetry] license = \"MIT\"\n const poetrySection = extractSection(content, /^\\[tool\\.poetry\\]/m);\n if (poetrySection) {\n const poetryLicenseMatch = poetrySection.match(/^license\\s*=\\s*[\"']([^\"']+)[\"']/m);\n if (poetryLicenseMatch && poetryLicenseMatch[1].trim() !== '') {\n return poetryLicenseMatch[1].trim();\n }\n }\n\n return undefined;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * pyproject.toml Version Extraction Plugin for KiroGraph-Sec\n *\n * Handles modern Python packaging: PEP 621 (standard), Poetry, Hatch, PDM, Flit.\n * pyproject.toml is the canonical manifest for virtually all Python projects\n * created after 2021 \u2014 requirements.txt is only used for pinned deployment deps.\n *\n * Supports:\n * - PEP 621 [project] dependencies = [\"package>=1.0\"]\n * - Poetry [tool.poetry.dependencies] { package = \"^1.0\" }\n * - PDM [tool.pdm.dev-dependencies] { dev = [\"package>=1.0\"] }\n * - Hatch [tool.hatch.envs.default.dependencies] = [\"package>=1.0\"]\n * Lock files: poetry.lock, pdm.lock, uv.lock (all share the same version extraction)\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\ntype ResolvedVersionMap = Map<string, string>;\n\n// \u2500\u2500 Entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 async function parsePyprojectManifest(\n manifestPath: string,\n projectRoot: string,\n): Promise<ParsedDependency[]> {\n const relativeManifest = path.relative(projectRoot, manifestPath).replace(/\\\\/g, '/');\n\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:pyproject] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n const manifestDir = path.dirname(manifestPath);\n const resolvedVersions = loadResolvedVersions(manifestDir);\n const license = extractPyprojectLicense(content);\n const dependencies: ParsedDependency[] = [];\n\n // \u2500\u2500 PEP 621 [project] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 const pep621Deps = extractPep621Dependencies(content, relativeManifest);\n for (const dep of pep621Deps) {\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion: resolvedVersions.get(dep.name.toLowerCase()),\n scope: dep.scope,\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n\n // \u2500\u2500 Poetry [tool.poetry.dependencies] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 if (dependencies.length === 0 || hasPoetrySection(content)) {\n const poetryDeps = extractPoetryDependencies(content, relativeManifest);\n for (const dep of poetryDeps) {\n if (!dependencies.find(d => d.name === dep.name)) {\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion: resolvedVersions.get(dep.name.toLowerCase()),\n scope: dep.scope,\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n }\n\n // \u2500\u2500 PDM [tool.pdm.dev-dependencies] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 const pdmDeps = extractPdmDependencies(content, relativeManifest);\n for (const dep of pdmDeps) {\n if (!dependencies.find(d => d.name === dep.name)) {\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion: resolvedVersions.get(dep.name.toLowerCase()),\n scope: dep.scope,\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n\n // Transitive-only packages (present in poetry.lock/pdm.lock/uv.lock but\n // never declared in pyproject.toml) would otherwise never become a\n // Dependency_Node and would silently skip vulnerability scanning.\n // resolvedVersions already has every package the lock file resolved,\n // direct or transitive (lower-cased \u2014 original casing isn't retained).\n const directNames = new Set(dependencies.map(d => d.name.toLowerCase()));\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: 'pypi',\n sourceManifest: relativeManifest,\n });\n }\n\n return dependencies;\n}\n\n// \u2500\u2500 PEP 621 extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface RawDep { name: string; constraint: string; scope: ParsedDependency['scope'] }\n\nfunction extractPep621Dependencies(content: string, relativeManifest: string): RawDep[] {\n const deps: RawDep[] = [];\n\n // [project] dependencies = [\"package>=1.0\", ...]\n const depsBlock = extractTomlArray(content, /^\\[project\\]/m, 'dependencies');\n for (const spec of depsBlock) {\n const dep = parsePep508Spec(spec, relativeManifest);\n if (dep) deps.push({ ...dep, scope: 'production' });\n }\n\n // [project.optional-dependencies] group = [\"package>=1.0\", ...]\n const optSection = extractSection(content, /^\\[project\\.optional-dependencies\\]/m);\n if (optSection) {\n for (const m of optSection.matchAll(/^\\s*([a-zA-Z0-9_-]+)\\s*=\\s*\\[([^\\]]*)\\]/gm)) {\n const groupName = m[1].toLowerCase();\n const isDevGroup = /^(dev|test|lint|docs|ci|build|check|debug)/.test(groupName);\n const items = parseTomlStringArray(m[2]);\n for (const spec of items) {\n const dep = parsePep508Spec(spec, relativeManifest);\n if (dep) deps.push({ ...dep, scope: isDevGroup ? 'development' : 'optional' });\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 Poetry extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction hasPoetrySection(content: string): boolean {\n return /^\\[tool\\.poetry/m.test(content);\n}\n\nfunction extractPoetryDependencies(content: string, relativeManifest: string): RawDep[] {\n const deps: RawDep[] = [];\n\n const sections: Array<{ pattern: RegExp; scope: ParsedDependency['scope'] }> = [\n { pattern: /^\\[tool\\.poetry\\.dependencies\\]/m, scope: 'production' },\n { pattern: /^\\[tool\\.poetry\\.dev-dependencies\\]/m, scope: 'development' },\n ];\n\n // Also handle [tool.poetry.group.*.dependencies]\n for (const m of content.matchAll(/^\\[tool\\.poetry\\.group\\.([a-zA-Z0-9_-]+)\\.dependencies\\]/gm)) {\n const groupName = m[1].toLowerCase();\n const scope: ParsedDependency['scope'] = /^(dev|test|lint|docs|ci)/.test(groupName) ? 'development' : 'optional';\n sections.push({ pattern: new RegExp(`^\\\\[tool\\\\.poetry\\\\.group\\\\.${m[1]}\\\\.dependencies\\\\]`, 'm'), scope });\n }\n\n for (const { pattern, scope } of sections) {\n const section = extractSection(content, pattern);\n if (!section) continue;\n\n for (const line of section.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('[')) break;\n\n // name = \"^1.0\" or name = { version = \"^1.0\", ... }\n const simpleMatch = trimmed.match(/^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)\\s*=\\s*\"([^\"]+)\"/);\n if (simpleMatch) {\n const name = simpleMatch[1];\n if (name === 'python') continue; // skip python version constraint\n deps.push({ name: normalizePypiName(name), constraint: simpleMatch[3], scope });\n continue;\n }\n\n // name = { version = \"^1.0\", ... }\n const tableMatch = trimmed.match(/^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)\\s*=\\s*\\{/);\n if (tableMatch) {\n const name = tableMatch[1];\n if (name === 'python') continue;\n const versionMatch = trimmed.match(/version\\s*=\\s*\"([^\"]+)\"/);\n if (versionMatch) {\n deps.push({ name: normalizePypiName(name), constraint: versionMatch[1], scope });\n } else {\n // path/git dep without version \u2014 record with wildcard\n deps.push({ name: normalizePypiName(name), constraint: '*', scope });\n }\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 PDM extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction extractPdmDependencies(content: string, relativeManifest: string): RawDep[] {\n const deps: RawDep[] = [];\n\n // [tool.pdm.dev-dependencies] groups with arrays\n const section = extractSection(content, /^\\[tool\\.pdm\\.dev-dependencies\\]/m);\n if (section) {\n for (const m of section.matchAll(/^\\s*([a-zA-Z0-9_-]+)\\s*=\\s*\\[([^\\]]*)\\]/gm)) {\n const items = parseTomlStringArray(m[2]);\n for (const spec of items) {\n const dep = parsePep508Spec(spec, relativeManifest);\n if (dep) deps.push({ ...dep, scope: 'development' });\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 PEP 508 spec parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction parsePep508Spec(spec: string, _relativeManifest: string): { name: string; constraint: string } | null {\n // PEP 508: name [extras] [version_spec] [; marker]\n // Strip markers (semicolon and beyond), extras ([...])\n const withoutMarker = spec.split(';')[0].trim();\n const withoutExtras = withoutMarker.replace(/\\[[^\\]]*\\]/, '').trim();\n\n const match = withoutExtras.match(/^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)\\s*(.*)/);\n if (!match) return null;\n\n const name = normalizePypiName(match[1]);\n const constraint = match[3].trim() || '*';\n return { name, constraint };\n}\n\n// \u2500\u2500 Lock file loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n // Try poetry.lock, pdm.lock, uv.lock in order\n const candidates = ['poetry.lock', 'pdm.lock', 'uv.lock'];\n for (const lockFile of candidates) {\n const lockPath = path.join(manifestDir, lockFile);\n if (!fs.existsSync(lockPath)) continue;\n try {\n const content = fs.readFileSync(lockPath, 'utf8');\n if (lockFile === 'poetry.lock') extractFromPoetryLock(content, map);\n else extractFromPdmUvLock(content, map);\n break; // only use the first lock file found\n } catch (err) {\n logWarn(`[sec:pyproject] Failed to parse ${lockFile}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return map;\n}\n\nfunction extractFromPoetryLock(content: string, map: ResolvedVersionMap): void {\n // [[package]] blocks with name and version fields\n for (const m of content.matchAll(/\\[\\[package\\]\\][^[]*?name\\s*=\\s*\"([^\"]+)\"[^[]*?version\\s*=\\s*\"([^\"]+)\"/gs)) {\n map.set(m[1].toLowerCase(), m[2]);\n }\n}\n\nfunction extractFromPdmUvLock(content: string, map: ResolvedVersionMap): void {\n // [[package]] or [[distribution]] blocks (pdm/uv use slightly different headers)\n for (const m of content.matchAll(/\\[\\[(?:package|distribution)\\]\\][^[]*?name\\s*=\\s*\"([^\"]+)\"[^[]*?version\\s*=\\s*\"([^\"]+)\"/gs)) {\n map.set(m[1].toLowerCase(), m[2]);\n }\n}\n\n// \u2500\u2500 TOML helpers (no library dependency) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Extract the content after a section header until the next header. */\nfunction extractSection(content: string, headerPattern: RegExp): string | null {\n const match = headerPattern.exec(content);\n if (!match) return null;\n const start = match.index + match[0].length;\n const remaining = content.slice(start);\n const nextSection = remaining.match(/^\\[/m);\n return nextSection ? remaining.slice(0, nextSection.index) : remaining;\n}\n\n/** Extract a TOML array value by key from a section. */\nfunction extractTomlArray(content: string, sectionPattern: RegExp, key: string): string[] {\n const section = extractSection(content, sectionPattern);\n if (!section) return [];\n\n // Match key = [\"...\", \"...\", ...] \u2014 may span multiple lines\n const keyPattern = new RegExp(`^${key}\\\\s*=\\\\s*\\\\[([^\\\\]]*(?:\\\\][^\\\\[\\\\]]*\\\\[)*[^\\\\]]*)\\\\]`, 'ms');\n const m = keyPattern.exec(section);\n if (!m) return [];\n return parseTomlStringArray(m[1]);\n}\n\n/** Parse a TOML string array body (contents inside [...]) into individual strings. */\nfunction parseTomlStringArray(body: string): string[] {\n const results: string[] = [];\n for (const m of body.matchAll(/\"([^\"]+)\"/g)) {\n results.push(m[1].trim());\n }\n for (const m of body.matchAll(/'([^']+)'/g)) {\n results.push(m[1].trim());\n }\n return results.filter(Boolean);\n}\n\n/** Normalize PyPI package name: lowercase, replace - and _ with - (canonical form). */\nfunction normalizePypiName(name: string): string {\n return name.toLowerCase().replace(/[-_.]+/g, '-');\n}\n\n/**\n * Extract the license from a pyproject.toml file.\n *\n * Handles:\n * - PEP 621 [project] license = {text = \"MIT\"} or license = \"MIT\"\n * - Poetry [tool.poetry] license = \"MIT\"\n */\nfunction extractPyprojectLicense(content: string): string | undefined {\n // PEP 621: license = {text = \"MIT\"} or license = {file = \"LICENSE\"}\n const pep621Section = extractSection(content, /^\\[project\\]/m);\n if (pep621Section) {\n // license = {text = \"MIT\"}\n const tableTextMatch = pep621Section.match(/^license\\s*=\\s*\\{[^}]*text\\s*=\\s*[\"']([^\"']+)[\"'][^}]*\\}/m);\n if (tableTextMatch && tableTextMatch[1].trim() !== '') {\n return tableTextMatch[1].trim();\n }\n // license = \"MIT\"\n const stringMatch = pep621Section.match(/^license\\s*=\\s*[\"']([^\"']+)[\"']/m);\n if (stringMatch && stringMatch[1].trim() !== '') {\n return stringMatch[1].trim();\n }\n }\n\n // Poetry: [tool.poetry] license = \"MIT\"\n const poetrySection = extractSection(content, /^\\[tool\\.poetry\\]/m);\n if (poetrySection) {\n const poetryLicenseMatch = poetrySection.match(/^license\\s*=\\s*[\"']([^\"']+)[\"']/m);\n if (poetryLicenseMatch && poetryLicenseMatch[1].trim() !== '') {\n return poetryLicenseMatch[1].trim();\n }\n }\n\n return undefined;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -84,6 +84,18 @@ async function parseRubygemsManifest(manifestPath, projectRoot) {
84
84
  });
85
85
  }
86
86
  }
87
+ const directNames = new Set(dependencies.map((d) => d.name));
88
+ for (const [name, version] of resolvedVersions) {
89
+ if (directNames.has(name)) continue;
90
+ dependencies.push({
91
+ name,
92
+ declaredConstraint: version,
93
+ resolvedVersion: version,
94
+ scope: "production",
95
+ ecosystem: "rubygems",
96
+ sourceManifest: relativeManifest
97
+ });
98
+ }
87
99
  return dependencies;
88
100
  }
89
101
  function parseGemLine(line, relativeManifest) {
@@ -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
  }
@@ -51,7 +51,10 @@ class SecurityPipeline {
51
51
  duration: 0
52
52
  };
53
53
  onProgress?.("manifest-parsing", 0, 4);
54
- const parser = new import_parser.ManifestParser(this.db, this.projectRoot);
54
+ const parser = new import_parser.ManifestParser(this.db, this.projectRoot, {
55
+ include: this.config.include,
56
+ exclude: this.config.exclude
57
+ });
55
58
  const parseResult = await parser.parseAll();
56
59
  result.manifestsDiscovered = parseResult.manifestsParsed;
57
60
  result.dependenciesCreated = parseResult.dependenciesCreated;
@@ -65,7 +68,7 @@ class SecurityPipeline {
65
68
  if (this.config.securityAutoEnrich) {
66
69
  onProgress?.("vulnerability-enrichment", 2, 4);
67
70
  const adapters = this.createVulnAdapters();
68
- const vulnClient = new import_client.VulnerabilityDatabaseClient(adapters, this.db);
71
+ const vulnClient = new import_client.VulnerabilityDatabaseClient(adapters, this.db, void 0, this.projectRoot);
69
72
  const enrichResult = await vulnClient.enrichAll();
70
73
  result.vulnerabilitiesFound = enrichResult.vulnerabilitiesFound;
71
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 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,WAAW;AAC3D,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 {
@@ -34,7 +36,7 @@ class ReachabilityAnalyzer {
34
36
  *
35
37
  * 1. Find the Dependency_Node linked via `has_vulnerability` edge
36
38
  * 2. Find all Entry_Points in the graph
37
- * 3. BFS from each entry point toward the dependency node
39
+ * 3. Reverse BFS from the dependency node through incoming edges toward entry points
38
40
  * 4. Assign verdict based on path existence and unresolved imports
39
41
  *
40
42
  * Requirements: 4.1, 4.2, 4.3, 4.4, 4.6
@@ -46,54 +48,79 @@ 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
- const allUnresolvedSymbols = /* @__PURE__ */ new Set();
72
- for (const ep of entryPoints) {
73
- const result2 = this.bfsFromEntryPoint(rawDb, ep.id, dependencyNodeId);
74
- if (result2.path) {
75
- reachingPaths.push({
76
- entryPoint: ep.id,
77
- path: result2.path
78
- });
79
- }
80
- for (const sym of result2.unresolvedSymbols) {
81
- allUnresolvedSymbols.add(sym);
82
- }
94
+ const entryPointIds = new Set(entryPoints.map((ep) => ep.id));
95
+ const { reached, unresolvedSymbols } = this.reverseBfsToEntryPoints(
96
+ rawDb,
97
+ dependencyNodeId,
98
+ entryPointIds
99
+ );
100
+ for (const { entryPointId, path } of reached) {
101
+ reachingPaths.push({
102
+ entryPoint: entryPointId,
103
+ path
104
+ });
83
105
  }
84
- const unresolvedSymbols = Array.from(allUnresolvedSymbols).slice(0, MAX_UNRESOLVED_SYMBOLS);
106
+ const cappedUnresolved = unresolvedSymbols.slice(0, MAX_UNRESOLVED_SYMBOLS);
85
107
  let verdict;
108
+ let reason;
86
109
  if (reachingPaths.length > 0) {
87
110
  verdict = "affected";
88
- } else if (allUnresolvedSymbols.size > 0) {
111
+ reason = "path_found";
112
+ } else if (cappedUnresolved.length > 0) {
89
113
  verdict = "under_investigation";
114
+ reason = "unresolved_imports";
90
115
  } else {
91
116
  verdict = "not_affected";
117
+ reason = "no_path_found";
92
118
  }
93
119
  const result = {
94
120
  verdict,
121
+ reason,
95
122
  paths: reachingPaths,
96
- unresolvedSymbols: verdict === "under_investigation" ? unresolvedSymbols : [],
123
+ unresolvedSymbols: verdict === "under_investigation" ? cappedUnresolved : [],
97
124
  reachingEntryPointCount: reachingPaths.length
98
125
  };
99
126
  this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result);
@@ -189,49 +216,66 @@ class ReachabilityAnalyzer {
189
216
  return results;
190
217
  }
191
218
  /**
192
- * BFS from a single entry point toward the dependency node.
219
+ * Reverse BFS: start from the dependency node and walk BACKWARDS through
220
+ * incoming edges to find which entry points can reach it.
221
+ *
222
+ * This is O(V+E) total (one traversal) regardless of how many entry points exist,
223
+ * compared to O(entryPoints × (V+E)) for forward BFS from each entry point.
193
224
  *
194
- * Uses outgoing edges of types: calls, imports, references.
195
- * Visits each node at most once (handles cycles).
196
- * Tracks unresolved imports encountered during traversal.
225
+ * While traversing, collects "unresolved imports" — import-kind nodes with no
226
+ * outgoing traversal edges. These are dead ends in the forward graph: the
227
+ * traversal reached them but couldn't follow them to a definition (dynamic
228
+ * dispatch, reflection, unindexed code). When no entry point reaches the
229
+ * dependency but such imports were encountered on the backward frontier,
230
+ * "not reachable" cannot be concluded and the caller reports
231
+ * under_investigation instead of not_affected.
197
232
  *
198
- * Returns the shortest path if found, plus any unresolved symbols encountered.
233
+ * Returns the entry points that have a path to the dependency (with their
234
+ * paths) and the set of unresolved import node IDs encountered.
199
235
  */
200
- bfsFromEntryPoint(rawDb, entryPointId, dependencyNodeId) {
236
+ reverseBfsToEntryPoints(rawDb, dependencyNodeId, entryPointIds) {
237
+ const reached = [];
238
+ const unresolvedSymbols = /* @__PURE__ */ new Set();
201
239
  const visited = /* @__PURE__ */ new Set();
202
- const unresolvedSymbols = [];
203
- const queue = [[entryPointId, [entryPointId]]];
204
- visited.add(entryPointId);
205
- const edgeKinds = TRAVERSAL_EDGE_KINDS.map((k) => `'${k}'`).join(",");
240
+ const parentMap = /* @__PURE__ */ new Map();
241
+ const edgeKinds = TRAVERSAL_EDGE_KINDS_SQL;
242
+ const queue = [dependencyNodeId];
243
+ visited.add(dependencyNodeId);
206
244
  while (queue.length > 0) {
207
- const [currentId, currentPath] = queue.shift();
208
- const outEdges = rawDb.all(
209
- `SELECT target, kind FROM edges WHERE source = ? AND kind IN (${edgeKinds})`,
245
+ const currentId = queue.shift();
246
+ if (currentId !== dependencyNodeId && entryPointIds.has(currentId)) {
247
+ const path = [];
248
+ let node = currentId;
249
+ while (node !== void 0) {
250
+ path.push(node);
251
+ node = parentMap.get(node);
252
+ }
253
+ reached.push({ entryPointId: currentId, path });
254
+ }
255
+ const inEdges = rawDb.all(
256
+ `SELECT source FROM edges WHERE target = ? AND kind IN (${edgeKinds})`,
210
257
  [currentId]
211
258
  );
212
- if (outEdges.length === 0) {
213
- const nodeRow = rawDb.get(
214
- `SELECT kind FROM nodes WHERE id = ?`,
259
+ if (currentId !== dependencyNodeId) {
260
+ const outCount = rawDb.get(
261
+ `SELECT COUNT(*) AS c FROM edges WHERE source = ? AND kind IN (${edgeKinds})`,
215
262
  [currentId]
216
263
  );
217
- if (nodeRow && nodeRow.kind === "import") {
218
- unresolvedSymbols.push(currentId);
264
+ if (outCount.c === 0) {
265
+ const nodeRow = rawDb.get(`SELECT kind FROM nodes WHERE id = ?`, [currentId]);
266
+ if (nodeRow && nodeRow.kind === "import") {
267
+ unresolvedSymbols.add(currentId);
268
+ }
219
269
  }
220
270
  }
221
- for (const edge of outEdges) {
222
- const targetId = edge.target;
223
- if (targetId === dependencyNodeId) {
224
- return {
225
- path: [...currentPath, targetId],
226
- unresolvedSymbols
227
- };
228
- }
229
- if (visited.has(targetId)) continue;
230
- visited.add(targetId);
231
- queue.push([targetId, [...currentPath, targetId]]);
271
+ for (const edge of inEdges) {
272
+ if (visited.has(edge.source)) continue;
273
+ visited.add(edge.source);
274
+ parentMap.set(edge.source, currentId);
275
+ queue.push(edge.source);
232
276
  }
233
277
  }
234
- return { path: null, unresolvedSymbols };
278
+ return { reached, unresolvedSymbols: Array.from(unresolvedSymbols) };
235
279
  }
236
280
  /**
237
281
  * Store reachability result in sec_reachability table.
@@ -269,8 +313,29 @@ class ReachabilityAnalyzer {
269
313
  );
270
314
  }
271
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
+ }
272
336
  // Annotate the CommonJS export names for ESM import in node:
273
337
  0 && (module.exports = {
274
- ReachabilityAnalyzer
338
+ ReachabilityAnalyzer,
339
+ explainReachability
275
340
  });
276
341
  //# sourceMappingURL=reachability.js.map