kirograph 1.2.1 → 1.2.3
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/dist/bin/kirograph.js +1 -1
- package/dist/patterns/library/sql-injection-concat-js.yaml +3 -0
- package/dist/patterns/library/sql-injection-template-js.yaml +4 -0
- package/dist/security/data-flows.js +0 -10
- package/dist/security/data-flows.js.map +2 -2
- package/dist/security/manifest/plugins/maven.js +39 -12
- package/dist/security/manifest/plugins/maven.js.map +2 -2
- package/package.json +1 -1
package/dist/bin/kirograph.js
CHANGED
|
@@ -128,7 +128,7 @@ process.on("uncaughtException", (err) => {
|
|
|
128
128
|
process.exit(1);
|
|
129
129
|
});
|
|
130
130
|
const program = new import_commander.Command();
|
|
131
|
-
program.name("kirograph").description("Semantic code knowledge graph for Kiro").version("1.2.
|
|
131
|
+
program.name("kirograph").description("Semantic code knowledge graph for Kiro").version("1.2.3").addHelpCommand(true).hook("preAction", (thisCommand) => {
|
|
132
132
|
const name = thisCommand.name();
|
|
133
133
|
if (name === "init") (0, import_banner.printBanner)();
|
|
134
134
|
});
|
|
@@ -8,5 +8,8 @@ fix: "/* TODO: use parameterized query */ $DB.query('?', [$B])"
|
|
|
8
8
|
rule:
|
|
9
9
|
any:
|
|
10
10
|
- pattern: $DB.query($A + $B)
|
|
11
|
+
- pattern: $DB.query($A + $B, $$$REST)
|
|
11
12
|
- pattern: $DB.execute($A + $B)
|
|
13
|
+
- pattern: $DB.execute($A + $B, $$$REST)
|
|
12
14
|
- pattern: $DB.run($A + $B)
|
|
15
|
+
- pattern: $DB.run($A + $B, $$$REST)
|
|
@@ -8,6 +8,10 @@ fix: "/* TODO: replace with parameterized query */"
|
|
|
8
8
|
rule:
|
|
9
9
|
any:
|
|
10
10
|
- pattern: $DB.query(`$$$`)
|
|
11
|
+
- pattern: $DB.query(`$$$`, $$$REST)
|
|
11
12
|
- pattern: $DB.execute(`$$$`)
|
|
13
|
+
- pattern: $DB.execute(`$$$`, $$$REST)
|
|
12
14
|
- pattern: $DB.run(`$$$`)
|
|
15
|
+
- pattern: $DB.run(`$$$`, $$$REST)
|
|
13
16
|
- pattern: $DB.exec(`$$$`)
|
|
17
|
+
- pattern: $DB.exec(`$$$`, $$$REST)
|
|
@@ -190,16 +190,6 @@ class DataFlowAnalyzer {
|
|
|
190
190
|
JOIN nodes n2 ON n2.id = e.target
|
|
191
191
|
WHERE e.kind = 'calls'
|
|
192
192
|
AND n2.name IN ('query', 'execute', 'exec', 'raw', 'runQuery', 'executeQuery', 'runSql', 'executeSql')
|
|
193
|
-
AND (
|
|
194
|
-
n.name LIKE '%handle%'
|
|
195
|
-
OR n.name LIKE '%controller%'
|
|
196
|
-
OR n.name LIKE '%route%'
|
|
197
|
-
OR n.name LIKE '%request%'
|
|
198
|
-
OR n.name LIKE '%req%'
|
|
199
|
-
OR n.name LIKE '%endpoint%'
|
|
200
|
-
OR n.name LIKE '%action%'
|
|
201
|
-
OR n.name LIKE '%handler%'
|
|
202
|
-
)
|
|
203
193
|
AND n.file_path != ''
|
|
204
194
|
`);
|
|
205
195
|
const sanitizeCallers = new Set(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/security/data-flows.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * KiroGraph SAST-lite: Dangerous Data Flow Detection\n *\n * Detects dangerous patterns by analysing call edges in the graph database.\n * Uses symbol names and callee chains rather than full AST traversal.\n */\n\nimport type { GraphDatabase } from '../db/database';\nimport type { OwaspCategory } from './owasp';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface DataFlowFinding {\n type: 'sql-injection' | 'path-traversal' | 'xss' | 'dangerous-eval' | 'unsafe-deserialize' | 'hardcoded-crypto';\n severity: 'critical' | 'high' | 'medium';\n owaspCategory: OwaspCategory;\n filePath: string;\n line: number;\n symbol: string;\n description: string;\n recommendation: string;\n}\n\n// \u2500\u2500 Internal row type returned by the DB queries \u2500\u2500\u2500\u2500\u2500\u2500\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 CallEdgeRow {\n caller: string;\n file_path: string;\n start_line: number;\n callee: string;\n}\n\n// \u2500\u2500 hasPatternsData helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 hasPatternsData(rawDb: any): boolean {\n try {\n const result = rawDb.get(\"SELECT COUNT(*) as cnt FROM pattern_matches\") as { cnt: number } | undefined;\n return (result?.cnt ?? 0) > 0;\n } catch {\n // Table doesn't exist (enablePatterns never ran)\n return false;\n }\n}\n\n// \u2500\u2500 DataFlowAnalyzer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 class DataFlowAnalyzer {\n constructor(private readonly db: GraphDatabase) {}\n\n async analyze(): Promise<DataFlowFinding[]> {\n const rawDb = (this.db as any).getRawDb();\n\n // Always run existing SQL heuristics (unchanged behavior)\n const sqlFindings = [\n ...this._detectDangerousEval(rawDb),\n ...this._detectUnsafeDeserialize(rawDb),\n ...this._detectHardcodedCrypto(rawDb),\n ...this._detectSqlInjection(rawDb),\n ...this._detectPathTraversal(rawDb),\n ];\n\n // When pattern_matches has data, merge AST findings (more precise)\n if (hasPatternsData(rawDb)) {\n const astFindings = this.analyzeFromPatternMatches(rawDb);\n // Deduplicate: AST entry wins over SQL entry at same (filePath, line)\n const astKeys = new Set(astFindings.map(f => `${f.filePath}:${f.line}`));\n const filteredSql = sqlFindings.filter(f => !astKeys.has(`${f.filePath}:${f.line}`));\n return [...astFindings, ...filteredSql];\n }\n\n return sqlFindings;\n }\n\n private analyzeFromPatternMatches(rawDb: any): DataFlowFinding[] {\n const rows: Array<{\n file_path: string;\n pattern_id: string;\n line: number;\n col: number;\n match_text: string;\n severity: string;\n owasp_category: string;\n language: string;\n }> = rawDb.all('SELECT file_path, pattern_id, line, col, match_text, severity, owasp_category, language FROM pattern_matches');\n\n return rows.map(row => {\n // Map pattern_id prefix to DataFlowFinding type\n const type = this._patternIdToType(row.pattern_id);\n // Look up nearest enclosing symbol\n const symbol = this._findEnclosingSymbol(rawDb, row.file_path, row.line);\n return {\n type,\n severity: row.severity as DataFlowFinding['severity'],\n owaspCategory: row.owasp_category as DataFlowFinding['owaspCategory'],\n filePath: row.file_path,\n line: row.line,\n symbol: symbol ?? row.pattern_id,\n description: `[AST pattern: ${row.pattern_id}] ${row.match_text.slice(0, 100)}`,\n recommendation: this._getRecommendation(type),\n };\n });\n }\n\n private _patternIdToType(patternId: string): DataFlowFinding['type'] {\n if (patternId.startsWith('sql-injection')) return 'sql-injection';\n if (patternId.startsWith('dangerous-eval') || patternId.startsWith('dangerous-exec')) return 'dangerous-eval';\n if (patternId.startsWith('path-traversal')) return 'path-traversal';\n if (patternId.startsWith('prototype-pollution')) return 'unsafe-deserialize';\n if (patternId.startsWith('weak-crypto')) return 'hardcoded-crypto';\n return 'dangerous-eval'; // fallback\n }\n\n private _findEnclosingSymbol(rawDb: any, filePath: string, line: number): string | null {\n const row = rawDb.get(\n `SELECT name FROM nodes WHERE file_path = ? AND start_line <= ? AND end_line >= ? AND kind IN ('function', 'method', 'class') ORDER BY (end_line - start_line) ASC LIMIT 1`,\n [filePath, line, line]\n ) as { name: string } | undefined;\n return row?.name ?? null;\n }\n\n private _getRecommendation(type: DataFlowFinding['type']): string {\n const recs: Record<DataFlowFinding['type'], string> = {\n 'sql-injection': 'Use parameterized queries. Never concatenate user input into SQL strings.',\n 'dangerous-eval': 'Remove eval()/exec(). Use structured alternatives.',\n 'path-traversal': 'Use path.resolve() and verify result is within an allowed base directory.',\n 'unsafe-deserialize': 'Validate object keys before merge. Avoid untrusted deserialization.',\n 'hardcoded-crypto': 'Use SHA-256+ for hashing. Use bcrypt/argon2 for passwords.',\n 'xss': 'Sanitize output. Use a trusted template engine with auto-escaping.',\n };\n return recs[type] ?? 'Review and fix this security issue.';\n }\n\n // \u2500\u2500 Detection: dangerous eval / exec \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 private _detectDangerousEval(rawDb: any): DataFlowFinding[] {\n const rows: CallEdgeRow[] = rawDb.all(`\n SELECT n.name as caller, n.file_path, n.start_line, n2.name as callee\n FROM edges e\n JOIN nodes n ON n.id = e.source\n JOIN nodes n2 ON n2.id = e.target\n WHERE e.kind = 'calls'\n AND n2.name IN ('eval', 'Function', 'execSync', 'exec', 'spawn', 'spawnSync', 'execFileSync', 'execFile')\n AND n.file_path != ''\n `);\n\n return rows.map((row) => ({\n type: 'dangerous-eval' as const,\n severity: 'critical' as const,\n owaspCategory: 'A03' as const,\n filePath: row.file_path,\n line: row.start_line,\n symbol: row.caller,\n description: `Function '${row.caller}' calls '${row.callee}', which can execute arbitrary code or OS commands.`,\n recommendation: `Avoid '${row.callee}'. Use structured APIs or allowlists for dynamic execution. Validate and sanitize all inputs.`,\n }));\n }\n\n // \u2500\u2500 Detection: unsafe deserialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 private _detectUnsafeDeserialize(rawDb: any): DataFlowFinding[] {\n const rows: CallEdgeRow[] = rawDb.all(`\n SELECT n.name as caller, n.file_path, n.start_line, n2.name as callee\n FROM edges e\n JOIN nodes n ON n.id = e.source\n JOIN nodes n2 ON n2.id = e.target\n WHERE e.kind = 'calls'\n AND (\n n2.name IN ('unserialize', 'deserialize', 'fromJson', 'loads')\n OR n2.name LIKE '%pickle%'\n OR n2.name LIKE '%marshal%'\n OR n2.name = 'yaml.load'\n )\n AND n.file_path != ''\n `);\n\n return rows.map((row) => ({\n type: 'unsafe-deserialize' as const,\n severity: 'high' as const,\n owaspCategory: 'A08' as const,\n filePath: row.file_path,\n line: row.start_line,\n symbol: row.caller,\n description: `Function '${row.caller}' calls '${row.callee}', which may deserialize untrusted data unsafely.`,\n recommendation: `Validate data before deserialization. Use safe alternatives (e.g. yaml.safe_load, JSON schema validation). Never deserialize data from untrusted sources.`,\n }));\n }\n\n // \u2500\u2500 Detection: hardcoded weak crypto \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 private _detectHardcodedCrypto(rawDb: any): DataFlowFinding[] {\n const rows: CallEdgeRow[] = rawDb.all(`\n SELECT n.name as caller, n.file_path, n.start_line, n2.name as callee\n FROM edges e\n JOIN nodes n ON n.id = e.source\n JOIN nodes n2 ON n2.id = e.target\n WHERE e.kind = 'calls'\n AND (\n n2.name LIKE '%md5%'\n OR n2.name LIKE '%sha1%'\n OR n2.name LIKE '%des%'\n OR n2.name LIKE '%rc4%'\n OR n2.name LIKE '%blowfish%'\n )\n AND (\n n.name LIKE '%crypto%'\n OR n.name LIKE '%hash%'\n OR n.name LIKE '%cipher%'\n OR n.name LIKE '%encrypt%'\n OR n.name LIKE '%digest%'\n OR n.file_path LIKE '%crypto%'\n OR n.file_path LIKE '%hash%'\n OR n.file_path LIKE '%cipher%'\n OR n.file_path LIKE '%auth%'\n )\n AND n.file_path != ''\n `);\n\n return rows.map((row) => ({\n type: 'hardcoded-crypto' as const,\n severity: 'medium' as const,\n owaspCategory: 'A02' as const,\n filePath: row.file_path,\n line: row.start_line,\n symbol: row.caller,\n description: `Function '${row.caller}' uses weak or deprecated cryptographic algorithm '${row.callee}'.`,\n recommendation: `Replace with a strong algorithm: SHA-256 or SHA-3 for hashing, AES-256-GCM for symmetric encryption. Avoid MD5 and SHA-1 for security purposes.`,\n }));\n }\n\n // \u2500\u2500 Detection: SQL injection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 private _detectSqlInjection(rawDb: any): DataFlowFinding[] {\n // Find functions that:\n // (a) call a known DB query function\n // (b) AND whose name suggests user-input handling (controller/route/request context)\n const rows: CallEdgeRow[] = rawDb.all(`\n SELECT n.name as caller, n.file_path, n.start_line, n2.name as callee\n FROM edges e\n JOIN nodes n ON n.id = e.source\n JOIN nodes n2 ON n2.id = e.target\n WHERE e.kind = 'calls'\n AND n2.name IN ('query', 'execute', 'exec', 'raw', 'runQuery', 'executeQuery', 'runSql', 'executeSql')\n AND (\n n.name LIKE '%handle%'\n OR n.name LIKE '%controller%'\n OR n.name LIKE '%route%'\n OR n.name LIKE '%request%'\n OR n.name LIKE '%req%'\n OR n.name LIKE '%endpoint%'\n OR n.name LIKE '%action%'\n OR n.name LIKE '%handler%'\n )\n AND n.file_path != ''\n `);\n\n // Only report entries that do NOT also have a sanitize/escape call in the same scope\n const sanitizeCallers: Set<string> = new Set<string>(\n (rawDb.all(`\n SELECT DISTINCT n.id\n FROM edges e\n JOIN nodes n ON n.id = e.source\n JOIN nodes n2 ON n2.id = e.target\n WHERE e.kind = 'calls'\n AND (\n n2.name LIKE '%sanitize%'\n OR n2.name LIKE '%escape%'\n OR n2.name LIKE '%parameteriz%'\n OR n2.name LIKE '%prepared%'\n OR n2.name LIKE '%placeholder%'\n )\n `) as Array<{ id: string }>).map((r) => r.id),\n );\n\n // Cross-reference: look up caller node IDs to filter sanitized callers\n const callerIds: Map<string, string> = new Map(\n (rawDb.all(`\n SELECT id, name FROM nodes WHERE kind IN ('function', 'method', 'arrow_function')\n `) as Array<{ id: string; name: string }>).map((r) => [r.name, r.id]),\n );\n\n return rows\n .filter((row) => {\n const callerId = callerIds.get(row.caller);\n return !callerId || !sanitizeCallers.has(callerId);\n })\n .map((row) => ({\n type: 'sql-injection' as const,\n severity: 'critical' as const,\n owaspCategory: 'A03' as const,\n filePath: row.file_path,\n line: row.start_line,\n symbol: row.caller,\n description: `Controller/handler '${row.caller}' calls database function '${row.callee}' without apparent input sanitization.`,\n recommendation: `Use parameterized queries or prepared statements. Never concatenate user input into SQL strings. Consider an ORM with query builders.`,\n }));\n }\n\n // \u2500\u2500 Detection: path traversal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 private _detectPathTraversal(rawDb: any): DataFlowFinding[] {\n // Find functions that call file I/O operations and look like they handle requests.\n // Exclusions:\n // - Internal analysis/infrastructure paths (mcp, resolution, security, extraction, graph, architecture)\n // - 'open' is excluded from file ops because KiroGraph.open() (DB) triggers false positives\n // - Class names ending in 'Handler' or 'Server' are excluded (too broad \u2014 use more specific patterns)\n const rows: CallEdgeRow[] = rawDb.all(`\n SELECT n.name as caller, n.file_path, n.start_line, n2.name as callee\n FROM edges e\n JOIN nodes n ON n.id = e.source\n JOIN nodes n2 ON n2.id = e.target\n WHERE e.kind = 'calls'\n AND n2.name IN ('readFile', 'readFileSync', 'writeFile', 'writeFileSync', 'createReadStream', 'createWriteStream', 'openSync', 'unlink', 'unlinkSync')\n AND (\n n.name LIKE '%controller%'\n OR n.name LIKE '%route%'\n OR n.name LIKE '%request%'\n OR n.name LIKE '%download%'\n OR n.name LIKE '%upload%'\n OR n.name LIKE '%get%file%'\n OR n.name LIKE '%fetch%file%'\n OR n.name LIKE '%serve%file%'\n OR n.name LIKE '%handle%file%'\n )\n AND n.file_path != ''\n AND n.file_path NOT LIKE '%/mcp/%'\n AND n.file_path NOT LIKE '%/resolution/%'\n AND n.file_path NOT LIKE '%/security/%'\n AND n.file_path NOT LIKE '%/extraction/%'\n AND n.file_path NOT LIKE '%/graph/%'\n AND n.file_path NOT LIKE '%/architecture/%'\n AND n.file_path NOT LIKE '%/frameworks/%'\n AND n.file_path NOT LIKE '%/bridges/%'\n `);\n\n return rows.map((row) => ({\n type: 'path-traversal' as const,\n severity: 'high' as const,\n owaspCategory: 'A01' as const,\n filePath: row.file_path,\n line: row.start_line,\n symbol: row.caller,\n description: `Handler '${row.caller}' calls file operation '${row.callee}'. Path may be influenced by user input, enabling directory traversal.`,\n recommendation: `Validate and normalize file paths. Use path.resolve() and verify the result is inside an allowed base directory. Reject paths containing '..'.`,\n }));\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * KiroGraph SAST-lite: Dangerous Data Flow Detection\n *\n * Detects dangerous patterns by analysing call edges in the graph database.\n * Uses symbol names and callee chains rather than full AST traversal.\n */\n\nimport type { GraphDatabase } from '../db/database';\nimport type { OwaspCategory } from './owasp';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface DataFlowFinding {\n type: 'sql-injection' | 'path-traversal' | 'xss' | 'dangerous-eval' | 'unsafe-deserialize' | 'hardcoded-crypto';\n severity: 'critical' | 'high' | 'medium';\n owaspCategory: OwaspCategory;\n filePath: string;\n line: number;\n symbol: string;\n description: string;\n recommendation: string;\n}\n\n// \u2500\u2500 Internal row type returned by the DB queries \u2500\u2500\u2500\u2500\u2500\u2500\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 CallEdgeRow {\n caller: string;\n file_path: string;\n start_line: number;\n callee: string;\n}\n\n// \u2500\u2500 hasPatternsData helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 hasPatternsData(rawDb: any): boolean {\n try {\n const result = rawDb.get(\"SELECT COUNT(*) as cnt FROM pattern_matches\") as { cnt: number } | undefined;\n return (result?.cnt ?? 0) > 0;\n } catch {\n // Table doesn't exist (enablePatterns never ran)\n return false;\n }\n}\n\n// \u2500\u2500 DataFlowAnalyzer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 class DataFlowAnalyzer {\n constructor(private readonly db: GraphDatabase) {}\n\n async analyze(): Promise<DataFlowFinding[]> {\n const rawDb = (this.db as any).getRawDb();\n\n // Always run existing SQL heuristics (unchanged behavior)\n const sqlFindings = [\n ...this._detectDangerousEval(rawDb),\n ...this._detectUnsafeDeserialize(rawDb),\n ...this._detectHardcodedCrypto(rawDb),\n ...this._detectSqlInjection(rawDb),\n ...this._detectPathTraversal(rawDb),\n ];\n\n // When pattern_matches has data, merge AST findings (more precise)\n if (hasPatternsData(rawDb)) {\n const astFindings = this.analyzeFromPatternMatches(rawDb);\n // Deduplicate: AST entry wins over SQL entry at same (filePath, line)\n const astKeys = new Set(astFindings.map(f => `${f.filePath}:${f.line}`));\n const filteredSql = sqlFindings.filter(f => !astKeys.has(`${f.filePath}:${f.line}`));\n return [...astFindings, ...filteredSql];\n }\n\n return sqlFindings;\n }\n\n private analyzeFromPatternMatches(rawDb: any): DataFlowFinding[] {\n const rows: Array<{\n file_path: string;\n pattern_id: string;\n line: number;\n col: number;\n match_text: string;\n severity: string;\n owasp_category: string;\n language: string;\n }> = rawDb.all('SELECT file_path, pattern_id, line, col, match_text, severity, owasp_category, language FROM pattern_matches');\n\n return rows.map(row => {\n // Map pattern_id prefix to DataFlowFinding type\n const type = this._patternIdToType(row.pattern_id);\n // Look up nearest enclosing symbol\n const symbol = this._findEnclosingSymbol(rawDb, row.file_path, row.line);\n return {\n type,\n severity: row.severity as DataFlowFinding['severity'],\n owaspCategory: row.owasp_category as DataFlowFinding['owaspCategory'],\n filePath: row.file_path,\n line: row.line,\n symbol: symbol ?? row.pattern_id,\n description: `[AST pattern: ${row.pattern_id}] ${row.match_text.slice(0, 100)}`,\n recommendation: this._getRecommendation(type),\n };\n });\n }\n\n private _patternIdToType(patternId: string): DataFlowFinding['type'] {\n if (patternId.startsWith('sql-injection')) return 'sql-injection';\n if (patternId.startsWith('dangerous-eval') || patternId.startsWith('dangerous-exec')) return 'dangerous-eval';\n if (patternId.startsWith('path-traversal')) return 'path-traversal';\n if (patternId.startsWith('prototype-pollution')) return 'unsafe-deserialize';\n if (patternId.startsWith('weak-crypto')) return 'hardcoded-crypto';\n return 'dangerous-eval'; // fallback\n }\n\n private _findEnclosingSymbol(rawDb: any, filePath: string, line: number): string | null {\n const row = rawDb.get(\n `SELECT name FROM nodes WHERE file_path = ? AND start_line <= ? AND end_line >= ? AND kind IN ('function', 'method', 'class') ORDER BY (end_line - start_line) ASC LIMIT 1`,\n [filePath, line, line]\n ) as { name: string } | undefined;\n return row?.name ?? null;\n }\n\n private _getRecommendation(type: DataFlowFinding['type']): string {\n const recs: Record<DataFlowFinding['type'], string> = {\n 'sql-injection': 'Use parameterized queries. Never concatenate user input into SQL strings.',\n 'dangerous-eval': 'Remove eval()/exec(). Use structured alternatives.',\n 'path-traversal': 'Use path.resolve() and verify result is within an allowed base directory.',\n 'unsafe-deserialize': 'Validate object keys before merge. Avoid untrusted deserialization.',\n 'hardcoded-crypto': 'Use SHA-256+ for hashing. Use bcrypt/argon2 for passwords.',\n 'xss': 'Sanitize output. Use a trusted template engine with auto-escaping.',\n };\n return recs[type] ?? 'Review and fix this security issue.';\n }\n\n // \u2500\u2500 Detection: dangerous eval / exec \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 private _detectDangerousEval(rawDb: any): DataFlowFinding[] {\n const rows: CallEdgeRow[] = rawDb.all(`\n SELECT n.name as caller, n.file_path, n.start_line, n2.name as callee\n FROM edges e\n JOIN nodes n ON n.id = e.source\n JOIN nodes n2 ON n2.id = e.target\n WHERE e.kind = 'calls'\n AND n2.name IN ('eval', 'Function', 'execSync', 'exec', 'spawn', 'spawnSync', 'execFileSync', 'execFile')\n AND n.file_path != ''\n `);\n\n return rows.map((row) => ({\n type: 'dangerous-eval' as const,\n severity: 'critical' as const,\n owaspCategory: 'A03' as const,\n filePath: row.file_path,\n line: row.start_line,\n symbol: row.caller,\n description: `Function '${row.caller}' calls '${row.callee}', which can execute arbitrary code or OS commands.`,\n recommendation: `Avoid '${row.callee}'. Use structured APIs or allowlists for dynamic execution. Validate and sanitize all inputs.`,\n }));\n }\n\n // \u2500\u2500 Detection: unsafe deserialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 private _detectUnsafeDeserialize(rawDb: any): DataFlowFinding[] {\n const rows: CallEdgeRow[] = rawDb.all(`\n SELECT n.name as caller, n.file_path, n.start_line, n2.name as callee\n FROM edges e\n JOIN nodes n ON n.id = e.source\n JOIN nodes n2 ON n2.id = e.target\n WHERE e.kind = 'calls'\n AND (\n n2.name IN ('unserialize', 'deserialize', 'fromJson', 'loads')\n OR n2.name LIKE '%pickle%'\n OR n2.name LIKE '%marshal%'\n OR n2.name = 'yaml.load'\n )\n AND n.file_path != ''\n `);\n\n return rows.map((row) => ({\n type: 'unsafe-deserialize' as const,\n severity: 'high' as const,\n owaspCategory: 'A08' as const,\n filePath: row.file_path,\n line: row.start_line,\n symbol: row.caller,\n description: `Function '${row.caller}' calls '${row.callee}', which may deserialize untrusted data unsafely.`,\n recommendation: `Validate data before deserialization. Use safe alternatives (e.g. yaml.safe_load, JSON schema validation). Never deserialize data from untrusted sources.`,\n }));\n }\n\n // \u2500\u2500 Detection: hardcoded weak crypto \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 private _detectHardcodedCrypto(rawDb: any): DataFlowFinding[] {\n const rows: CallEdgeRow[] = rawDb.all(`\n SELECT n.name as caller, n.file_path, n.start_line, n2.name as callee\n FROM edges e\n JOIN nodes n ON n.id = e.source\n JOIN nodes n2 ON n2.id = e.target\n WHERE e.kind = 'calls'\n AND (\n n2.name LIKE '%md5%'\n OR n2.name LIKE '%sha1%'\n OR n2.name LIKE '%des%'\n OR n2.name LIKE '%rc4%'\n OR n2.name LIKE '%blowfish%'\n )\n AND (\n n.name LIKE '%crypto%'\n OR n.name LIKE '%hash%'\n OR n.name LIKE '%cipher%'\n OR n.name LIKE '%encrypt%'\n OR n.name LIKE '%digest%'\n OR n.file_path LIKE '%crypto%'\n OR n.file_path LIKE '%hash%'\n OR n.file_path LIKE '%cipher%'\n OR n.file_path LIKE '%auth%'\n )\n AND n.file_path != ''\n `);\n\n return rows.map((row) => ({\n type: 'hardcoded-crypto' as const,\n severity: 'medium' as const,\n owaspCategory: 'A02' as const,\n filePath: row.file_path,\n line: row.start_line,\n symbol: row.caller,\n description: `Function '${row.caller}' uses weak or deprecated cryptographic algorithm '${row.callee}'.`,\n recommendation: `Replace with a strong algorithm: SHA-256 or SHA-3 for hashing, AES-256-GCM for symmetric encryption. Avoid MD5 and SHA-1 for security purposes.`,\n }));\n }\n\n // \u2500\u2500 Detection: SQL injection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 private _detectSqlInjection(rawDb: any): DataFlowFinding[] {\n // Find every function that calls a known raw-DB-query function. Used to\n // require the caller's own name to look like a controller/route/handler\n // (issue #39 follow-up: verified against OWASP Juice Shop that this\n // missed both of its best-known SQL injections \u2014 `login()` and\n // `searchProducts()` call `sequelize.query()` directly, zero hops away,\n // but neither name matched the old allowlist). The callee name is\n // already the specific signal here (a handful of literal raw-query\n // method names); gating on the caller's name on top of that was a leaky\n // proxy for \"receives user input\" that excluded real vulnerable code\n // sitting in repository/service/route-closure functions with ordinary\n // names, so it's dropped \u2014 the sanitize/escape filter below still keeps\n // properly-parameterized callers out of the results.\n const rows: CallEdgeRow[] = rawDb.all(`\n SELECT n.name as caller, n.file_path, n.start_line, n2.name as callee\n FROM edges e\n JOIN nodes n ON n.id = e.source\n JOIN nodes n2 ON n2.id = e.target\n WHERE e.kind = 'calls'\n AND n2.name IN ('query', 'execute', 'exec', 'raw', 'runQuery', 'executeQuery', 'runSql', 'executeSql')\n AND n.file_path != ''\n `);\n\n // Only report entries that do NOT also have a sanitize/escape call in the same scope\n const sanitizeCallers: Set<string> = new Set<string>(\n (rawDb.all(`\n SELECT DISTINCT n.id\n FROM edges e\n JOIN nodes n ON n.id = e.source\n JOIN nodes n2 ON n2.id = e.target\n WHERE e.kind = 'calls'\n AND (\n n2.name LIKE '%sanitize%'\n OR n2.name LIKE '%escape%'\n OR n2.name LIKE '%parameteriz%'\n OR n2.name LIKE '%prepared%'\n OR n2.name LIKE '%placeholder%'\n )\n `) as Array<{ id: string }>).map((r) => r.id),\n );\n\n // Cross-reference: look up caller node IDs to filter sanitized callers\n const callerIds: Map<string, string> = new Map(\n (rawDb.all(`\n SELECT id, name FROM nodes WHERE kind IN ('function', 'method', 'arrow_function')\n `) as Array<{ id: string; name: string }>).map((r) => [r.name, r.id]),\n );\n\n return rows\n .filter((row) => {\n const callerId = callerIds.get(row.caller);\n return !callerId || !sanitizeCallers.has(callerId);\n })\n .map((row) => ({\n type: 'sql-injection' as const,\n severity: 'critical' as const,\n owaspCategory: 'A03' as const,\n filePath: row.file_path,\n line: row.start_line,\n symbol: row.caller,\n description: `Controller/handler '${row.caller}' calls database function '${row.callee}' without apparent input sanitization.`,\n recommendation: `Use parameterized queries or prepared statements. Never concatenate user input into SQL strings. Consider an ORM with query builders.`,\n }));\n }\n\n // \u2500\u2500 Detection: path traversal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 private _detectPathTraversal(rawDb: any): DataFlowFinding[] {\n // Find functions that call file I/O operations and look like they handle requests.\n // Exclusions:\n // - Internal analysis/infrastructure paths (mcp, resolution, security, extraction, graph, architecture)\n // - 'open' is excluded from file ops because KiroGraph.open() (DB) triggers false positives\n // - Class names ending in 'Handler' or 'Server' are excluded (too broad \u2014 use more specific patterns)\n const rows: CallEdgeRow[] = rawDb.all(`\n SELECT n.name as caller, n.file_path, n.start_line, n2.name as callee\n FROM edges e\n JOIN nodes n ON n.id = e.source\n JOIN nodes n2 ON n2.id = e.target\n WHERE e.kind = 'calls'\n AND n2.name IN ('readFile', 'readFileSync', 'writeFile', 'writeFileSync', 'createReadStream', 'createWriteStream', 'openSync', 'unlink', 'unlinkSync')\n AND (\n n.name LIKE '%controller%'\n OR n.name LIKE '%route%'\n OR n.name LIKE '%request%'\n OR n.name LIKE '%download%'\n OR n.name LIKE '%upload%'\n OR n.name LIKE '%get%file%'\n OR n.name LIKE '%fetch%file%'\n OR n.name LIKE '%serve%file%'\n OR n.name LIKE '%handle%file%'\n )\n AND n.file_path != ''\n AND n.file_path NOT LIKE '%/mcp/%'\n AND n.file_path NOT LIKE '%/resolution/%'\n AND n.file_path NOT LIKE '%/security/%'\n AND n.file_path NOT LIKE '%/extraction/%'\n AND n.file_path NOT LIKE '%/graph/%'\n AND n.file_path NOT LIKE '%/architecture/%'\n AND n.file_path NOT LIKE '%/frameworks/%'\n AND n.file_path NOT LIKE '%/bridges/%'\n `);\n\n return rows.map((row) => ({\n type: 'path-traversal' as const,\n severity: 'high' as const,\n owaspCategory: 'A01' as const,\n filePath: row.file_path,\n line: row.start_line,\n symbol: row.caller,\n description: `Handler '${row.caller}' calls file operation '${row.callee}'. Path may be influenced by user input, enabling directory traversal.`,\n recommendation: `Validate and normalize file paths. Use path.resolve() and verify the result is inside an allowed base directory. Reject paths containing '..'.`,\n }));\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -178,15 +178,34 @@ function parseMavenDependencyTree(content) {
|
|
|
178
178
|
}
|
|
179
179
|
return result;
|
|
180
180
|
}
|
|
181
|
-
function
|
|
181
|
+
function extractMavenLicenseNames(content) {
|
|
182
182
|
const licensesMatch = content.match(/<licenses\b[^>]*>([\s\S]*?)<\/licenses>/i);
|
|
183
|
-
if (!licensesMatch) return
|
|
183
|
+
if (!licensesMatch) return [];
|
|
184
184
|
const licenseBlock = licensesMatch[1];
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
const names = [];
|
|
186
|
+
const licenseEntryRegex = /<license\b[^>]*>([\s\S]*?)<\/license>/gi;
|
|
187
|
+
let entryMatch;
|
|
188
|
+
while ((entryMatch = licenseEntryRegex.exec(licenseBlock)) !== null) {
|
|
189
|
+
const nameMatch = entryMatch[1].match(/<name>([^<]+)<\/name>/i);
|
|
190
|
+
if (nameMatch && nameMatch[1].trim() !== "") {
|
|
191
|
+
names.push(nameMatch[1].trim());
|
|
192
|
+
}
|
|
188
193
|
}
|
|
189
|
-
return
|
|
194
|
+
return names;
|
|
195
|
+
}
|
|
196
|
+
function extractMavenLicense(content) {
|
|
197
|
+
const names = extractMavenLicenseNames(content);
|
|
198
|
+
return names.length > 0 ? names.join(" OR ") : void 0;
|
|
199
|
+
}
|
|
200
|
+
function extractMavenParentCoords(content) {
|
|
201
|
+
const parentMatch = content.match(/<parent\b[^>]*>([\s\S]*?)<\/parent>/i);
|
|
202
|
+
if (!parentMatch) return void 0;
|
|
203
|
+
const block = parentMatch[1];
|
|
204
|
+
const groupId = extractXmlElement(block, "groupId");
|
|
205
|
+
const artifactId = extractXmlElement(block, "artifactId");
|
|
206
|
+
const version = extractXmlElement(block, "version");
|
|
207
|
+
if (!groupId || !artifactId || !version) return void 0;
|
|
208
|
+
return { groupId, artifactId, version };
|
|
190
209
|
}
|
|
191
210
|
function resolveMavenLocalRepoRoot() {
|
|
192
211
|
const settingsPath = path.join(os.homedir(), ".m2", "settings.xml");
|
|
@@ -198,14 +217,22 @@ function resolveMavenLocalRepoRoot() {
|
|
|
198
217
|
}
|
|
199
218
|
return path.join(os.homedir(), ".m2", "repository");
|
|
200
219
|
}
|
|
220
|
+
const MAX_MAVEN_PARENT_DEPTH = 10;
|
|
201
221
|
function findLocalMavenPomLicense(repoRoot, groupId, artifactId, version) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
222
|
+
let coords = { groupId, artifactId, version };
|
|
223
|
+
for (let depth = 0; coords && depth < MAX_MAVEN_PARENT_DEPTH; depth++) {
|
|
224
|
+
const pomPath = path.join(repoRoot, ...coords.groupId.split("."), coords.artifactId, coords.version, `${coords.artifactId}-${coords.version}.pom`);
|
|
225
|
+
let content;
|
|
226
|
+
try {
|
|
227
|
+
content = fs.readFileSync(pomPath, "utf8");
|
|
228
|
+
} catch {
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
const license = extractMavenLicense(content);
|
|
232
|
+
if (license !== void 0) return license;
|
|
233
|
+
coords = extractMavenParentCoords(content);
|
|
208
234
|
}
|
|
235
|
+
return void 0;
|
|
209
236
|
}
|
|
210
237
|
function applyLocalMavenLicenses(dependencies) {
|
|
211
238
|
const repoRoot = resolveMavenLocalRepoRoot();
|
|
@@ -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 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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
|
|
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 every license name from a pom.xml `<licenses>` block, in document\n * order. 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 extractMavenLicenseNames(content: string): string[] {\n const licensesMatch = content.match(/<licenses\\b[^>]*>([\\s\\S]*?)<\\/licenses>/i);\n if (!licensesMatch) return [];\n const licenseBlock = licensesMatch[1];\n const names: string[] = [];\n const licenseEntryRegex = /<license\\b[^>]*>([\\s\\S]*?)<\\/license>/gi;\n let entryMatch: RegExpExecArray | null;\n while ((entryMatch = licenseEntryRegex.exec(licenseBlock)) !== null) {\n const nameMatch = entryMatch[1].match(/<name>([^<]+)<\\/name>/i);\n if (nameMatch && nameMatch[1].trim() !== '') {\n names.push(nameMatch[1].trim());\n }\n }\n return names;\n}\n\n/**\n * Extract a pom.xml's license(s) as a single string. Multiple `<license>`\n * entries (dual/multi-licensed packages) are joined with \" OR \", matching\n * the SPDX-expression style already used for npm/Cargo license strings.\n */\nfunction extractMavenLicense(content: string): string | undefined {\n const names = extractMavenLicenseNames(content);\n return names.length > 0 ? names.join(' OR ') : undefined;\n}\n\n/**\n * Extract a pom.xml's `<parent>` coordinates, if present.\n * Looks for: <parent><groupId>...</groupId><artifactId>...</artifactId><version>...</version></parent>\n */\nfunction extractMavenParentCoords(content: string): { groupId: string; artifactId: string; version: string } | undefined {\n const parentMatch = content.match(/<parent\\b[^>]*>([\\s\\S]*?)<\\/parent>/i);\n if (!parentMatch) return undefined;\n const block = parentMatch[1];\n const groupId = extractXmlElement(block, 'groupId');\n const artifactId = extractXmlElement(block, 'artifactId');\n const version = extractXmlElement(block, 'version');\n if (!groupId || !artifactId || !version) return undefined;\n return { groupId, artifactId, version };\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 * Maven POMs commonly omit `<licenses>` and inherit it from a `<parent>` POM\n * instead \u2014 often several levels up (e.g. commons-codec -> commons-parent ->\n * the ASF parent). This caps how far up that chain is followed, both to\n * bound the number of file reads and to guard against a malformed/cyclic\n * parent reference.\n */\nconst MAX_MAVEN_PARENT_DEPTH = 10;\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 * When that POM has no `<licenses>` of its own, its `<parent>` POM is tried\n * next (also read from the local repository), and so on up the chain \u2014 this\n * is exactly how Maven itself resolves inherited license metadata.\n * Local files only, no network calls. Returns undefined as soon as a POM in\n * the chain isn't cached locally (e.g. CI running from a clean cache) or no\n * POM in the chain declares a license \u2014 same as before this fix, this fills\n * in real data on top of an \"unknown\" default, it never invents one.\n */\nfunction findLocalMavenPomLicense(repoRoot: string, groupId: string, artifactId: string, version: string): string | undefined {\n let coords: { groupId: string; artifactId: string; version: string } | undefined = { groupId, artifactId, version };\n\n for (let depth = 0; coords && depth < MAX_MAVEN_PARENT_DEPTH; depth++) {\n const pomPath = path.join(repoRoot, ...coords.groupId.split('.'), coords.artifactId, coords.version, `${coords.artifactId}-${coords.version}.pom`);\n let content: string;\n try {\n content = fs.readFileSync(pomPath, 'utf8');\n } catch {\n return undefined;\n }\n\n const license = extractMavenLicense(content);\n if (license !== undefined) return license;\n\n coords = extractMavenParentCoords(content);\n }\n\n return undefined;\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
|
}
|
package/package.json
CHANGED