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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/security/manifest/parser.ts"],
4
- "sourcesContent": ["/**\n * ManifestParser Orchestrator\n *\n * Orchestrates manifest discovery, version extraction, deduplication, and\n * database persistence. Uses the SecurityManifestAdapter for discovery and\n * extraction, registers all 12 version extraction plugins:\n * Wrapped (arch parser exists): npm, maven, go, pip, cargo, nuget, gradle\n * Standalone (no arch parser): rubygems, composer, swift, pubspec, hex\n *\n * Node ID format: `dep:<ecosystem>:<name>` (e.g. `dep:npm:express`)\n */\nimport { SecurityManifestAdapter, type VersionExtractionPlugin } from './adapter';\nimport { parseNpmManifest } from './plugins/npm';\nimport { parseMavenManifest } from './plugins/maven';\nimport { parseGoManifest } from './plugins/go';\nimport { parsePipManifest } from './plugins/pip';\nimport { parseCargoManifest } from './plugins/cargo';\nimport { parseNugetManifest } from './plugins/nuget';\nimport { parseGradleManifest } from './plugins/gradle';\nimport { parseRubygemsManifest } from './plugins/rubygems';\nimport { parseComposerManifest } from './plugins/composer';\nimport { parseSwiftManifest } from './plugins/swift';\nimport { parsePubspecManifest } from './plugins/pubspec';\nimport { parseHexManifest } from './plugins/hex';\nimport { parsePyprojectManifest } from './plugins/pyproject';\nimport type { GraphDatabase } from '../../db/database';\nimport type { ParsedDependency, ManifestParseResult } from '../types';\nimport type { Node, Edge } from '../../types';\nimport { logWarn, logError } from '../../errors';\n\n// \u2500\u2500 Plugin Definitions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Create the npm version extraction plugin wrapping parseNpmManifest.\n */\nfunction createNpmPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'npm',\n manifestFiles: ['package.json'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('package.json');\n },\n extract: parseNpmManifest,\n };\n}\n\n/**\n * Create the Maven version extraction plugin wrapping parseMavenManifest.\n */\nfunction createMavenPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'maven',\n manifestFiles: ['pom.xml'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('pom.xml');\n },\n extract: parseMavenManifest,\n };\n}\n\n/**\n * Create the Go version extraction plugin wrapping parseGoManifest.\n */\nfunction createGoPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'go',\n manifestFiles: ['go.mod'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('go.mod');\n },\n extract: parseGoManifest,\n };\n}\n\n/**\n * Create the pip version extraction plugin wrapping parsePipManifest.\n */\nfunction createPipPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'python',\n manifestFiles: ['requirements.txt'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('requirements.txt');\n },\n extract: parsePipManifest,\n };\n}\n\n/**\n * Create the Cargo version extraction plugin wrapping parseCargoManifest.\n */\nfunction createCargoPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'cargo',\n manifestFiles: ['Cargo.toml'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('Cargo.toml');\n },\n extract: parseCargoManifest,\n };\n}\n\n/**\n * Create the NuGet version extraction plugin wrapping parseNugetManifest.\n * ecosystem = 'csproj' to match the architecture parser name for lookup;\n * ParsedDependency.ecosystem is set to 'nuget' inside the plugin function.\n */\nfunction createNugetPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'csproj',\n manifestFiles: ['.csproj'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('.csproj');\n },\n extract: parseNugetManifest,\n };\n}\n\n/**\n * Create the Gradle version extraction plugin wrapping parseGradleManifest.\n */\nfunction createGradlePlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'gradle',\n manifestFiles: ['build.gradle', 'build.gradle.kts'],\n canExtract(manifestPath: string): boolean {\n const base = manifestPath.split('/').pop() ?? '';\n return base === 'build.gradle' || base === 'build.gradle.kts';\n },\n extract: parseGradleManifest,\n };\n}\n\nfunction createPyprojectPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'pyproject',\n manifestFiles: ['pyproject.toml'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('pyproject.toml');\n },\n extract: parsePyprojectManifest,\n };\n}\n\n// \u2500\u2500 Standalone Plugin Factories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 createRubygemsPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'rubygems',\n manifestFiles: ['Gemfile'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('/Gemfile') || manifestPath === 'Gemfile';\n },\n extract: parseRubygemsManifest,\n };\n}\n\nfunction createComposerPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'composer',\n manifestFiles: ['composer.json'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('composer.json');\n },\n extract: parseComposerManifest,\n };\n}\n\nfunction createSwiftPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'swift',\n manifestFiles: ['Package.swift'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('Package.swift');\n },\n extract: parseSwiftManifest,\n };\n}\n\nfunction createPubspecPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'pub',\n manifestFiles: ['pubspec.yaml'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('pubspec.yaml');\n },\n extract: parsePubspecManifest,\n };\n}\n\nfunction createHexPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'hex',\n manifestFiles: ['mix.exs'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('mix.exs');\n },\n extract: parseHexManifest,\n };\n}\n\n// \u2500\u2500 Version Comparison \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Compare two version strings. Returns:\n * - positive if a > b\n * - negative if a < b\n * - 0 if equal\n *\n * Handles semver-like versions (1.2.3), Go pseudo-versions, and constraint\n * strings. Falls back to lexicographic comparison for non-numeric versions.\n */\nexport function compareVersions(a: string, b: string): number {\n // Strip leading 'v' (common in Go modules)\n const cleanA = a.startsWith('v') ? a.slice(1) : a;\n const cleanB = b.startsWith('v') ? b.slice(1) : b;\n\n // Strip common constraint prefixes (^, ~, >=, <=, =, >, <)\n const numericA = cleanA.replace(/^[~^>=<]+/, '');\n const numericB = cleanB.replace(/^[~^>=<]+/, '');\n\n const partsA = numericA.split('.');\n const partsB = numericB.split('.');\n\n const maxLen = Math.max(partsA.length, partsB.length);\n for (let i = 0; i < maxLen; i++) {\n const segA = partsA[i] ?? '0';\n const segB = partsB[i] ?? '0';\n\n // Try numeric comparison first\n const numA = parseInt(segA, 10);\n const numB = parseInt(segB, 10);\n\n if (!isNaN(numA) && !isNaN(numB)) {\n if (numA !== numB) return numA - numB;\n // If numeric parts are equal but segments differ (e.g. \"3-beta\" vs \"3\"),\n // compare the remaining suffix\n const suffixA = segA.slice(String(numA).length);\n const suffixB = segB.slice(String(numB).length);\n if (suffixA !== suffixB) {\n // Pre-release suffixes (starting with -) are lower than no suffix\n if (suffixA && !suffixB) return -1;\n if (!suffixA && suffixB) return 1;\n return suffixA.localeCompare(suffixB);\n }\n } else {\n // Lexicographic fallback for non-numeric segments\n const cmp = segA.localeCompare(segB);\n if (cmp !== 0) return cmp;\n }\n }\n\n return 0;\n}\n\n/**\n * Get the effective version string for comparison purposes.\n * Prefers resolvedVersion over declaredConstraint.\n */\nfunction getEffectiveVersion(dep: ParsedDependency): string {\n return dep.resolvedVersion ?? dep.declaredConstraint;\n}\n\n// \u2500\u2500 Deduplication \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * A deduplicated dependency entry combining data from multiple manifests.\n */\ninterface DeduplicatedDependency {\n name: string;\n ecosystem: string;\n /** The highest declared constraint across all manifests */\n declaredConstraint: string;\n /** The highest resolved version across all manifests (if any) */\n resolvedVersion?: string;\n /** Scope from the manifest with the highest version */\n scope: ParsedDependency['scope'];\n /** All source manifests that declared this dependency */\n sourceManifests: string[];\n /** License from manifest, if available */\n license?: string;\n}\n\n/**\n * Deduplicate dependencies by (name, ecosystem) pair, keeping the entry\n * with the highest version and collecting all source manifests.\n */\nexport function deduplicateDependencies(deps: ParsedDependency[]): DeduplicatedDependency[] {\n const map = new Map<string, DeduplicatedDependency>();\n\n for (const dep of deps) {\n const key = `${dep.ecosystem}:${dep.name}`;\n const existing = map.get(key);\n\n if (!existing) {\n map.set(key, {\n name: dep.name,\n ecosystem: dep.ecosystem,\n declaredConstraint: dep.declaredConstraint,\n resolvedVersion: dep.resolvedVersion,\n scope: dep.scope,\n sourceManifests: [dep.sourceManifest],\n license: dep.license,\n });\n } else {\n // Add source manifest if not already present\n if (!existing.sourceManifests.includes(dep.sourceManifest)) {\n existing.sourceManifests.push(dep.sourceManifest);\n }\n\n // Keep the highest version\n const existingVersion = existing.resolvedVersion ?? existing.declaredConstraint;\n const newVersion = getEffectiveVersion(dep);\n\n if (compareVersions(newVersion, existingVersion) > 0) {\n existing.declaredConstraint = dep.declaredConstraint;\n existing.resolvedVersion = dep.resolvedVersion;\n existing.scope = dep.scope;\n // Update license if the winning version entry has one\n if (dep.license !== undefined) {\n existing.license = dep.license;\n }\n } else if (existing.license === undefined && dep.license !== undefined) {\n // Even if version didn't win, adopt the license if we don't have one\n existing.license = dep.license;\n }\n }\n }\n\n return [...map.values()];\n}\n\n// \u2500\u2500 ManifestParser Class \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * ManifestParser orchestrates manifest discovery, parsing, deduplication,\n * and database persistence for the security module.\n */\nexport class ManifestParser {\n private readonly adapter: SecurityManifestAdapter;\n private readonly db: GraphDatabase;\n private readonly projectRoot: string;\n\n constructor(db: GraphDatabase, projectRoot: string) {\n this.db = db;\n this.projectRoot = projectRoot;\n this.adapter = new SecurityManifestAdapter(projectRoot);\n\n // Wrapped plugins (arch parser exists \u2014 ecosystem must match archParser.name)\n this.adapter.registerPlugin(createNpmPlugin());\n this.adapter.registerPlugin(createMavenPlugin());\n this.adapter.registerPlugin(createGoPlugin());\n this.adapter.registerPlugin(createPipPlugin());\n this.adapter.registerPlugin(createCargoPlugin());\n this.adapter.registerPlugin(createNugetPlugin());\n this.adapter.registerPlugin(createGradlePlugin());\n\n // Standalone plugins (no arch parser \u2014 own manifest discovery via manifestFiles)\n this.adapter.registerStandalonePlugin(createPyprojectPlugin());\n this.adapter.registerStandalonePlugin(createRubygemsPlugin());\n this.adapter.registerStandalonePlugin(createComposerPlugin());\n this.adapter.registerStandalonePlugin(createSwiftPlugin());\n this.adapter.registerStandalonePlugin(createPubspecPlugin());\n this.adapter.registerStandalonePlugin(createHexPlugin());\n }\n\n /**\n * Get the underlying adapter (useful for testing).\n */\n getAdapter(): SecurityManifestAdapter {\n return this.adapter;\n }\n\n /**\n * Run full manifest discovery, parsing, deduplication, and database persistence.\n *\n * Steps:\n * 1. Discover manifest files via the adapter (reuses architecture parsers)\n * 2. Extract dependencies with version/scope data via registered plugins\n * 3. Deduplicate: same (name, ecosystem) \u2192 single entry with highest version\n * 4. Create Dependency_Nodes in the database (nodes + sec_dependencies tables)\n * 5. Create `declared_in` edges linking dependencies to their source manifests\n */\n async parseAll(): Promise<ManifestParseResult> {\n const warnings: Array<{ file: string; line?: number; message: string }> = [];\n const errors: Array<{ file: string; message: string }> = [];\n\n // Step 1 & 2: Discover and extract all dependencies\n const allDeps: ParsedDependency[] = [];\n const manifestPaths = this.adapter.discoverManifests();\n let manifestsParsed = 0;\n\n for (const manifestPath of manifestPaths) {\n try {\n const deps = await this.adapter.extractFromManifest(manifestPath);\n if (deps.length > 0) {\n allDeps.push(...deps);\n manifestsParsed++;\n } else {\n // Manifest was parsed but had no dependencies \u2014 still counts\n manifestsParsed++;\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const relPath = manifestPath.replace(this.projectRoot + '/', '').replace(/\\\\/g, '/');\n errors.push({ file: relPath, message });\n logError(`[sec:parser] Failed to parse manifest ${relPath}: ${message}`);\n }\n }\n\n if (allDeps.length === 0) {\n return {\n dependenciesCreated: 0,\n manifestsParsed,\n warnings,\n errors,\n };\n }\n\n // Step 3: Deduplicate by (name, ecosystem)\n const deduplicated = deduplicateDependencies(allDeps);\n\n // Step 4 & 5: Persist to database\n let dependenciesCreated = 0;\n const rawDb = this.db.getRawDb();\n\n for (const dep of deduplicated) {\n try {\n const nodeId = `dep:${dep.ecosystem}:${dep.name}`;\n\n // Create the node in the nodes table via upsertNode\n const node: Node = {\n id: nodeId,\n kind: 'dependency',\n name: dep.name,\n qualifiedName: `${dep.ecosystem}/${dep.name}`,\n filePath: dep.sourceManifests[0] ?? '',\n language: 'unknown',\n startLine: 0,\n endLine: 0,\n startColumn: 0,\n endColumn: 0,\n signature: dep.resolvedVersion ?? dep.declaredConstraint,\n isExported: false,\n isAsync: false,\n isStatic: false,\n isAbstract: false,\n updatedAt: Date.now(),\n };\n\n this.db.upsertNode(node);\n\n // Insert/update the sec_dependencies row\n rawDb.run(\n `INSERT OR REPLACE INTO sec_dependencies\n (node_id, ecosystem, package_name, declared_constraint, resolved_version, scope, source_manifests, license)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n nodeId,\n dep.ecosystem,\n dep.name,\n dep.declaredConstraint,\n dep.resolvedVersion ?? null,\n dep.scope,\n JSON.stringify(dep.sourceManifests),\n dep.license ?? null,\n ],\n );\n\n // Create `declared_in` edges from the dependency to each source manifest\n for (const manifest of dep.sourceManifests) {\n const edge: Edge = {\n source: nodeId,\n target: manifest,\n kind: 'declared_in',\n };\n this.db.insertEdge(edge);\n }\n\n dependenciesCreated++;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n warnings.push({\n file: dep.sourceManifests[0] ?? 'unknown',\n message: `Failed to persist dependency ${dep.ecosystem}/${dep.name}: ${message}`,\n });\n logWarn(`[sec:parser] Failed to persist dependency ${dep.ecosystem}/${dep.name}: ${message}`);\n }\n }\n\n return {\n dependenciesCreated,\n manifestsParsed,\n warnings,\n errors,\n };\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * ManifestParser Orchestrator\n *\n * Orchestrates manifest discovery, version extraction, deduplication, and\n * database persistence. Uses the SecurityManifestAdapter for discovery and\n * extraction, registers all 12 version extraction plugins:\n * Wrapped (arch parser exists): npm, maven, go, pip, cargo, nuget, gradle\n * Standalone (no arch parser): rubygems, composer, swift, pubspec, hex\n *\n * Node ID format: `dep:<ecosystem>:<name>` (e.g. `dep:npm:express`)\n */\nimport { SecurityManifestAdapter, type VersionExtractionPlugin, type ManifestScanOptions } from './adapter';\nimport { parseNpmManifest } from './plugins/npm';\nimport { parseMavenManifest } from './plugins/maven';\nimport { parseGoManifest } from './plugins/go';\nimport { parsePipManifest } from './plugins/pip';\nimport { parseCargoManifest } from './plugins/cargo';\nimport { parseNugetManifest } from './plugins/nuget';\nimport { parseGradleManifest } from './plugins/gradle';\nimport { parseRubygemsManifest } from './plugins/rubygems';\nimport { parseComposerManifest } from './plugins/composer';\nimport { parseSwiftManifest } from './plugins/swift';\nimport { parsePubspecManifest } from './plugins/pubspec';\nimport { parseHexManifest } from './plugins/hex';\nimport { parsePyprojectManifest } from './plugins/pyproject';\nimport type { GraphDatabase } from '../../db/database';\nimport type { ParsedDependency, ManifestParseResult } from '../types';\nimport type { Node, Edge } from '../../types';\nimport { logWarn, logError } from '../../errors';\n\n// \u2500\u2500 Plugin Definitions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Create the npm version extraction plugin wrapping parseNpmManifest.\n */\nfunction createNpmPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'npm',\n manifestFiles: ['package.json'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('package.json');\n },\n extract: parseNpmManifest,\n };\n}\n\n/**\n * Create the Maven version extraction plugin wrapping parseMavenManifest.\n */\nfunction createMavenPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'maven',\n manifestFiles: ['pom.xml'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('pom.xml');\n },\n extract: parseMavenManifest,\n };\n}\n\n/**\n * Create the Go version extraction plugin wrapping parseGoManifest.\n */\nfunction createGoPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'go',\n manifestFiles: ['go.mod'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('go.mod');\n },\n extract: parseGoManifest,\n };\n}\n\n/**\n * Create the pip version extraction plugin wrapping parsePipManifest.\n */\nfunction createPipPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'python',\n manifestFiles: ['requirements.txt'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('requirements.txt');\n },\n extract: parsePipManifest,\n };\n}\n\n/**\n * Create the Cargo version extraction plugin wrapping parseCargoManifest.\n */\nfunction createCargoPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'cargo',\n manifestFiles: ['Cargo.toml'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('Cargo.toml');\n },\n extract: parseCargoManifest,\n };\n}\n\n/**\n * Create the NuGet version extraction plugin wrapping parseNugetManifest.\n * ecosystem = 'csproj' to match the architecture parser name for lookup;\n * ParsedDependency.ecosystem is set to 'nuget' inside the plugin function.\n */\nfunction createNugetPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'csproj',\n manifestFiles: ['.csproj'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('.csproj');\n },\n extract: parseNugetManifest,\n };\n}\n\n/**\n * Create the Gradle version extraction plugin wrapping parseGradleManifest.\n */\nfunction createGradlePlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'gradle',\n manifestFiles: ['build.gradle', 'build.gradle.kts'],\n canExtract(manifestPath: string): boolean {\n const base = manifestPath.split('/').pop() ?? '';\n return base === 'build.gradle' || base === 'build.gradle.kts';\n },\n extract: parseGradleManifest,\n };\n}\n\nfunction createPyprojectPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'pyproject',\n manifestFiles: ['pyproject.toml'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('pyproject.toml');\n },\n extract: parsePyprojectManifest,\n };\n}\n\n// \u2500\u2500 Standalone Plugin Factories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 createRubygemsPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'rubygems',\n manifestFiles: ['Gemfile'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('/Gemfile') || manifestPath === 'Gemfile';\n },\n extract: parseRubygemsManifest,\n };\n}\n\nfunction createComposerPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'composer',\n manifestFiles: ['composer.json'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('composer.json');\n },\n extract: parseComposerManifest,\n };\n}\n\nfunction createSwiftPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'swift',\n manifestFiles: ['Package.swift'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('Package.swift');\n },\n extract: parseSwiftManifest,\n };\n}\n\nfunction createPubspecPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'pub',\n manifestFiles: ['pubspec.yaml'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('pubspec.yaml');\n },\n extract: parsePubspecManifest,\n };\n}\n\nfunction createHexPlugin(): VersionExtractionPlugin {\n return {\n ecosystem: 'hex',\n manifestFiles: ['mix.exs'],\n canExtract(manifestPath: string): boolean {\n return manifestPath.endsWith('mix.exs');\n },\n extract: parseHexManifest,\n };\n}\n\n// \u2500\u2500 Version Comparison \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Compare two version strings. Returns:\n * - positive if a > b\n * - negative if a < b\n * - 0 if equal\n *\n * Handles semver-like versions (1.2.3), Go pseudo-versions, and constraint\n * strings. Falls back to lexicographic comparison for non-numeric versions.\n */\nexport function compareVersions(a: string, b: string): number {\n // Strip leading 'v' (common in Go modules)\n const cleanA = a.startsWith('v') ? a.slice(1) : a;\n const cleanB = b.startsWith('v') ? b.slice(1) : b;\n\n // Strip common constraint prefixes (^, ~, >=, <=, =, >, <)\n const numericA = cleanA.replace(/^[~^>=<]+/, '');\n const numericB = cleanB.replace(/^[~^>=<]+/, '');\n\n const partsA = numericA.split('.');\n const partsB = numericB.split('.');\n\n const maxLen = Math.max(partsA.length, partsB.length);\n for (let i = 0; i < maxLen; i++) {\n const segA = partsA[i] ?? '0';\n const segB = partsB[i] ?? '0';\n\n // Try numeric comparison first\n const numA = parseInt(segA, 10);\n const numB = parseInt(segB, 10);\n\n if (!isNaN(numA) && !isNaN(numB)) {\n if (numA !== numB) return numA - numB;\n // If numeric parts are equal but segments differ (e.g. \"3-beta\" vs \"3\"),\n // compare the remaining suffix\n const suffixA = segA.slice(String(numA).length);\n const suffixB = segB.slice(String(numB).length);\n if (suffixA !== suffixB) {\n // Pre-release suffixes (starting with -) are lower than no suffix\n if (suffixA && !suffixB) return -1;\n if (!suffixA && suffixB) return 1;\n return suffixA.localeCompare(suffixB);\n }\n } else {\n // Lexicographic fallback for non-numeric segments\n const cmp = segA.localeCompare(segB);\n if (cmp !== 0) return cmp;\n }\n }\n\n return 0;\n}\n\n/**\n * Get the effective version string for comparison purposes.\n * Prefers resolvedVersion over declaredConstraint.\n */\nfunction getEffectiveVersion(dep: ParsedDependency): string {\n return dep.resolvedVersion ?? dep.declaredConstraint;\n}\n\n// \u2500\u2500 Deduplication \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * A deduplicated dependency entry combining data from multiple manifests.\n */\ninterface DeduplicatedDependency {\n name: string;\n ecosystem: string;\n /** The highest declared constraint across all manifests */\n declaredConstraint: string;\n /** The highest resolved version across all manifests (if any) */\n resolvedVersion?: string;\n /** Scope from the manifest with the highest version */\n scope: ParsedDependency['scope'];\n /** All source manifests that declared this dependency */\n sourceManifests: string[];\n /** License from manifest, if available */\n license?: string;\n}\n\n/**\n * Deduplicate dependencies by (name, ecosystem) pair, keeping the entry\n * with the highest version and collecting all source manifests.\n */\nexport function deduplicateDependencies(deps: ParsedDependency[]): DeduplicatedDependency[] {\n const map = new Map<string, DeduplicatedDependency>();\n\n for (const dep of deps) {\n const key = `${dep.ecosystem}:${dep.name}`;\n const existing = map.get(key);\n\n if (!existing) {\n map.set(key, {\n name: dep.name,\n ecosystem: dep.ecosystem,\n declaredConstraint: dep.declaredConstraint,\n resolvedVersion: dep.resolvedVersion,\n scope: dep.scope,\n sourceManifests: [dep.sourceManifest],\n license: dep.license,\n });\n } else {\n // Add source manifest if not already present\n if (!existing.sourceManifests.includes(dep.sourceManifest)) {\n existing.sourceManifests.push(dep.sourceManifest);\n }\n\n // Keep the highest version\n const existingVersion = existing.resolvedVersion ?? existing.declaredConstraint;\n const newVersion = getEffectiveVersion(dep);\n\n if (compareVersions(newVersion, existingVersion) > 0) {\n existing.declaredConstraint = dep.declaredConstraint;\n existing.resolvedVersion = dep.resolvedVersion;\n existing.scope = dep.scope;\n // Update license if the winning version entry has one\n if (dep.license !== undefined) {\n existing.license = dep.license;\n }\n } else if (existing.license === undefined && dep.license !== undefined) {\n // Even if version didn't win, adopt the license if we don't have one\n existing.license = dep.license;\n }\n }\n }\n\n return [...map.values()];\n}\n\n// \u2500\u2500 ManifestParser Class \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * ManifestParser orchestrates manifest discovery, parsing, deduplication,\n * and database persistence for the security module.\n */\nexport class ManifestParser {\n private readonly adapter: SecurityManifestAdapter;\n private readonly db: GraphDatabase;\n private readonly projectRoot: string;\n\n constructor(db: GraphDatabase, projectRoot: string, scanOptions?: ManifestScanOptions) {\n this.db = db;\n this.projectRoot = projectRoot;\n this.adapter = new SecurityManifestAdapter(projectRoot, scanOptions);\n\n // Wrapped plugins (arch parser exists \u2014 ecosystem must match archParser.name)\n this.adapter.registerPlugin(createNpmPlugin());\n this.adapter.registerPlugin(createMavenPlugin());\n this.adapter.registerPlugin(createGoPlugin());\n this.adapter.registerPlugin(createPipPlugin());\n this.adapter.registerPlugin(createCargoPlugin());\n this.adapter.registerPlugin(createNugetPlugin());\n this.adapter.registerPlugin(createGradlePlugin());\n\n // Standalone plugins (no arch parser \u2014 own manifest discovery via manifestFiles)\n this.adapter.registerStandalonePlugin(createPyprojectPlugin());\n this.adapter.registerStandalonePlugin(createRubygemsPlugin());\n this.adapter.registerStandalonePlugin(createComposerPlugin());\n this.adapter.registerStandalonePlugin(createSwiftPlugin());\n this.adapter.registerStandalonePlugin(createPubspecPlugin());\n this.adapter.registerStandalonePlugin(createHexPlugin());\n }\n\n /**\n * Get the underlying adapter (useful for testing).\n */\n getAdapter(): SecurityManifestAdapter {\n return this.adapter;\n }\n\n /**\n * Run full manifest discovery, parsing, deduplication, and database persistence.\n *\n * Steps:\n * 1. Discover manifest files via the adapter (reuses architecture parsers)\n * 2. Extract dependencies with version/scope data via registered plugins\n * 3. Deduplicate: same (name, ecosystem) \u2192 single entry with highest version\n * 4. Create Dependency_Nodes in the database (nodes + sec_dependencies tables)\n * 5. Create `declared_in` edges linking dependencies to their source manifests\n */\n async parseAll(): Promise<ManifestParseResult> {\n const warnings: Array<{ file: string; line?: number; message: string }> = [];\n const errors: Array<{ file: string; message: string }> = [];\n\n // Step 1 & 2: Discover and extract all dependencies\n const allDeps: ParsedDependency[] = [];\n const manifestPaths = this.adapter.discoverManifests();\n let manifestsParsed = 0;\n\n for (const manifestPath of manifestPaths) {\n try {\n const deps = await this.adapter.extractFromManifest(manifestPath);\n if (deps.length > 0) {\n allDeps.push(...deps);\n manifestsParsed++;\n } else {\n // Manifest was parsed but had no dependencies \u2014 still counts\n manifestsParsed++;\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const relPath = manifestPath.replace(this.projectRoot + '/', '').replace(/\\\\/g, '/');\n errors.push({ file: relPath, message });\n logError(`[sec:parser] Failed to parse manifest ${relPath}: ${message}`);\n }\n }\n\n if (allDeps.length === 0) {\n return {\n dependenciesCreated: 0,\n manifestsParsed,\n warnings,\n errors,\n };\n }\n\n // Step 3: Deduplicate by (name, ecosystem)\n const deduplicated = deduplicateDependencies(allDeps);\n\n // Step 4 & 5: Persist to database\n let dependenciesCreated = 0;\n const rawDb = this.db.getRawDb();\n\n for (const dep of deduplicated) {\n try {\n const nodeId = `dep:${dep.ecosystem}:${dep.name}`;\n\n // Create the node in the nodes table via upsertNode\n const node: Node = {\n id: nodeId,\n kind: 'dependency',\n name: dep.name,\n qualifiedName: `${dep.ecosystem}/${dep.name}`,\n filePath: dep.sourceManifests[0] ?? '',\n language: 'unknown',\n startLine: 0,\n endLine: 0,\n startColumn: 0,\n endColumn: 0,\n signature: dep.resolvedVersion ?? dep.declaredConstraint,\n isExported: false,\n isAsync: false,\n isStatic: false,\n isAbstract: false,\n updatedAt: Date.now(),\n };\n\n this.db.upsertNode(node);\n\n // Insert/update the sec_dependencies row\n rawDb.run(\n `INSERT OR REPLACE INTO sec_dependencies\n (node_id, ecosystem, package_name, declared_constraint, resolved_version, scope, source_manifests, license)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n nodeId,\n dep.ecosystem,\n dep.name,\n dep.declaredConstraint,\n dep.resolvedVersion ?? null,\n dep.scope,\n JSON.stringify(dep.sourceManifests),\n dep.license ?? null,\n ],\n );\n\n // Create `declared_in` edges from the dependency to each source manifest\n for (const manifest of dep.sourceManifests) {\n const edge: Edge = {\n source: nodeId,\n target: manifest,\n kind: 'declared_in',\n };\n this.db.insertEdge(edge);\n }\n\n dependenciesCreated++;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n warnings.push({\n file: dep.sourceManifests[0] ?? 'unknown',\n message: `Failed to persist dependency ${dep.ecosystem}/${dep.name}: ${message}`,\n });\n logWarn(`[sec:parser] Failed to persist dependency ${dep.ecosystem}/${dep.name}: ${message}`);\n }\n }\n\n return {\n dependenciesCreated,\n manifestsParsed,\n warnings,\n errors,\n };\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -67,8 +67,31 @@ async function parseCargoManifest(manifestPath, projectRoot) {
67
67
  });
68
68
  }
69
69
  }
70
+ const rootCrateName = extractCargoPackageName(content);
71
+ const directNames = new Set(dependencies.map((d) => d.name));
72
+ for (const [name, version] of resolvedVersions) {
73
+ if (directNames.has(name) || name === rootCrateName) continue;
74
+ dependencies.push({
75
+ name,
76
+ declaredConstraint: version,
77
+ resolvedVersion: version,
78
+ scope: "production",
79
+ ecosystem: "cargo",
80
+ sourceManifest: relativeManifest
81
+ });
82
+ }
70
83
  return dependencies;
71
84
  }
85
+ function extractCargoPackageName(content) {
86
+ const packageSectionMatch = /^\[package\]\s*$/m.exec(content);
87
+ if (!packageSectionMatch) return void 0;
88
+ const startIdx = packageSectionMatch.index + packageSectionMatch[0].length;
89
+ const remaining = content.slice(startIdx);
90
+ const nextSection = remaining.match(/^\[/m);
91
+ const sectionContent = nextSection ? remaining.slice(0, nextSection.index) : remaining;
92
+ const nameMatch = sectionContent.match(/^name\s*=\s*"([^"]+)"/m);
93
+ return nameMatch ? nameMatch[1].trim() : void 0;
94
+ }
72
95
  function extractSectionDependencies(content, section, relativeManifest) {
73
96
  const deps = [];
74
97
  const sectionRegex = new RegExp(`^\\[${escapeRegex(section)}\\]\\s*$`, "m");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/security/manifest/plugins/cargo.ts"],
4
- "sourcesContent": ["/**\n * Cargo Version Extraction Plugin for KiroGraph-Sec\n *\n * Extends the existing architecture cargo parser to extract version constraints,\n * resolved versions (from Cargo.lock), and dependency scopes for security analysis.\n *\n * Reuses `cargoParser` from `src/architecture/manifest/cargo.ts` for discovery and\n * basic parsing, then layers on version/scope extraction.\n *\n * Handles both simple format (`serde = \"1.0\"`) and table format\n * (`serde = { version = \"1.0\", features = [\"derive\"] }`).\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\n/**\n * Dependency section headers in Cargo.toml mapped to their ParsedDependency scope.\n */\nconst SECTION_SCOPES: Array<{ section: string; scope: ParsedDependency['scope'] }> = [\n { section: 'dependencies', scope: 'production' },\n { section: 'dev-dependencies', scope: 'development' },\n { section: 'build-dependencies', scope: 'development' },\n];\n\n/**\n * Resolved version map: package name \u2192 resolved version string.\n * Built from Cargo.lock.\n */\ntype ResolvedVersionMap = Map<string, string>;\n\n/**\n * Parse a Cargo.toml manifest and extract dependency declarations\n * with version constraints, scopes, and resolved versions from Cargo.lock.\n *\n * @param manifestPath - Absolute path to the Cargo.toml file\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 parseCargoManifest(\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 Cargo.toml content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:cargo] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Attempt to load resolved versions from Cargo.lock\n const resolvedVersions = loadResolvedVersions(manifestDir);\n\n // Extract the package-level license from [package] section\n const license = extractCargoLicense(content);\n\n // Extract dependencies from each section\n const dependencies: ParsedDependency[] = [];\n\n for (const { section, scope } of SECTION_SCOPES) {\n const sectionDeps = extractSectionDependencies(content, section, relativeManifest);\n for (const dep of sectionDeps) {\n const resolvedVersion = resolvedVersions.get(dep.name);\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion,\n scope,\n ecosystem: 'cargo',\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 TOML section.\n */\ninterface RawDependency {\n name: string;\n constraint: string;\n}\n\n/**\n * Extract dependencies from a specific section of a Cargo.toml file.\n * Handles both simple (`name = \"version\"`) and table (`name = { version = \"...\" }`) formats.\n * Also handles `[<section>.<name>]` table headers for individual dependency configuration.\n */\nfunction extractSectionDependencies(\n content: string,\n section: string,\n relativeManifest: string,\n): RawDependency[] {\n const deps: RawDependency[] = [];\n\n // Pattern 1: Inline section [dependencies] / [dev-dependencies] / [build-dependencies]\n // Match the section header and capture everything until the next section header\n const sectionRegex = new RegExp(`^\\\\[${escapeRegex(section)}\\\\]\\\\s*$`, 'm');\n const sectionMatch = sectionRegex.exec(content);\n\n if (sectionMatch) {\n const startIdx = sectionMatch.index + sectionMatch[0].length;\n // Find the next section header (line starting with [)\n const remaining = content.slice(startIdx);\n const nextSectionMatch = remaining.match(/^\\[/m);\n const sectionContent = nextSectionMatch\n ? remaining.slice(0, nextSectionMatch.index)\n : remaining;\n\n const lines = sectionContent.split('\\n');\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n\n const dep = parseDependencyLine(trimmed, relativeManifest, section);\n if (dep) {\n deps.push(dep);\n }\n }\n }\n\n // Pattern 2: Table-style individual dependency sections\n // e.g., [dependencies.serde] or [dev-dependencies.tokio]\n const tableRegex = new RegExp(`^\\\\[${escapeRegex(section)}\\\\.([^\\\\]]+)\\\\]\\\\s*$`, 'gm');\n let tableMatch: RegExpExecArray | null;\n while ((tableMatch = tableRegex.exec(content)) !== null) {\n const depName = tableMatch[1].trim();\n const startIdx = tableMatch.index + tableMatch[0].length;\n const remaining = content.slice(startIdx);\n const nextSectionMatch = remaining.match(/^\\[/m);\n const tableContent = nextSectionMatch\n ? remaining.slice(0, nextSectionMatch.index)\n : remaining;\n\n const version = extractVersionFromTable(tableContent);\n if (version) {\n deps.push({ name: depName, constraint: version });\n } else {\n // If no version specified, it might use path/git \u2014 use \"*\" as constraint\n deps.push({ name: depName, constraint: '*' });\n }\n }\n\n return deps;\n}\n\n/**\n * Parse a single dependency line from a Cargo.toml section.\n *\n * Handles:\n * - Simple string: `serde = \"1.0\"`\n * - Inline table: `serde = { version = \"1.0\", features = [\"derive\"] }`\n * - Path/git dependencies without version: `my-lib = { path = \"../my-lib\" }`\n */\nfunction parseDependencyLine(\n line: string,\n relativeManifest: string,\n section: string,\n): RawDependency | null {\n // Match: name = \"version\" or name = '...'\n const simpleMatch = line.match(/^([a-zA-Z0-9_-]+)\\s*=\\s*\"([^\"]*)\"$/);\n if (simpleMatch) {\n const name = simpleMatch[1];\n const constraint = simpleMatch[2];\n if (!isValidCrateName(name)) {\n logWarn(`[sec:cargo] Invalid crate name \"${name}\" in ${relativeManifest} [${section}] \u2014 skipping`);\n return null;\n }\n if (constraint.trim() === '') {\n logWarn(`[sec:cargo] Empty version constraint for \"${name}\" in ${relativeManifest} [${section}] \u2014 skipping`);\n return null;\n }\n return { name, constraint };\n }\n\n // Match: name = { version = \"...\", ... }\n const tableMatch = line.match(/^([a-zA-Z0-9_-]+)\\s*=\\s*\\{(.*)\\}\\s*$/);\n if (tableMatch) {\n const name = tableMatch[1];\n const tableContent = tableMatch[2];\n if (!isValidCrateName(name)) {\n logWarn(`[sec:cargo] Invalid crate name \"${name}\" in ${relativeManifest} [${section}] \u2014 skipping`);\n return null;\n }\n\n const version = extractVersionFromInlineTable(tableContent);\n if (version) {\n return { name, constraint: version };\n }\n\n // No version field \u2014 path or git dependency, use \"*\" as constraint\n return { name, constraint: '*' };\n }\n\n // Line doesn't match expected formats \u2014 could be a comment or malformed\n // Only warn if it looks like it was intended to be a dependency\n if (line.includes('=') && !line.startsWith('#')) {\n const possibleName = line.split('=')[0].trim();\n if (possibleName && /^[a-zA-Z0-9_-]+$/.test(possibleName)) {\n logWarn(`[sec:cargo] Could not parse dependency line in ${relativeManifest} [${section}]: \"${line}\" \u2014 skipping`);\n }\n }\n\n return null;\n}\n\n/**\n * Extract the version field from an inline table string.\n * e.g., from `version = \"1.0\", features = [\"derive\"]` \u2192 \"1.0\"\n */\nfunction extractVersionFromInlineTable(tableContent: string): string | null {\n const versionMatch = tableContent.match(/version\\s*=\\s*\"([^\"]*)\"/);\n if (versionMatch && versionMatch[1].trim() !== '') {\n return versionMatch[1];\n }\n return null;\n}\n\n/**\n * Extract the version field from a multi-line table section.\n * e.g., from lines like `version = \"1.0\"`\n */\nfunction extractVersionFromTable(tableContent: string): string | null {\n const lines = tableContent.split('\\n');\n for (const line of lines) {\n const trimmed = line.trim();\n const versionMatch = trimmed.match(/^version\\s*=\\s*\"([^\"]*)\"$/);\n if (versionMatch && versionMatch[1].trim() !== '') {\n return versionMatch[1];\n }\n }\n return null;\n}\n\n/**\n * Attempt to load resolved versions from Cargo.lock in the given directory.\n * Returns an empty map if Cargo.lock is not available.\n *\n * Cargo.lock format uses [[package]] blocks:\n * ```\n * [[package]]\n * name = \"serde\"\n * version = \"1.0.193\"\n * ```\n */\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n const lockPath = path.join(manifestDir, 'Cargo.lock');\n if (!fs.existsSync(lockPath)) {\n return map;\n }\n\n try {\n const lockContent = fs.readFileSync(lockPath, 'utf8');\n extractFromCargoLock(lockContent, map);\n } catch (err) {\n logWarn(`[sec:cargo] Failed to parse Cargo.lock: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n return map;\n}\n\n/**\n * Extract resolved versions from Cargo.lock content.\n *\n * Cargo.lock uses [[package]] blocks with name and version fields:\n * ```\n * [[package]]\n * name = \"serde\"\n * version = \"1.0.193\"\n * source = \"registry+https://github.com/rust-lang/crates.io-index\"\n * ```\n */\nfunction extractFromCargoLock(content: string, map: ResolvedVersionMap): void {\n const lines = content.split('\\n');\n let currentName: string | null = null;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // New package block\n if (trimmed === '[[package]]') {\n currentName = null;\n continue;\n }\n\n // Name field within a package block\n if (currentName === null) {\n const nameMatch = trimmed.match(/^name\\s*=\\s*\"([^\"]+)\"$/);\n if (nameMatch) {\n currentName = nameMatch[1];\n continue;\n }\n }\n\n // Version field within a package block (after name is set)\n if (currentName !== null) {\n const versionMatch = trimmed.match(/^version\\s*=\\s*\"([^\"]+)\"$/);\n if (versionMatch) {\n // Only store the first occurrence (highest version is typically last,\n // but Cargo.lock lists the resolved version for each unique instance)\n if (!map.has(currentName)) {\n map.set(currentName, versionMatch[1]);\n }\n currentName = null;\n continue;\n }\n }\n\n // Reset on empty line or new section\n if (trimmed === '' || trimmed.startsWith('[')) {\n currentName = null;\n }\n }\n}\n\n/**\n * Validate that a crate name follows Rust/Cargo naming conventions.\n * Crate names must be non-empty and contain only alphanumeric characters,\n * hyphens, or underscores.\n */\nfunction isValidCrateName(name: string): boolean {\n if (!name || typeof name !== 'string') return false;\n if (name.length > 64) return false;\n return /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name);\n}\n\n/**\n * Escape special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Extract the license field from the [package] section of a Cargo.toml.\n * Handles: license = \"MIT\" or license = \"MIT OR Apache-2.0\"\n */\nfunction extractCargoLicense(content: string): string | undefined {\n // Find the [package] section\n const packageSectionMatch = /^\\[package\\]\\s*$/m.exec(content);\n if (!packageSectionMatch) return undefined;\n\n const startIdx = packageSectionMatch.index + packageSectionMatch[0].length;\n const remaining = content.slice(startIdx);\n // Grab content up to the next section header\n const nextSection = remaining.match(/^\\[/m);\n const sectionContent = nextSection ? remaining.slice(0, nextSection.index) : remaining;\n\n const licenseMatch = sectionContent.match(/^license\\s*=\\s*\"([^\"]+)\"/m);\n if (licenseMatch && licenseMatch[1].trim() !== '') {\n return licenseMatch[1].trim();\n }\n return undefined;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Cargo Version Extraction Plugin for KiroGraph-Sec\n *\n * Extends the existing architecture cargo parser to extract version constraints,\n * resolved versions (from Cargo.lock), and dependency scopes for security analysis.\n *\n * Reuses `cargoParser` from `src/architecture/manifest/cargo.ts` for discovery and\n * basic parsing, then layers on version/scope extraction.\n *\n * Handles both simple format (`serde = \"1.0\"`) and table format\n * (`serde = { version = \"1.0\", features = [\"derive\"] }`).\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\n/**\n * Dependency section headers in Cargo.toml mapped to their ParsedDependency scope.\n */\nconst SECTION_SCOPES: Array<{ section: string; scope: ParsedDependency['scope'] }> = [\n { section: 'dependencies', scope: 'production' },\n { section: 'dev-dependencies', scope: 'development' },\n { section: 'build-dependencies', scope: 'development' },\n];\n\n/**\n * Resolved version map: package name \u2192 resolved version string.\n * Built from Cargo.lock.\n */\ntype ResolvedVersionMap = Map<string, string>;\n\n/**\n * Parse a Cargo.toml manifest and extract dependency declarations\n * with version constraints, scopes, and resolved versions from Cargo.lock.\n *\n * @param manifestPath - Absolute path to the Cargo.toml file\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 parseCargoManifest(\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 Cargo.toml content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:cargo] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Attempt to load resolved versions from Cargo.lock\n const resolvedVersions = loadResolvedVersions(manifestDir);\n\n // Extract the package-level license from [package] section\n const license = extractCargoLicense(content);\n\n // Extract dependencies from each section\n const dependencies: ParsedDependency[] = [];\n\n for (const { section, scope } of SECTION_SCOPES) {\n const sectionDeps = extractSectionDependencies(content, section, relativeManifest);\n for (const dep of sectionDeps) {\n const resolvedVersion = resolvedVersions.get(dep.name);\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion,\n scope,\n ecosystem: 'cargo',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n\n // Transitive-only crates (present in Cargo.lock but never declared in\n // Cargo.toml) would otherwise never become a Dependency_Node and would\n // silently skip vulnerability scanning. resolvedVersions already has every\n // crate Cargo.lock resolved, direct or transitive \u2014 add whichever aren't\n // already covered by a direct dependency. Exclude the workspace's own\n // crate, which Cargo.lock also lists as a [[package]] entry.\n const rootCrateName = extractCargoPackageName(content);\n const directNames = new Set(dependencies.map(d => d.name));\n for (const [name, version] of resolvedVersions) {\n if (directNames.has(name) || name === rootCrateName) continue;\n dependencies.push({\n name,\n declaredConstraint: version,\n resolvedVersion: version,\n scope: 'production',\n ecosystem: 'cargo',\n sourceManifest: relativeManifest,\n });\n }\n\n return dependencies;\n}\n\n/**\n * Extract the crate's own name from the [package] section of Cargo.toml, so\n * it can be excluded when adding transitive dependencies from Cargo.lock\n * (which also lists the workspace's own crate as a [[package]] entry).\n */\nfunction extractCargoPackageName(content: string): string | undefined {\n const packageSectionMatch = /^\\[package\\]\\s*$/m.exec(content);\n if (!packageSectionMatch) return undefined;\n\n const startIdx = packageSectionMatch.index + packageSectionMatch[0].length;\n const remaining = content.slice(startIdx);\n const nextSection = remaining.match(/^\\[/m);\n const sectionContent = nextSection ? remaining.slice(0, nextSection.index) : remaining;\n\n const nameMatch = sectionContent.match(/^name\\s*=\\s*\"([^\"]+)\"/m);\n return nameMatch ? nameMatch[1].trim() : undefined;\n}\n\n/**\n * A raw dependency entry extracted from a TOML section.\n */\ninterface RawDependency {\n name: string;\n constraint: string;\n}\n\n/**\n * Extract dependencies from a specific section of a Cargo.toml file.\n * Handles both simple (`name = \"version\"`) and table (`name = { version = \"...\" }`) formats.\n * Also handles `[<section>.<name>]` table headers for individual dependency configuration.\n */\nfunction extractSectionDependencies(\n content: string,\n section: string,\n relativeManifest: string,\n): RawDependency[] {\n const deps: RawDependency[] = [];\n\n // Pattern 1: Inline section [dependencies] / [dev-dependencies] / [build-dependencies]\n // Match the section header and capture everything until the next section header\n const sectionRegex = new RegExp(`^\\\\[${escapeRegex(section)}\\\\]\\\\s*$`, 'm');\n const sectionMatch = sectionRegex.exec(content);\n\n if (sectionMatch) {\n const startIdx = sectionMatch.index + sectionMatch[0].length;\n // Find the next section header (line starting with [)\n const remaining = content.slice(startIdx);\n const nextSectionMatch = remaining.match(/^\\[/m);\n const sectionContent = nextSectionMatch\n ? remaining.slice(0, nextSectionMatch.index)\n : remaining;\n\n const lines = sectionContent.split('\\n');\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n\n const dep = parseDependencyLine(trimmed, relativeManifest, section);\n if (dep) {\n deps.push(dep);\n }\n }\n }\n\n // Pattern 2: Table-style individual dependency sections\n // e.g., [dependencies.serde] or [dev-dependencies.tokio]\n const tableRegex = new RegExp(`^\\\\[${escapeRegex(section)}\\\\.([^\\\\]]+)\\\\]\\\\s*$`, 'gm');\n let tableMatch: RegExpExecArray | null;\n while ((tableMatch = tableRegex.exec(content)) !== null) {\n const depName = tableMatch[1].trim();\n const startIdx = tableMatch.index + tableMatch[0].length;\n const remaining = content.slice(startIdx);\n const nextSectionMatch = remaining.match(/^\\[/m);\n const tableContent = nextSectionMatch\n ? remaining.slice(0, nextSectionMatch.index)\n : remaining;\n\n const version = extractVersionFromTable(tableContent);\n if (version) {\n deps.push({ name: depName, constraint: version });\n } else {\n // If no version specified, it might use path/git \u2014 use \"*\" as constraint\n deps.push({ name: depName, constraint: '*' });\n }\n }\n\n return deps;\n}\n\n/**\n * Parse a single dependency line from a Cargo.toml section.\n *\n * Handles:\n * - Simple string: `serde = \"1.0\"`\n * - Inline table: `serde = { version = \"1.0\", features = [\"derive\"] }`\n * - Path/git dependencies without version: `my-lib = { path = \"../my-lib\" }`\n */\nfunction parseDependencyLine(\n line: string,\n relativeManifest: string,\n section: string,\n): RawDependency | null {\n // Match: name = \"version\" or name = '...'\n const simpleMatch = line.match(/^([a-zA-Z0-9_-]+)\\s*=\\s*\"([^\"]*)\"$/);\n if (simpleMatch) {\n const name = simpleMatch[1];\n const constraint = simpleMatch[2];\n if (!isValidCrateName(name)) {\n logWarn(`[sec:cargo] Invalid crate name \"${name}\" in ${relativeManifest} [${section}] \u2014 skipping`);\n return null;\n }\n if (constraint.trim() === '') {\n logWarn(`[sec:cargo] Empty version constraint for \"${name}\" in ${relativeManifest} [${section}] \u2014 skipping`);\n return null;\n }\n return { name, constraint };\n }\n\n // Match: name = { version = \"...\", ... }\n const tableMatch = line.match(/^([a-zA-Z0-9_-]+)\\s*=\\s*\\{(.*)\\}\\s*$/);\n if (tableMatch) {\n const name = tableMatch[1];\n const tableContent = tableMatch[2];\n if (!isValidCrateName(name)) {\n logWarn(`[sec:cargo] Invalid crate name \"${name}\" in ${relativeManifest} [${section}] \u2014 skipping`);\n return null;\n }\n\n const version = extractVersionFromInlineTable(tableContent);\n if (version) {\n return { name, constraint: version };\n }\n\n // No version field \u2014 path or git dependency, use \"*\" as constraint\n return { name, constraint: '*' };\n }\n\n // Line doesn't match expected formats \u2014 could be a comment or malformed\n // Only warn if it looks like it was intended to be a dependency\n if (line.includes('=') && !line.startsWith('#')) {\n const possibleName = line.split('=')[0].trim();\n if (possibleName && /^[a-zA-Z0-9_-]+$/.test(possibleName)) {\n logWarn(`[sec:cargo] Could not parse dependency line in ${relativeManifest} [${section}]: \"${line}\" \u2014 skipping`);\n }\n }\n\n return null;\n}\n\n/**\n * Extract the version field from an inline table string.\n * e.g., from `version = \"1.0\", features = [\"derive\"]` \u2192 \"1.0\"\n */\nfunction extractVersionFromInlineTable(tableContent: string): string | null {\n const versionMatch = tableContent.match(/version\\s*=\\s*\"([^\"]*)\"/);\n if (versionMatch && versionMatch[1].trim() !== '') {\n return versionMatch[1];\n }\n return null;\n}\n\n/**\n * Extract the version field from a multi-line table section.\n * e.g., from lines like `version = \"1.0\"`\n */\nfunction extractVersionFromTable(tableContent: string): string | null {\n const lines = tableContent.split('\\n');\n for (const line of lines) {\n const trimmed = line.trim();\n const versionMatch = trimmed.match(/^version\\s*=\\s*\"([^\"]*)\"$/);\n if (versionMatch && versionMatch[1].trim() !== '') {\n return versionMatch[1];\n }\n }\n return null;\n}\n\n/**\n * Attempt to load resolved versions from Cargo.lock in the given directory.\n * Returns an empty map if Cargo.lock is not available.\n *\n * Cargo.lock format uses [[package]] blocks:\n * ```\n * [[package]]\n * name = \"serde\"\n * version = \"1.0.193\"\n * ```\n */\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n const lockPath = path.join(manifestDir, 'Cargo.lock');\n if (!fs.existsSync(lockPath)) {\n return map;\n }\n\n try {\n const lockContent = fs.readFileSync(lockPath, 'utf8');\n extractFromCargoLock(lockContent, map);\n } catch (err) {\n logWarn(`[sec:cargo] Failed to parse Cargo.lock: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n return map;\n}\n\n/**\n * Extract resolved versions from Cargo.lock content.\n *\n * Cargo.lock uses [[package]] blocks with name and version fields:\n * ```\n * [[package]]\n * name = \"serde\"\n * version = \"1.0.193\"\n * source = \"registry+https://github.com/rust-lang/crates.io-index\"\n * ```\n */\nfunction extractFromCargoLock(content: string, map: ResolvedVersionMap): void {\n const lines = content.split('\\n');\n let currentName: string | null = null;\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // New package block\n if (trimmed === '[[package]]') {\n currentName = null;\n continue;\n }\n\n // Name field within a package block\n if (currentName === null) {\n const nameMatch = trimmed.match(/^name\\s*=\\s*\"([^\"]+)\"$/);\n if (nameMatch) {\n currentName = nameMatch[1];\n continue;\n }\n }\n\n // Version field within a package block (after name is set)\n if (currentName !== null) {\n const versionMatch = trimmed.match(/^version\\s*=\\s*\"([^\"]+)\"$/);\n if (versionMatch) {\n // Only store the first occurrence (highest version is typically last,\n // but Cargo.lock lists the resolved version for each unique instance)\n if (!map.has(currentName)) {\n map.set(currentName, versionMatch[1]);\n }\n currentName = null;\n continue;\n }\n }\n\n // Reset on empty line or new section\n if (trimmed === '' || trimmed.startsWith('[')) {\n currentName = null;\n }\n }\n}\n\n/**\n * Validate that a crate name follows Rust/Cargo naming conventions.\n * Crate names must be non-empty and contain only alphanumeric characters,\n * hyphens, or underscores.\n */\nfunction isValidCrateName(name: string): boolean {\n if (!name || typeof name !== 'string') return false;\n if (name.length > 64) return false;\n return /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name);\n}\n\n/**\n * Escape special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * Extract the license field from the [package] section of a Cargo.toml.\n * Handles: license = \"MIT\" or license = \"MIT OR Apache-2.0\"\n */\nfunction extractCargoLicense(content: string): string | undefined {\n // Find the [package] section\n const packageSectionMatch = /^\\[package\\]\\s*$/m.exec(content);\n if (!packageSectionMatch) return undefined;\n\n const startIdx = packageSectionMatch.index + packageSectionMatch[0].length;\n const remaining = content.slice(startIdx);\n // Grab content up to the next section header\n const nextSection = remaining.match(/^\\[/m);\n const sectionContent = nextSection ? remaining.slice(0, nextSection.index) : remaining;\n\n const licenseMatch = sectionContent.match(/^license\\s*=\\s*\"([^\"]+)\"/m);\n if (licenseMatch && licenseMatch[1].trim() !== '') {\n return licenseMatch[1].trim();\n }\n return undefined;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -83,6 +83,18 @@ async function parseComposerManifest(manifestPath, projectRoot) {
83
83
  });
84
84
  }
85
85
  }
86
+ const directNames = new Set(dependencies.map((d) => d.name.toLowerCase()));
87
+ for (const [name, version] of resolvedVersions) {
88
+ if (directNames.has(name)) continue;
89
+ dependencies.push({
90
+ name,
91
+ declaredConstraint: version,
92
+ resolvedVersion: version,
93
+ scope: "production",
94
+ ecosystem: "composer",
95
+ sourceManifest: relativeManifest
96
+ });
97
+ }
86
98
  return dependencies;
87
99
  }
88
100
  function shouldSkipEntry(name) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/security/manifest/plugins/composer.ts"],
4
- "sourcesContent": ["/**\n * Composer Version Extraction Plugin for KiroGraph-Sec\n *\n * Parses composer.json manifests to extract PHP package declarations with\n * version constraints, scopes, and resolved versions from composer.lock.\n *\n * Handles:\n * - `require` section \u2192 'production' scope\n * - `require-dev` section \u2192 'development' scope\n * - Skips `php`, `ext-*`, and `platform` entries (not installable packages)\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: package name \u2192 resolved version string.\n * Built from composer.lock.\n */\ntype ResolvedVersionMap = Map<string, string>;\n\n/**\n * Parse a composer.json manifest and extract package declarations with\n * version constraints, scopes, and resolved versions from composer.lock.\n *\n * @param manifestPath - Absolute path to the composer.json file\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 parseComposerManifest(\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 composer.json content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:composer] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Parse JSON\n let manifest: Record<string, unknown>;\n try {\n manifest = JSON.parse(content) as Record<string, unknown>;\n } catch (err) {\n logWarn(`[sec:composer] Failed to parse JSON in ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Attempt to load resolved versions from composer.lock\n const resolvedVersions = loadResolvedVersions(manifestDir);\n\n // Extract the project-level license\n const license = extractComposerLicense(manifest);\n\n const dependencies: ParsedDependency[] = [];\n\n // Process `require` (production) and `require-dev` (development) sections\n const sections: Array<{ key: string; scope: ParsedDependency['scope'] }> = [\n { key: 'require', scope: 'production' },\n { key: 'require-dev', scope: 'development' },\n ];\n\n for (const { key, scope } of sections) {\n const section = manifest[key];\n if (!section || typeof section !== 'object' || Array.isArray(section)) continue;\n\n for (const [pkgName, versionConstraint] of Object.entries(section as Record<string, unknown>)) {\n // Skip PHP runtime and extensions\n if (shouldSkipEntry(pkgName)) continue;\n\n if (typeof versionConstraint !== 'string') {\n logWarn(`[sec:composer] Non-string version for \"${pkgName}\" in ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n if (!isValidPackageName(pkgName)) {\n logWarn(`[sec:composer] Invalid package name \"${pkgName}\" in ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n const resolvedVersion = resolvedVersions.get(pkgName.toLowerCase());\n dependencies.push({\n name: pkgName,\n declaredConstraint: versionConstraint,\n resolvedVersion,\n scope,\n ecosystem: 'composer',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n\n return dependencies;\n}\n\n/**\n * Determine whether a composer.json key should be skipped.\n * Skips `php`, `ext-*`, and `platform` entries.\n */\nfunction shouldSkipEntry(name: string): boolean {\n if (name === 'php') return true;\n if (name.startsWith('ext-')) return true;\n if (name === 'platform') return true;\n return false;\n}\n\n/**\n * Validate that a Composer package name follows the `vendor/package` convention.\n * Also accepts bare names for legacy packages, but rejects obviously invalid entries.\n */\nfunction isValidPackageName(name: string): boolean {\n if (!name || typeof name !== 'string') return false;\n if (name.length > 255) return false;\n // Composer names must be vendor/package (lowercase, hyphens, digits allowed)\n return /^[a-z0-9]([a-z0-9_.-]*\\/[a-z0-9][a-z0-9_.-]*)$/.test(name);\n}\n\n/**\n * Attempt to load resolved versions from composer.lock in the given directory.\n * Returns an empty map if composer.lock is not available.\n *\n * composer.lock format (JSON):\n * ```json\n * {\n * \"packages\": [{ \"name\": \"vendor/package\", \"version\": \"1.2.3\" }],\n * \"packages-dev\": [{ \"name\": \"vendor/dev-package\", \"version\": \"v2.0.0\" }]\n * }\n * ```\n * Version strings may be prefixed with `v` \u2014 these are stripped.\n */\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n const lockPath = path.join(manifestDir, 'composer.lock');\n if (!fs.existsSync(lockPath)) {\n return map;\n }\n\n try {\n const lockContent = fs.readFileSync(lockPath, 'utf8');\n extractFromComposerLock(lockContent, map);\n } catch (err) {\n logWarn(`[sec:composer] Failed to parse composer.lock: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n return map;\n}\n\n/**\n * Extract resolved versions from composer.lock content.\n *\n * Reads both `packages` (production) and `packages-dev` arrays.\n * Normalises version strings by stripping a leading `v`.\n */\nfunction extractFromComposerLock(content: string, map: ResolvedVersionMap): void {\n let lockData: Record<string, unknown>;\n try {\n lockData = JSON.parse(content) as Record<string, unknown>;\n } catch {\n logWarn('[sec:composer] composer.lock is not valid JSON \u2014 resolved versions unavailable');\n return;\n }\n\n const arrays = [lockData['packages'], lockData['packages-dev']];\n for (const arr of arrays) {\n if (!Array.isArray(arr)) continue;\n for (const entry of arr) {\n if (!entry || typeof entry !== 'object') continue;\n const pkg = entry as Record<string, unknown>;\n const name = typeof pkg['name'] === 'string' ? pkg['name'].toLowerCase() : null;\n const version = typeof pkg['version'] === 'string' ? pkg['version'] : null;\n if (name && version) {\n // Strip leading `v` from version strings (e.g. \"v1.2.3\" \u2192 \"1.2.3\")\n const normalised = version.startsWith('v') ? version.slice(1) : version;\n if (!map.has(name)) {\n map.set(name, normalised);\n }\n }\n }\n }\n}\n\n/**\n * Extract the license field from a parsed composer.json manifest.\n * The \"license\" field can be a string or an array of strings (SPDX).\n */\nfunction extractComposerLicense(manifest: Record<string, unknown>): string | undefined {\n const raw = manifest['license'];\n if (typeof raw === 'string' && raw.trim() !== '') {\n return raw.trim();\n }\n if (Array.isArray(raw) && raw.length > 0 && typeof raw[0] === 'string') {\n return (raw as string[]).join(' OR ');\n }\n return undefined;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Composer Version Extraction Plugin for KiroGraph-Sec\n *\n * Parses composer.json manifests to extract PHP package declarations with\n * version constraints, scopes, and resolved versions from composer.lock.\n *\n * Handles:\n * - `require` section \u2192 'production' scope\n * - `require-dev` section \u2192 'development' scope\n * - Skips `php`, `ext-*`, and `platform` entries (not installable packages)\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: package name \u2192 resolved version string.\n * Built from composer.lock.\n */\ntype ResolvedVersionMap = Map<string, string>;\n\n/**\n * Parse a composer.json manifest and extract package declarations with\n * version constraints, scopes, and resolved versions from composer.lock.\n *\n * @param manifestPath - Absolute path to the composer.json file\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 parseComposerManifest(\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 composer.json content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:composer] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Parse JSON\n let manifest: Record<string, unknown>;\n try {\n manifest = JSON.parse(content) as Record<string, unknown>;\n } catch (err) {\n logWarn(`[sec:composer] Failed to parse JSON in ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Attempt to load resolved versions from composer.lock\n const resolvedVersions = loadResolvedVersions(manifestDir);\n\n // Extract the project-level license\n const license = extractComposerLicense(manifest);\n\n const dependencies: ParsedDependency[] = [];\n\n // Process `require` (production) and `require-dev` (development) sections\n const sections: Array<{ key: string; scope: ParsedDependency['scope'] }> = [\n { key: 'require', scope: 'production' },\n { key: 'require-dev', scope: 'development' },\n ];\n\n for (const { key, scope } of sections) {\n const section = manifest[key];\n if (!section || typeof section !== 'object' || Array.isArray(section)) continue;\n\n for (const [pkgName, versionConstraint] of Object.entries(section as Record<string, unknown>)) {\n // Skip PHP runtime and extensions\n if (shouldSkipEntry(pkgName)) continue;\n\n if (typeof versionConstraint !== 'string') {\n logWarn(`[sec:composer] Non-string version for \"${pkgName}\" in ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n if (!isValidPackageName(pkgName)) {\n logWarn(`[sec:composer] Invalid package name \"${pkgName}\" in ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n const resolvedVersion = resolvedVersions.get(pkgName.toLowerCase());\n dependencies.push({\n name: pkgName,\n declaredConstraint: versionConstraint,\n resolvedVersion,\n scope,\n ecosystem: 'composer',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n\n // Transitive-only packages (present in composer.lock but never declared in\n // composer.json) would otherwise never become a Dependency_Node and would\n // silently skip vulnerability scanning. resolvedVersions already has every\n // package composer.lock resolved, direct or transitive (lower-cased \u2014\n // production/dev origin isn't tracked separately, default to production).\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: 'composer',\n sourceManifest: relativeManifest,\n });\n }\n\n return dependencies;\n}\n\n/**\n * Determine whether a composer.json key should be skipped.\n * Skips `php`, `ext-*`, and `platform` entries.\n */\nfunction shouldSkipEntry(name: string): boolean {\n if (name === 'php') return true;\n if (name.startsWith('ext-')) return true;\n if (name === 'platform') return true;\n return false;\n}\n\n/**\n * Validate that a Composer package name follows the `vendor/package` convention.\n * Also accepts bare names for legacy packages, but rejects obviously invalid entries.\n */\nfunction isValidPackageName(name: string): boolean {\n if (!name || typeof name !== 'string') return false;\n if (name.length > 255) return false;\n // Composer names must be vendor/package (lowercase, hyphens, digits allowed)\n return /^[a-z0-9]([a-z0-9_.-]*\\/[a-z0-9][a-z0-9_.-]*)$/.test(name);\n}\n\n/**\n * Attempt to load resolved versions from composer.lock in the given directory.\n * Returns an empty map if composer.lock is not available.\n *\n * composer.lock format (JSON):\n * ```json\n * {\n * \"packages\": [{ \"name\": \"vendor/package\", \"version\": \"1.2.3\" }],\n * \"packages-dev\": [{ \"name\": \"vendor/dev-package\", \"version\": \"v2.0.0\" }]\n * }\n * ```\n * Version strings may be prefixed with `v` \u2014 these are stripped.\n */\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n const lockPath = path.join(manifestDir, 'composer.lock');\n if (!fs.existsSync(lockPath)) {\n return map;\n }\n\n try {\n const lockContent = fs.readFileSync(lockPath, 'utf8');\n extractFromComposerLock(lockContent, map);\n } catch (err) {\n logWarn(`[sec:composer] Failed to parse composer.lock: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n return map;\n}\n\n/**\n * Extract resolved versions from composer.lock content.\n *\n * Reads both `packages` (production) and `packages-dev` arrays.\n * Normalises version strings by stripping a leading `v`.\n */\nfunction extractFromComposerLock(content: string, map: ResolvedVersionMap): void {\n let lockData: Record<string, unknown>;\n try {\n lockData = JSON.parse(content) as Record<string, unknown>;\n } catch {\n logWarn('[sec:composer] composer.lock is not valid JSON \u2014 resolved versions unavailable');\n return;\n }\n\n const arrays = [lockData['packages'], lockData['packages-dev']];\n for (const arr of arrays) {\n if (!Array.isArray(arr)) continue;\n for (const entry of arr) {\n if (!entry || typeof entry !== 'object') continue;\n const pkg = entry as Record<string, unknown>;\n const name = typeof pkg['name'] === 'string' ? pkg['name'].toLowerCase() : null;\n const version = typeof pkg['version'] === 'string' ? pkg['version'] : null;\n if (name && version) {\n // Strip leading `v` from version strings (e.g. \"v1.2.3\" \u2192 \"1.2.3\")\n const normalised = version.startsWith('v') ? version.slice(1) : version;\n if (!map.has(name)) {\n map.set(name, normalised);\n }\n }\n }\n }\n}\n\n/**\n * Extract the license field from a parsed composer.json manifest.\n * The \"license\" field can be a string or an array of strings (SPDX).\n */\nfunction extractComposerLicense(manifest: Record<string, unknown>): string | undefined {\n const raw = manifest['license'];\n if (typeof raw === 'string' && raw.trim() !== '') {\n return raw.trim();\n }\n if (Array.isArray(raw) && raw.length > 0 && typeof raw[0] === 'string') {\n return (raw as string[]).join(' OR ');\n }\n return undefined;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -91,6 +91,18 @@ async function parseGradleManifest(manifestPath, projectRoot) {
91
91
  sourceManifest: relativeManifest
92
92
  });
93
93
  }
94
+ const directNames = new Set(dependencies.map((d) => d.name.toLowerCase()));
95
+ for (const [key, version] of resolvedVersions) {
96
+ if (directNames.has(key)) continue;
97
+ dependencies.push({
98
+ name: key,
99
+ declaredConstraint: version,
100
+ resolvedVersion: version,
101
+ scope: "production",
102
+ ecosystem: "gradle",
103
+ sourceManifest: relativeManifest
104
+ });
105
+ }
94
106
  return dependencies;
95
107
  }
96
108
  function parseGradleNotation(notation, configuration, relativeManifest) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/security/manifest/plugins/gradle.ts"],
4
- "sourcesContent": ["/**\n * Gradle Version Extraction Plugin for KiroGraph-Sec\n *\n * Extends the existing architecture gradle parser to extract version constraints,\n * resolved versions (from gradle.lockfile), and dependency scopes for security analysis.\n *\n * Reuses `gradleParser` from `src/architecture/manifest/gradle.ts` for discovery and\n * basic parsing, then layers on version/scope extraction.\n *\n * Handles Groovy DSL (`build.gradle`) and Kotlin DSL (`build.gradle.kts`):\n * - Groovy: `implementation 'group:artifact:version'`\n * - Kotlin: `implementation(\"group:artifact:version\")`\n * - Configuration variants: `api`, `compile`, `testImplementation`, `testApi`,\n * `androidTestImplementation`, `debugImplementation`, `releaseImplementation`\n * - Version catalog references (`libs.someDep`) are skipped \u2014 no version available\n * - Lock file: `gradle.lockfile` for resolved versions\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\n/**\n * Gradle dependency configurations mapped to ParsedDependency scope.\n * Configurations not listed here are treated as 'production'.\n */\nconst TEST_CONFIGURATIONS = new Set([\n 'testimplementation',\n 'testapi',\n 'testcompile',\n 'testruntimeonly',\n 'androidtestimplementation',\n 'androidtestapi',\n]);\n\n/**\n * All known Gradle dependency configuration prefixes we parse.\n * Listed from longest to shortest so regex alternation matches greedily.\n */\nconst KNOWN_CONFIGURATIONS = [\n 'androidTestImplementation',\n 'androidTestApi',\n 'debugImplementation',\n 'releaseImplementation',\n 'testImplementation',\n 'testRuntimeOnly',\n 'testCompile',\n 'testApi',\n 'implementation',\n 'runtimeOnly',\n 'compileOnly',\n 'compile',\n 'api',\n];\n\n/**\n * Resolved version map: `group:artifact` (lower-cased) \u2192 resolved version string.\n * Built from gradle.lockfile.\n */\ntype ResolvedVersionMap = Map<string, string>;\n\n/**\n * Parse a Gradle build script (`build.gradle` or `build.gradle.kts`) and extract\n * dependency declarations with version constraints, scopes, and resolved versions\n * from `gradle.lockfile`.\n *\n * @param manifestPath - Absolute path to the build.gradle or build.gradle.kts file\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 parseGradleManifest(\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 build script content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:gradle] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Attempt to load resolved versions from gradle.lockfile\n const resolvedVersions = loadResolvedVersions(manifestDir);\n\n const dependencies: ParsedDependency[] = [];\n\n // Build a regex that matches any known configuration followed by a dependency notation\n // Groovy DSL: implementation 'group:artifact:version'\n // implementation \"group:artifact:version\"\n // Kotlin DSL: implementation(\"group:artifact:version\")\n const configPattern = KNOWN_CONFIGURATIONS.map(c => escapeRegex(c)).join('|');\n // Match: <config> [optional (] ['\"] group:artifact:version ['\"] [optional )]\n const depRegex = new RegExp(\n `^\\\\s*(${configPattern})\\\\s*\\\\(?\\\\s*[\"']([^\"']+)[\"']\\\\s*\\\\)?`,\n 'gmi',\n );\n\n let match: RegExpExecArray | null;\n while ((match = depRegex.exec(content)) !== null) {\n const configuration = match[1];\n const notation = match[2].trim();\n\n const dep = parseGradleNotation(notation, configuration, relativeManifest);\n if (!dep) continue;\n\n const lookupKey = `${dep.group}:${dep.artifact}`.toLowerCase();\n const resolvedVersion = resolvedVersions.get(lookupKey);\n\n dependencies.push({\n name: `${dep.group}:${dep.artifact}`,\n declaredConstraint: dep.version,\n resolvedVersion,\n scope: resolveScope(configuration),\n ecosystem: 'gradle',\n sourceManifest: relativeManifest,\n });\n }\n\n return dependencies;\n}\n\n/**\n * A parsed Gradle dependency notation (group:artifact:version).\n */\ninterface GradleCoordinate {\n group: string;\n artifact: string;\n version: string;\n}\n\n/**\n * Parse a Gradle dependency notation string of the form `group:artifact:version`.\n *\n * Skips version catalog references (e.g. `libs.someDep`) and notations\n * that do not follow the three-part `group:artifact:version` format.\n */\nfunction parseGradleNotation(\n notation: string,\n configuration: string,\n relativeManifest: string,\n): GradleCoordinate | null {\n // Skip version catalog references \u2014 they don't carry version info\n if (notation.startsWith('libs.') || notation.startsWith('libs(')) {\n return null;\n }\n\n const parts = notation.split(':');\n if (parts.length < 2) {\n // Not a standard Maven/Gradle coordinate \u2014 skip silently\n return null;\n }\n\n const group = parts[0].trim();\n const artifact = parts[1].trim();\n const version = parts.length >= 3 ? parts[2].trim() : '';\n\n if (!group || !artifact) {\n logWarn(`[sec:gradle] Empty group or artifact in notation \"${notation}\" (${configuration}) at ${relativeManifest} \u2014 skipping`);\n return null;\n }\n\n // Dynamic/incomplete versions (e.g. \"+\", \"\", \"latest.release\") use \"*\" sentinel\n const normalizedVersion =\n version === '' || version === '+' || version.toLowerCase() === 'latest.release'\n ? '*'\n : version;\n\n return { group, artifact, version: normalizedVersion };\n}\n\n/**\n * Map a Gradle configuration name to a ParsedDependency scope.\n *\n * Test configurations (`testImplementation`, `testApi`, etc.) map to 'development'.\n * All others (including `implementation`, `api`, `compile`, etc.) map to 'production'.\n */\nfunction resolveScope(configuration: string): ParsedDependency['scope'] {\n return TEST_CONFIGURATIONS.has(configuration.toLowerCase()) ? 'development' : 'production';\n}\n\n/**\n * Attempt to load resolved versions from `gradle.lockfile` in the given directory.\n * Returns an empty map if the lock file is not available.\n *\n * gradle.lockfile format (one entry per line):\n * ```\n * # This is a comment\n * com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath\n * junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath\n * empty=\n * ```\n */\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n const lockPath = path.join(manifestDir, 'gradle.lockfile');\n if (!fs.existsSync(lockPath)) {\n return map;\n }\n\n try {\n const content = fs.readFileSync(lockPath, 'utf8');\n extractFromGradleLockfile(content, map);\n } catch (err) {\n logWarn(`[sec:gradle] Failed to parse gradle.lockfile: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n return map;\n}\n\n/**\n * Extract resolved versions from gradle.lockfile content.\n *\n * Each non-comment, non-empty line has the form:\n * `group:artifact:version=configurationList`\n *\n * The special line `empty=` indicates no locked dependencies and is ignored.\n */\nfunction extractFromGradleLockfile(content: string, map: ResolvedVersionMap): void {\n const lines = content.split('\\n');\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip comments and empty lines\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n\n // Skip the sentinel \"empty=\" line\n if (trimmed === 'empty=') continue;\n\n // Format: group:artifact:version=configurationList\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex === -1) continue;\n\n const coordinate = trimmed.slice(0, eqIndex).trim();\n const parts = coordinate.split(':');\n if (parts.length < 3) continue;\n\n const group = parts[0].trim();\n const artifact = parts[1].trim();\n const version = parts[2].trim();\n\n if (!group || !artifact || !version) continue;\n\n const key = `${group}:${artifact}`.toLowerCase();\n if (!map.has(key)) {\n map.set(key, version);\n }\n }\n}\n\n/**\n * Escape special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,SAAoB;AACpB,WAAsB;AAEtB,oBAAwB;AAMxB,MAAM,sBAAsB,oBAAI,IAAI;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAMD,MAAM,uBAAuB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAiBA,eAAsB,oBACpB,cACA,aAC6B;AAC7B,QAAM,cAAc,KAAK,QAAQ,YAAY;AAC7C,QAAM,mBAAmB,KAAK,SAAS,aAAa,YAAY,EAAE,QAAQ,OAAO,GAAG;AAGpF,MAAI;AACJ,MAAI;AACF,cAAU,GAAG,aAAa,cAAc,MAAM;AAAA,EAChD,SAAS,KAAK;AACZ,+BAAQ,+BAA+B,gBAAgB,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAC9G,WAAO,CAAC;AAAA,EACV;AAGA,QAAM,mBAAmB,qBAAqB,WAAW;AAEzD,QAAM,eAAmC,CAAC;AAM1C,QAAM,gBAAgB,qBAAqB,IAAI,OAAK,YAAY,CAAC,CAAC,EAAE,KAAK,GAAG;AAE5E,QAAM,WAAW,IAAI;AAAA,IACnB,SAAS,aAAa;AAAA,IACtB;AAAA,EACF;AAEA,MAAI;AACJ,UAAQ,QAAQ,SAAS,KAAK,OAAO,OAAO,MAAM;AAChD,UAAM,gBAAgB,MAAM,CAAC;AAC7B,UAAM,WAAW,MAAM,CAAC,EAAE,KAAK;AAE/B,UAAM,MAAM,oBAAoB,UAAU,eAAe,gBAAgB;AACzE,QAAI,CAAC,IAAK;AAEV,UAAM,YAAY,GAAG,IAAI,KAAK,IAAI,IAAI,QAAQ,GAAG,YAAY;AAC7D,UAAM,kBAAkB,iBAAiB,IAAI,SAAS;AAEtD,iBAAa,KAAK;AAAA,MAChB,MAAM,GAAG,IAAI,KAAK,IAAI,IAAI,QAAQ;AAAA,MAClC,oBAAoB,IAAI;AAAA,MACxB;AAAA,MACA,OAAO,aAAa,aAAa;AAAA,MACjC,WAAW;AAAA,MACX,gBAAgB;AAAA,IAClB,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAiBA,SAAS,oBACP,UACA,eACA,kBACyB;AAEzB,MAAI,SAAS,WAAW,OAAO,KAAK,SAAS,WAAW,OAAO,GAAG;AAChE,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,SAAS,MAAM,GAAG;AAChC,MAAI,MAAM,SAAS,GAAG;AAEpB,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,MAAM,CAAC,EAAE,KAAK;AAC5B,QAAM,WAAW,MAAM,CAAC,EAAE,KAAK;AAC/B,QAAM,UAAU,MAAM,UAAU,IAAI,MAAM,CAAC,EAAE,KAAK,IAAI;AAEtD,MAAI,CAAC,SAAS,CAAC,UAAU;AACvB,+BAAQ,qDAAqD,QAAQ,MAAM,aAAa,QAAQ,gBAAgB,kBAAa;AAC7H,WAAO;AAAA,EACT;AAGA,QAAM,oBACJ,YAAY,MAAM,YAAY,OAAO,QAAQ,YAAY,MAAM,mBAC3D,MACA;AAEN,SAAO,EAAE,OAAO,UAAU,SAAS,kBAAkB;AACvD;AAQA,SAAS,aAAa,eAAkD;AACtE,SAAO,oBAAoB,IAAI,cAAc,YAAY,CAAC,IAAI,gBAAgB;AAChF;AAcA,SAAS,qBAAqB,aAAyC;AACrE,QAAM,MAA0B,oBAAI,IAAI;AAExC,QAAM,WAAW,KAAK,KAAK,aAAa,iBAAiB;AACzD,MAAI,CAAC,GAAG,WAAW,QAAQ,GAAG;AAC5B,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,UAAU,GAAG,aAAa,UAAU,MAAM;AAChD,8BAA0B,SAAS,GAAG;AAAA,EACxC,SAAS,KAAK;AACZ,+BAAQ,iDAAiD,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,EAC7G;AAEA,SAAO;AACT;AAUA,SAAS,0BAA0B,SAAiB,KAA+B;AACjF,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAEhC,aAAW,QAAQ,OAAO;AACxB,UAAM,UAAU,KAAK,KAAK;AAG1B,QAAI,YAAY,MAAM,QAAQ,WAAW,GAAG,EAAG;AAG/C,QAAI,YAAY,SAAU;AAG1B,UAAM,UAAU,QAAQ,QAAQ,GAAG;AACnC,QAAI,YAAY,GAAI;AAEpB,UAAM,aAAa,QAAQ,MAAM,GAAG,OAAO,EAAE,KAAK;AAClD,UAAM,QAAQ,WAAW,MAAM,GAAG;AAClC,QAAI,MAAM,SAAS,EAAG;AAEtB,UAAM,QAAQ,MAAM,CAAC,EAAE,KAAK;AAC5B,UAAM,WAAW,MAAM,CAAC,EAAE,KAAK;AAC/B,UAAM,UAAU,MAAM,CAAC,EAAE,KAAK;AAE9B,QAAI,CAAC,SAAS,CAAC,YAAY,CAAC,QAAS;AAErC,UAAM,MAAM,GAAG,KAAK,IAAI,QAAQ,GAAG,YAAY;AAC/C,QAAI,CAAC,IAAI,IAAI,GAAG,GAAG;AACjB,UAAI,IAAI,KAAK,OAAO;AAAA,IACtB;AAAA,EACF;AACF;AAKA,SAAS,YAAY,KAAqB;AACxC,SAAO,IAAI,QAAQ,uBAAuB,MAAM;AAClD;",
4
+ "sourcesContent": ["/**\n * Gradle Version Extraction Plugin for KiroGraph-Sec\n *\n * Extends the existing architecture gradle parser to extract version constraints,\n * resolved versions (from gradle.lockfile), and dependency scopes for security analysis.\n *\n * Reuses `gradleParser` from `src/architecture/manifest/gradle.ts` for discovery and\n * basic parsing, then layers on version/scope extraction.\n *\n * Handles Groovy DSL (`build.gradle`) and Kotlin DSL (`build.gradle.kts`):\n * - Groovy: `implementation 'group:artifact:version'`\n * - Kotlin: `implementation(\"group:artifact:version\")`\n * - Configuration variants: `api`, `compile`, `testImplementation`, `testApi`,\n * `androidTestImplementation`, `debugImplementation`, `releaseImplementation`\n * - Version catalog references (`libs.someDep`) are skipped \u2014 no version available\n * - Lock file: `gradle.lockfile` for resolved versions\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\n/**\n * Gradle dependency configurations mapped to ParsedDependency scope.\n * Configurations not listed here are treated as 'production'.\n */\nconst TEST_CONFIGURATIONS = new Set([\n 'testimplementation',\n 'testapi',\n 'testcompile',\n 'testruntimeonly',\n 'androidtestimplementation',\n 'androidtestapi',\n]);\n\n/**\n * All known Gradle dependency configuration prefixes we parse.\n * Listed from longest to shortest so regex alternation matches greedily.\n */\nconst KNOWN_CONFIGURATIONS = [\n 'androidTestImplementation',\n 'androidTestApi',\n 'debugImplementation',\n 'releaseImplementation',\n 'testImplementation',\n 'testRuntimeOnly',\n 'testCompile',\n 'testApi',\n 'implementation',\n 'runtimeOnly',\n 'compileOnly',\n 'compile',\n 'api',\n];\n\n/**\n * Resolved version map: `group:artifact` (lower-cased) \u2192 resolved version string.\n * Built from gradle.lockfile.\n */\ntype ResolvedVersionMap = Map<string, string>;\n\n/**\n * Parse a Gradle build script (`build.gradle` or `build.gradle.kts`) and extract\n * dependency declarations with version constraints, scopes, and resolved versions\n * from `gradle.lockfile`.\n *\n * @param manifestPath - Absolute path to the build.gradle or build.gradle.kts file\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 parseGradleManifest(\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 build script content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:gradle] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Attempt to load resolved versions from gradle.lockfile\n const resolvedVersions = loadResolvedVersions(manifestDir);\n\n const dependencies: ParsedDependency[] = [];\n\n // Build a regex that matches any known configuration followed by a dependency notation\n // Groovy DSL: implementation 'group:artifact:version'\n // implementation \"group:artifact:version\"\n // Kotlin DSL: implementation(\"group:artifact:version\")\n const configPattern = KNOWN_CONFIGURATIONS.map(c => escapeRegex(c)).join('|');\n // Match: <config> [optional (] ['\"] group:artifact:version ['\"] [optional )]\n const depRegex = new RegExp(\n `^\\\\s*(${configPattern})\\\\s*\\\\(?\\\\s*[\"']([^\"']+)[\"']\\\\s*\\\\)?`,\n 'gmi',\n );\n\n let match: RegExpExecArray | null;\n while ((match = depRegex.exec(content)) !== null) {\n const configuration = match[1];\n const notation = match[2].trim();\n\n const dep = parseGradleNotation(notation, configuration, relativeManifest);\n if (!dep) continue;\n\n const lookupKey = `${dep.group}:${dep.artifact}`.toLowerCase();\n const resolvedVersion = resolvedVersions.get(lookupKey);\n\n dependencies.push({\n name: `${dep.group}:${dep.artifact}`,\n declaredConstraint: dep.version,\n resolvedVersion,\n scope: resolveScope(configuration),\n ecosystem: 'gradle',\n sourceManifest: relativeManifest,\n });\n }\n\n // Transitive-only artifacts (present in gradle.lockfile but never declared\n // in the build script) would otherwise never become a Dependency_Node and\n // would silently skip vulnerability scanning. resolvedVersions already has\n // every \"group:artifact\" gradle.lockfile resolved, direct or transitive.\n const directNames = new Set(dependencies.map(d => d.name.toLowerCase()));\n for (const [key, version] of resolvedVersions) {\n if (directNames.has(key)) continue;\n dependencies.push({\n name: key,\n declaredConstraint: version,\n resolvedVersion: version,\n scope: 'production',\n ecosystem: 'gradle',\n sourceManifest: relativeManifest,\n });\n }\n\n return dependencies;\n}\n\n/**\n * A parsed Gradle dependency notation (group:artifact:version).\n */\ninterface GradleCoordinate {\n group: string;\n artifact: string;\n version: string;\n}\n\n/**\n * Parse a Gradle dependency notation string of the form `group:artifact:version`.\n *\n * Skips version catalog references (e.g. `libs.someDep`) and notations\n * that do not follow the three-part `group:artifact:version` format.\n */\nfunction parseGradleNotation(\n notation: string,\n configuration: string,\n relativeManifest: string,\n): GradleCoordinate | null {\n // Skip version catalog references \u2014 they don't carry version info\n if (notation.startsWith('libs.') || notation.startsWith('libs(')) {\n return null;\n }\n\n const parts = notation.split(':');\n if (parts.length < 2) {\n // Not a standard Maven/Gradle coordinate \u2014 skip silently\n return null;\n }\n\n const group = parts[0].trim();\n const artifact = parts[1].trim();\n const version = parts.length >= 3 ? parts[2].trim() : '';\n\n if (!group || !artifact) {\n logWarn(`[sec:gradle] Empty group or artifact in notation \"${notation}\" (${configuration}) at ${relativeManifest} \u2014 skipping`);\n return null;\n }\n\n // Dynamic/incomplete versions (e.g. \"+\", \"\", \"latest.release\") use \"*\" sentinel\n const normalizedVersion =\n version === '' || version === '+' || version.toLowerCase() === 'latest.release'\n ? '*'\n : version;\n\n return { group, artifact, version: normalizedVersion };\n}\n\n/**\n * Map a Gradle configuration name to a ParsedDependency scope.\n *\n * Test configurations (`testImplementation`, `testApi`, etc.) map to 'development'.\n * All others (including `implementation`, `api`, `compile`, etc.) map to 'production'.\n */\nfunction resolveScope(configuration: string): ParsedDependency['scope'] {\n return TEST_CONFIGURATIONS.has(configuration.toLowerCase()) ? 'development' : 'production';\n}\n\n/**\n * Attempt to load resolved versions from `gradle.lockfile` in the given directory.\n * Returns an empty map if the lock file is not available.\n *\n * gradle.lockfile format (one entry per line):\n * ```\n * # This is a comment\n * com.google.guava:guava:32.1.2-jre=compileClasspath,runtimeClasspath\n * junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath\n * empty=\n * ```\n */\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n const lockPath = path.join(manifestDir, 'gradle.lockfile');\n if (!fs.existsSync(lockPath)) {\n return map;\n }\n\n try {\n const content = fs.readFileSync(lockPath, 'utf8');\n extractFromGradleLockfile(content, map);\n } catch (err) {\n logWarn(`[sec:gradle] Failed to parse gradle.lockfile: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n return map;\n}\n\n/**\n * Extract resolved versions from gradle.lockfile content.\n *\n * Each non-comment, non-empty line has the form:\n * `group:artifact:version=configurationList`\n *\n * The special line `empty=` indicates no locked dependencies and is ignored.\n */\nfunction extractFromGradleLockfile(content: string, map: ResolvedVersionMap): void {\n const lines = content.split('\\n');\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip comments and empty lines\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n\n // Skip the sentinel \"empty=\" line\n if (trimmed === 'empty=') continue;\n\n // Format: group:artifact:version=configurationList\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex === -1) continue;\n\n const coordinate = trimmed.slice(0, eqIndex).trim();\n const parts = coordinate.split(':');\n if (parts.length < 3) continue;\n\n const group = parts[0].trim();\n const artifact = parts[1].trim();\n const version = parts[2].trim();\n\n if (!group || !artifact || !version) continue;\n\n const key = `${group}:${artifact}`.toLowerCase();\n if (!map.has(key)) {\n map.set(key, version);\n }\n }\n}\n\n/**\n * Escape special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -58,6 +58,18 @@ async function parseHexManifest(manifestPath, projectRoot) {
58
58
  sourceManifest: relativeManifest
59
59
  });
60
60
  }
61
+ const directNames = new Set(dependencies.map((d) => d.name));
62
+ for (const [name, version] of resolvedVersions) {
63
+ if (directNames.has(name)) continue;
64
+ dependencies.push({
65
+ name,
66
+ declaredConstraint: version,
67
+ resolvedVersion: version,
68
+ scope: "production",
69
+ ecosystem: "hex",
70
+ sourceManifest: relativeManifest
71
+ });
72
+ }
61
73
  return dependencies;
62
74
  }
63
75
  function extractDepsFromMixExs(content, relativeManifest) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/security/manifest/plugins/hex.ts"],
4
- "sourcesContent": ["/**\n * Elixir/Hex Version Extraction Plugin for KiroGraph-Sec\n *\n * Parses mix.exs manifests to extract Elixir package declarations with\n * version constraints, scopes, and resolved versions from mix.lock.\n *\n * Handles:\n * - `{:package_name, \"~> 1.0\"}` \u2014 standard version constraint\n * - `{:package_name, \">= 1.0.0\"}` \u2014 range constraint\n * - `{:package_name, \"1.0.0\"}` \u2014 exact version\n * - `{:package_name, \"~> 1.0\", only: [:dev, :test]}` \u2192 'development' scope\n * - `{:package_name, \"~> 1.0\", only: :dev}` \u2192 'development' scope\n * - Skips git and path dependencies\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: package name \u2192 resolved version string.\n * Built from mix.lock.\n */\ntype ResolvedVersionMap = Map<string, string>;\n\n/**\n * Parse a mix.exs manifest and extract package declarations with\n * version constraints, scopes, and resolved versions from mix.lock.\n *\n * @param manifestPath - Absolute path to the mix.exs file\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 parseHexManifest(\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 mix.exs content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:hex] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Attempt to load resolved versions from mix.lock\n const resolvedVersions = loadResolvedVersions(manifestDir);\n\n // Extract dependencies from the deps function\n const rawDeps = extractDepsFromMixExs(content, relativeManifest);\n const dependencies: ParsedDependency[] = [];\n\n for (const dep of rawDeps) {\n const resolvedVersion = resolvedVersions.get(dep.name);\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion,\n scope: dep.scope,\n ecosystem: 'hex',\n sourceManifest: relativeManifest,\n });\n }\n\n return dependencies;\n}\n\n/**\n * A raw dependency entry extracted from mix.exs.\n */\ninterface RawDependency {\n name: string;\n constraint: string;\n scope: ParsedDependency['scope'];\n}\n\n/**\n * Extract dependencies from mix.exs content.\n *\n * Locates the `defp deps do` function and parses each dependency tuple.\n * Also handles `def deps do` (public deps function).\n *\n * Each dep tuple may span multiple lines, so we work with the full\n * content after extracting the deps list body.\n */\nfunction extractDepsFromMixExs(content: string, relativeManifest: string): RawDependency[] {\n const deps: RawDependency[] = [];\n\n // Find the deps function body \u2014 match `defp deps do` or `def deps do`\n // and extract up to the matching `end`\n const depsBody = extractDepsFunctionBody(content, relativeManifest);\n if (!depsBody) {\n logWarn(`[sec:hex] Could not locate deps function in ${relativeManifest}`);\n return [];\n }\n\n // Match individual dependency tuples: {:name, \"version\", ...optional...}\n // The tuple may contain nested content, so we match balanced braces roughly\n const tuplePattern = /\\{:([a-zA-Z0-9_]+)\\s*,([^}]*)\\}/g;\n let match: RegExpExecArray | null;\n\n while ((match = tuplePattern.exec(depsBody)) !== null) {\n const name = match[1];\n const tupleBody = match[2];\n\n // Skip git/path dependencies\n if (isGitOrPathDep(tupleBody)) continue;\n\n const version = extractVersionFromTuple(tupleBody);\n if (!version) {\n // Might be a non-version dep (e.g. manager: :rebar3) \u2014 skip silently\n continue;\n }\n\n const scope = extractScopeFromTuple(tupleBody);\n deps.push({ name, constraint: version, scope });\n }\n\n return deps;\n}\n\n/**\n * Extract the body of the `deps` function from mix.exs.\n * Returns the content between the `[` and closing `]` of the deps list,\n * or null if the deps function cannot be located.\n */\nfunction extractDepsFunctionBody(content: string, relativeManifest: string): string | null {\n // Match `defp deps do` or `def deps do` followed by content and `end`\n const funcMatch = content.match(/def[p]?\\s+deps\\s+do([\\s\\S]*?)^ end/m);\n if (funcMatch) {\n return funcMatch[1];\n }\n\n // Alternative: `defp deps,` with a list returned directly (single-expression)\n // e.g. `def deps, do: [...]`\n const inlineMatch = content.match(/def[p]?\\s+deps,\\s*do:\\s*\\[([\\s\\S]*?)\\]/);\n if (inlineMatch) {\n return inlineMatch[1];\n }\n\n // Also try matching a `[...]` list inside the function\n const listMatch = content.match(/def[p]?\\s+deps\\s+do\\s*\\[([\\s\\S]*?)\\]\\s*end/);\n if (listMatch) {\n return listMatch[1];\n }\n\n logWarn(`[sec:hex] No deps function found in ${relativeManifest} \u2014 trying full file scan`);\n // Last resort: scan entire file for dep tuples (less accurate but better than nothing)\n return content;\n}\n\n/**\n * Determine if a dependency tuple body represents a git or path dependency.\n * These lack a standard version string and should be skipped.\n */\nfunction isGitOrPathDep(tupleBody: string): boolean {\n return /\\bgit:\\s*\"/.test(tupleBody) || /\\bpath:\\s*\"/.test(tupleBody);\n}\n\n/**\n * Extract the version constraint string from a dependency tuple body.\n * The first string literal in the tuple body is taken as the version.\n *\n * Returns null if no version string is found (e.g. pure option tuples).\n */\nfunction extractVersionFromTuple(tupleBody: string): string | null {\n // Match the first quoted string in the tuple body\n const versionMatch = tupleBody.match(/^\\s*\"([^\"]+)\"/);\n if (versionMatch) {\n return versionMatch[1].trim();\n }\n return null;\n}\n\n/**\n * Extract the scope from a dependency tuple body by inspecting the `only:` option.\n *\n * `only: [:dev, :test]` \u2192 'development'\n * `only: :dev` \u2192 'development'\n * `only: :test` \u2192 'development'\n * `only: :prod` \u2192 'production'\n * No `only:` option \u2192 'production'\n */\nfunction extractScopeFromTuple(tupleBody: string): ParsedDependency['scope'] {\n const onlyMatch = tupleBody.match(/\\bonly\\s*:\\s*(.+?)(?:,\\s*\\w+:|$)/s);\n if (!onlyMatch) return 'production';\n\n const onlyValue = onlyMatch[1].trim();\n\n // List form: [:dev, :test] or [:test, :dev, ...]\n if (onlyValue.startsWith('[')) {\n const atoms = onlyValue.match(/:(\\w+)/g) || [];\n const values = atoms.map((a) => a.slice(1));\n if (values.some((v) => v === 'dev' || v === 'test')) {\n return 'development';\n }\n return 'production';\n }\n\n // Atom form: :dev or :test or :prod\n const atom = onlyValue.match(/^:(\\w+)/);\n if (atom) {\n if (atom[1] === 'dev' || atom[1] === 'test') return 'development';\n return 'production';\n }\n\n return 'production';\n}\n\n/**\n * Attempt to load resolved versions from mix.lock in the given directory.\n * Returns an empty map if mix.lock is not available.\n *\n * mix.lock format:\n * ```elixir\n * %{\n * \"package\" => {:hex, :package_name, \"1.2.3\", \"hash\", ...},\n * \"another\" => {:hex, :another, \"2.0.0\", ...},\n * }\n * ```\n */\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n const lockPath = path.join(manifestDir, 'mix.lock');\n if (!fs.existsSync(lockPath)) {\n return map;\n }\n\n try {\n const lockContent = fs.readFileSync(lockPath, 'utf8');\n extractFromMixLock(lockContent, map);\n } catch (err) {\n logWarn(`[sec:hex] Failed to parse mix.lock: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n return map;\n}\n\n/**\n * Extract resolved versions from mix.lock content.\n *\n * mix.lock lines follow the pattern:\n * ` \"package_name\" => {:hex, :package_atom, \"1.2.3\", \"checksum\", [...]},`\n *\n * We extract the outer quoted key and the third positional element (version string).\n */\nfunction extractFromMixLock(content: string, map: ResolvedVersionMap): void {\n // Match: \"name\" => {:hex, :name_atom, \"version\", ...\n const lockEntryPattern = /^\\s*\"([^\"]+)\"\\s*=>\\s*\\{:hex\\s*,\\s*:[a-zA-Z0-9_]+\\s*,\\s*\"([^\"]+)\"/gm;\n let match: RegExpExecArray | null;\n\n while ((match = lockEntryPattern.exec(content)) !== null) {\n const name = match[1];\n const version = match[2];\n if (!map.has(name)) {\n map.set(name, version);\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Elixir/Hex Version Extraction Plugin for KiroGraph-Sec\n *\n * Parses mix.exs manifests to extract Elixir package declarations with\n * version constraints, scopes, and resolved versions from mix.lock.\n *\n * Handles:\n * - `{:package_name, \"~> 1.0\"}` \u2014 standard version constraint\n * - `{:package_name, \">= 1.0.0\"}` \u2014 range constraint\n * - `{:package_name, \"1.0.0\"}` \u2014 exact version\n * - `{:package_name, \"~> 1.0\", only: [:dev, :test]}` \u2192 'development' scope\n * - `{:package_name, \"~> 1.0\", only: :dev}` \u2192 'development' scope\n * - Skips git and path dependencies\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: package name \u2192 resolved version string.\n * Built from mix.lock.\n */\ntype ResolvedVersionMap = Map<string, string>;\n\n/**\n * Parse a mix.exs manifest and extract package declarations with\n * version constraints, scopes, and resolved versions from mix.lock.\n *\n * @param manifestPath - Absolute path to the mix.exs file\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 parseHexManifest(\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 mix.exs content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:hex] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Attempt to load resolved versions from mix.lock\n const resolvedVersions = loadResolvedVersions(manifestDir);\n\n // Extract dependencies from the deps function\n const rawDeps = extractDepsFromMixExs(content, relativeManifest);\n const dependencies: ParsedDependency[] = [];\n\n for (const dep of rawDeps) {\n const resolvedVersion = resolvedVersions.get(dep.name);\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion,\n scope: dep.scope,\n ecosystem: 'hex',\n sourceManifest: relativeManifest,\n });\n }\n\n // Transitive-only deps (present in mix.lock but never declared in mix.exs)\n // would otherwise never become a Dependency_Node and would silently skip\n // vulnerability scanning. resolvedVersions already has every dep mix.lock\n // 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: 'hex',\n sourceManifest: relativeManifest,\n });\n }\n\n return dependencies;\n}\n\n/**\n * A raw dependency entry extracted from mix.exs.\n */\ninterface RawDependency {\n name: string;\n constraint: string;\n scope: ParsedDependency['scope'];\n}\n\n/**\n * Extract dependencies from mix.exs content.\n *\n * Locates the `defp deps do` function and parses each dependency tuple.\n * Also handles `def deps do` (public deps function).\n *\n * Each dep tuple may span multiple lines, so we work with the full\n * content after extracting the deps list body.\n */\nfunction extractDepsFromMixExs(content: string, relativeManifest: string): RawDependency[] {\n const deps: RawDependency[] = [];\n\n // Find the deps function body \u2014 match `defp deps do` or `def deps do`\n // and extract up to the matching `end`\n const depsBody = extractDepsFunctionBody(content, relativeManifest);\n if (!depsBody) {\n logWarn(`[sec:hex] Could not locate deps function in ${relativeManifest}`);\n return [];\n }\n\n // Match individual dependency tuples: {:name, \"version\", ...optional...}\n // The tuple may contain nested content, so we match balanced braces roughly\n const tuplePattern = /\\{:([a-zA-Z0-9_]+)\\s*,([^}]*)\\}/g;\n let match: RegExpExecArray | null;\n\n while ((match = tuplePattern.exec(depsBody)) !== null) {\n const name = match[1];\n const tupleBody = match[2];\n\n // Skip git/path dependencies\n if (isGitOrPathDep(tupleBody)) continue;\n\n const version = extractVersionFromTuple(tupleBody);\n if (!version) {\n // Might be a non-version dep (e.g. manager: :rebar3) \u2014 skip silently\n continue;\n }\n\n const scope = extractScopeFromTuple(tupleBody);\n deps.push({ name, constraint: version, scope });\n }\n\n return deps;\n}\n\n/**\n * Extract the body of the `deps` function from mix.exs.\n * Returns the content between the `[` and closing `]` of the deps list,\n * or null if the deps function cannot be located.\n */\nfunction extractDepsFunctionBody(content: string, relativeManifest: string): string | null {\n // Match `defp deps do` or `def deps do` followed by content and `end`\n const funcMatch = content.match(/def[p]?\\s+deps\\s+do([\\s\\S]*?)^ end/m);\n if (funcMatch) {\n return funcMatch[1];\n }\n\n // Alternative: `defp deps,` with a list returned directly (single-expression)\n // e.g. `def deps, do: [...]`\n const inlineMatch = content.match(/def[p]?\\s+deps,\\s*do:\\s*\\[([\\s\\S]*?)\\]/);\n if (inlineMatch) {\n return inlineMatch[1];\n }\n\n // Also try matching a `[...]` list inside the function\n const listMatch = content.match(/def[p]?\\s+deps\\s+do\\s*\\[([\\s\\S]*?)\\]\\s*end/);\n if (listMatch) {\n return listMatch[1];\n }\n\n logWarn(`[sec:hex] No deps function found in ${relativeManifest} \u2014 trying full file scan`);\n // Last resort: scan entire file for dep tuples (less accurate but better than nothing)\n return content;\n}\n\n/**\n * Determine if a dependency tuple body represents a git or path dependency.\n * These lack a standard version string and should be skipped.\n */\nfunction isGitOrPathDep(tupleBody: string): boolean {\n return /\\bgit:\\s*\"/.test(tupleBody) || /\\bpath:\\s*\"/.test(tupleBody);\n}\n\n/**\n * Extract the version constraint string from a dependency tuple body.\n * The first string literal in the tuple body is taken as the version.\n *\n * Returns null if no version string is found (e.g. pure option tuples).\n */\nfunction extractVersionFromTuple(tupleBody: string): string | null {\n // Match the first quoted string in the tuple body\n const versionMatch = tupleBody.match(/^\\s*\"([^\"]+)\"/);\n if (versionMatch) {\n return versionMatch[1].trim();\n }\n return null;\n}\n\n/**\n * Extract the scope from a dependency tuple body by inspecting the `only:` option.\n *\n * `only: [:dev, :test]` \u2192 'development'\n * `only: :dev` \u2192 'development'\n * `only: :test` \u2192 'development'\n * `only: :prod` \u2192 'production'\n * No `only:` option \u2192 'production'\n */\nfunction extractScopeFromTuple(tupleBody: string): ParsedDependency['scope'] {\n const onlyMatch = tupleBody.match(/\\bonly\\s*:\\s*(.+?)(?:,\\s*\\w+:|$)/s);\n if (!onlyMatch) return 'production';\n\n const onlyValue = onlyMatch[1].trim();\n\n // List form: [:dev, :test] or [:test, :dev, ...]\n if (onlyValue.startsWith('[')) {\n const atoms = onlyValue.match(/:(\\w+)/g) || [];\n const values = atoms.map((a) => a.slice(1));\n if (values.some((v) => v === 'dev' || v === 'test')) {\n return 'development';\n }\n return 'production';\n }\n\n // Atom form: :dev or :test or :prod\n const atom = onlyValue.match(/^:(\\w+)/);\n if (atom) {\n if (atom[1] === 'dev' || atom[1] === 'test') return 'development';\n return 'production';\n }\n\n return 'production';\n}\n\n/**\n * Attempt to load resolved versions from mix.lock in the given directory.\n * Returns an empty map if mix.lock is not available.\n *\n * mix.lock format:\n * ```elixir\n * %{\n * \"package\" => {:hex, :package_name, \"1.2.3\", \"hash\", ...},\n * \"another\" => {:hex, :another, \"2.0.0\", ...},\n * }\n * ```\n */\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n const lockPath = path.join(manifestDir, 'mix.lock');\n if (!fs.existsSync(lockPath)) {\n return map;\n }\n\n try {\n const lockContent = fs.readFileSync(lockPath, 'utf8');\n extractFromMixLock(lockContent, map);\n } catch (err) {\n logWarn(`[sec:hex] Failed to parse mix.lock: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n return map;\n}\n\n/**\n * Extract resolved versions from mix.lock content.\n *\n * mix.lock lines follow the pattern:\n * ` \"package_name\" => {:hex, :package_atom, \"1.2.3\", \"checksum\", [...]},`\n *\n * We extract the outer quoted key and the third positional element (version string).\n */\nfunction extractFromMixLock(content: string, map: ResolvedVersionMap): void {\n // Match: \"name\" => {:hex, :name_atom, \"version\", ...\n const lockEntryPattern = /^\\s*\"([^\"]+)\"\\s*=>\\s*\\{:hex\\s*,\\s*:[a-zA-Z0-9_]+\\s*,\\s*\"([^\"]+)\"/gm;\n let match: RegExpExecArray | null;\n\n while ((match = lockEntryPattern.exec(content)) !== null) {\n const name = match[1];\n const version = match[2];\n if (!map.has(name)) {\n map.set(name, version);\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }