kirograph 1.0.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +4 -2
  2. package/dist/architecture/manifest/python.js +2 -2
  3. package/dist/architecture/manifest/python.js.map +2 -2
  4. package/dist/bin/commands/help.js +3 -1
  5. package/dist/bin/commands/help.js.map +2 -2
  6. package/dist/bin/commands/memory.js +29 -9
  7. package/dist/bin/commands/memory.js.map +2 -2
  8. package/dist/bin/commands/reachability.js +7 -2
  9. package/dist/bin/commands/reachability.js.map +2 -2
  10. package/dist/bin/commands/vulns.js +16 -1
  11. package/dist/bin/commands/vulns.js.map +2 -2
  12. package/dist/bin/commands/wiki.js +59 -2
  13. package/dist/bin/commands/wiki.js.map +2 -2
  14. package/dist/bin/installer/steering.js +9 -0
  15. package/dist/bin/installer/steering.js.map +2 -2
  16. package/dist/bin/kirograph.js +1 -1
  17. package/dist/config.js +12 -0
  18. package/dist/config.js.map +2 -2
  19. package/dist/db/memory-schema.sql +2 -1
  20. package/dist/extraction/extractor.js +46 -41
  21. package/dist/extraction/extractor.js.map +2 -2
  22. package/dist/extraction/notebook.js +15 -7
  23. package/dist/extraction/notebook.js.map +2 -2
  24. package/dist/jev/client.js +99 -0
  25. package/dist/jev/client.js.map +7 -0
  26. package/dist/jev/types.js +17 -0
  27. package/dist/jev/types.js.map +7 -0
  28. package/dist/mcp/handlers/memory.js +15 -8
  29. package/dist/mcp/handlers/memory.js.map +2 -2
  30. package/dist/mcp/handlers/security.js +5 -3
  31. package/dist/mcp/handlers/security.js.map +2 -2
  32. package/dist/mcp/handlers/wiki.js +4 -2
  33. package/dist/mcp/handlers/wiki.js.map +2 -2
  34. package/dist/mcp/tools.js +3 -2
  35. package/dist/mcp/tools.js.map +2 -2
  36. package/dist/memory/database.js +17 -4
  37. package/dist/memory/database.js.map +2 -2
  38. package/dist/memory/index.js +31 -2
  39. package/dist/memory/index.js.map +3 -3
  40. package/dist/memory/relation-jev.js +59 -0
  41. package/dist/memory/relation-jev.js.map +7 -0
  42. package/dist/memory/schema.js +92 -0
  43. package/dist/memory/schema.js.map +7 -0
  44. package/dist/memory/types.js.map +1 -1
  45. package/dist/patterns/loader.js +2 -146
  46. package/dist/patterns/loader.js.map +3 -3
  47. package/dist/security/auth-detection-jev.js +47 -0
  48. package/dist/security/auth-detection-jev.js.map +7 -0
  49. package/dist/security/manifest/adapter.js +17 -3
  50. package/dist/security/manifest/adapter.js.map +3 -3
  51. package/dist/security/manifest/plugins/cargo.js +23 -0
  52. package/dist/security/manifest/plugins/cargo.js.map +2 -2
  53. package/dist/security/manifest/plugins/composer.js +12 -0
  54. package/dist/security/manifest/plugins/composer.js.map +2 -2
  55. package/dist/security/manifest/plugins/gradle.js +12 -0
  56. package/dist/security/manifest/plugins/gradle.js.map +2 -2
  57. package/dist/security/manifest/plugins/hex.js +12 -0
  58. package/dist/security/manifest/plugins/hex.js.map +2 -2
  59. package/dist/security/manifest/plugins/maven.js +101 -4
  60. package/dist/security/manifest/plugins/maven.js.map +2 -2
  61. package/dist/security/manifest/plugins/npm.js +117 -2
  62. package/dist/security/manifest/plugins/npm.js.map +2 -2
  63. package/dist/security/manifest/plugins/nuget.js +13 -0
  64. package/dist/security/manifest/plugins/nuget.js.map +2 -2
  65. package/dist/security/manifest/plugins/pubspec.js +12 -0
  66. package/dist/security/manifest/plugins/pubspec.js.map +2 -2
  67. package/dist/security/manifest/plugins/pyproject.js +12 -0
  68. package/dist/security/manifest/plugins/pyproject.js.map +2 -2
  69. package/dist/security/manifest/plugins/rubygems.js +12 -0
  70. package/dist/security/manifest/plugins/rubygems.js.map +2 -2
  71. package/dist/security/pipeline.js +1 -1
  72. package/dist/security/pipeline.js.map +2 -2
  73. package/dist/security/reachability.js +54 -5
  74. package/dist/security/reachability.js.map +3 -3
  75. package/dist/security/types.js.map +1 -1
  76. package/dist/security/vuln/client.js +45 -1
  77. package/dist/security/vuln/client.js.map +2 -2
  78. package/dist/security/vuln/osv-adapter.js +45 -1
  79. package/dist/security/vuln/osv-adapter.js.map +2 -2
  80. package/dist/shared/json-schema.js +136 -0
  81. package/dist/shared/json-schema.js.map +7 -0
  82. package/dist/shared/yaml.js +174 -0
  83. package/dist/shared/yaml.js.map +7 -0
  84. package/dist/wiki/contradiction-jev.js +49 -0
  85. package/dist/wiki/contradiction-jev.js.map +7 -0
  86. package/dist/wiki/index.js +16 -1
  87. package/dist/wiki/index.js.map +2 -2
  88. package/dist/wiki/ingest.js +2 -0
  89. package/dist/wiki/ingest.js.map +2 -2
  90. package/dist/wiki/links.js +53 -0
  91. package/dist/wiki/links.js.map +7 -0
  92. package/dist/wiki/lint.js +17 -7
  93. package/dist/wiki/lint.js.map +2 -2
  94. package/dist/wiki/rename.js +103 -0
  95. package/dist/wiki/rename.js.map +7 -0
  96. package/dist/wiki/typed-pages.js +67 -0
  97. package/dist/wiki/typed-pages.js.map +7 -0
  98. package/dist/wiki/types.js.map +1 -1
  99. package/package.json +1 -1
@@ -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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAYA,SAAoB;AACpB,WAAsB;AAEtB,oBAAwB;AAKxB,MAAM,iBAA+E;AAAA,EACnF,EAAE,SAAS,gBAAgB,OAAO,aAAa;AAAA,EAC/C,EAAE,SAAS,oBAAoB,OAAO,cAAc;AAAA,EACpD,EAAE,SAAS,sBAAsB,OAAO,cAAc;AACxD;AAgBA,eAAsB,mBACpB,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,8BAA8B,gBAAgB,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAC7G,WAAO,CAAC;AAAA,EACV;AAGA,QAAM,mBAAmB,qBAAqB,WAAW;AAGzD,QAAM,UAAU,oBAAoB,OAAO;AAG3C,QAAM,eAAmC,CAAC;AAE1C,aAAW,EAAE,SAAS,MAAM,KAAK,gBAAgB;AAC/C,UAAM,cAAc,2BAA2B,SAAS,SAAS,gBAAgB;AACjF,eAAW,OAAO,aAAa;AAC7B,YAAM,kBAAkB,iBAAiB,IAAI,IAAI,IAAI;AACrD,mBAAa,KAAK;AAAA,QAChB,MAAM,IAAI;AAAA,QACV,oBAAoB,IAAI;AAAA,QACxB;AAAA,QACA;AAAA,QACA,WAAW;AAAA,QACX,gBAAgB;AAAA,QAChB,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC7C,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAeA,SAAS,2BACP,SACA,SACA,kBACiB;AACjB,QAAM,OAAwB,CAAC;AAI/B,QAAM,eAAe,IAAI,OAAO,OAAO,YAAY,OAAO,CAAC,YAAY,GAAG;AAC1E,QAAM,eAAe,aAAa,KAAK,OAAO;AAE9C,MAAI,cAAc;AAChB,UAAM,WAAW,aAAa,QAAQ,aAAa,CAAC,EAAE;AAEtD,UAAM,YAAY,QAAQ,MAAM,QAAQ;AACxC,UAAM,mBAAmB,UAAU,MAAM,MAAM;AAC/C,UAAM,iBAAiB,mBACnB,UAAU,MAAM,GAAG,iBAAiB,KAAK,IACzC;AAEJ,UAAM,QAAQ,eAAe,MAAM,IAAI;AACvC,eAAW,QAAQ,OAAO;AACxB,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,YAAY,MAAM,QAAQ,WAAW,GAAG,EAAG;AAE/C,YAAM,MAAM,oBAAoB,SAAS,kBAAkB,OAAO;AAClE,UAAI,KAAK;AACP,aAAK,KAAK,GAAG;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAIA,QAAM,aAAa,IAAI,OAAO,OAAO,YAAY,OAAO,CAAC,wBAAwB,IAAI;AACrF,MAAI;AACJ,UAAQ,aAAa,WAAW,KAAK,OAAO,OAAO,MAAM;AACvD,UAAM,UAAU,WAAW,CAAC,EAAE,KAAK;AACnC,UAAM,WAAW,WAAW,QAAQ,WAAW,CAAC,EAAE;AAClD,UAAM,YAAY,QAAQ,MAAM,QAAQ;AACxC,UAAM,mBAAmB,UAAU,MAAM,MAAM;AAC/C,UAAM,eAAe,mBACjB,UAAU,MAAM,GAAG,iBAAiB,KAAK,IACzC;AAEJ,UAAM,UAAU,wBAAwB,YAAY;AACpD,QAAI,SAAS;AACX,WAAK,KAAK,EAAE,MAAM,SAAS,YAAY,QAAQ,CAAC;AAAA,IAClD,OAAO;AAEL,WAAK,KAAK,EAAE,MAAM,SAAS,YAAY,IAAI,CAAC;AAAA,IAC9C;AAAA,EACF;AAEA,SAAO;AACT;AAUA,SAAS,oBACP,MACA,kBACA,SACsB;AAEtB,QAAM,cAAc,KAAK,MAAM,oCAAoC;AACnE,MAAI,aAAa;AACf,UAAM,OAAO,YAAY,CAAC;AAC1B,UAAM,aAAa,YAAY,CAAC;AAChC,QAAI,CAAC,iBAAiB,IAAI,GAAG;AAC3B,iCAAQ,mCAAmC,IAAI,QAAQ,gBAAgB,KAAK,OAAO,mBAAc;AACjG,aAAO;AAAA,IACT;AACA,QAAI,WAAW,KAAK,MAAM,IAAI;AAC5B,iCAAQ,6CAA6C,IAAI,QAAQ,gBAAgB,KAAK,OAAO,mBAAc;AAC3G,aAAO;AAAA,IACT;AACA,WAAO,EAAE,MAAM,WAAW;AAAA,EAC5B;AAGA,QAAM,aAAa,KAAK,MAAM,sCAAsC;AACpE,MAAI,YAAY;AACd,UAAM,OAAO,WAAW,CAAC;AACzB,UAAM,eAAe,WAAW,CAAC;AACjC,QAAI,CAAC,iBAAiB,IAAI,GAAG;AAC3B,iCAAQ,mCAAmC,IAAI,QAAQ,gBAAgB,KAAK,OAAO,mBAAc;AACjG,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,8BAA8B,YAAY;AAC1D,QAAI,SAAS;AACX,aAAO,EAAE,MAAM,YAAY,QAAQ;AAAA,IACrC;AAGA,WAAO,EAAE,MAAM,YAAY,IAAI;AAAA,EACjC;AAIA,MAAI,KAAK,SAAS,GAAG,KAAK,CAAC,KAAK,WAAW,GAAG,GAAG;AAC/C,UAAM,eAAe,KAAK,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAC7C,QAAI,gBAAgB,mBAAmB,KAAK,YAAY,GAAG;AACzD,iCAAQ,kDAAkD,gBAAgB,KAAK,OAAO,OAAO,IAAI,mBAAc;AAAA,IACjH;AAAA,EACF;AAEA,SAAO;AACT;AAMA,SAAS,8BAA8B,cAAqC;AAC1E,QAAM,eAAe,aAAa,MAAM,yBAAyB;AACjE,MAAI,gBAAgB,aAAa,CAAC,EAAE,KAAK,MAAM,IAAI;AACjD,WAAO,aAAa,CAAC;AAAA,EACvB;AACA,SAAO;AACT;AAMA,SAAS,wBAAwB,cAAqC;AACpE,QAAM,QAAQ,aAAa,MAAM,IAAI;AACrC,aAAW,QAAQ,OAAO;AACxB,UAAM,UAAU,KAAK,KAAK;AAC1B,UAAM,eAAe,QAAQ,MAAM,2BAA2B;AAC9D,QAAI,gBAAgB,aAAa,CAAC,EAAE,KAAK,MAAM,IAAI;AACjD,aAAO,aAAa,CAAC;AAAA,IACvB;AAAA,EACF;AACA,SAAO;AACT;AAaA,SAAS,qBAAqB,aAAyC;AACrE,QAAM,MAA0B,oBAAI,IAAI;AAExC,QAAM,WAAW,KAAK,KAAK,aAAa,YAAY;AACpD,MAAI,CAAC,GAAG,WAAW,QAAQ,GAAG;AAC5B,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,cAAc,GAAG,aAAa,UAAU,MAAM;AACpD,yBAAqB,aAAa,GAAG;AAAA,EACvC,SAAS,KAAK;AACZ,+BAAQ,2CAA2C,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,EACvG;AAEA,SAAO;AACT;AAaA,SAAS,qBAAqB,SAAiB,KAA+B;AAC5E,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,MAAI,cAA6B;AAEjC,aAAW,QAAQ,OAAO;AACxB,UAAM,UAAU,KAAK,KAAK;AAG1B,QAAI,YAAY,eAAe;AAC7B,oBAAc;AACd;AAAA,IACF;AAGA,QAAI,gBAAgB,MAAM;AACxB,YAAM,YAAY,QAAQ,MAAM,wBAAwB;AACxD,UAAI,WAAW;AACb,sBAAc,UAAU,CAAC;AACzB;AAAA,MACF;AAAA,IACF;AAGA,QAAI,gBAAgB,MAAM;AACxB,YAAM,eAAe,QAAQ,MAAM,2BAA2B;AAC9D,UAAI,cAAc;AAGhB,YAAI,CAAC,IAAI,IAAI,WAAW,GAAG;AACzB,cAAI,IAAI,aAAa,aAAa,CAAC,CAAC;AAAA,QACtC;AACA,sBAAc;AACd;AAAA,MACF;AAAA,IACF;AAGA,QAAI,YAAY,MAAM,QAAQ,WAAW,GAAG,GAAG;AAC7C,oBAAc;AAAA,IAChB;AAAA,EACF;AACF;AAOA,SAAS,iBAAiB,MAAuB;AAC/C,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,MAAI,KAAK,SAAS,GAAI,QAAO;AAC7B,SAAO,2BAA2B,KAAK,IAAI;AAC7C;AAKA,SAAS,YAAY,KAAqB;AACxC,SAAO,IAAI,QAAQ,uBAAuB,MAAM;AAClD;AAMA,SAAS,oBAAoB,SAAqC;AAEhE,QAAM,sBAAsB,oBAAoB,KAAK,OAAO;AAC5D,MAAI,CAAC,oBAAqB,QAAO;AAEjC,QAAM,WAAW,oBAAoB,QAAQ,oBAAoB,CAAC,EAAE;AACpE,QAAM,YAAY,QAAQ,MAAM,QAAQ;AAExC,QAAM,cAAc,UAAU,MAAM,MAAM;AAC1C,QAAM,iBAAiB,cAAc,UAAU,MAAM,GAAG,YAAY,KAAK,IAAI;AAE7E,QAAM,eAAe,eAAe,MAAM,2BAA2B;AACrE,MAAI,gBAAgB,aAAa,CAAC,EAAE,KAAK,MAAM,IAAI;AACjD,WAAO,aAAa,CAAC,EAAE,KAAK;AAAA,EAC9B;AACA,SAAO;AACT;",
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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,SAAoB;AACpB,WAAsB;AAEtB,oBAAwB;AAgBxB,eAAsB,sBACpB,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,iCAAiC,gBAAgB,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAChH,WAAO,CAAC;AAAA,EACV;AAGA,MAAI;AACJ,MAAI;AACF,eAAW,KAAK,MAAM,OAAO;AAAA,EAC/B,SAAS,KAAK;AACZ,+BAAQ,0CAA0C,gBAAgB,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AACzH,WAAO,CAAC;AAAA,EACV;AAGA,QAAM,mBAAmB,qBAAqB,WAAW;AAGzD,QAAM,UAAU,uBAAuB,QAAQ;AAE/C,QAAM,eAAmC,CAAC;AAG1C,QAAM,WAAqE;AAAA,IACzE,EAAE,KAAK,WAAW,OAAO,aAAa;AAAA,IACtC,EAAE,KAAK,eAAe,OAAO,cAAc;AAAA,EAC7C;AAEA,aAAW,EAAE,KAAK,MAAM,KAAK,UAAU;AACrC,UAAM,UAAU,SAAS,GAAG;AAC5B,QAAI,CAAC,WAAW,OAAO,YAAY,YAAY,MAAM,QAAQ,OAAO,EAAG;AAEvE,eAAW,CAAC,SAAS,iBAAiB,KAAK,OAAO,QAAQ,OAAkC,GAAG;AAE7F,UAAI,gBAAgB,OAAO,EAAG;AAE9B,UAAI,OAAO,sBAAsB,UAAU;AACzC,mCAAQ,0CAA0C,OAAO,QAAQ,gBAAgB,kBAAa;AAC9F;AAAA,MACF;AAEA,UAAI,CAAC,mBAAmB,OAAO,GAAG;AAChC,mCAAQ,wCAAwC,OAAO,QAAQ,gBAAgB,kBAAa;AAC5F;AAAA,MACF;AAEA,YAAM,kBAAkB,iBAAiB,IAAI,QAAQ,YAAY,CAAC;AAClE,mBAAa,KAAK;AAAA,QAChB,MAAM;AAAA,QACN,oBAAoB;AAAA,QACpB;AAAA,QACA;AAAA,QACA,WAAW;AAAA,QACX,gBAAgB;AAAA,QAChB,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC7C,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAMA,SAAS,gBAAgB,MAAuB;AAC9C,MAAI,SAAS,MAAO,QAAO;AAC3B,MAAI,KAAK,WAAW,MAAM,EAAG,QAAO;AACpC,MAAI,SAAS,WAAY,QAAO;AAChC,SAAO;AACT;AAMA,SAAS,mBAAmB,MAAuB;AACjD,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,MAAI,KAAK,SAAS,IAAK,QAAO;AAE9B,SAAO,iDAAiD,KAAK,IAAI;AACnE;AAeA,SAAS,qBAAqB,aAAyC;AACrE,QAAM,MAA0B,oBAAI,IAAI;AAExC,QAAM,WAAW,KAAK,KAAK,aAAa,eAAe;AACvD,MAAI,CAAC,GAAG,WAAW,QAAQ,GAAG;AAC5B,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,cAAc,GAAG,aAAa,UAAU,MAAM;AACpD,4BAAwB,aAAa,GAAG;AAAA,EAC1C,SAAS,KAAK;AACZ,+BAAQ,iDAAiD,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,EAC7G;AAEA,SAAO;AACT;AAQA,SAAS,wBAAwB,SAAiB,KAA+B;AAC/E,MAAI;AACJ,MAAI;AACF,eAAW,KAAK,MAAM,OAAO;AAAA,EAC/B,QAAQ;AACN,+BAAQ,qFAAgF;AACxF;AAAA,EACF;AAEA,QAAM,SAAS,CAAC,SAAS,UAAU,GAAG,SAAS,cAAc,CAAC;AAC9D,aAAW,OAAO,QAAQ;AACxB,QAAI,CAAC,MAAM,QAAQ,GAAG,EAAG;AACzB,eAAW,SAAS,KAAK;AACvB,UAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,YAAM,MAAM;AACZ,YAAM,OAAO,OAAO,IAAI,MAAM,MAAM,WAAW,IAAI,MAAM,EAAE,YAAY,IAAI;AAC3E,YAAM,UAAU,OAAO,IAAI,SAAS,MAAM,WAAW,IAAI,SAAS,IAAI;AACtE,UAAI,QAAQ,SAAS;AAEnB,cAAM,aAAa,QAAQ,WAAW,GAAG,IAAI,QAAQ,MAAM,CAAC,IAAI;AAChE,YAAI,CAAC,IAAI,IAAI,IAAI,GAAG;AAClB,cAAI,IAAI,MAAM,UAAU;AAAA,QAC1B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAMA,SAAS,uBAAuB,UAAuD;AACrF,QAAM,MAAM,SAAS,SAAS;AAC9B,MAAI,OAAO,QAAQ,YAAY,IAAI,KAAK,MAAM,IAAI;AAChD,WAAO,IAAI,KAAK;AAAA,EAClB;AACA,MAAI,MAAM,QAAQ,GAAG,KAAK,IAAI,SAAS,KAAK,OAAO,IAAI,CAAC,MAAM,UAAU;AACtE,WAAQ,IAAiB,KAAK,MAAM;AAAA,EACtC;AACA,SAAO;AACT;",
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": "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",
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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;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;AAMA,QAAM,cAAc,IAAI,IAAI,aAAa,IAAI,OAAK,EAAE,KAAK,YAAY,CAAC,CAAC;AACvE,aAAW,CAAC,KAAK,OAAO,KAAK,kBAAkB;AAC7C,QAAI,YAAY,IAAI,GAAG,EAAG;AAC1B,iBAAa,KAAK;AAAA,MAChB,MAAM;AAAA,MACN,oBAAoB;AAAA,MACpB,iBAAiB;AAAA,MACjB,OAAO;AAAA,MACP,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;",
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
  }
@@ -28,10 +28,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var maven_exports = {};
30
30
  __export(maven_exports, {
31
+ parseMavenDependencyTree: () => parseMavenDependencyTree,
31
32
  parseMavenManifest: () => parseMavenManifest
32
33
  });
33
34
  module.exports = __toCommonJS(maven_exports);
34
35
  var fs = __toESM(require("fs"));
36
+ var os = __toESM(require("os"));
35
37
  var path = __toESM(require("path"));
36
38
  var import_errors = require("../../../errors");
37
39
  function mapMavenScope(mavenScope) {
@@ -70,9 +72,9 @@ async function parseMavenManifest(manifestPath, projectRoot) {
70
72
  (0, import_errors.logWarn)(`[sec:maven] Invalid pom.xml structure at ${relativeManifest} \u2014 missing <project> element`);
71
73
  return [];
72
74
  }
73
- const license = extractMavenLicense(content);
74
75
  const dependencies = [];
75
- const dependencyBlocks = content.matchAll(/<dependency>([\s\S]*?)<\/dependency>/g);
76
+ const dependenciesOnlyContent = content.replace(/<dependencyManagement>[\s\S]*?<\/dependencyManagement>/g, "");
77
+ const dependencyBlocks = dependenciesOnlyContent.matchAll(/<dependency>([\s\S]*?)<\/dependency>/g);
76
78
  for (const block of dependencyBlocks) {
77
79
  const depXml = block[1];
78
80
  const groupId = extractXmlElement(depXml, "groupId");
@@ -105,12 +107,77 @@ async function parseMavenManifest(manifestPath, projectRoot) {
105
107
  resolvedVersion: declaredConstraint || void 0,
106
108
  scope: mappedScope,
107
109
  ecosystem: "maven",
108
- sourceManifest: relativeManifest,
109
- ...license !== void 0 ? { license } : {}
110
+ sourceManifest: relativeManifest
110
111
  });
111
112
  }
113
+ const manifestDir = path.dirname(manifestPath);
114
+ const treeFile = findMavenDependencyTreeFile(manifestDir);
115
+ if (treeFile) {
116
+ const treeRelativePath = path.relative(projectRoot, treeFile.path).replace(/\\/g, "/");
117
+ let treeContent;
118
+ try {
119
+ treeContent = fs.readFileSync(treeFile.path, "utf8");
120
+ } catch (err) {
121
+ (0, import_errors.logWarn)(`[sec:maven] Failed to read ${treeRelativePath}: ${err instanceof Error ? err.message : String(err)}`);
122
+ applyLocalMavenLicenses(dependencies);
123
+ return dependencies;
124
+ }
125
+ const treeEntries = parseMavenDependencyTree(treeContent);
126
+ for (const dep of dependencies) {
127
+ if (dep.resolvedVersion) continue;
128
+ const treeEntry = treeEntries.get(dep.name);
129
+ if (treeEntry) {
130
+ dep.resolvedVersion = treeEntry.version;
131
+ }
132
+ }
133
+ const directNames = new Set(dependencies.map((d) => d.name));
134
+ for (const [name, entry] of treeEntries) {
135
+ if (directNames.has(name)) continue;
136
+ dependencies.push({
137
+ name,
138
+ // No declared range exists at the project level for a transitive
139
+ // package — the resolved version from the tree is the only constraint we have.
140
+ declaredConstraint: entry.version,
141
+ resolvedVersion: entry.version,
142
+ scope: mapMavenScope(entry.scope),
143
+ ecosystem: "maven",
144
+ sourceManifest: treeRelativePath
145
+ });
146
+ }
147
+ }
148
+ applyLocalMavenLicenses(dependencies);
112
149
  return dependencies;
113
150
  }
151
+ function findMavenDependencyTreeFile(manifestDir) {
152
+ const candidates = ["dependency-tree.txt", path.join("target", "dependency-tree.txt")];
153
+ for (const candidate of candidates) {
154
+ const candidatePath = path.join(manifestDir, candidate);
155
+ if (fs.existsSync(candidatePath)) {
156
+ return { path: candidatePath };
157
+ }
158
+ }
159
+ return void 0;
160
+ }
161
+ function parseMavenDependencyTree(content) {
162
+ const result = /* @__PURE__ */ new Map();
163
+ for (const rawLine of content.split(/\r?\n/)) {
164
+ if (!rawLine.trim()) continue;
165
+ const stripped = rawLine.replace(/^[\s|+\\-]+/, "").trim();
166
+ if (!stripped || stripped.startsWith("(")) continue;
167
+ const parts = stripped.split(":");
168
+ if (parts.length < 5) continue;
169
+ const groupId = parts[0];
170
+ const artifactId = parts[1];
171
+ const version = parts[parts.length - 2];
172
+ const scope = parts[parts.length - 1];
173
+ if (!groupId || !artifactId || !version) continue;
174
+ const name = `${groupId}:${artifactId}`;
175
+ if (!result.has(name)) {
176
+ result.set(name, { version, scope });
177
+ }
178
+ }
179
+ return result;
180
+ }
114
181
  function extractMavenLicense(content) {
115
182
  const licensesMatch = content.match(/<licenses\b[^>]*>([\s\S]*?)<\/licenses>/i);
116
183
  if (!licensesMatch) return void 0;
@@ -121,8 +188,38 @@ function extractMavenLicense(content) {
121
188
  }
122
189
  return void 0;
123
190
  }
191
+ function resolveMavenLocalRepoRoot() {
192
+ const settingsPath = path.join(os.homedir(), ".m2", "settings.xml");
193
+ try {
194
+ const settings = fs.readFileSync(settingsPath, "utf8");
195
+ const match = settings.match(/<localRepository>([^<]+)<\/localRepository>/i);
196
+ if (match && match[1].trim() !== "") return match[1].trim();
197
+ } catch {
198
+ }
199
+ return path.join(os.homedir(), ".m2", "repository");
200
+ }
201
+ function findLocalMavenPomLicense(repoRoot, groupId, artifactId, version) {
202
+ const pomPath = path.join(repoRoot, ...groupId.split("."), artifactId, version, `${artifactId}-${version}.pom`);
203
+ try {
204
+ const content = fs.readFileSync(pomPath, "utf8");
205
+ return extractMavenLicense(content);
206
+ } catch {
207
+ return void 0;
208
+ }
209
+ }
210
+ function applyLocalMavenLicenses(dependencies) {
211
+ const repoRoot = resolveMavenLocalRepoRoot();
212
+ for (const dep of dependencies) {
213
+ if (!dep.resolvedVersion) continue;
214
+ const [groupId, artifactId] = dep.name.split(":");
215
+ if (!groupId || !artifactId) continue;
216
+ const license = findLocalMavenPomLicense(repoRoot, groupId, artifactId, dep.resolvedVersion);
217
+ if (license !== void 0) dep.license = license;
218
+ }
219
+ }
124
220
  // Annotate the CommonJS export names for ESM import in node:
125
221
  0 && (module.exports = {
222
+ parseMavenDependencyTree,
126
223
  parseMavenManifest
127
224
  });
128
225
  //# sourceMappingURL=maven.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/security/manifest/plugins/maven.ts"],
4
- "sourcesContent": ["/**\n * Maven Version Extraction Plugin for KiroGraph-Sec\n *\n * Extends the existing architecture maven parser to extract version constraints,\n * dependency scopes, and groupId/artifactId for security analysis.\n *\n * Reuses `mavenParser` from `src/architecture/manifest/maven.ts` for discovery and\n * basic parsing, then layers on version/scope extraction from pom.xml.\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\n/**\n * Maven scope to ParsedDependency scope mapping.\n *\n * - compile (default), runtime \u2192 production\n * - test \u2192 development\n * - provided, system \u2192 optional\n */\nfunction mapMavenScope(mavenScope: string | undefined): ParsedDependency['scope'] {\n if (!mavenScope || mavenScope.trim() === '') {\n return 'production';\n }\n\n const normalized = mavenScope.trim().toLowerCase();\n switch (normalized) {\n case 'compile':\n case 'runtime':\n return 'production';\n case 'test':\n return 'development';\n case 'provided':\n case 'system':\n return 'optional';\n default:\n // Unknown scope defaults to production\n return 'production';\n }\n}\n\n/**\n * Extract text content from an XML element by tag name within a given XML fragment.\n * Returns undefined if the element is not found.\n */\nfunction extractXmlElement(xml: string, tagName: string): string | undefined {\n const regex = new RegExp(`<${tagName}>([^<]*)</${tagName}>`);\n const match = xml.match(regex);\n return match ? match[1].trim() : undefined;\n}\n\n/**\n * Parse a Maven pom.xml manifest and extract dependency declarations\n * with groupId, artifactId, version constraints, and scopes.\n *\n * @param manifestPath - Absolute path to the pom.xml 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 parseMavenManifest(\n manifestPath: string,\n projectRoot: string,\n): Promise<ParsedDependency[]> {\n const relativeManifest = path.relative(projectRoot, manifestPath).replace(/\\\\/g, '/');\n\n // Read the pom.xml content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:maven] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Validate basic XML structure\n if (!content.includes('<project')) {\n logWarn(`[sec:maven] Invalid pom.xml structure at ${relativeManifest} \u2014 missing <project> element`);\n return [];\n }\n\n // Extract the project-level license\n const license = extractMavenLicense(content);\n\n // Extract all <dependency> blocks from the <dependencies> sections\n const dependencies: ParsedDependency[] = [];\n\n // Match all <dependency>...</dependency> blocks\n const dependencyBlocks = content.matchAll(/<dependency>([\\s\\S]*?)<\\/dependency>/g);\n\n for (const block of dependencyBlocks) {\n const depXml = block[1];\n\n const groupId = extractXmlElement(depXml, 'groupId');\n const artifactId = extractXmlElement(depXml, 'artifactId');\n const version = extractXmlElement(depXml, 'version');\n const scope = extractXmlElement(depXml, 'scope');\n\n // Validate required fields\n if (!groupId || groupId.trim() === '') {\n logWarn(`[sec:maven] Missing groupId in dependency at ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n if (!artifactId || artifactId.trim() === '') {\n logWarn(`[sec:maven] Missing artifactId in dependency at ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n // Skip dependencies with unresolved Maven properties (e.g. ${project.version})\n if (groupId.includes('${') || artifactId.includes('${')) {\n logWarn(`[sec:maven] Unresolved property in dependency ${groupId}:${artifactId} at ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n // Build the dependency name as groupId:artifactId (Maven convention)\n const name = `${groupId}:${artifactId}`;\n\n // Version constraint \u2014 may be absent (managed by parent POM or BOM)\n let declaredConstraint: string;\n if (version && version.trim() !== '' && !version.includes('${')) {\n declaredConstraint = version.trim();\n } else {\n // No version or unresolved property \u2014 use empty string to indicate managed externally\n declaredConstraint = version && !version.includes('${') ? version.trim() : '';\n }\n\n const mappedScope = mapMavenScope(scope);\n\n dependencies.push({\n name,\n declaredConstraint,\n resolvedVersion: declaredConstraint || undefined,\n scope: mappedScope,\n ecosystem: 'maven',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n\n return dependencies;\n}\n\n/**\n * Extract the first license name from a pom.xml <licenses> block.\n * Looks for: <licenses><license><name>...</name></license></licenses>\n */\nfunction extractMavenLicense(content: string): string | undefined {\n const licensesMatch = content.match(/<licenses\\b[^>]*>([\\s\\S]*?)<\\/licenses>/i);\n if (!licensesMatch) return undefined;\n const licenseBlock = licensesMatch[1];\n const nameMatch = licenseBlock.match(/<name>([^<]+)<\\/name>/i);\n if (nameMatch && nameMatch[1].trim() !== '') {\n return nameMatch[1].trim();\n }\n return undefined;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Maven Version Extraction Plugin for KiroGraph-Sec\n *\n * Extends the existing architecture maven parser to extract version constraints,\n * dependency scopes, and groupId/artifactId for security analysis.\n *\n * Reuses `mavenParser` from `src/architecture/manifest/maven.ts` for discovery and\n * basic parsing, then layers on version/scope extraction from pom.xml.\n */\nimport * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\n/**\n * Maven scope to ParsedDependency scope mapping.\n *\n * - compile (default), runtime \u2192 production\n * - test \u2192 development\n * - provided, system \u2192 optional\n */\nfunction mapMavenScope(mavenScope: string | undefined): ParsedDependency['scope'] {\n if (!mavenScope || mavenScope.trim() === '') {\n return 'production';\n }\n\n const normalized = mavenScope.trim().toLowerCase();\n switch (normalized) {\n case 'compile':\n case 'runtime':\n return 'production';\n case 'test':\n return 'development';\n case 'provided':\n case 'system':\n return 'optional';\n default:\n // Unknown scope defaults to production\n return 'production';\n }\n}\n\n/**\n * Extract text content from an XML element by tag name within a given XML fragment.\n * Returns undefined if the element is not found.\n */\nfunction extractXmlElement(xml: string, tagName: string): string | undefined {\n const regex = new RegExp(`<${tagName}>([^<]*)</${tagName}>`);\n const match = xml.match(regex);\n return match ? match[1].trim() : undefined;\n}\n\n/**\n * Parse a Maven pom.xml manifest and extract dependency declarations\n * with groupId, artifactId, version constraints, and scopes.\n *\n * @param manifestPath - Absolute path to the pom.xml 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 parseMavenManifest(\n manifestPath: string,\n projectRoot: string,\n): Promise<ParsedDependency[]> {\n const relativeManifest = path.relative(projectRoot, manifestPath).replace(/\\\\/g, '/');\n\n // Read the pom.xml content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:maven] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Validate basic XML structure\n if (!content.includes('<project')) {\n logWarn(`[sec:maven] Invalid pom.xml structure at ${relativeManifest} \u2014 missing <project> element`);\n return [];\n }\n\n // Extract all <dependency> blocks from the <dependencies> sections\n const dependencies: ParsedDependency[] = [];\n\n // <dependencyManagement> entries (BOM imports, version overrides for child\n // modules) are not themselves project dependencies \u2014 strip that block before\n // scanning so they don't get misread as declared dependencies.\n const dependenciesOnlyContent = content.replace(/<dependencyManagement>[\\s\\S]*?<\\/dependencyManagement>/g, '');\n\n // Match all <dependency>...</dependency> blocks\n const dependencyBlocks = dependenciesOnlyContent.matchAll(/<dependency>([\\s\\S]*?)<\\/dependency>/g);\n\n for (const block of dependencyBlocks) {\n const depXml = block[1];\n\n const groupId = extractXmlElement(depXml, 'groupId');\n const artifactId = extractXmlElement(depXml, 'artifactId');\n const version = extractXmlElement(depXml, 'version');\n const scope = extractXmlElement(depXml, 'scope');\n\n // Validate required fields\n if (!groupId || groupId.trim() === '') {\n logWarn(`[sec:maven] Missing groupId in dependency at ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n if (!artifactId || artifactId.trim() === '') {\n logWarn(`[sec:maven] Missing artifactId in dependency at ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n // Skip dependencies with unresolved Maven properties (e.g. ${project.version})\n if (groupId.includes('${') || artifactId.includes('${')) {\n logWarn(`[sec:maven] Unresolved property in dependency ${groupId}:${artifactId} at ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n // Build the dependency name as groupId:artifactId (Maven convention)\n const name = `${groupId}:${artifactId}`;\n\n // Version constraint \u2014 may be absent (managed by parent POM or BOM)\n let declaredConstraint: string;\n if (version && version.trim() !== '' && !version.includes('${')) {\n declaredConstraint = version.trim();\n } else {\n // No version or unresolved property \u2014 use empty string to indicate managed externally\n declaredConstraint = version && !version.includes('${') ? version.trim() : '';\n }\n\n const mappedScope = mapMavenScope(scope);\n\n dependencies.push({\n name,\n declaredConstraint,\n resolvedVersion: declaredConstraint || undefined,\n scope: mappedScope,\n ecosystem: 'maven',\n sourceManifest: relativeManifest,\n });\n }\n\n // Maven has no lock file, so a <dependency> with no explicit <version> (the\n // common case under a BOM parent like spring-boot-starter-parent) never gets\n // a resolvedVersion above, and packages pulled in only transitively (e.g.\n // tomcat-embed-core via spring-boot-starter-web) never appear in pom.xml at\n // all. If the project has a pre-generated `mvn dependency:tree` text file\n // (the closest Maven equivalent of a lock file), use it to fill in resolved\n // versions and capture transitive-only packages, mirroring how the npm\n // plugin layers package-lock.json on top of package.json.\n const manifestDir = path.dirname(manifestPath);\n const treeFile = findMavenDependencyTreeFile(manifestDir);\n\n if (treeFile) {\n const treeRelativePath = path.relative(projectRoot, treeFile.path).replace(/\\\\/g, '/');\n let treeContent: string;\n try {\n treeContent = fs.readFileSync(treeFile.path, 'utf8');\n } catch (err) {\n logWarn(`[sec:maven] Failed to read ${treeRelativePath}: ${err instanceof Error ? err.message : String(err)}`);\n applyLocalMavenLicenses(dependencies);\n return dependencies;\n }\n\n const treeEntries = parseMavenDependencyTree(treeContent);\n\n for (const dep of dependencies) {\n if (dep.resolvedVersion) continue;\n const treeEntry = treeEntries.get(dep.name);\n if (treeEntry) {\n dep.resolvedVersion = treeEntry.version;\n }\n }\n\n const directNames = new Set(dependencies.map(d => d.name));\n for (const [name, entry] of treeEntries) {\n if (directNames.has(name)) continue;\n dependencies.push({\n name,\n // No declared range exists at the project level for a transitive\n // package \u2014 the resolved version from the tree is the only constraint we have.\n declaredConstraint: entry.version,\n resolvedVersion: entry.version,\n scope: mapMavenScope(entry.scope),\n ecosystem: 'maven',\n sourceManifest: treeRelativePath,\n });\n }\n }\n\n applyLocalMavenLicenses(dependencies);\n return dependencies;\n}\n\n/**\n * Look for a pre-generated Maven dependency-tree text file next to pom.xml,\n * e.g. produced with:\n * mvn dependency:tree -DoutputFile=dependency-tree.txt\n * Checked locations, in order: `dependency-tree.txt` next to pom.xml, then\n * `target/dependency-tree.txt` (Maven's default build output directory).\n */\nfunction findMavenDependencyTreeFile(manifestDir: string): { path: string } | undefined {\n const candidates = ['dependency-tree.txt', path.join('target', 'dependency-tree.txt')];\n for (const candidate of candidates) {\n const candidatePath = path.join(manifestDir, candidate);\n if (fs.existsSync(candidatePath)) {\n return { path: candidatePath };\n }\n }\n return undefined;\n}\n\n/**\n * Parse `mvn dependency:tree` text output into a map of `groupId:artifactId`\n * to its resolved version and scope.\n *\n * Handles both:\n * groupId:artifactId:type:version:scope\n * groupId:artifactId:type:classifier:version:scope\n *\n * Skips the root project line (no scope segment) and parenthesized entries\n * like `(commons-logging:commons-logging:jar:1.1.1:compile - omitted for conflict with 1.2)`\n * \u2014 Maven's conflict resolution means only the winning version ends up on the\n * classpath, so omitted entries aren't actually present in the build.\n */\nexport function parseMavenDependencyTree(content: string): Map<string, { version: string; scope: string }> {\n const result = new Map<string, { version: string; scope: string }>();\n\n for (const rawLine of content.split(/\\r?\\n/)) {\n if (!rawLine.trim()) continue;\n\n // Strip Maven's ASCII tree-drawing prefix (e.g. \"| +- \", \" \\- \")\n const stripped = rawLine.replace(/^[\\s|+\\\\-]+/, '').trim();\n if (!stripped || stripped.startsWith('(')) continue;\n\n const parts = stripped.split(':');\n if (parts.length < 5) continue; // root project line (\"groupId:artifactId:version\") or malformed\n\n const groupId = parts[0];\n const artifactId = parts[1];\n const version = parts[parts.length - 2];\n const scope = parts[parts.length - 1];\n if (!groupId || !artifactId || !version) continue;\n\n const name = `${groupId}:${artifactId}`;\n if (!result.has(name)) {\n result.set(name, { version, scope });\n }\n }\n\n return result;\n}\n\n/**\n * Extract the first license name from a pom.xml `<licenses>` block.\n * Looks for: <licenses><license><name>...</name></license></licenses>\n * Works on any pom.xml content \u2014 the project's own, or a dependency's own\n * POM read from the local Maven repository.\n */\nfunction extractMavenLicense(content: string): string | undefined {\n const licensesMatch = content.match(/<licenses\\b[^>]*>([\\s\\S]*?)<\\/licenses>/i);\n if (!licensesMatch) return undefined;\n const licenseBlock = licensesMatch[1];\n const nameMatch = licenseBlock.match(/<name>([^<]+)<\\/name>/i);\n if (nameMatch && nameMatch[1].trim() !== '') {\n return nameMatch[1].trim();\n }\n return undefined;\n}\n\n/**\n * Resolve the local Maven repository root: `~/.m2/settings.xml`'s\n * `<localRepository>` override if set, else the conventional `~/.m2/repository`.\n */\nfunction resolveMavenLocalRepoRoot(): string {\n const settingsPath = path.join(os.homedir(), '.m2', 'settings.xml');\n try {\n const settings = fs.readFileSync(settingsPath, 'utf8');\n const match = settings.match(/<localRepository>([^<]+)<\\/localRepository>/i);\n if (match && match[1].trim() !== '') return match[1].trim();\n } catch {\n // No settings.xml, or unreadable \u2014 fall through to the default.\n }\n return path.join(os.homedir(), '.m2', 'repository');\n}\n\n/**\n * Each dependency's own license lives in its own POM, not the consuming\n * project's \u2014 Maven has no lock file to carry that data, but a dependency\n * that has ever been built or downloaded locally has its POM cached at the\n * standard local-repository layout:\n * <repoRoot>/<groupId with . -> />/<artifactId>/<version>/<artifactId>-<version>.pom\n * Local file only, no network call. Returns undefined when the dependency\n * has never been resolved locally (e.g. CI running from a clean cache), same\n * as before this fix \u2014 this fills in real data on top of an \"unknown\" default,\n * it never invents one.\n */\nfunction findLocalMavenPomLicense(repoRoot: string, groupId: string, artifactId: string, version: string): string | undefined {\n const pomPath = path.join(repoRoot, ...groupId.split('.'), artifactId, version, `${artifactId}-${version}.pom`);\n try {\n const content = fs.readFileSync(pomPath, 'utf8');\n return extractMavenLicense(content);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Fill in `license` for every dependency that has a resolved version, by\n * reading that exact groupId:artifactId:version's own POM from the local\n * Maven repository. Mutates `dependencies` in place.\n */\nfunction applyLocalMavenLicenses(dependencies: ParsedDependency[]): void {\n const repoRoot = resolveMavenLocalRepoRoot();\n for (const dep of dependencies) {\n if (!dep.resolvedVersion) continue;\n const [groupId, artifactId] = dep.name.split(':');\n if (!groupId || !artifactId) continue;\n const license = findLocalMavenPomLicense(repoRoot, groupId, artifactId, dep.resolvedVersion);\n if (license !== undefined) dep.license = license;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }