kirograph 0.28.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/architecture/index.js +4 -1
- package/dist/architecture/index.js.map +2 -2
- package/dist/architecture/manifest/index.js +13 -2
- package/dist/architecture/manifest/index.js.map +2 -2
- package/dist/architecture/manifest/python.js +2 -2
- package/dist/architecture/manifest/python.js.map +2 -2
- package/dist/bin/commands/help.js +3 -1
- package/dist/bin/commands/help.js.map +2 -2
- package/dist/bin/commands/install.js +2 -1
- package/dist/bin/commands/install.js.map +2 -2
- package/dist/bin/commands/memory.js +18 -8
- package/dist/bin/commands/memory.js.map +2 -2
- package/dist/bin/commands/reachability.js +7 -2
- package/dist/bin/commands/reachability.js.map +2 -2
- package/dist/bin/commands/status.js +2 -1
- package/dist/bin/commands/status.js.map +2 -2
- package/dist/bin/commands/uninit.js +2 -1
- package/dist/bin/commands/uninit.js.map +2 -2
- package/dist/bin/commands/vulns.js +26 -6
- package/dist/bin/commands/vulns.js.map +2 -2
- package/dist/bin/commands/wiki.js +56 -0
- package/dist/bin/commands/wiki.js.map +2 -2
- package/dist/bin/installer/common.js +7 -1
- package/dist/bin/installer/common.js.map +2 -2
- package/dist/bin/installer/detect.js +10 -0
- package/dist/bin/installer/detect.js.map +2 -2
- package/dist/bin/installer/hooks.js +13 -8
- package/dist/bin/installer/hooks.js.map +2 -2
- package/dist/bin/installer/steering.js +9 -0
- package/dist/bin/installer/steering.js.map +2 -2
- package/dist/bin/installer/targets/antigravity.js +1 -1
- package/dist/bin/installer/targets/antigravity.js.map +2 -2
- package/dist/bin/installer/targets/claude.js +1 -1
- package/dist/bin/installer/targets/claude.js.map +2 -2
- package/dist/bin/installer/targets/codex.js +1 -1
- package/dist/bin/installer/targets/codex.js.map +2 -2
- package/dist/bin/installer/targets/commandcode.js +84 -0
- package/dist/bin/installer/targets/commandcode.js.map +7 -0
- package/dist/bin/installer/targets/copilot.js +1 -1
- package/dist/bin/installer/targets/copilot.js.map +2 -2
- package/dist/bin/installer/targets/cursor.js +2 -2
- package/dist/bin/installer/targets/cursor.js.map +2 -2
- package/dist/bin/installer/targets/devin.js +1 -1
- package/dist/bin/installer/targets/devin.js.map +2 -2
- package/dist/bin/installer/targets/gemini-cli.js +1 -1
- package/dist/bin/installer/targets/gemini-cli.js.map +2 -2
- package/dist/bin/installer/targets/index.js +10 -0
- package/dist/bin/installer/targets/index.js.map +2 -2
- package/dist/bin/installer/targets/opencode.js +1 -1
- package/dist/bin/installer/targets/opencode.js.map +2 -2
- package/dist/bin/installer/targets/windsurf.js +1 -1
- package/dist/bin/installer/targets/windsurf.js.map +2 -2
- package/dist/bin/kirograph.js +1 -1
- package/dist/bin/kirograph.js.map +1 -1
- package/dist/config.js +25 -1
- package/dist/config.js.map +2 -2
- package/dist/core/pipeline.js +18 -6
- package/dist/core/pipeline.js.map +2 -2
- package/dist/extraction/extractor.js +88 -26
- package/dist/extraction/extractor.js.map +2 -2
- package/dist/extraction/grammars.js +38 -3
- package/dist/extraction/grammars.js.map +2 -2
- package/dist/extraction/notebook.js +15 -7
- package/dist/extraction/notebook.js.map +2 -2
- package/dist/extraction/wasm/tree-sitter-bash.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-c.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-csharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-css.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-dart.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-elisp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-elixir.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-elm.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-embedded_template.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-embeddedtemplate.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-html.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-json.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-kotlin.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-objc.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-ocaml.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-php.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-ql.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-rescript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-rust.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-solidity.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-swift.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-systemrdl.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-tlaplus.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-toml.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-vue.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-yaml.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-zig.wasm +0 -0
- package/dist/frameworks/index.js +68 -7
- package/dist/frameworks/index.js.map +2 -2
- package/dist/jev/client.js +99 -0
- package/dist/jev/client.js.map +7 -0
- package/dist/jev/types.js +17 -0
- package/dist/jev/types.js.map +7 -0
- package/dist/mcp/handlers/memory.js +14 -8
- package/dist/mcp/handlers/memory.js.map +2 -2
- package/dist/mcp/handlers/security.js +5 -3
- package/dist/mcp/handlers/security.js.map +2 -2
- package/dist/mcp/tools.js +1 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/memory/index.js +13 -1
- package/dist/memory/index.js.map +3 -3
- package/dist/memory/relation-jev.js +59 -0
- package/dist/memory/relation-jev.js.map +7 -0
- package/dist/memory/schema.js +57 -0
- package/dist/memory/schema.js.map +7 -0
- package/dist/security/auth-detection-jev.js +47 -0
- package/dist/security/auth-detection-jev.js.map +7 -0
- package/dist/security/manifest/adapter.js +34 -5
- package/dist/security/manifest/adapter.js.map +3 -3
- package/dist/security/manifest/parser.js +2 -2
- package/dist/security/manifest/parser.js.map +2 -2
- package/dist/security/manifest/plugins/cargo.js +23 -0
- package/dist/security/manifest/plugins/cargo.js.map +2 -2
- package/dist/security/manifest/plugins/composer.js +12 -0
- package/dist/security/manifest/plugins/composer.js.map +2 -2
- package/dist/security/manifest/plugins/gradle.js +12 -0
- package/dist/security/manifest/plugins/gradle.js.map +2 -2
- package/dist/security/manifest/plugins/hex.js +12 -0
- package/dist/security/manifest/plugins/hex.js.map +2 -2
- package/dist/security/manifest/plugins/maven.js +68 -1
- package/dist/security/manifest/plugins/maven.js.map +2 -2
- package/dist/security/manifest/plugins/npm.js +86 -0
- package/dist/security/manifest/plugins/npm.js.map +2 -2
- package/dist/security/manifest/plugins/nuget.js +13 -0
- package/dist/security/manifest/plugins/nuget.js.map +2 -2
- package/dist/security/manifest/plugins/pubspec.js +12 -0
- package/dist/security/manifest/plugins/pubspec.js.map +2 -2
- package/dist/security/manifest/plugins/pyproject.js +12 -0
- package/dist/security/manifest/plugins/pyproject.js.map +2 -2
- package/dist/security/manifest/plugins/rubygems.js +12 -0
- package/dist/security/manifest/plugins/rubygems.js.map +2 -2
- package/dist/security/pipeline.js +5 -2
- package/dist/security/pipeline.js.map +2 -2
- package/dist/security/reachability.js +115 -50
- package/dist/security/reachability.js.map +2 -2
- package/dist/security/types.js.map +1 -1
- package/dist/security/vuln/client.js +45 -1
- package/dist/security/vuln/client.js.map +2 -2
- package/dist/security/vuln/epss-client.js +1 -1
- package/dist/security/vuln/epss-client.js.map +1 -1
- package/dist/security/vuln/osv-adapter.js +144 -32
- package/dist/security/vuln/osv-adapter.js.map +2 -2
- package/dist/sync/index.js +9 -2
- package/dist/sync/index.js.map +2 -2
- package/dist/vectors/libsql-index.js +287 -0
- package/dist/vectors/libsql-index.js.map +7 -0
- package/dist/vectors/turso-index.js +307 -0
- package/dist/vectors/turso-index.js.map +7 -0
- package/dist/wiki/contradiction-jev.js +49 -0
- package/dist/wiki/contradiction-jev.js.map +7 -0
- package/dist/wiki/index.js +13 -0
- package/dist/wiki/index.js.map +2 -2
- package/dist/wiki/ingest.js +2 -0
- package/dist/wiki/ingest.js.map +2 -2
- package/dist/wiki/links.js +53 -0
- package/dist/wiki/links.js.map +7 -0
- package/dist/wiki/lint.js +3 -6
- package/dist/wiki/lint.js.map +2 -2
- package/dist/wiki/rename.js +103 -0
- package/dist/wiki/rename.js.map +7 -0
- package/package.json +3 -2
|
@@ -89,6 +89,18 @@ async function parsePyprojectManifest(manifestPath, projectRoot) {
|
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
const directNames = new Set(dependencies.map((d) => d.name.toLowerCase()));
|
|
93
|
+
for (const [name, version] of resolvedVersions) {
|
|
94
|
+
if (directNames.has(name)) continue;
|
|
95
|
+
dependencies.push({
|
|
96
|
+
name,
|
|
97
|
+
declaredConstraint: version,
|
|
98
|
+
resolvedVersion: version,
|
|
99
|
+
scope: "production",
|
|
100
|
+
ecosystem: "pypi",
|
|
101
|
+
sourceManifest: relativeManifest
|
|
102
|
+
});
|
|
103
|
+
}
|
|
92
104
|
return dependencies;
|
|
93
105
|
}
|
|
94
106
|
function extractPep621Dependencies(content, relativeManifest) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/security/manifest/plugins/pyproject.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * pyproject.toml Version Extraction Plugin for KiroGraph-Sec\n *\n * Handles modern Python packaging: PEP 621 (standard), Poetry, Hatch, PDM, Flit.\n * pyproject.toml is the canonical manifest for virtually all Python projects\n * created after 2021 \u2014 requirements.txt is only used for pinned deployment deps.\n *\n * Supports:\n * - PEP 621 [project] dependencies = [\"package>=1.0\"]\n * - Poetry [tool.poetry.dependencies] { package = \"^1.0\" }\n * - PDM [tool.pdm.dev-dependencies] { dev = [\"package>=1.0\"] }\n * - Hatch [tool.hatch.envs.default.dependencies] = [\"package>=1.0\"]\n * Lock files: poetry.lock, pdm.lock, uv.lock (all share the same version extraction)\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\ntype ResolvedVersionMap = Map<string, string>;\n\n// \u2500\u2500 Entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport async function parsePyprojectManifest(\n manifestPath: string,\n projectRoot: string,\n): Promise<ParsedDependency[]> {\n const relativeManifest = path.relative(projectRoot, manifestPath).replace(/\\\\/g, '/');\n\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:pyproject] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n const manifestDir = path.dirname(manifestPath);\n const resolvedVersions = loadResolvedVersions(manifestDir);\n const license = extractPyprojectLicense(content);\n const dependencies: ParsedDependency[] = [];\n\n // \u2500\u2500 PEP 621 [project] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const pep621Deps = extractPep621Dependencies(content, relativeManifest);\n for (const dep of pep621Deps) {\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion: resolvedVersions.get(dep.name.toLowerCase()),\n scope: dep.scope,\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n\n // \u2500\u2500 Poetry [tool.poetry.dependencies] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (dependencies.length === 0 || hasPoetrySection(content)) {\n const poetryDeps = extractPoetryDependencies(content, relativeManifest);\n for (const dep of poetryDeps) {\n if (!dependencies.find(d => d.name === dep.name)) {\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion: resolvedVersions.get(dep.name.toLowerCase()),\n scope: dep.scope,\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n }\n\n // \u2500\u2500 PDM [tool.pdm.dev-dependencies] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const pdmDeps = extractPdmDependencies(content, relativeManifest);\n for (const dep of pdmDeps) {\n if (!dependencies.find(d => d.name === dep.name)) {\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion: resolvedVersions.get(dep.name.toLowerCase()),\n scope: dep.scope,\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n\n return dependencies;\n}\n\n// \u2500\u2500 PEP 621 extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface RawDep { name: string; constraint: string; scope: ParsedDependency['scope'] }\n\nfunction extractPep621Dependencies(content: string, relativeManifest: string): RawDep[] {\n const deps: RawDep[] = [];\n\n // [project] dependencies = [\"package>=1.0\", ...]\n const depsBlock = extractTomlArray(content, /^\\[project\\]/m, 'dependencies');\n for (const spec of depsBlock) {\n const dep = parsePep508Spec(spec, relativeManifest);\n if (dep) deps.push({ ...dep, scope: 'production' });\n }\n\n // [project.optional-dependencies] group = [\"package>=1.0\", ...]\n const optSection = extractSection(content, /^\\[project\\.optional-dependencies\\]/m);\n if (optSection) {\n for (const m of optSection.matchAll(/^\\s*([a-zA-Z0-9_-]+)\\s*=\\s*\\[([^\\]]*)\\]/gm)) {\n const groupName = m[1].toLowerCase();\n const isDevGroup = /^(dev|test|lint|docs|ci|build|check|debug)/.test(groupName);\n const items = parseTomlStringArray(m[2]);\n for (const spec of items) {\n const dep = parsePep508Spec(spec, relativeManifest);\n if (dep) deps.push({ ...dep, scope: isDevGroup ? 'development' : 'optional' });\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 Poetry extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction hasPoetrySection(content: string): boolean {\n return /^\\[tool\\.poetry/m.test(content);\n}\n\nfunction extractPoetryDependencies(content: string, relativeManifest: string): RawDep[] {\n const deps: RawDep[] = [];\n\n const sections: Array<{ pattern: RegExp; scope: ParsedDependency['scope'] }> = [\n { pattern: /^\\[tool\\.poetry\\.dependencies\\]/m, scope: 'production' },\n { pattern: /^\\[tool\\.poetry\\.dev-dependencies\\]/m, scope: 'development' },\n ];\n\n // Also handle [tool.poetry.group.*.dependencies]\n for (const m of content.matchAll(/^\\[tool\\.poetry\\.group\\.([a-zA-Z0-9_-]+)\\.dependencies\\]/gm)) {\n const groupName = m[1].toLowerCase();\n const scope: ParsedDependency['scope'] = /^(dev|test|lint|docs|ci)/.test(groupName) ? 'development' : 'optional';\n sections.push({ pattern: new RegExp(`^\\\\[tool\\\\.poetry\\\\.group\\\\.${m[1]}\\\\.dependencies\\\\]`, 'm'), scope });\n }\n\n for (const { pattern, scope } of sections) {\n const section = extractSection(content, pattern);\n if (!section) continue;\n\n for (const line of section.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('[')) break;\n\n // name = \"^1.0\" or name = { version = \"^1.0\", ... }\n const simpleMatch = trimmed.match(/^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)\\s*=\\s*\"([^\"]+)\"/);\n if (simpleMatch) {\n const name = simpleMatch[1];\n if (name === 'python') continue; // skip python version constraint\n deps.push({ name: normalizePypiName(name), constraint: simpleMatch[3], scope });\n continue;\n }\n\n // name = { version = \"^1.0\", ... }\n const tableMatch = trimmed.match(/^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)\\s*=\\s*\\{/);\n if (tableMatch) {\n const name = tableMatch[1];\n if (name === 'python') continue;\n const versionMatch = trimmed.match(/version\\s*=\\s*\"([^\"]+)\"/);\n if (versionMatch) {\n deps.push({ name: normalizePypiName(name), constraint: versionMatch[1], scope });\n } else {\n // path/git dep without version \u2014 record with wildcard\n deps.push({ name: normalizePypiName(name), constraint: '*', scope });\n }\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 PDM extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction extractPdmDependencies(content: string, relativeManifest: string): RawDep[] {\n const deps: RawDep[] = [];\n\n // [tool.pdm.dev-dependencies] groups with arrays\n const section = extractSection(content, /^\\[tool\\.pdm\\.dev-dependencies\\]/m);\n if (section) {\n for (const m of section.matchAll(/^\\s*([a-zA-Z0-9_-]+)\\s*=\\s*\\[([^\\]]*)\\]/gm)) {\n const items = parseTomlStringArray(m[2]);\n for (const spec of items) {\n const dep = parsePep508Spec(spec, relativeManifest);\n if (dep) deps.push({ ...dep, scope: 'development' });\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 PEP 508 spec parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction parsePep508Spec(spec: string, _relativeManifest: string): { name: string; constraint: string } | null {\n // PEP 508: name [extras] [version_spec] [; marker]\n // Strip markers (semicolon and beyond), extras ([...])\n const withoutMarker = spec.split(';')[0].trim();\n const withoutExtras = withoutMarker.replace(/\\[[^\\]]*\\]/, '').trim();\n\n const match = withoutExtras.match(/^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)\\s*(.*)/);\n if (!match) return null;\n\n const name = normalizePypiName(match[1]);\n const constraint = match[3].trim() || '*';\n return { name, constraint };\n}\n\n// \u2500\u2500 Lock file loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n // Try poetry.lock, pdm.lock, uv.lock in order\n const candidates = ['poetry.lock', 'pdm.lock', 'uv.lock'];\n for (const lockFile of candidates) {\n const lockPath = path.join(manifestDir, lockFile);\n if (!fs.existsSync(lockPath)) continue;\n try {\n const content = fs.readFileSync(lockPath, 'utf8');\n if (lockFile === 'poetry.lock') extractFromPoetryLock(content, map);\n else extractFromPdmUvLock(content, map);\n break; // only use the first lock file found\n } catch (err) {\n logWarn(`[sec:pyproject] Failed to parse ${lockFile}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return map;\n}\n\nfunction extractFromPoetryLock(content: string, map: ResolvedVersionMap): void {\n // [[package]] blocks with name and version fields\n for (const m of content.matchAll(/\\[\\[package\\]\\][^[]*?name\\s*=\\s*\"([^\"]+)\"[^[]*?version\\s*=\\s*\"([^\"]+)\"/gs)) {\n map.set(m[1].toLowerCase(), m[2]);\n }\n}\n\nfunction extractFromPdmUvLock(content: string, map: ResolvedVersionMap): void {\n // [[package]] or [[distribution]] blocks (pdm/uv use slightly different headers)\n for (const m of content.matchAll(/\\[\\[(?:package|distribution)\\]\\][^[]*?name\\s*=\\s*\"([^\"]+)\"[^[]*?version\\s*=\\s*\"([^\"]+)\"/gs)) {\n map.set(m[1].toLowerCase(), m[2]);\n }\n}\n\n// \u2500\u2500 TOML helpers (no library dependency) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Extract the content after a section header until the next header. */\nfunction extractSection(content: string, headerPattern: RegExp): string | null {\n const match = headerPattern.exec(content);\n if (!match) return null;\n const start = match.index + match[0].length;\n const remaining = content.slice(start);\n const nextSection = remaining.match(/^\\[/m);\n return nextSection ? remaining.slice(0, nextSection.index) : remaining;\n}\n\n/** Extract a TOML array value by key from a section. */\nfunction extractTomlArray(content: string, sectionPattern: RegExp, key: string): string[] {\n const section = extractSection(content, sectionPattern);\n if (!section) return [];\n\n // Match key = [\"...\", \"...\", ...] \u2014 may span multiple lines\n const keyPattern = new RegExp(`^${key}\\\\s*=\\\\s*\\\\[([^\\\\]]*(?:\\\\][^\\\\[\\\\]]*\\\\[)*[^\\\\]]*)\\\\]`, 'ms');\n const m = keyPattern.exec(section);\n if (!m) return [];\n return parseTomlStringArray(m[1]);\n}\n\n/** Parse a TOML string array body (contents inside [...]) into individual strings. */\nfunction parseTomlStringArray(body: string): string[] {\n const results: string[] = [];\n for (const m of body.matchAll(/\"([^\"]+)\"/g)) {\n results.push(m[1].trim());\n }\n for (const m of body.matchAll(/'([^']+)'/g)) {\n results.push(m[1].trim());\n }\n return results.filter(Boolean);\n}\n\n/** Normalize PyPI package name: lowercase, replace - and _ with - (canonical form). */\nfunction normalizePypiName(name: string): string {\n return name.toLowerCase().replace(/[-_.]+/g, '-');\n}\n\n/**\n * Extract the license from a pyproject.toml file.\n *\n * Handles:\n * - PEP 621 [project] license = {text = \"MIT\"} or license = \"MIT\"\n * - Poetry [tool.poetry] license = \"MIT\"\n */\nfunction extractPyprojectLicense(content: string): string | undefined {\n // PEP 621: license = {text = \"MIT\"} or license = {file = \"LICENSE\"}\n const pep621Section = extractSection(content, /^\\[project\\]/m);\n if (pep621Section) {\n // license = {text = \"MIT\"}\n const tableTextMatch = pep621Section.match(/^license\\s*=\\s*\\{[^}]*text\\s*=\\s*[\"']([^\"']+)[\"'][^}]*\\}/m);\n if (tableTextMatch && tableTextMatch[1].trim() !== '') {\n return tableTextMatch[1].trim();\n }\n // license = \"MIT\"\n const stringMatch = pep621Section.match(/^license\\s*=\\s*[\"']([^\"']+)[\"']/m);\n if (stringMatch && stringMatch[1].trim() !== '') {\n return stringMatch[1].trim();\n }\n }\n\n // Poetry: [tool.poetry] license = \"MIT\"\n const poetrySection = extractSection(content, /^\\[tool\\.poetry\\]/m);\n if (poetrySection) {\n const poetryLicenseMatch = poetrySection.match(/^license\\s*=\\s*[\"']([^\"']+)[\"']/m);\n if (poetryLicenseMatch && poetryLicenseMatch[1].trim() !== '') {\n return poetryLicenseMatch[1].trim();\n }\n }\n\n return undefined;\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\n * pyproject.toml Version Extraction Plugin for KiroGraph-Sec\n *\n * Handles modern Python packaging: PEP 621 (standard), Poetry, Hatch, PDM, Flit.\n * pyproject.toml is the canonical manifest for virtually all Python projects\n * created after 2021 \u2014 requirements.txt is only used for pinned deployment deps.\n *\n * Supports:\n * - PEP 621 [project] dependencies = [\"package>=1.0\"]\n * - Poetry [tool.poetry.dependencies] { package = \"^1.0\" }\n * - PDM [tool.pdm.dev-dependencies] { dev = [\"package>=1.0\"] }\n * - Hatch [tool.hatch.envs.default.dependencies] = [\"package>=1.0\"]\n * Lock files: poetry.lock, pdm.lock, uv.lock (all share the same version extraction)\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\ntype ResolvedVersionMap = Map<string, string>;\n\n// \u2500\u2500 Entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport async function parsePyprojectManifest(\n manifestPath: string,\n projectRoot: string,\n): Promise<ParsedDependency[]> {\n const relativeManifest = path.relative(projectRoot, manifestPath).replace(/\\\\/g, '/');\n\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:pyproject] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n const manifestDir = path.dirname(manifestPath);\n const resolvedVersions = loadResolvedVersions(manifestDir);\n const license = extractPyprojectLicense(content);\n const dependencies: ParsedDependency[] = [];\n\n // \u2500\u2500 PEP 621 [project] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const pep621Deps = extractPep621Dependencies(content, relativeManifest);\n for (const dep of pep621Deps) {\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion: resolvedVersions.get(dep.name.toLowerCase()),\n scope: dep.scope,\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n\n // \u2500\u2500 Poetry [tool.poetry.dependencies] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (dependencies.length === 0 || hasPoetrySection(content)) {\n const poetryDeps = extractPoetryDependencies(content, relativeManifest);\n for (const dep of poetryDeps) {\n if (!dependencies.find(d => d.name === dep.name)) {\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion: resolvedVersions.get(dep.name.toLowerCase()),\n scope: dep.scope,\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n }\n\n // \u2500\u2500 PDM [tool.pdm.dev-dependencies] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const pdmDeps = extractPdmDependencies(content, relativeManifest);\n for (const dep of pdmDeps) {\n if (!dependencies.find(d => d.name === dep.name)) {\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion: resolvedVersions.get(dep.name.toLowerCase()),\n scope: dep.scope,\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n\n // Transitive-only packages (present in poetry.lock/pdm.lock/uv.lock but\n // never declared in pyproject.toml) would otherwise never become a\n // Dependency_Node and would silently skip vulnerability scanning.\n // resolvedVersions already has every package the lock file resolved,\n // direct or transitive (lower-cased \u2014 original casing isn't retained).\n const directNames = new Set(dependencies.map(d => d.name.toLowerCase()));\n for (const [name, version] of resolvedVersions) {\n if (directNames.has(name)) continue;\n dependencies.push({\n name,\n declaredConstraint: version,\n resolvedVersion: version,\n scope: 'production',\n ecosystem: 'pypi',\n sourceManifest: relativeManifest,\n });\n }\n\n return dependencies;\n}\n\n// \u2500\u2500 PEP 621 extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface RawDep { name: string; constraint: string; scope: ParsedDependency['scope'] }\n\nfunction extractPep621Dependencies(content: string, relativeManifest: string): RawDep[] {\n const deps: RawDep[] = [];\n\n // [project] dependencies = [\"package>=1.0\", ...]\n const depsBlock = extractTomlArray(content, /^\\[project\\]/m, 'dependencies');\n for (const spec of depsBlock) {\n const dep = parsePep508Spec(spec, relativeManifest);\n if (dep) deps.push({ ...dep, scope: 'production' });\n }\n\n // [project.optional-dependencies] group = [\"package>=1.0\", ...]\n const optSection = extractSection(content, /^\\[project\\.optional-dependencies\\]/m);\n if (optSection) {\n for (const m of optSection.matchAll(/^\\s*([a-zA-Z0-9_-]+)\\s*=\\s*\\[([^\\]]*)\\]/gm)) {\n const groupName = m[1].toLowerCase();\n const isDevGroup = /^(dev|test|lint|docs|ci|build|check|debug)/.test(groupName);\n const items = parseTomlStringArray(m[2]);\n for (const spec of items) {\n const dep = parsePep508Spec(spec, relativeManifest);\n if (dep) deps.push({ ...dep, scope: isDevGroup ? 'development' : 'optional' });\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 Poetry extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction hasPoetrySection(content: string): boolean {\n return /^\\[tool\\.poetry/m.test(content);\n}\n\nfunction extractPoetryDependencies(content: string, relativeManifest: string): RawDep[] {\n const deps: RawDep[] = [];\n\n const sections: Array<{ pattern: RegExp; scope: ParsedDependency['scope'] }> = [\n { pattern: /^\\[tool\\.poetry\\.dependencies\\]/m, scope: 'production' },\n { pattern: /^\\[tool\\.poetry\\.dev-dependencies\\]/m, scope: 'development' },\n ];\n\n // Also handle [tool.poetry.group.*.dependencies]\n for (const m of content.matchAll(/^\\[tool\\.poetry\\.group\\.([a-zA-Z0-9_-]+)\\.dependencies\\]/gm)) {\n const groupName = m[1].toLowerCase();\n const scope: ParsedDependency['scope'] = /^(dev|test|lint|docs|ci)/.test(groupName) ? 'development' : 'optional';\n sections.push({ pattern: new RegExp(`^\\\\[tool\\\\.poetry\\\\.group\\\\.${m[1]}\\\\.dependencies\\\\]`, 'm'), scope });\n }\n\n for (const { pattern, scope } of sections) {\n const section = extractSection(content, pattern);\n if (!section) continue;\n\n for (const line of section.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('[')) break;\n\n // name = \"^1.0\" or name = { version = \"^1.0\", ... }\n const simpleMatch = trimmed.match(/^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)\\s*=\\s*\"([^\"]+)\"/);\n if (simpleMatch) {\n const name = simpleMatch[1];\n if (name === 'python') continue; // skip python version constraint\n deps.push({ name: normalizePypiName(name), constraint: simpleMatch[3], scope });\n continue;\n }\n\n // name = { version = \"^1.0\", ... }\n const tableMatch = trimmed.match(/^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)\\s*=\\s*\\{/);\n if (tableMatch) {\n const name = tableMatch[1];\n if (name === 'python') continue;\n const versionMatch = trimmed.match(/version\\s*=\\s*\"([^\"]+)\"/);\n if (versionMatch) {\n deps.push({ name: normalizePypiName(name), constraint: versionMatch[1], scope });\n } else {\n // path/git dep without version \u2014 record with wildcard\n deps.push({ name: normalizePypiName(name), constraint: '*', scope });\n }\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 PDM extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction extractPdmDependencies(content: string, relativeManifest: string): RawDep[] {\n const deps: RawDep[] = [];\n\n // [tool.pdm.dev-dependencies] groups with arrays\n const section = extractSection(content, /^\\[tool\\.pdm\\.dev-dependencies\\]/m);\n if (section) {\n for (const m of section.matchAll(/^\\s*([a-zA-Z0-9_-]+)\\s*=\\s*\\[([^\\]]*)\\]/gm)) {\n const items = parseTomlStringArray(m[2]);\n for (const spec of items) {\n const dep = parsePep508Spec(spec, relativeManifest);\n if (dep) deps.push({ ...dep, scope: 'development' });\n }\n }\n }\n\n return deps;\n}\n\n// \u2500\u2500 PEP 508 spec parser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction parsePep508Spec(spec: string, _relativeManifest: string): { name: string; constraint: string } | null {\n // PEP 508: name [extras] [version_spec] [; marker]\n // Strip markers (semicolon and beyond), extras ([...])\n const withoutMarker = spec.split(';')[0].trim();\n const withoutExtras = withoutMarker.replace(/\\[[^\\]]*\\]/, '').trim();\n\n const match = withoutExtras.match(/^([a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?)\\s*(.*)/);\n if (!match) return null;\n\n const name = normalizePypiName(match[1]);\n const constraint = match[3].trim() || '*';\n return { name, constraint };\n}\n\n// \u2500\u2500 Lock file loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n // Try poetry.lock, pdm.lock, uv.lock in order\n const candidates = ['poetry.lock', 'pdm.lock', 'uv.lock'];\n for (const lockFile of candidates) {\n const lockPath = path.join(manifestDir, lockFile);\n if (!fs.existsSync(lockPath)) continue;\n try {\n const content = fs.readFileSync(lockPath, 'utf8');\n if (lockFile === 'poetry.lock') extractFromPoetryLock(content, map);\n else extractFromPdmUvLock(content, map);\n break; // only use the first lock file found\n } catch (err) {\n logWarn(`[sec:pyproject] Failed to parse ${lockFile}: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return map;\n}\n\nfunction extractFromPoetryLock(content: string, map: ResolvedVersionMap): void {\n // [[package]] blocks with name and version fields\n for (const m of content.matchAll(/\\[\\[package\\]\\][^[]*?name\\s*=\\s*\"([^\"]+)\"[^[]*?version\\s*=\\s*\"([^\"]+)\"/gs)) {\n map.set(m[1].toLowerCase(), m[2]);\n }\n}\n\nfunction extractFromPdmUvLock(content: string, map: ResolvedVersionMap): void {\n // [[package]] or [[distribution]] blocks (pdm/uv use slightly different headers)\n for (const m of content.matchAll(/\\[\\[(?:package|distribution)\\]\\][^[]*?name\\s*=\\s*\"([^\"]+)\"[^[]*?version\\s*=\\s*\"([^\"]+)\"/gs)) {\n map.set(m[1].toLowerCase(), m[2]);\n }\n}\n\n// \u2500\u2500 TOML helpers (no library dependency) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Extract the content after a section header until the next header. */\nfunction extractSection(content: string, headerPattern: RegExp): string | null {\n const match = headerPattern.exec(content);\n if (!match) return null;\n const start = match.index + match[0].length;\n const remaining = content.slice(start);\n const nextSection = remaining.match(/^\\[/m);\n return nextSection ? remaining.slice(0, nextSection.index) : remaining;\n}\n\n/** Extract a TOML array value by key from a section. */\nfunction extractTomlArray(content: string, sectionPattern: RegExp, key: string): string[] {\n const section = extractSection(content, sectionPattern);\n if (!section) return [];\n\n // Match key = [\"...\", \"...\", ...] \u2014 may span multiple lines\n const keyPattern = new RegExp(`^${key}\\\\s*=\\\\s*\\\\[([^\\\\]]*(?:\\\\][^\\\\[\\\\]]*\\\\[)*[^\\\\]]*)\\\\]`, 'ms');\n const m = keyPattern.exec(section);\n if (!m) return [];\n return parseTomlStringArray(m[1]);\n}\n\n/** Parse a TOML string array body (contents inside [...]) into individual strings. */\nfunction parseTomlStringArray(body: string): string[] {\n const results: string[] = [];\n for (const m of body.matchAll(/\"([^\"]+)\"/g)) {\n results.push(m[1].trim());\n }\n for (const m of body.matchAll(/'([^']+)'/g)) {\n results.push(m[1].trim());\n }\n return results.filter(Boolean);\n}\n\n/** Normalize PyPI package name: lowercase, replace - and _ with - (canonical form). */\nfunction normalizePypiName(name: string): string {\n return name.toLowerCase().replace(/[-_.]+/g, '-');\n}\n\n/**\n * Extract the license from a pyproject.toml file.\n *\n * Handles:\n * - PEP 621 [project] license = {text = \"MIT\"} or license = \"MIT\"\n * - Poetry [tool.poetry] license = \"MIT\"\n */\nfunction extractPyprojectLicense(content: string): string | undefined {\n // PEP 621: license = {text = \"MIT\"} or license = {file = \"LICENSE\"}\n const pep621Section = extractSection(content, /^\\[project\\]/m);\n if (pep621Section) {\n // license = {text = \"MIT\"}\n const tableTextMatch = pep621Section.match(/^license\\s*=\\s*\\{[^}]*text\\s*=\\s*[\"']([^\"']+)[\"'][^}]*\\}/m);\n if (tableTextMatch && tableTextMatch[1].trim() !== '') {\n return tableTextMatch[1].trim();\n }\n // license = \"MIT\"\n const stringMatch = pep621Section.match(/^license\\s*=\\s*[\"']([^\"']+)[\"']/m);\n if (stringMatch && stringMatch[1].trim() !== '') {\n return stringMatch[1].trim();\n }\n }\n\n // Poetry: [tool.poetry] license = \"MIT\"\n const poetrySection = extractSection(content, /^\\[tool\\.poetry\\]/m);\n if (poetrySection) {\n const poetryLicenseMatch = poetrySection.match(/^license\\s*=\\s*[\"']([^\"']+)[\"']/m);\n if (poetryLicenseMatch && poetryLicenseMatch[1].trim() !== '') {\n return poetryLicenseMatch[1].trim();\n }\n }\n\n return undefined;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -84,6 +84,18 @@ async function parseRubygemsManifest(manifestPath, projectRoot) {
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
const directNames = new Set(dependencies.map((d) => d.name));
|
|
88
|
+
for (const [name, version] of resolvedVersions) {
|
|
89
|
+
if (directNames.has(name)) continue;
|
|
90
|
+
dependencies.push({
|
|
91
|
+
name,
|
|
92
|
+
declaredConstraint: version,
|
|
93
|
+
resolvedVersion: version,
|
|
94
|
+
scope: "production",
|
|
95
|
+
ecosystem: "rubygems",
|
|
96
|
+
sourceManifest: relativeManifest
|
|
97
|
+
});
|
|
98
|
+
}
|
|
87
99
|
return dependencies;
|
|
88
100
|
}
|
|
89
101
|
function parseGemLine(line, relativeManifest) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/security/manifest/plugins/rubygems.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * RubyGems Version Extraction Plugin for KiroGraph-Sec\n *\n * Parses Gemfile manifests to extract gem declarations with version constraints,\n * scopes (derived from `group` blocks), and resolved versions from Gemfile.lock.\n *\n * Handles:\n * - `gem 'name', '~> 1.0'` \u2014 single version constraint\n * - `gem 'name', '>= 1.0', '< 2.0'` \u2014 multiple constraints (joined with ', ')\n * - `gem 'name'` \u2014 no version constraint\n * - `group :development, :test do ... end` blocks \u2192 'development' scope\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\n/**\n * Resolved version map: gem name \u2192 resolved version string.\n * Built from Gemfile.lock.\n */\ntype ResolvedVersionMap = Map<string, string>;\n\n/**\n * Parse a Gemfile manifest and extract gem declarations with version constraints,\n * scopes, and resolved versions from Gemfile.lock.\n *\n * @param manifestPath - Absolute path to the Gemfile\n * @param projectRoot - Absolute path to the project root directory\n * @returns Array of parsed dependencies with version and scope information\n */\nexport async function parseRubygemsManifest(\n manifestPath: string,\n projectRoot: string,\n): Promise<ParsedDependency[]> {\n const manifestDir = path.dirname(manifestPath);\n const relativeManifest = path.relative(projectRoot, manifestPath).replace(/\\\\/g, '/');\n\n // Read the Gemfile content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:rubygems] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Attempt to load resolved versions from Gemfile.lock\n const resolvedVersions = loadResolvedVersions(manifestDir);\n\n // Attempt to extract license from a gemspec in the same directory\n const license = loadGemspecLicense(manifestDir);\n\n // Extract dependencies from the Gemfile\n const dependencies: ParsedDependency[] = [];\n const lines = content.split('\\n');\n\n // Track current group scope \u2014 null means top-level (production)\n let currentScope: ParsedDependency['scope'] = 'production';\n let groupDepth = 0; // tracks nesting depth inside a group block\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n\n // Detect start of a group block: `group :test, :development do`\n const groupMatch = trimmed.match(/^group\\s+(.+?)\\s+do\\s*(?:#.*)?$/);\n if (groupMatch) {\n groupDepth++;\n if (groupDepth === 1) {\n currentScope = resolveGroupScope(groupMatch[1]);\n }\n continue;\n }\n\n // Detect `end` \u2014 only reset scope when we exit the outermost group block\n if (trimmed === 'end' || trimmed.startsWith('end ') || trimmed.startsWith('end#')) {\n if (groupDepth > 0) {\n groupDepth--;\n if (groupDepth === 0) {\n currentScope = 'production';\n }\n }\n continue;\n }\n\n // Match gem declarations\n const dep = parseGemLine(trimmed, relativeManifest);\n if (dep) {\n const resolvedVersion = resolvedVersions.get(dep.name);\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion,\n scope: currentScope,\n ecosystem: 'rubygems',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n\n return dependencies;\n}\n\n/**\n * A raw dependency entry extracted from a Gemfile line.\n */\ninterface RawDependency {\n name: string;\n constraint: string;\n}\n\n/**\n * Parse a single `gem` declaration line from a Gemfile.\n *\n * Handles:\n * - `gem 'name'`\n * - `gem 'name', '~> 1.0'`\n * - `gem \"name\", \"~> 1.0\"`\n * - `gem 'name', '>= 1.0', '< 2.0'` (multiple constraints joined with ', ')\n */\nfunction parseGemLine(line: string, relativeManifest: string): RawDependency | null {\n // Strip inline comments\n const withoutComment = line.replace(/\\s*#.*$/, '');\n\n // Match: gem 'name' or gem \"name\" followed by optional version strings\n const gemMatch = withoutComment.match(/^gem\\s+(['\"])([^'\"]+)\\1(.*)?$/);\n if (!gemMatch) return null;\n\n const name = gemMatch[2].trim();\n const rest = (gemMatch[3] || '').trim();\n\n if (!isValidGemName(name)) {\n logWarn(`[sec:rubygems] Invalid gem name \"${name}\" in ${relativeManifest} \u2014 skipping`);\n return null;\n }\n\n // No version constraint\n if (rest === '' || rest.startsWith(':') || rest.startsWith(',\\s*:')) {\n return { name, constraint: '*' };\n }\n\n // Extract all string literals from the rest of the line that look like version constraints\n const versionConstraints: string[] = [];\n const versionPattern = /['\"]([^'\"]*)['\"]/g;\n let versionMatch: RegExpExecArray | null;\n while ((versionMatch = versionPattern.exec(rest)) !== null) {\n const candidate = versionMatch[1].trim();\n // Only capture strings that look like version constraints (start with ~>, >=, <=, !=, >, <, =, or a digit)\n if (/^(~>|>=|<=|!=|>|<|=|\\d)/.test(candidate)) {\n versionConstraints.push(candidate);\n }\n }\n\n if (versionConstraints.length === 0) {\n return { name, constraint: '*' };\n }\n\n return { name, constraint: versionConstraints.join(', ') };\n}\n\n/**\n * Determine the dependency scope from a group specifier string.\n * Any group containing :test or :development maps to 'development'.\n *\n * @param groupSpec - The group specifier string, e.g., `:development, :test`\n */\nfunction resolveGroupScope(groupSpec: string): ParsedDependency['scope'] {\n if (/:test\\b/.test(groupSpec) || /:development\\b/.test(groupSpec)) {\n return 'development';\n }\n return 'production';\n}\n\n/**\n * Validate that a gem name follows RubyGems naming conventions.\n */\nfunction isValidGemName(name: string): boolean {\n if (!name || typeof name !== 'string') return false;\n if (name.length > 128) return false;\n return /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name);\n}\n\n/**\n * Attempt to load resolved versions from Gemfile.lock in the given directory.\n * Returns an empty map if Gemfile.lock is not available.\n *\n * Gemfile.lock GEM section format:\n * ```\n * GEM\n * remote: https://rubygems.org/\n * specs:\n * rails (7.0.4)\n * rake (13.0.6)\n * ```\n */\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n const lockPath = path.join(manifestDir, 'Gemfile.lock');\n if (!fs.existsSync(lockPath)) {\n return map;\n }\n\n try {\n const lockContent = fs.readFileSync(lockPath, 'utf8');\n extractFromGemfileLock(lockContent, map);\n } catch (err) {\n logWarn(`[sec:rubygems] Failed to parse Gemfile.lock: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n return map;\n}\n\n/**\n * Extract resolved versions from Gemfile.lock content.\n *\n * The GEM section contains a `specs:` subsection with lines of the form:\n * ` gemname (1.2.3)` (4-space indent for direct deps, 6-space for sub-deps).\n * We capture all entries under `specs:`.\n */\nfunction extractFromGemfileLock(content: string, map: ResolvedVersionMap): void {\n const lines = content.split('\\n');\n let inGemSection = false;\n let inSpecsSection = false;\n\n for (const line of lines) {\n // Detect top-level section headers (no indent)\n if (/^\\S/.test(line)) {\n inGemSection = line.trimEnd() === 'GEM';\n inSpecsSection = false;\n continue;\n }\n\n if (!inGemSection) continue;\n\n // Detect ` specs:` subsection (2-space indent)\n if (/^ specs:\\s*$/.test(line)) {\n inSpecsSection = true;\n continue;\n }\n\n if (!inSpecsSection) continue;\n\n // Parse gem entries: ` gemname (1.2.3)` \u2014 4+ spaces indent\n const specMatch = line.match(/^ ([a-zA-Z0-9][a-zA-Z0-9_.-]*)\\s+\\(([^)]+)\\)\\s*$/);\n if (specMatch) {\n const name = specMatch[1];\n const version = specMatch[2].trim();\n if (!map.has(name)) {\n map.set(name, version);\n }\n }\n }\n}\n\n/**\n * Attempt to load the license from a .gemspec file in the given directory.\n * Looks for: s.license = 'MIT' or spec.license = \"MIT\"\n */\nfunction loadGemspecLicense(dir: string): string | undefined {\n try {\n const entries = fs.readdirSync(dir);\n const gemspecFile = entries.find(f => f.endsWith('.gemspec'));\n if (!gemspecFile) return undefined;\n const content = fs.readFileSync(path.join(dir, gemspecFile), 'utf8');\n const match = content.match(/\\.license\\s*=\\s*['\"]([^'\"]+)['\"]/);\n if (match && match[1].trim() !== '') {\n return match[1].trim();\n }\n } catch {\n // best-effort \u2014 ignore errors\n }\n return undefined;\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\n * RubyGems Version Extraction Plugin for KiroGraph-Sec\n *\n * Parses Gemfile manifests to extract gem declarations with version constraints,\n * scopes (derived from `group` blocks), and resolved versions from Gemfile.lock.\n *\n * Handles:\n * - `gem 'name', '~> 1.0'` \u2014 single version constraint\n * - `gem 'name', '>= 1.0', '< 2.0'` \u2014 multiple constraints (joined with ', ')\n * - `gem 'name'` \u2014 no version constraint\n * - `group :development, :test do ... end` blocks \u2192 'development' scope\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\n/**\n * Resolved version map: gem name \u2192 resolved version string.\n * Built from Gemfile.lock.\n */\ntype ResolvedVersionMap = Map<string, string>;\n\n/**\n * Parse a Gemfile manifest and extract gem declarations with version constraints,\n * scopes, and resolved versions from Gemfile.lock.\n *\n * @param manifestPath - Absolute path to the Gemfile\n * @param projectRoot - Absolute path to the project root directory\n * @returns Array of parsed dependencies with version and scope information\n */\nexport async function parseRubygemsManifest(\n manifestPath: string,\n projectRoot: string,\n): Promise<ParsedDependency[]> {\n const manifestDir = path.dirname(manifestPath);\n const relativeManifest = path.relative(projectRoot, manifestPath).replace(/\\\\/g, '/');\n\n // Read the Gemfile content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:rubygems] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Attempt to load resolved versions from Gemfile.lock\n const resolvedVersions = loadResolvedVersions(manifestDir);\n\n // Attempt to extract license from a gemspec in the same directory\n const license = loadGemspecLicense(manifestDir);\n\n // Extract dependencies from the Gemfile\n const dependencies: ParsedDependency[] = [];\n const lines = content.split('\\n');\n\n // Track current group scope \u2014 null means top-level (production)\n let currentScope: ParsedDependency['scope'] = 'production';\n let groupDepth = 0; // tracks nesting depth inside a group block\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n\n // Detect start of a group block: `group :test, :development do`\n const groupMatch = trimmed.match(/^group\\s+(.+?)\\s+do\\s*(?:#.*)?$/);\n if (groupMatch) {\n groupDepth++;\n if (groupDepth === 1) {\n currentScope = resolveGroupScope(groupMatch[1]);\n }\n continue;\n }\n\n // Detect `end` \u2014 only reset scope when we exit the outermost group block\n if (trimmed === 'end' || trimmed.startsWith('end ') || trimmed.startsWith('end#')) {\n if (groupDepth > 0) {\n groupDepth--;\n if (groupDepth === 0) {\n currentScope = 'production';\n }\n }\n continue;\n }\n\n // Match gem declarations\n const dep = parseGemLine(trimmed, relativeManifest);\n if (dep) {\n const resolvedVersion = resolvedVersions.get(dep.name);\n dependencies.push({\n name: dep.name,\n declaredConstraint: dep.constraint,\n resolvedVersion,\n scope: currentScope,\n ecosystem: 'rubygems',\n sourceManifest: relativeManifest,\n ...(license !== undefined ? { license } : {}),\n });\n }\n }\n\n // Transitive-only gems (present in Gemfile.lock but never declared in the\n // Gemfile) would otherwise never become a Dependency_Node and would\n // silently skip vulnerability scanning. resolvedVersions already has every\n // gem Gemfile.lock resolved, direct or transitive.\n const directNames = new Set(dependencies.map(d => d.name));\n for (const [name, version] of resolvedVersions) {\n if (directNames.has(name)) continue;\n dependencies.push({\n name,\n declaredConstraint: version,\n resolvedVersion: version,\n scope: 'production',\n ecosystem: 'rubygems',\n sourceManifest: relativeManifest,\n });\n }\n\n return dependencies;\n}\n\n/**\n * A raw dependency entry extracted from a Gemfile line.\n */\ninterface RawDependency {\n name: string;\n constraint: string;\n}\n\n/**\n * Parse a single `gem` declaration line from a Gemfile.\n *\n * Handles:\n * - `gem 'name'`\n * - `gem 'name', '~> 1.0'`\n * - `gem \"name\", \"~> 1.0\"`\n * - `gem 'name', '>= 1.0', '< 2.0'` (multiple constraints joined with ', ')\n */\nfunction parseGemLine(line: string, relativeManifest: string): RawDependency | null {\n // Strip inline comments\n const withoutComment = line.replace(/\\s*#.*$/, '');\n\n // Match: gem 'name' or gem \"name\" followed by optional version strings\n const gemMatch = withoutComment.match(/^gem\\s+(['\"])([^'\"]+)\\1(.*)?$/);\n if (!gemMatch) return null;\n\n const name = gemMatch[2].trim();\n const rest = (gemMatch[3] || '').trim();\n\n if (!isValidGemName(name)) {\n logWarn(`[sec:rubygems] Invalid gem name \"${name}\" in ${relativeManifest} \u2014 skipping`);\n return null;\n }\n\n // No version constraint\n if (rest === '' || rest.startsWith(':') || rest.startsWith(',\\s*:')) {\n return { name, constraint: '*' };\n }\n\n // Extract all string literals from the rest of the line that look like version constraints\n const versionConstraints: string[] = [];\n const versionPattern = /['\"]([^'\"]*)['\"]/g;\n let versionMatch: RegExpExecArray | null;\n while ((versionMatch = versionPattern.exec(rest)) !== null) {\n const candidate = versionMatch[1].trim();\n // Only capture strings that look like version constraints (start with ~>, >=, <=, !=, >, <, =, or a digit)\n if (/^(~>|>=|<=|!=|>|<|=|\\d)/.test(candidate)) {\n versionConstraints.push(candidate);\n }\n }\n\n if (versionConstraints.length === 0) {\n return { name, constraint: '*' };\n }\n\n return { name, constraint: versionConstraints.join(', ') };\n}\n\n/**\n * Determine the dependency scope from a group specifier string.\n * Any group containing :test or :development maps to 'development'.\n *\n * @param groupSpec - The group specifier string, e.g., `:development, :test`\n */\nfunction resolveGroupScope(groupSpec: string): ParsedDependency['scope'] {\n if (/:test\\b/.test(groupSpec) || /:development\\b/.test(groupSpec)) {\n return 'development';\n }\n return 'production';\n}\n\n/**\n * Validate that a gem name follows RubyGems naming conventions.\n */\nfunction isValidGemName(name: string): boolean {\n if (!name || typeof name !== 'string') return false;\n if (name.length > 128) return false;\n return /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name);\n}\n\n/**\n * Attempt to load resolved versions from Gemfile.lock in the given directory.\n * Returns an empty map if Gemfile.lock is not available.\n *\n * Gemfile.lock GEM section format:\n * ```\n * GEM\n * remote: https://rubygems.org/\n * specs:\n * rails (7.0.4)\n * rake (13.0.6)\n * ```\n */\nfunction loadResolvedVersions(manifestDir: string): ResolvedVersionMap {\n const map: ResolvedVersionMap = new Map();\n\n const lockPath = path.join(manifestDir, 'Gemfile.lock');\n if (!fs.existsSync(lockPath)) {\n return map;\n }\n\n try {\n const lockContent = fs.readFileSync(lockPath, 'utf8');\n extractFromGemfileLock(lockContent, map);\n } catch (err) {\n logWarn(`[sec:rubygems] Failed to parse Gemfile.lock: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n return map;\n}\n\n/**\n * Extract resolved versions from Gemfile.lock content.\n *\n * The GEM section contains a `specs:` subsection with lines of the form:\n * ` gemname (1.2.3)` (4-space indent for direct deps, 6-space for sub-deps).\n * We capture all entries under `specs:`.\n */\nfunction extractFromGemfileLock(content: string, map: ResolvedVersionMap): void {\n const lines = content.split('\\n');\n let inGemSection = false;\n let inSpecsSection = false;\n\n for (const line of lines) {\n // Detect top-level section headers (no indent)\n if (/^\\S/.test(line)) {\n inGemSection = line.trimEnd() === 'GEM';\n inSpecsSection = false;\n continue;\n }\n\n if (!inGemSection) continue;\n\n // Detect ` specs:` subsection (2-space indent)\n if (/^ specs:\\s*$/.test(line)) {\n inSpecsSection = true;\n continue;\n }\n\n if (!inSpecsSection) continue;\n\n // Parse gem entries: ` gemname (1.2.3)` \u2014 4+ spaces indent\n const specMatch = line.match(/^ ([a-zA-Z0-9][a-zA-Z0-9_.-]*)\\s+\\(([^)]+)\\)\\s*$/);\n if (specMatch) {\n const name = specMatch[1];\n const version = specMatch[2].trim();\n if (!map.has(name)) {\n map.set(name, version);\n }\n }\n }\n}\n\n/**\n * Attempt to load the license from a .gemspec file in the given directory.\n * Looks for: s.license = 'MIT' or spec.license = \"MIT\"\n */\nfunction loadGemspecLicense(dir: string): string | undefined {\n try {\n const entries = fs.readdirSync(dir);\n const gemspecFile = entries.find(f => f.endsWith('.gemspec'));\n if (!gemspecFile) return undefined;\n const content = fs.readFileSync(path.join(dir, gemspecFile), 'utf8');\n const match = content.match(/\\.license\\s*=\\s*['\"]([^'\"]+)['\"]/);\n if (match && match[1].trim() !== '') {\n return match[1].trim();\n }\n } catch {\n // best-effort \u2014 ignore errors\n }\n return undefined;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -51,7 +51,10 @@ class SecurityPipeline {
|
|
|
51
51
|
duration: 0
|
|
52
52
|
};
|
|
53
53
|
onProgress?.("manifest-parsing", 0, 4);
|
|
54
|
-
const parser = new import_parser.ManifestParser(this.db, this.projectRoot
|
|
54
|
+
const parser = new import_parser.ManifestParser(this.db, this.projectRoot, {
|
|
55
|
+
include: this.config.include,
|
|
56
|
+
exclude: this.config.exclude
|
|
57
|
+
});
|
|
55
58
|
const parseResult = await parser.parseAll();
|
|
56
59
|
result.manifestsDiscovered = parseResult.manifestsParsed;
|
|
57
60
|
result.dependenciesCreated = parseResult.dependenciesCreated;
|
|
@@ -65,7 +68,7 @@ class SecurityPipeline {
|
|
|
65
68
|
if (this.config.securityAutoEnrich) {
|
|
66
69
|
onProgress?.("vulnerability-enrichment", 2, 4);
|
|
67
70
|
const adapters = this.createVulnAdapters();
|
|
68
|
-
const vulnClient = new import_client.VulnerabilityDatabaseClient(adapters, this.db);
|
|
71
|
+
const vulnClient = new import_client.VulnerabilityDatabaseClient(adapters, this.db, void 0, this.projectRoot);
|
|
69
72
|
const enrichResult = await vulnClient.enrichAll();
|
|
70
73
|
result.vulnerabilitiesFound = enrichResult.vulnerabilitiesFound;
|
|
71
74
|
onProgress?.("vulnerability-enrichment", 3, 4);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/security/pipeline.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * SecurityPipeline \u2014 Orchestrates the security analysis phases.\n *\n * Phases (in order):\n * 1. Manifest parsing \u2014 discover and parse dependency manifests\n * 2. Dependency integration \u2014 link dependencies to code symbols\n * 3. Vulnerability enrichment \u2014 query databases for CVEs (if securityAutoEnrich)\n * 4. Reachability analysis \u2014 determine if vulnerable code is reachable\n *\n * Reports progress via callback and returns a SecurityResult with counts and duration.\n *\n * Requirements: 9.3, 9.5\n */\n\nimport type { GraphDatabase } from '../db/database';\nimport type { KiroGraphConfig } from '../config';\nimport type { SecurityResult } from './types';\nimport { ManifestParser } from './manifest/parser';\nimport { DependencyGraphIntegrator } from './integrator';\nimport { VulnerabilityDatabaseClient } from './vuln/client';\nimport { OsvAdapter } from './vuln/osv-adapter';\nimport { ReachabilityAnalyzer } from './reachability';\nimport { logWarn } from '../errors';\n\n/**\n * SecurityPipeline orchestrates the full security analysis workflow.\n */\nexport class SecurityPipeline {\n private readonly db: GraphDatabase;\n private readonly config: KiroGraphConfig;\n private readonly projectRoot: string;\n\n constructor(db: GraphDatabase, config: KiroGraphConfig, projectRoot: string) {\n this.db = db;\n this.config = config;\n this.projectRoot = projectRoot;\n }\n\n /**\n * Run the full security analysis pipeline.\n *\n * @param onProgress - Optional callback reporting (phase, current, total)\n * @returns SecurityResult with counts and duration\n */\n async run(\n onProgress?: (phase: string, current: number, total: number) => void,\n ): Promise<SecurityResult> {\n const start = Date.now();\n\n const result: SecurityResult = {\n manifestsDiscovered: 0,\n dependenciesCreated: 0,\n vulnerabilitiesFound: 0,\n affectedCount: 0,\n notAffectedCount: 0,\n underInvestigationCount: 0,\n duration: 0,\n };\n\n // \u2500\u2500 Phase 1: Manifest Parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.('manifest-parsing', 0, 4);\n\n const parser = new ManifestParser(this.db, this.projectRoot);\n const parseResult = await parser.parseAll();\n\n result.manifestsDiscovered = parseResult.manifestsParsed;\n result.dependenciesCreated = parseResult.dependenciesCreated;\n\n onProgress?.('manifest-parsing', 1, 4);\n\n // \u2500\u2500 Phase 2: Dependency Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.('dependency-integration', 1, 4);\n\n const integrator = new DependencyGraphIntegrator(this.db, this.projectRoot);\n await integrator.integrate();\n await integrator.resolveTransitives(10);\n await integrator.cleanup();\n\n onProgress?.('dependency-integration', 2, 4);\n\n // \u2500\u2500 Phase 3: Vulnerability Enrichment (conditional) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (this.config.securityAutoEnrich) {\n onProgress?.('vulnerability-enrichment', 2, 4);\n\n const adapters = this.createVulnAdapters();\n const vulnClient = new VulnerabilityDatabaseClient(adapters, this.db);\n const enrichResult = await vulnClient.enrichAll();\n\n result.vulnerabilitiesFound = enrichResult.vulnerabilitiesFound;\n\n onProgress?.('vulnerability-enrichment', 3, 4);\n } else {\n onProgress?.('vulnerability-enrichment', 3, 4);\n }\n\n // \u2500\u2500 Phase 4: Reachability Analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.('reachability-analysis', 3, 4);\n\n const reachability = new ReachabilityAnalyzer(this.db, this.config);\n const reachResults = await reachability.analyzeAll();\n\n // Count verdicts\n for (const [, reachResult] of reachResults) {\n switch (reachResult.verdict) {\n case 'affected':\n result.affectedCount++;\n break;\n case 'not_affected':\n result.notAffectedCount++;\n break;\n case 'under_investigation':\n result.underInvestigationCount++;\n break;\n }\n }\n\n // Generate impact summaries for affected vulnerabilities\n for (const [vulnNodeId, reachResult] of reachResults) {\n if (reachResult.verdict === 'affected') {\n await reachability.getImpactSummary(vulnNodeId);\n }\n }\n\n onProgress?.('reachability-analysis', 4, 4);\n\n result.duration = Date.now() - start;\n return result;\n }\n\n /**\n * Create vulnerability database adapters based on config.\n */\n private createVulnAdapters(): import('./vuln/types').VulnDatabaseAdapter[] {\n const adapters: import('./vuln/types').VulnDatabaseAdapter[] = [];\n\n for (const dbName of this.config.securityDatabases) {\n switch (dbName.toUpperCase()) {\n case 'OSV':\n adapters.push(new OsvAdapter());\n break;\n default:\n logWarn(`[sec:pipeline] Unknown vulnerability database \"${dbName}\", skipping`);\n break;\n }\n }\n\n // Fallback to OSV if no valid adapters configured\n if (adapters.length === 0) {\n adapters.push(new OsvAdapter());\n }\n\n return adapters;\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,oBAA+B;AAC/B,wBAA0C;AAC1C,oBAA4C;AAC5C,yBAA2B;AAC3B,0BAAqC;AACrC,oBAAwB;AAKjB,MAAM,iBAAiB;AAAA,EAK5B,YAAY,IAAmB,QAAyB,aAAqB;AAC3E,SAAK,KAAK;AACV,SAAK,SAAS;AACd,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,IACJ,YACyB;AACzB,UAAM,QAAQ,KAAK,IAAI;AAEvB,UAAM,SAAyB;AAAA,MAC7B,qBAAqB;AAAA,MACrB,qBAAqB;AAAA,MACrB,sBAAsB;AAAA,MACtB,eAAe;AAAA,MACf,kBAAkB;AAAA,MAClB,yBAAyB;AAAA,MACzB,UAAU;AAAA,IACZ;AAGA,iBAAa,oBAAoB,GAAG,CAAC;AAErC,UAAM,SAAS,IAAI,6BAAe,KAAK,IAAI,KAAK,
|
|
4
|
+
"sourcesContent": ["/**\n * SecurityPipeline \u2014 Orchestrates the security analysis phases.\n *\n * Phases (in order):\n * 1. Manifest parsing \u2014 discover and parse dependency manifests\n * 2. Dependency integration \u2014 link dependencies to code symbols\n * 3. Vulnerability enrichment \u2014 query databases for CVEs (if securityAutoEnrich)\n * 4. Reachability analysis \u2014 determine if vulnerable code is reachable\n *\n * Reports progress via callback and returns a SecurityResult with counts and duration.\n *\n * Requirements: 9.3, 9.5\n */\n\nimport type { GraphDatabase } from '../db/database';\nimport type { KiroGraphConfig } from '../config';\nimport type { SecurityResult } from './types';\nimport { ManifestParser } from './manifest/parser';\nimport { DependencyGraphIntegrator } from './integrator';\nimport { VulnerabilityDatabaseClient } from './vuln/client';\nimport { OsvAdapter } from './vuln/osv-adapter';\nimport { ReachabilityAnalyzer } from './reachability';\nimport { logWarn } from '../errors';\n\n/**\n * SecurityPipeline orchestrates the full security analysis workflow.\n */\nexport class SecurityPipeline {\n private readonly db: GraphDatabase;\n private readonly config: KiroGraphConfig;\n private readonly projectRoot: string;\n\n constructor(db: GraphDatabase, config: KiroGraphConfig, projectRoot: string) {\n this.db = db;\n this.config = config;\n this.projectRoot = projectRoot;\n }\n\n /**\n * Run the full security analysis pipeline.\n *\n * @param onProgress - Optional callback reporting (phase, current, total)\n * @returns SecurityResult with counts and duration\n */\n async run(\n onProgress?: (phase: string, current: number, total: number) => void,\n ): Promise<SecurityResult> {\n const start = Date.now();\n\n const result: SecurityResult = {\n manifestsDiscovered: 0,\n dependenciesCreated: 0,\n vulnerabilitiesFound: 0,\n affectedCount: 0,\n notAffectedCount: 0,\n underInvestigationCount: 0,\n duration: 0,\n };\n\n // \u2500\u2500 Phase 1: Manifest Parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.('manifest-parsing', 0, 4);\n\n const parser = new ManifestParser(this.db, this.projectRoot, {\n include: this.config.include,\n exclude: this.config.exclude,\n });\n const parseResult = await parser.parseAll();\n\n result.manifestsDiscovered = parseResult.manifestsParsed;\n result.dependenciesCreated = parseResult.dependenciesCreated;\n\n onProgress?.('manifest-parsing', 1, 4);\n\n // \u2500\u2500 Phase 2: Dependency Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.('dependency-integration', 1, 4);\n\n const integrator = new DependencyGraphIntegrator(this.db, this.projectRoot);\n await integrator.integrate();\n await integrator.resolveTransitives(10);\n await integrator.cleanup();\n\n onProgress?.('dependency-integration', 2, 4);\n\n // \u2500\u2500 Phase 3: Vulnerability Enrichment (conditional) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (this.config.securityAutoEnrich) {\n onProgress?.('vulnerability-enrichment', 2, 4);\n\n const adapters = this.createVulnAdapters();\n const vulnClient = new VulnerabilityDatabaseClient(adapters, this.db, undefined, this.projectRoot);\n const enrichResult = await vulnClient.enrichAll();\n\n result.vulnerabilitiesFound = enrichResult.vulnerabilitiesFound;\n\n onProgress?.('vulnerability-enrichment', 3, 4);\n } else {\n onProgress?.('vulnerability-enrichment', 3, 4);\n }\n\n // \u2500\u2500 Phase 4: Reachability Analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n onProgress?.('reachability-analysis', 3, 4);\n\n const reachability = new ReachabilityAnalyzer(this.db, this.config);\n const reachResults = await reachability.analyzeAll();\n\n // Count verdicts\n for (const [, reachResult] of reachResults) {\n switch (reachResult.verdict) {\n case 'affected':\n result.affectedCount++;\n break;\n case 'not_affected':\n result.notAffectedCount++;\n break;\n case 'under_investigation':\n result.underInvestigationCount++;\n break;\n }\n }\n\n // Generate impact summaries for affected vulnerabilities\n for (const [vulnNodeId, reachResult] of reachResults) {\n if (reachResult.verdict === 'affected') {\n await reachability.getImpactSummary(vulnNodeId);\n }\n }\n\n onProgress?.('reachability-analysis', 4, 4);\n\n result.duration = Date.now() - start;\n return result;\n }\n\n /**\n * Create vulnerability database adapters based on config.\n */\n private createVulnAdapters(): import('./vuln/types').VulnDatabaseAdapter[] {\n const adapters: import('./vuln/types').VulnDatabaseAdapter[] = [];\n\n for (const dbName of this.config.securityDatabases) {\n switch (dbName.toUpperCase()) {\n case 'OSV':\n adapters.push(new OsvAdapter());\n break;\n default:\n logWarn(`[sec:pipeline] Unknown vulnerability database \"${dbName}\", skipping`);\n break;\n }\n }\n\n // Fallback to OSV if no valid adapters configured\n if (adapters.length === 0) {\n adapters.push(new OsvAdapter());\n }\n\n return adapters;\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,oBAA+B;AAC/B,wBAA0C;AAC1C,oBAA4C;AAC5C,yBAA2B;AAC3B,0BAAqC;AACrC,oBAAwB;AAKjB,MAAM,iBAAiB;AAAA,EAK5B,YAAY,IAAmB,QAAyB,aAAqB;AAC3E,SAAK,KAAK;AACV,SAAK,SAAS;AACd,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,IACJ,YACyB;AACzB,UAAM,QAAQ,KAAK,IAAI;AAEvB,UAAM,SAAyB;AAAA,MAC7B,qBAAqB;AAAA,MACrB,qBAAqB;AAAA,MACrB,sBAAsB;AAAA,MACtB,eAAe;AAAA,MACf,kBAAkB;AAAA,MAClB,yBAAyB;AAAA,MACzB,UAAU;AAAA,IACZ;AAGA,iBAAa,oBAAoB,GAAG,CAAC;AAErC,UAAM,SAAS,IAAI,6BAAe,KAAK,IAAI,KAAK,aAAa;AAAA,MAC3D,SAAS,KAAK,OAAO;AAAA,MACrB,SAAS,KAAK,OAAO;AAAA,IACvB,CAAC;AACD,UAAM,cAAc,MAAM,OAAO,SAAS;AAE1C,WAAO,sBAAsB,YAAY;AACzC,WAAO,sBAAsB,YAAY;AAEzC,iBAAa,oBAAoB,GAAG,CAAC;AAGrC,iBAAa,0BAA0B,GAAG,CAAC;AAE3C,UAAM,aAAa,IAAI,4CAA0B,KAAK,IAAI,KAAK,WAAW;AAC1E,UAAM,WAAW,UAAU;AAC3B,UAAM,WAAW,mBAAmB,EAAE;AACtC,UAAM,WAAW,QAAQ;AAEzB,iBAAa,0BAA0B,GAAG,CAAC;AAG3C,QAAI,KAAK,OAAO,oBAAoB;AAClC,mBAAa,4BAA4B,GAAG,CAAC;AAE7C,YAAM,WAAW,KAAK,mBAAmB;AACzC,YAAM,aAAa,IAAI,0CAA4B,UAAU,KAAK,IAAI,QAAW,KAAK,WAAW;AACjG,YAAM,eAAe,MAAM,WAAW,UAAU;AAEhD,aAAO,uBAAuB,aAAa;AAE3C,mBAAa,4BAA4B,GAAG,CAAC;AAAA,IAC/C,OAAO;AACL,mBAAa,4BAA4B,GAAG,CAAC;AAAA,IAC/C;AAGA,iBAAa,yBAAyB,GAAG,CAAC;AAE1C,UAAM,eAAe,IAAI,yCAAqB,KAAK,IAAI,KAAK,MAAM;AAClE,UAAM,eAAe,MAAM,aAAa,WAAW;AAGnD,eAAW,CAAC,EAAE,WAAW,KAAK,cAAc;AAC1C,cAAQ,YAAY,SAAS;AAAA,QAC3B,KAAK;AACH,iBAAO;AACP;AAAA,QACF,KAAK;AACH,iBAAO;AACP;AAAA,QACF,KAAK;AACH,iBAAO;AACP;AAAA,MACJ;AAAA,IACF;AAGA,eAAW,CAAC,YAAY,WAAW,KAAK,cAAc;AACpD,UAAI,YAAY,YAAY,YAAY;AACtC,cAAM,aAAa,iBAAiB,UAAU;AAAA,MAChD;AAAA,IACF;AAEA,iBAAa,yBAAyB,GAAG,CAAC;AAE1C,WAAO,WAAW,KAAK,IAAI,IAAI;AAC/B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,qBAAmE;AACzE,UAAM,WAAyD,CAAC;AAEhE,eAAW,UAAU,KAAK,OAAO,mBAAmB;AAClD,cAAQ,OAAO,YAAY,GAAG;AAAA,QAC5B,KAAK;AACH,mBAAS,KAAK,IAAI,8BAAW,CAAC;AAC9B;AAAA,QACF;AACE,qCAAQ,kDAAkD,MAAM,aAAa;AAC7E;AAAA,MACJ;AAAA,IACF;AAGA,QAAI,SAAS,WAAW,GAAG;AACzB,eAAS,KAAK,IAAI,8BAAW,CAAC;AAAA,IAChC;AAEA,WAAO;AAAA,EACT;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -18,10 +18,12 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var reachability_exports = {};
|
|
20
20
|
__export(reachability_exports, {
|
|
21
|
-
ReachabilityAnalyzer: () => ReachabilityAnalyzer
|
|
21
|
+
ReachabilityAnalyzer: () => ReachabilityAnalyzer,
|
|
22
|
+
explainReachability: () => explainReachability
|
|
22
23
|
});
|
|
23
24
|
module.exports = __toCommonJS(reachability_exports);
|
|
24
25
|
const TRAVERSAL_EDGE_KINDS = ["calls", "imports", "references"];
|
|
26
|
+
const TRAVERSAL_EDGE_KINDS_SQL = TRAVERSAL_EDGE_KINDS.map((k) => `'${k}'`).join(",");
|
|
25
27
|
const MAX_UNRESOLVED_SYMBOLS = 50;
|
|
26
28
|
const MAX_DISTINCT_PATHS = 100;
|
|
27
29
|
class ReachabilityAnalyzer {
|
|
@@ -34,7 +36,7 @@ class ReachabilityAnalyzer {
|
|
|
34
36
|
*
|
|
35
37
|
* 1. Find the Dependency_Node linked via `has_vulnerability` edge
|
|
36
38
|
* 2. Find all Entry_Points in the graph
|
|
37
|
-
* 3. BFS from
|
|
39
|
+
* 3. Reverse BFS from the dependency node through incoming edges toward entry points
|
|
38
40
|
* 4. Assign verdict based on path existence and unresolved imports
|
|
39
41
|
*
|
|
40
42
|
* Requirements: 4.1, 4.2, 4.3, 4.4, 4.6
|
|
@@ -46,54 +48,79 @@ class ReachabilityAnalyzer {
|
|
|
46
48
|
[vulnerabilityNodeId]
|
|
47
49
|
);
|
|
48
50
|
if (!depEdge) {
|
|
49
|
-
|
|
51
|
+
const result2 = {
|
|
50
52
|
verdict: "under_investigation",
|
|
53
|
+
reason: "no_dependency_link",
|
|
51
54
|
paths: [],
|
|
52
55
|
unresolvedSymbols: [],
|
|
53
56
|
reachingEntryPointCount: 0
|
|
54
57
|
};
|
|
58
|
+
this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result2);
|
|
59
|
+
return result2;
|
|
55
60
|
}
|
|
56
61
|
const dependencyNodeId = depEdge.source;
|
|
62
|
+
const incomingEdgeCount = rawDb.get(
|
|
63
|
+
`SELECT COUNT(*) as c FROM edges WHERE target = ? AND kind IN (${TRAVERSAL_EDGE_KINDS_SQL})`,
|
|
64
|
+
[dependencyNodeId]
|
|
65
|
+
);
|
|
66
|
+
if (incomingEdgeCount.c === 0) {
|
|
67
|
+
const result2 = {
|
|
68
|
+
verdict: "under_investigation",
|
|
69
|
+
reason: "no_call_graph_signal",
|
|
70
|
+
paths: [],
|
|
71
|
+
unresolvedSymbols: [],
|
|
72
|
+
reachingEntryPointCount: 0
|
|
73
|
+
};
|
|
74
|
+
this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result2);
|
|
75
|
+
return result2;
|
|
76
|
+
}
|
|
57
77
|
const entryPoints = rawDb.all(
|
|
58
78
|
`SELECT id FROM nodes WHERE kind = 'route'
|
|
59
79
|
UNION
|
|
60
80
|
SELECT id FROM nodes WHERE kind = 'function' AND is_exported = 1`
|
|
61
81
|
);
|
|
62
82
|
if (entryPoints.length === 0) {
|
|
63
|
-
|
|
83
|
+
const result2 = {
|
|
64
84
|
verdict: "not_affected",
|
|
85
|
+
reason: "no_entry_points",
|
|
65
86
|
paths: [],
|
|
66
87
|
unresolvedSymbols: [],
|
|
67
88
|
reachingEntryPointCount: 0
|
|
68
89
|
};
|
|
90
|
+
this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result2);
|
|
91
|
+
return result2;
|
|
69
92
|
}
|
|
70
93
|
const reachingPaths = [];
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
94
|
+
const entryPointIds = new Set(entryPoints.map((ep) => ep.id));
|
|
95
|
+
const { reached, unresolvedSymbols } = this.reverseBfsToEntryPoints(
|
|
96
|
+
rawDb,
|
|
97
|
+
dependencyNodeId,
|
|
98
|
+
entryPointIds
|
|
99
|
+
);
|
|
100
|
+
for (const { entryPointId, path } of reached) {
|
|
101
|
+
reachingPaths.push({
|
|
102
|
+
entryPoint: entryPointId,
|
|
103
|
+
path
|
|
104
|
+
});
|
|
83
105
|
}
|
|
84
|
-
const
|
|
106
|
+
const cappedUnresolved = unresolvedSymbols.slice(0, MAX_UNRESOLVED_SYMBOLS);
|
|
85
107
|
let verdict;
|
|
108
|
+
let reason;
|
|
86
109
|
if (reachingPaths.length > 0) {
|
|
87
110
|
verdict = "affected";
|
|
88
|
-
|
|
111
|
+
reason = "path_found";
|
|
112
|
+
} else if (cappedUnresolved.length > 0) {
|
|
89
113
|
verdict = "under_investigation";
|
|
114
|
+
reason = "unresolved_imports";
|
|
90
115
|
} else {
|
|
91
116
|
verdict = "not_affected";
|
|
117
|
+
reason = "no_path_found";
|
|
92
118
|
}
|
|
93
119
|
const result = {
|
|
94
120
|
verdict,
|
|
121
|
+
reason,
|
|
95
122
|
paths: reachingPaths,
|
|
96
|
-
unresolvedSymbols: verdict === "under_investigation" ?
|
|
123
|
+
unresolvedSymbols: verdict === "under_investigation" ? cappedUnresolved : [],
|
|
97
124
|
reachingEntryPointCount: reachingPaths.length
|
|
98
125
|
};
|
|
99
126
|
this.storeReachabilityResult(rawDb, vulnerabilityNodeId, result);
|
|
@@ -189,49 +216,66 @@ class ReachabilityAnalyzer {
|
|
|
189
216
|
return results;
|
|
190
217
|
}
|
|
191
218
|
/**
|
|
192
|
-
* BFS from
|
|
219
|
+
* Reverse BFS: start from the dependency node and walk BACKWARDS through
|
|
220
|
+
* incoming edges to find which entry points can reach it.
|
|
221
|
+
*
|
|
222
|
+
* This is O(V+E) total (one traversal) regardless of how many entry points exist,
|
|
223
|
+
* compared to O(entryPoints × (V+E)) for forward BFS from each entry point.
|
|
193
224
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
225
|
+
* While traversing, collects "unresolved imports" — import-kind nodes with no
|
|
226
|
+
* outgoing traversal edges. These are dead ends in the forward graph: the
|
|
227
|
+
* traversal reached them but couldn't follow them to a definition (dynamic
|
|
228
|
+
* dispatch, reflection, unindexed code). When no entry point reaches the
|
|
229
|
+
* dependency but such imports were encountered on the backward frontier,
|
|
230
|
+
* "not reachable" cannot be concluded and the caller reports
|
|
231
|
+
* under_investigation instead of not_affected.
|
|
197
232
|
*
|
|
198
|
-
* Returns the
|
|
233
|
+
* Returns the entry points that have a path to the dependency (with their
|
|
234
|
+
* paths) and the set of unresolved import node IDs encountered.
|
|
199
235
|
*/
|
|
200
|
-
|
|
236
|
+
reverseBfsToEntryPoints(rawDb, dependencyNodeId, entryPointIds) {
|
|
237
|
+
const reached = [];
|
|
238
|
+
const unresolvedSymbols = /* @__PURE__ */ new Set();
|
|
201
239
|
const visited = /* @__PURE__ */ new Set();
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
240
|
+
const parentMap = /* @__PURE__ */ new Map();
|
|
241
|
+
const edgeKinds = TRAVERSAL_EDGE_KINDS_SQL;
|
|
242
|
+
const queue = [dependencyNodeId];
|
|
243
|
+
visited.add(dependencyNodeId);
|
|
206
244
|
while (queue.length > 0) {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
245
|
+
const currentId = queue.shift();
|
|
246
|
+
if (currentId !== dependencyNodeId && entryPointIds.has(currentId)) {
|
|
247
|
+
const path = [];
|
|
248
|
+
let node = currentId;
|
|
249
|
+
while (node !== void 0) {
|
|
250
|
+
path.push(node);
|
|
251
|
+
node = parentMap.get(node);
|
|
252
|
+
}
|
|
253
|
+
reached.push({ entryPointId: currentId, path });
|
|
254
|
+
}
|
|
255
|
+
const inEdges = rawDb.all(
|
|
256
|
+
`SELECT source FROM edges WHERE target = ? AND kind IN (${edgeKinds})`,
|
|
210
257
|
[currentId]
|
|
211
258
|
);
|
|
212
|
-
if (
|
|
213
|
-
const
|
|
214
|
-
`SELECT
|
|
259
|
+
if (currentId !== dependencyNodeId) {
|
|
260
|
+
const outCount = rawDb.get(
|
|
261
|
+
`SELECT COUNT(*) AS c FROM edges WHERE source = ? AND kind IN (${edgeKinds})`,
|
|
215
262
|
[currentId]
|
|
216
263
|
);
|
|
217
|
-
if (
|
|
218
|
-
|
|
264
|
+
if (outCount.c === 0) {
|
|
265
|
+
const nodeRow = rawDb.get(`SELECT kind FROM nodes WHERE id = ?`, [currentId]);
|
|
266
|
+
if (nodeRow && nodeRow.kind === "import") {
|
|
267
|
+
unresolvedSymbols.add(currentId);
|
|
268
|
+
}
|
|
219
269
|
}
|
|
220
270
|
}
|
|
221
|
-
for (const edge of
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
unresolvedSymbols
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
if (visited.has(targetId)) continue;
|
|
230
|
-
visited.add(targetId);
|
|
231
|
-
queue.push([targetId, [...currentPath, targetId]]);
|
|
271
|
+
for (const edge of inEdges) {
|
|
272
|
+
if (visited.has(edge.source)) continue;
|
|
273
|
+
visited.add(edge.source);
|
|
274
|
+
parentMap.set(edge.source, currentId);
|
|
275
|
+
queue.push(edge.source);
|
|
232
276
|
}
|
|
233
277
|
}
|
|
234
|
-
return {
|
|
278
|
+
return { reached, unresolvedSymbols: Array.from(unresolvedSymbols) };
|
|
235
279
|
}
|
|
236
280
|
/**
|
|
237
281
|
* Store reachability result in sec_reachability table.
|
|
@@ -269,8 +313,29 @@ class ReachabilityAnalyzer {
|
|
|
269
313
|
);
|
|
270
314
|
}
|
|
271
315
|
}
|
|
316
|
+
function explainReachability(result) {
|
|
317
|
+
switch (result.reason) {
|
|
318
|
+
case "path_found":
|
|
319
|
+
return `Reachable: ${result.reachingEntryPointCount} entry point${result.reachingEntryPointCount === 1 ? "" : "s"} have a call/import/reference path to this dependency (see paths below).`;
|
|
320
|
+
case "no_dependency_link":
|
|
321
|
+
return `This vulnerability isn't linked to any dependency node in the graph, so reachability can't be evaluated at all. This usually means the vulnerability was registered manually (e.g. via "kirograph vulns --add") against a package that isn't currently indexed.`;
|
|
322
|
+
case "no_call_graph_signal":
|
|
323
|
+
return `Nothing in the indexed code explicitly imports, calls, or references this dependency \u2014 it has zero incoming edges in the call graph. This is common for dependencies a framework wires in via classpath scanning or reflection rather than an explicit reference (e.g. an embedded servlet container like Tomcat, Netty, or Jetty). The call graph has no signal about it either way, so "not reachable" can't be concluded.`;
|
|
324
|
+
case "unresolved_imports":
|
|
325
|
+
return `No call/import/reference path was found from any entry point, but the backward traversal hit ${result.unresolvedSymbols.length} unresolved import${result.unresolvedSymbols.length === 1 ? "" : "s"} along the way (dynamic dispatch, reflection, or unindexed code \u2014 see below). The graph is incomplete near this dependency, so "not reachable" can't be concluded with confidence.`;
|
|
326
|
+
case "no_entry_points":
|
|
327
|
+
return `No entry points (HTTP routes or exported functions) were found anywhere in the indexed project, so there's nowhere to start a reachability search from. This can mean the project genuinely has none (a library, not a server), or that entry-point detection missed them for this codebase.`;
|
|
328
|
+
case "no_path_found":
|
|
329
|
+
return `No call/import/reference path was found from any entry point to this dependency, and the traversal completed without hitting any unresolved imports \u2014 a high-confidence result.`;
|
|
330
|
+
default: {
|
|
331
|
+
const _exhaustive = result.reason;
|
|
332
|
+
return _exhaustive;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
272
336
|
// Annotate the CommonJS export names for ESM import in node:
|
|
273
337
|
0 && (module.exports = {
|
|
274
|
-
ReachabilityAnalyzer
|
|
338
|
+
ReachabilityAnalyzer,
|
|
339
|
+
explainReachability
|
|
275
340
|
});
|
|
276
341
|
//# sourceMappingURL=reachability.js.map
|