kirograph 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +3 -2
  2. package/dist/bin/commands/memory.js +12 -2
  3. package/dist/bin/commands/memory.js.map +2 -2
  4. package/dist/bin/commands/wiki.js +3 -2
  5. package/dist/bin/commands/wiki.js.map +2 -2
  6. package/dist/bin/kirograph.js +1 -1
  7. package/dist/config.js +8 -0
  8. package/dist/config.js.map +2 -2
  9. package/dist/db/memory-schema.sql +2 -1
  10. package/dist/mcp/handlers/memory.js +2 -1
  11. package/dist/mcp/handlers/memory.js.map +2 -2
  12. package/dist/mcp/handlers/wiki.js +4 -2
  13. package/dist/mcp/handlers/wiki.js.map +2 -2
  14. package/dist/mcp/tools.js +2 -1
  15. package/dist/mcp/tools.js.map +2 -2
  16. package/dist/memory/database.js +17 -4
  17. package/dist/memory/database.js.map +2 -2
  18. package/dist/memory/index.js +18 -1
  19. package/dist/memory/index.js.map +2 -2
  20. package/dist/memory/schema.js +35 -0
  21. package/dist/memory/schema.js.map +2 -2
  22. package/dist/memory/types.js.map +1 -1
  23. package/dist/patterns/loader.js +2 -146
  24. package/dist/patterns/loader.js.map +3 -3
  25. package/dist/security/manifest/plugins/maven.js +33 -3
  26. package/dist/security/manifest/plugins/maven.js.map +2 -2
  27. package/dist/security/manifest/plugins/npm.js +32 -3
  28. package/dist/security/manifest/plugins/npm.js.map +2 -2
  29. package/dist/shared/json-schema.js +136 -0
  30. package/dist/shared/json-schema.js.map +7 -0
  31. package/dist/shared/yaml.js +174 -0
  32. package/dist/shared/yaml.js.map +7 -0
  33. package/dist/wiki/index.js +3 -1
  34. package/dist/wiki/index.js.map +2 -2
  35. package/dist/wiki/lint.js +14 -1
  36. package/dist/wiki/lint.js.map +2 -2
  37. package/dist/wiki/typed-pages.js +67 -0
  38. package/dist/wiki/typed-pages.js.map +7 -0
  39. package/dist/wiki/types.js.map +1 -1
  40. package/package.json +1 -1
@@ -34,151 +34,7 @@ module.exports = __toCommonJS(loader_exports);
34
34
  var fs = __toESM(require("fs"));
35
35
  var path = __toESM(require("path"));
36
36
  var import_errors = require("../errors");
37
- function _parseYaml(text) {
38
- const lines = text.split("\n");
39
- return _parseBlock(lines, 0, 0).result;
40
- }
41
- function _parseBlock(lines, startIndex, baseIndent) {
42
- const result = {};
43
- let i = startIndex;
44
- while (i < lines.length) {
45
- const rawLine = lines[i];
46
- const commentIdx = rawLine.indexOf(" #");
47
- const line = commentIdx >= 0 ? rawLine.slice(0, commentIdx) : rawLine;
48
- if (line.trim() === "" || line.trim().startsWith("#")) {
49
- i++;
50
- continue;
51
- }
52
- const indent = _countIndent(rawLine);
53
- if (indent < baseIndent) break;
54
- if (indent > baseIndent && Object.keys(result).length === 0) {
55
- i++;
56
- continue;
57
- }
58
- if (indent > baseIndent) {
59
- break;
60
- }
61
- const trimmed = line.trim();
62
- const colonIdx = trimmed.indexOf(":");
63
- if (colonIdx <= 0) {
64
- i++;
65
- continue;
66
- }
67
- const key = trimmed.slice(0, colonIdx).trim();
68
- const rest = trimmed.slice(colonIdx + 1).trimStart();
69
- if (rest === "" || rest === "|" || rest === ">") {
70
- const nextContentLine = _findNextContent(lines, i + 1);
71
- if (nextContentLine === -1) {
72
- result[key] = null;
73
- i++;
74
- continue;
75
- }
76
- const nextIndent = _countIndent(lines[nextContentLine]);
77
- if (nextIndent > indent) {
78
- if (lines[nextContentLine].trim().startsWith("- ")) {
79
- const { items, nextIndex } = _parseSequence(lines, nextContentLine, nextIndent);
80
- result[key] = items;
81
- i = nextIndex;
82
- } else {
83
- const { result: subResult, nextIndex } = _parseBlock(lines, nextContentLine, nextIndent);
84
- result[key] = subResult;
85
- i = nextIndex;
86
- }
87
- } else {
88
- result[key] = null;
89
- i++;
90
- }
91
- } else if (rest.startsWith("[")) {
92
- result[key] = _parseInlineArray(rest);
93
- i++;
94
- } else {
95
- result[key] = _parseScalar(rest);
96
- i++;
97
- }
98
- }
99
- return { result, nextIndex: i };
100
- }
101
- function _parseSequence(lines, startIndex, baseIndent) {
102
- const items = [];
103
- let i = startIndex;
104
- while (i < lines.length) {
105
- const rawLine = lines[i];
106
- if (rawLine.trim() === "" || rawLine.trim().startsWith("#")) {
107
- i++;
108
- continue;
109
- }
110
- const indent = _countIndent(rawLine);
111
- if (indent < baseIndent) break;
112
- const trimmed = rawLine.trim();
113
- if (!trimmed.startsWith("- ") && trimmed !== "-") {
114
- break;
115
- }
116
- const itemValue = trimmed.startsWith("- ") ? trimmed.slice(2).trim() : "";
117
- if (itemValue === "" || itemValue === "|" || itemValue === ">") {
118
- const nextContentLine = _findNextContent(lines, i + 1);
119
- if (nextContentLine !== -1 && _countIndent(lines[nextContentLine]) > indent) {
120
- const { result: subResult, nextIndex } = _parseBlock(lines, nextContentLine, _countIndent(lines[nextContentLine]));
121
- items.push(subResult);
122
- i = nextIndex;
123
- } else {
124
- items.push(null);
125
- i++;
126
- }
127
- } else if (itemValue.includes(":")) {
128
- const itemIndent = indent + 2;
129
- const continuationLines = [];
130
- let j = i + 1;
131
- while (j < lines.length) {
132
- const nextRaw = lines[j];
133
- if (nextRaw.trim() === "" || nextRaw.trim().startsWith("#")) {
134
- j++;
135
- continue;
136
- }
137
- if (_countIndent(nextRaw) >= itemIndent) {
138
- continuationLines.push(nextRaw);
139
- j++;
140
- } else {
141
- break;
142
- }
143
- }
144
- const blockLines = [" ".repeat(itemIndent) + itemValue, ...continuationLines];
145
- const { result: subResult } = _parseBlock(blockLines, 0, itemIndent);
146
- items.push(subResult);
147
- i = j;
148
- } else {
149
- items.push(_parseScalar(itemValue));
150
- i++;
151
- }
152
- }
153
- return { items, nextIndex: i };
154
- }
155
- function _parseInlineArray(text) {
156
- const inner = text.replace(/^\[/, "").replace(/\].*$/, "");
157
- return inner.split(",").map((s) => _parseScalar(s.trim())).filter((s) => s !== null && s !== void 0);
158
- }
159
- function _parseScalar(text) {
160
- if (text === "true") return true;
161
- if (text === "false") return false;
162
- if (text === "null" || text === "~") return null;
163
- if (/^-?\d+$/.test(text)) return parseInt(text, 10);
164
- if (/^-?\d+\.\d+$/.test(text)) return parseFloat(text);
165
- if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) {
166
- return text.slice(1, -1);
167
- }
168
- return text;
169
- }
170
- function _countIndent(line) {
171
- let i = 0;
172
- while (i < line.length && line[i] === " ") i++;
173
- return i;
174
- }
175
- function _findNextContent(lines, startIndex) {
176
- for (let i = startIndex; i < lines.length; i++) {
177
- const t = lines[i].trim();
178
- if (t !== "" && !t.startsWith("#")) return i;
179
- }
180
- return -1;
181
- }
37
+ var import_yaml = require("../shared/yaml");
182
38
  class PatternLibraryLoader {
183
39
  /**
184
40
  * Load, validate, and merge rules from the builtin path and optional custom path.
@@ -217,7 +73,7 @@ class PatternLibraryLoader {
217
73
  const filePath = path.join(dirPath, file);
218
74
  try {
219
75
  const text = fs.readFileSync(filePath, "utf8");
220
- const parsed = _parseYaml(text);
76
+ const parsed = (0, import_yaml.parseYaml)(text);
221
77
  const rule = this._validate(parsed, filePath);
222
78
  if (!rule) continue;
223
79
  if (seenIds.has(rule.id)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/patterns/loader.ts"],
4
- "sourcesContent": ["/**\n * KiroGraph PatternLibraryLoader \u2014 loads and merges YAML pattern rule files.\n *\n * js-yaml is not a declared dependency, so this uses a lightweight inline parser\n * that handles the flat key:value + nested rule: block format used by KiroGraph\n * pattern rule files.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { PatternRule } from './types';\nimport { logWarn } from '../errors';\n\n// \u2500\u2500 Minimal YAML 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//\n// Supports the subset of YAML used by KiroGraph pattern files:\n// - Flat key: value (string, boolean, number)\n// - key: [item1, item2] (inline array)\n// - key:\\n - item (block sequence for simple values)\n// - rule:\\n (nested block as raw sub-object \u2014 parsed recursively)\n// - Quoted strings: \"...\" and '...'\n// - Comments: # ...\n\nfunction _parseYaml(text: string): Record<string, unknown> {\n const lines = text.split('\\n');\n return _parseBlock(lines, 0, 0).result;\n}\n\ninterface BlockResult {\n result: Record<string, unknown>;\n nextIndex: number;\n}\n\nfunction _parseBlock(lines: string[], startIndex: number, baseIndent: number): BlockResult {\n const result: Record<string, unknown> = {};\n let i = startIndex;\n\n while (i < lines.length) {\n const rawLine = lines[i];\n // Strip trailing comments (but not inside strings \u2014 simplified approach)\n const commentIdx = rawLine.indexOf(' #');\n const line = commentIdx >= 0 ? rawLine.slice(0, commentIdx) : rawLine;\n\n // Skip blank lines\n if (line.trim() === '' || line.trim().startsWith('#')) {\n i++;\n continue;\n }\n\n const indent = _countIndent(rawLine);\n\n // If we've de-indented past our base, stop\n if (indent < baseIndent) break;\n\n // Skip lines that are at an indent level below our block's base\n if (indent > baseIndent && Object.keys(result).length === 0) {\n // Haven't started the block yet \u2014 shouldn't happen but skip\n i++;\n continue;\n }\n\n if (indent > baseIndent) {\n // This belongs to a child block already being processed \u2014 stop\n break;\n }\n\n const trimmed = line.trim();\n\n // Detect key: ... pattern\n const colonIdx = trimmed.indexOf(':');\n if (colonIdx <= 0) {\n i++;\n continue;\n }\n\n const key = trimmed.slice(0, colonIdx).trim();\n const rest = trimmed.slice(colonIdx + 1).trimStart();\n\n if (rest === '' || rest === '|' || rest === '>') {\n // Value is on the following lines \u2014 could be a nested block or block scalar\n // Peek ahead: if next non-empty line is indented more, recurse\n const nextContentLine = _findNextContent(lines, i + 1);\n if (nextContentLine === -1) {\n result[key] = null;\n i++;\n continue;\n }\n const nextIndent = _countIndent(lines[nextContentLine]);\n if (nextIndent > indent) {\n // Check if next line is a sequence item (- ...)\n if (lines[nextContentLine].trim().startsWith('- ')) {\n const { items, nextIndex } = _parseSequence(lines, nextContentLine, nextIndent);\n result[key] = items;\n i = nextIndex;\n } else {\n const { result: subResult, nextIndex } = _parseBlock(lines, nextContentLine, nextIndent);\n result[key] = subResult;\n i = nextIndex;\n }\n } else {\n result[key] = null;\n i++;\n }\n } else if (rest.startsWith('[')) {\n // Inline array: [a, b, c]\n result[key] = _parseInlineArray(rest);\n i++;\n } else {\n // Scalar value\n result[key] = _parseScalar(rest);\n i++;\n }\n }\n\n return { result, nextIndex: i };\n}\n\nfunction _parseSequence(lines: string[], startIndex: number, baseIndent: number): { items: unknown[]; nextIndex: number } {\n const items: unknown[] = [];\n let i = startIndex;\n\n while (i < lines.length) {\n const rawLine = lines[i];\n if (rawLine.trim() === '' || rawLine.trim().startsWith('#')) {\n i++;\n continue;\n }\n const indent = _countIndent(rawLine);\n if (indent < baseIndent) break;\n\n const trimmed = rawLine.trim();\n if (!trimmed.startsWith('- ') && trimmed !== '-') {\n break;\n }\n\n const itemValue = trimmed.startsWith('- ') ? trimmed.slice(2).trim() : '';\n\n if (itemValue === '' || itemValue === '|' || itemValue === '>') {\n // Multi-line sequence item\n const nextContentLine = _findNextContent(lines, i + 1);\n if (nextContentLine !== -1 && _countIndent(lines[nextContentLine]) > indent) {\n const { result: subResult, nextIndex } = _parseBlock(lines, nextContentLine, _countIndent(lines[nextContentLine]));\n items.push(subResult);\n i = nextIndex;\n } else {\n items.push(null);\n i++;\n }\n } else if (itemValue.includes(':')) {\n // Inline key:value item \u2014 parse as sub-object, collecting any indented continuation lines\n // Find continuation lines indented deeper than this item\n const itemIndent = indent + 2;\n const continuationLines: string[] = [];\n let j = i + 1;\n while (j < lines.length) {\n const nextRaw = lines[j];\n if (nextRaw.trim() === '' || nextRaw.trim().startsWith('#')) { j++; continue; }\n if (_countIndent(nextRaw) >= itemIndent) {\n continuationLines.push(nextRaw);\n j++;\n } else {\n break;\n }\n }\n const blockLines = [' '.repeat(itemIndent) + itemValue, ...continuationLines];\n const { result: subResult } = _parseBlock(blockLines, 0, itemIndent);\n items.push(subResult);\n i = j;\n } else {\n items.push(_parseScalar(itemValue));\n i++;\n }\n }\n\n return { items, nextIndex: i };\n}\n\nfunction _parseInlineArray(text: string): string[] {\n const inner = text.replace(/^\\[/, '').replace(/\\].*$/, '');\n return inner.split(',').map(s => _parseScalar(s.trim()) as string).filter(s => s !== null && s !== undefined) as string[];\n}\n\nfunction _parseScalar(text: string): unknown {\n if (text === 'true') return true;\n if (text === 'false') return false;\n if (text === 'null' || text === '~') return null;\n if (/^-?\\d+$/.test(text)) return parseInt(text, 10);\n if (/^-?\\d+\\.\\d+$/.test(text)) return parseFloat(text);\n // Quoted string\n if ((text.startsWith('\"') && text.endsWith('\"')) || (text.startsWith(\"'\") && text.endsWith(\"'\"))) {\n return text.slice(1, -1);\n }\n return text;\n}\n\nfunction _countIndent(line: string): number {\n let i = 0;\n while (i < line.length && line[i] === ' ') i++;\n return i;\n}\n\nfunction _findNextContent(lines: string[], startIndex: number): number {\n for (let i = startIndex; i < lines.length; i++) {\n const t = lines[i].trim();\n if (t !== '' && !t.startsWith('#')) return i;\n }\n return -1;\n}\n\n// \u2500\u2500 PatternLibraryLoader \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 PatternLibraryLoader {\n /**\n * Load, validate, and merge rules from the builtin path and optional custom path.\n * User-supplied rules override bundled rules on id collision.\n */\n load(builtinPath: string, customPath?: string): PatternRule[] {\n const builtin = this._loadDirectory(builtinPath, 'builtin');\n const builtinMap = new Map<string, PatternRule>();\n for (const rule of builtin) builtinMap.set(rule.id, rule);\n\n if (!customPath) return builtin;\n\n const custom = this._loadDirectory(customPath, 'custom');\n const merged = new Map<string, PatternRule>(builtinMap);\n for (const rule of custom) {\n if (merged.has(rule.id)) {\n logWarn(`PatternLibraryLoader: custom rule \"${rule.id}\" overrides bundled rule`);\n }\n merged.set(rule.id, rule);\n }\n\n return [...merged.values()];\n }\n\n private _loadDirectory(dirPath: string, source: string): PatternRule[] {\n if (!fs.existsSync(dirPath)) {\n logWarn(`PatternLibraryLoader: ${source} library path does not exist: ${dirPath}`);\n return [];\n }\n\n const rules: PatternRule[] = [];\n const seenIds = new Map<string, string>();\n\n let files: string[];\n try {\n files = fs.readdirSync(dirPath).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));\n } catch (err) {\n logWarn(`PatternLibraryLoader: failed to read ${source} directory \"${dirPath}\": ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n for (const file of files) {\n const filePath = path.join(dirPath, file);\n try {\n const text = fs.readFileSync(filePath, 'utf8');\n const parsed = _parseYaml(text);\n const rule = this._validate(parsed, filePath);\n if (!rule) continue;\n\n if (seenIds.has(rule.id)) {\n logWarn(`PatternLibraryLoader: duplicate rule id \"${rule.id}\" in ${source} library \u2014 found in \"${seenIds.get(rule.id)}\" and \"${filePath}\", using last loaded`);\n }\n seenIds.set(rule.id, filePath);\n rules.push(rule);\n } catch (err) {\n logWarn(`PatternLibraryLoader: failed to load rule file \"${filePath}\": ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return rules;\n }\n\n private _validate(parsed: Record<string, unknown>, filePath: string): PatternRule | null {\n const required = ['id', 'language', 'severity', 'owaspCategory', 'description', 'fixHint', 'rule'];\n for (const field of required) {\n if (parsed[field] === undefined || parsed[field] === null) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" is missing required field \"${field}\", skipping`);\n return null;\n }\n }\n\n const id = String(parsed['id']);\n const language = parsed['language'];\n const severity = String(parsed['severity']) as PatternRule['severity'];\n const owaspCategory = String(parsed['owaspCategory']);\n const description = String(parsed['description']);\n const fixHint = String(parsed['fixHint']);\n const rule = parsed['rule'];\n\n const validSeverities = new Set(['critical', 'high', 'medium', 'low']);\n if (!validSeverities.has(severity)) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" has invalid severity \"${severity}\", skipping`);\n return null;\n }\n\n if (typeof language !== 'string' && !Array.isArray(language)) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" has invalid language field, skipping`);\n return null;\n }\n\n if (typeof rule !== 'object' || Array.isArray(rule) || rule === null) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" has invalid rule field (must be an object), skipping`);\n return null;\n }\n\n return {\n id,\n language: Array.isArray(language) ? (language as string[]) : String(language),\n severity,\n owaspCategory,\n description,\n fixHint,\n rule: rule as Record<string, unknown>,\n };\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQA,SAAoB;AACpB,WAAsB;AAEtB,oBAAwB;AAYxB,SAAS,WAAW,MAAuC;AACzD,QAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,SAAO,YAAY,OAAO,GAAG,CAAC,EAAE;AAClC;AAOA,SAAS,YAAY,OAAiB,YAAoB,YAAiC;AACzF,QAAM,SAAkC,CAAC;AACzC,MAAI,IAAI;AAER,SAAO,IAAI,MAAM,QAAQ;AACvB,UAAM,UAAU,MAAM,CAAC;AAEvB,UAAM,aAAa,QAAQ,QAAQ,IAAI;AACvC,UAAM,OAAO,cAAc,IAAI,QAAQ,MAAM,GAAG,UAAU,IAAI;AAG9D,QAAI,KAAK,KAAK,MAAM,MAAM,KAAK,KAAK,EAAE,WAAW,GAAG,GAAG;AACrD;AACA;AAAA,IACF;AAEA,UAAM,SAAS,aAAa,OAAO;AAGnC,QAAI,SAAS,WAAY;AAGzB,QAAI,SAAS,cAAc,OAAO,KAAK,MAAM,EAAE,WAAW,GAAG;AAE3D;AACA;AAAA,IACF;AAEA,QAAI,SAAS,YAAY;AAEvB;AAAA,IACF;AAEA,UAAM,UAAU,KAAK,KAAK;AAG1B,UAAM,WAAW,QAAQ,QAAQ,GAAG;AACpC,QAAI,YAAY,GAAG;AACjB;AACA;AAAA,IACF;AAEA,UAAM,MAAM,QAAQ,MAAM,GAAG,QAAQ,EAAE,KAAK;AAC5C,UAAM,OAAO,QAAQ,MAAM,WAAW,CAAC,EAAE,UAAU;AAEnD,QAAI,SAAS,MAAM,SAAS,OAAO,SAAS,KAAK;AAG/C,YAAM,kBAAkB,iBAAiB,OAAO,IAAI,CAAC;AACrD,UAAI,oBAAoB,IAAI;AAC1B,eAAO,GAAG,IAAI;AACd;AACA;AAAA,MACF;AACA,YAAM,aAAa,aAAa,MAAM,eAAe,CAAC;AACtD,UAAI,aAAa,QAAQ;AAEvB,YAAI,MAAM,eAAe,EAAE,KAAK,EAAE,WAAW,IAAI,GAAG;AAClD,gBAAM,EAAE,OAAO,UAAU,IAAI,eAAe,OAAO,iBAAiB,UAAU;AAC9E,iBAAO,GAAG,IAAI;AACd,cAAI;AAAA,QACN,OAAO;AACL,gBAAM,EAAE,QAAQ,WAAW,UAAU,IAAI,YAAY,OAAO,iBAAiB,UAAU;AACvF,iBAAO,GAAG,IAAI;AACd,cAAI;AAAA,QACN;AAAA,MACF,OAAO;AACL,eAAO,GAAG,IAAI;AACd;AAAA,MACF;AAAA,IACF,WAAW,KAAK,WAAW,GAAG,GAAG;AAE/B,aAAO,GAAG,IAAI,kBAAkB,IAAI;AACpC;AAAA,IACF,OAAO;AAEL,aAAO,GAAG,IAAI,aAAa,IAAI;AAC/B;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,QAAQ,WAAW,EAAE;AAChC;AAEA,SAAS,eAAe,OAAiB,YAAoB,YAA6D;AACxH,QAAM,QAAmB,CAAC;AAC1B,MAAI,IAAI;AAER,SAAO,IAAI,MAAM,QAAQ;AACvB,UAAM,UAAU,MAAM,CAAC;AACvB,QAAI,QAAQ,KAAK,MAAM,MAAM,QAAQ,KAAK,EAAE,WAAW,GAAG,GAAG;AAC3D;AACA;AAAA,IACF;AACA,UAAM,SAAS,aAAa,OAAO;AACnC,QAAI,SAAS,WAAY;AAEzB,UAAM,UAAU,QAAQ,KAAK;AAC7B,QAAI,CAAC,QAAQ,WAAW,IAAI,KAAK,YAAY,KAAK;AAChD;AAAA,IACF;AAEA,UAAM,YAAY,QAAQ,WAAW,IAAI,IAAI,QAAQ,MAAM,CAAC,EAAE,KAAK,IAAI;AAEvE,QAAI,cAAc,MAAM,cAAc,OAAO,cAAc,KAAK;AAE9D,YAAM,kBAAkB,iBAAiB,OAAO,IAAI,CAAC;AACrD,UAAI,oBAAoB,MAAM,aAAa,MAAM,eAAe,CAAC,IAAI,QAAQ;AAC3E,cAAM,EAAE,QAAQ,WAAW,UAAU,IAAI,YAAY,OAAO,iBAAiB,aAAa,MAAM,eAAe,CAAC,CAAC;AACjH,cAAM,KAAK,SAAS;AACpB,YAAI;AAAA,MACN,OAAO;AACL,cAAM,KAAK,IAAI;AACf;AAAA,MACF;AAAA,IACF,WAAW,UAAU,SAAS,GAAG,GAAG;AAGlC,YAAM,aAAa,SAAS;AAC5B,YAAM,oBAA8B,CAAC;AACrC,UAAI,IAAI,IAAI;AACZ,aAAO,IAAI,MAAM,QAAQ;AACvB,cAAM,UAAU,MAAM,CAAC;AACvB,YAAI,QAAQ,KAAK,MAAM,MAAM,QAAQ,KAAK,EAAE,WAAW,GAAG,GAAG;AAAE;AAAK;AAAA,QAAU;AAC9E,YAAI,aAAa,OAAO,KAAK,YAAY;AACvC,4BAAkB,KAAK,OAAO;AAC9B;AAAA,QACF,OAAO;AACL;AAAA,QACF;AAAA,MACF;AACA,YAAM,aAAa,CAAC,IAAI,OAAO,UAAU,IAAI,WAAW,GAAG,iBAAiB;AAC5E,YAAM,EAAE,QAAQ,UAAU,IAAI,YAAY,YAAY,GAAG,UAAU;AACnE,YAAM,KAAK,SAAS;AACpB,UAAI;AAAA,IACN,OAAO;AACL,YAAM,KAAK,aAAa,SAAS,CAAC;AAClC;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,WAAW,EAAE;AAC/B;AAEA,SAAS,kBAAkB,MAAwB;AACjD,QAAM,QAAQ,KAAK,QAAQ,OAAO,EAAE,EAAE,QAAQ,SAAS,EAAE;AACzD,SAAO,MAAM,MAAM,GAAG,EAAE,IAAI,OAAK,aAAa,EAAE,KAAK,CAAC,CAAW,EAAE,OAAO,OAAK,MAAM,QAAQ,MAAM,MAAS;AAC9G;AAEA,SAAS,aAAa,MAAuB;AAC3C,MAAI,SAAS,OAAQ,QAAO;AAC5B,MAAI,SAAS,QAAS,QAAO;AAC7B,MAAI,SAAS,UAAU,SAAS,IAAK,QAAO;AAC5C,MAAI,UAAU,KAAK,IAAI,EAAG,QAAO,SAAS,MAAM,EAAE;AAClD,MAAI,eAAe,KAAK,IAAI,EAAG,QAAO,WAAW,IAAI;AAErD,MAAK,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,KAAO,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,GAAI;AAChG,WAAO,KAAK,MAAM,GAAG,EAAE;AAAA,EACzB;AACA,SAAO;AACT;AAEA,SAAS,aAAa,MAAsB;AAC1C,MAAI,IAAI;AACR,SAAO,IAAI,KAAK,UAAU,KAAK,CAAC,MAAM,IAAK;AAC3C,SAAO;AACT;AAEA,SAAS,iBAAiB,OAAiB,YAA4B;AACrE,WAAS,IAAI,YAAY,IAAI,MAAM,QAAQ,KAAK;AAC9C,UAAM,IAAI,MAAM,CAAC,EAAE,KAAK;AACxB,QAAI,MAAM,MAAM,CAAC,EAAE,WAAW,GAAG,EAAG,QAAO;AAAA,EAC7C;AACA,SAAO;AACT;AAIO,MAAM,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKhC,KAAK,aAAqB,YAAoC;AAC5D,UAAM,UAAU,KAAK,eAAe,aAAa,SAAS;AAC1D,UAAM,aAAa,oBAAI,IAAyB;AAChD,eAAW,QAAQ,QAAS,YAAW,IAAI,KAAK,IAAI,IAAI;AAExD,QAAI,CAAC,WAAY,QAAO;AAExB,UAAM,SAAS,KAAK,eAAe,YAAY,QAAQ;AACvD,UAAM,SAAS,IAAI,IAAyB,UAAU;AACtD,eAAW,QAAQ,QAAQ;AACzB,UAAI,OAAO,IAAI,KAAK,EAAE,GAAG;AACvB,mCAAQ,sCAAsC,KAAK,EAAE,0BAA0B;AAAA,MACjF;AACA,aAAO,IAAI,KAAK,IAAI,IAAI;AAAA,IAC1B;AAEA,WAAO,CAAC,GAAG,OAAO,OAAO,CAAC;AAAA,EAC5B;AAAA,EAEQ,eAAe,SAAiB,QAA+B;AACrE,QAAI,CAAC,GAAG,WAAW,OAAO,GAAG;AAC3B,iCAAQ,yBAAyB,MAAM,iCAAiC,OAAO,EAAE;AACjF,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,QAAuB,CAAC;AAC9B,UAAM,UAAU,oBAAI,IAAoB;AAExC,QAAI;AACJ,QAAI;AACF,cAAQ,GAAG,YAAY,OAAO,EAAE,OAAO,OAAK,EAAE,SAAS,OAAO,KAAK,EAAE,SAAS,MAAM,CAAC;AAAA,IACvF,SAAS,KAAK;AACZ,iCAAQ,wCAAwC,MAAM,eAAe,OAAO,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AACpI,aAAO,CAAC;AAAA,IACV;AAEA,eAAW,QAAQ,OAAO;AACxB,YAAM,WAAW,KAAK,KAAK,SAAS,IAAI;AACxC,UAAI;AACF,cAAM,OAAO,GAAG,aAAa,UAAU,MAAM;AAC7C,cAAM,SAAS,WAAW,IAAI;AAC9B,cAAM,OAAO,KAAK,UAAU,QAAQ,QAAQ;AAC5C,YAAI,CAAC,KAAM;AAEX,YAAI,QAAQ,IAAI,KAAK,EAAE,GAAG;AACxB,qCAAQ,4CAA4C,KAAK,EAAE,QAAQ,MAAM,6BAAwB,QAAQ,IAAI,KAAK,EAAE,CAAC,UAAU,QAAQ,sBAAsB;AAAA,QAC/J;AACA,gBAAQ,IAAI,KAAK,IAAI,QAAQ;AAC7B,cAAM,KAAK,IAAI;AAAA,MACjB,SAAS,KAAK;AACZ,mCAAQ,mDAAmD,QAAQ,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,MAC7H;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,UAAU,QAAiC,UAAsC;AACvF,UAAM,WAAW,CAAC,MAAM,YAAY,YAAY,iBAAiB,eAAe,WAAW,MAAM;AACjG,eAAW,SAAS,UAAU;AAC5B,UAAI,OAAO,KAAK,MAAM,UAAa,OAAO,KAAK,MAAM,MAAM;AACzD,mCAAQ,oCAAoC,QAAQ,gCAAgC,KAAK,aAAa;AACtG,eAAO;AAAA,MACT;AAAA,IACF;AAEA,UAAM,KAAK,OAAO,OAAO,IAAI,CAAC;AAC9B,UAAM,WAAW,OAAO,UAAU;AAClC,UAAM,WAAW,OAAO,OAAO,UAAU,CAAC;AAC1C,UAAM,gBAAgB,OAAO,OAAO,eAAe,CAAC;AACpD,UAAM,cAAc,OAAO,OAAO,aAAa,CAAC;AAChD,UAAM,UAAU,OAAO,OAAO,SAAS,CAAC;AACxC,UAAM,OAAO,OAAO,MAAM;AAE1B,UAAM,kBAAkB,oBAAI,IAAI,CAAC,YAAY,QAAQ,UAAU,KAAK,CAAC;AACrE,QAAI,CAAC,gBAAgB,IAAI,QAAQ,GAAG;AAClC,iCAAQ,oCAAoC,QAAQ,2BAA2B,QAAQ,aAAa;AACpG,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,aAAa,YAAY,CAAC,MAAM,QAAQ,QAAQ,GAAG;AAC5D,iCAAQ,oCAAoC,QAAQ,wCAAwC;AAC5F,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,KAAK,SAAS,MAAM;AACpE,iCAAQ,oCAAoC,QAAQ,wDAAwD;AAC5G,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL;AAAA,MACA,UAAU,MAAM,QAAQ,QAAQ,IAAK,WAAwB,OAAO,QAAQ;AAAA,MAC5E;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;",
6
- "names": []
4
+ "sourcesContent": ["/**\n * KiroGraph PatternLibraryLoader \u2014 loads and merges YAML pattern rule files.\n *\n * js-yaml is not a declared dependency, so this uses the lightweight shared\n * YAML parser (src/shared/yaml.ts) that handles the flat key:value + nested\n * rule: block format used by KiroGraph pattern rule files.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { PatternRule } from './types';\nimport { logWarn } from '../errors';\nimport { parseYaml as _parseYaml } from '../shared/yaml';\n\n// \u2500\u2500 PatternLibraryLoader \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 PatternLibraryLoader {\n /**\n * Load, validate, and merge rules from the builtin path and optional custom path.\n * User-supplied rules override bundled rules on id collision.\n */\n load(builtinPath: string, customPath?: string): PatternRule[] {\n const builtin = this._loadDirectory(builtinPath, 'builtin');\n const builtinMap = new Map<string, PatternRule>();\n for (const rule of builtin) builtinMap.set(rule.id, rule);\n\n if (!customPath) return builtin;\n\n const custom = this._loadDirectory(customPath, 'custom');\n const merged = new Map<string, PatternRule>(builtinMap);\n for (const rule of custom) {\n if (merged.has(rule.id)) {\n logWarn(`PatternLibraryLoader: custom rule \"${rule.id}\" overrides bundled rule`);\n }\n merged.set(rule.id, rule);\n }\n\n return [...merged.values()];\n }\n\n private _loadDirectory(dirPath: string, source: string): PatternRule[] {\n if (!fs.existsSync(dirPath)) {\n logWarn(`PatternLibraryLoader: ${source} library path does not exist: ${dirPath}`);\n return [];\n }\n\n const rules: PatternRule[] = [];\n const seenIds = new Map<string, string>();\n\n let files: string[];\n try {\n files = fs.readdirSync(dirPath).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));\n } catch (err) {\n logWarn(`PatternLibraryLoader: failed to read ${source} directory \"${dirPath}\": ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n for (const file of files) {\n const filePath = path.join(dirPath, file);\n try {\n const text = fs.readFileSync(filePath, 'utf8');\n const parsed = _parseYaml(text);\n const rule = this._validate(parsed, filePath);\n if (!rule) continue;\n\n if (seenIds.has(rule.id)) {\n logWarn(`PatternLibraryLoader: duplicate rule id \"${rule.id}\" in ${source} library \u2014 found in \"${seenIds.get(rule.id)}\" and \"${filePath}\", using last loaded`);\n }\n seenIds.set(rule.id, filePath);\n rules.push(rule);\n } catch (err) {\n logWarn(`PatternLibraryLoader: failed to load rule file \"${filePath}\": ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n return rules;\n }\n\n private _validate(parsed: Record<string, unknown>, filePath: string): PatternRule | null {\n const required = ['id', 'language', 'severity', 'owaspCategory', 'description', 'fixHint', 'rule'];\n for (const field of required) {\n if (parsed[field] === undefined || parsed[field] === null) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" is missing required field \"${field}\", skipping`);\n return null;\n }\n }\n\n const id = String(parsed['id']);\n const language = parsed['language'];\n const severity = String(parsed['severity']) as PatternRule['severity'];\n const owaspCategory = String(parsed['owaspCategory']);\n const description = String(parsed['description']);\n const fixHint = String(parsed['fixHint']);\n const rule = parsed['rule'];\n\n const validSeverities = new Set(['critical', 'high', 'medium', 'low']);\n if (!validSeverities.has(severity)) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" has invalid severity \"${severity}\", skipping`);\n return null;\n }\n\n if (typeof language !== 'string' && !Array.isArray(language)) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" has invalid language field, skipping`);\n return null;\n }\n\n if (typeof rule !== 'object' || Array.isArray(rule) || rule === null) {\n logWarn(`PatternLibraryLoader: rule file \"${filePath}\" has invalid rule field (must be an object), skipping`);\n return null;\n }\n\n return {\n id,\n language: Array.isArray(language) ? (language as string[]) : String(language),\n severity,\n owaspCategory,\n description,\n fixHint,\n rule: rule as Record<string, unknown>,\n };\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["_parseYaml"]
7
7
  }
@@ -33,6 +33,7 @@ __export(maven_exports, {
33
33
  });
34
34
  module.exports = __toCommonJS(maven_exports);
35
35
  var fs = __toESM(require("fs"));
36
+ var os = __toESM(require("os"));
36
37
  var path = __toESM(require("path"));
37
38
  var import_errors = require("../../../errors");
38
39
  function mapMavenScope(mavenScope) {
@@ -71,7 +72,6 @@ async function parseMavenManifest(manifestPath, projectRoot) {
71
72
  (0, import_errors.logWarn)(`[sec:maven] Invalid pom.xml structure at ${relativeManifest} \u2014 missing <project> element`);
72
73
  return [];
73
74
  }
74
- const license = extractMavenLicense(content);
75
75
  const dependencies = [];
76
76
  const dependenciesOnlyContent = content.replace(/<dependencyManagement>[\s\S]*?<\/dependencyManagement>/g, "");
77
77
  const dependencyBlocks = dependenciesOnlyContent.matchAll(/<dependency>([\s\S]*?)<\/dependency>/g);
@@ -107,8 +107,7 @@ async function parseMavenManifest(manifestPath, projectRoot) {
107
107
  resolvedVersion: declaredConstraint || void 0,
108
108
  scope: mappedScope,
109
109
  ecosystem: "maven",
110
- sourceManifest: relativeManifest,
111
- ...license !== void 0 ? { license } : {}
110
+ sourceManifest: relativeManifest
112
111
  });
113
112
  }
114
113
  const manifestDir = path.dirname(manifestPath);
@@ -120,6 +119,7 @@ async function parseMavenManifest(manifestPath, projectRoot) {
120
119
  treeContent = fs.readFileSync(treeFile.path, "utf8");
121
120
  } catch (err) {
122
121
  (0, import_errors.logWarn)(`[sec:maven] Failed to read ${treeRelativePath}: ${err instanceof Error ? err.message : String(err)}`);
122
+ applyLocalMavenLicenses(dependencies);
123
123
  return dependencies;
124
124
  }
125
125
  const treeEntries = parseMavenDependencyTree(treeContent);
@@ -145,6 +145,7 @@ async function parseMavenManifest(manifestPath, projectRoot) {
145
145
  });
146
146
  }
147
147
  }
148
+ applyLocalMavenLicenses(dependencies);
148
149
  return dependencies;
149
150
  }
150
151
  function findMavenDependencyTreeFile(manifestDir) {
@@ -187,6 +188,35 @@ function extractMavenLicense(content) {
187
188
  }
188
189
  return void 0;
189
190
  }
191
+ function resolveMavenLocalRepoRoot() {
192
+ const settingsPath = path.join(os.homedir(), ".m2", "settings.xml");
193
+ try {
194
+ const settings = fs.readFileSync(settingsPath, "utf8");
195
+ const match = settings.match(/<localRepository>([^<]+)<\/localRepository>/i);
196
+ if (match && match[1].trim() !== "") return match[1].trim();
197
+ } catch {
198
+ }
199
+ return path.join(os.homedir(), ".m2", "repository");
200
+ }
201
+ function findLocalMavenPomLicense(repoRoot, groupId, artifactId, version) {
202
+ const pomPath = path.join(repoRoot, ...groupId.split("."), artifactId, version, `${artifactId}-${version}.pom`);
203
+ try {
204
+ const content = fs.readFileSync(pomPath, "utf8");
205
+ return extractMavenLicense(content);
206
+ } catch {
207
+ return void 0;
208
+ }
209
+ }
210
+ function applyLocalMavenLicenses(dependencies) {
211
+ const repoRoot = resolveMavenLocalRepoRoot();
212
+ for (const dep of dependencies) {
213
+ if (!dep.resolvedVersion) continue;
214
+ const [groupId, artifactId] = dep.name.split(":");
215
+ if (!groupId || !artifactId) continue;
216
+ const license = findLocalMavenPomLicense(repoRoot, groupId, artifactId, dep.resolvedVersion);
217
+ if (license !== void 0) dep.license = license;
218
+ }
219
+ }
190
220
  // Annotate the CommonJS export names for ESM import in node:
191
221
  0 && (module.exports = {
192
222
  parseMavenDependencyTree,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/security/manifest/plugins/maven.ts"],
4
- "sourcesContent": ["/**\n * Maven Version Extraction Plugin for KiroGraph-Sec\n *\n * Extends the existing architecture maven parser to extract version constraints,\n * dependency scopes, and groupId/artifactId for security analysis.\n *\n * Reuses `mavenParser` from `src/architecture/manifest/maven.ts` for discovery and\n * basic parsing, then layers on version/scope extraction from pom.xml.\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\n/**\n * Maven scope to ParsedDependency scope mapping.\n *\n * - compile (default), runtime \u2192 production\n * - test \u2192 development\n * - provided, system \u2192 optional\n */\nfunction mapMavenScope(mavenScope: string | undefined): ParsedDependency['scope'] {\n if (!mavenScope || mavenScope.trim() === '') {\n return 'production';\n }\n\n const normalized = mavenScope.trim().toLowerCase();\n switch (normalized) {\n case 'compile':\n case 'runtime':\n return 'production';\n case 'test':\n return 'development';\n case 'provided':\n case 'system':\n return 'optional';\n default:\n // Unknown scope defaults to production\n return 'production';\n }\n}\n\n/**\n * Extract text content from an XML element by tag name within a given XML fragment.\n * Returns undefined if the element is not found.\n */\nfunction extractXmlElement(xml: string, tagName: string): string | undefined {\n const regex = new RegExp(`<${tagName}>([^<]*)</${tagName}>`);\n const match = xml.match(regex);\n return match ? match[1].trim() : undefined;\n}\n\n/**\n * Parse a Maven pom.xml manifest and extract dependency declarations\n * with groupId, artifactId, version constraints, and scopes.\n *\n * @param manifestPath - Absolute path to the pom.xml file\n * @param projectRoot - Absolute path to the project root directory\n * @returns Array of parsed dependencies with version and scope information\n */\nexport async function parseMavenManifest(\n manifestPath: string,\n projectRoot: string,\n): Promise<ParsedDependency[]> {\n const relativeManifest = path.relative(projectRoot, manifestPath).replace(/\\\\/g, '/');\n\n // Read the pom.xml content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:maven] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Validate basic XML structure\n if (!content.includes('<project')) {\n logWarn(`[sec:maven] Invalid pom.xml structure at ${relativeManifest} \u2014 missing <project> element`);\n return [];\n }\n\n // Extract the project-level license\n const license = extractMavenLicense(content);\n\n // Extract all <dependency> blocks from the <dependencies> sections\n const dependencies: ParsedDependency[] = [];\n\n // <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 ...(license !== undefined ? { license } : {}),\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 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 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 */\nfunction extractMavenLicense(content: string): string | undefined {\n const licensesMatch = content.match(/<licenses\\b[^>]*>([\\s\\S]*?)<\\/licenses>/i);\n if (!licensesMatch) return undefined;\n const licenseBlock = licensesMatch[1];\n const nameMatch = licenseBlock.match(/<name>([^<]+)<\\/name>/i);\n if (nameMatch && nameMatch[1].trim() !== '') {\n return nameMatch[1].trim();\n }\n return undefined;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Maven Version Extraction Plugin for KiroGraph-Sec\n *\n * Extends the existing architecture maven parser to extract version constraints,\n * dependency scopes, and groupId/artifactId for security analysis.\n *\n * Reuses `mavenParser` from `src/architecture/manifest/maven.ts` for discovery and\n * basic parsing, then layers on version/scope extraction from pom.xml.\n */\nimport * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport type { ParsedDependency } from '../../types';\nimport { logWarn } from '../../../errors';\n\n/**\n * Maven scope to ParsedDependency scope mapping.\n *\n * - compile (default), runtime \u2192 production\n * - test \u2192 development\n * - provided, system \u2192 optional\n */\nfunction mapMavenScope(mavenScope: string | undefined): ParsedDependency['scope'] {\n if (!mavenScope || mavenScope.trim() === '') {\n return 'production';\n }\n\n const normalized = mavenScope.trim().toLowerCase();\n switch (normalized) {\n case 'compile':\n case 'runtime':\n return 'production';\n case 'test':\n return 'development';\n case 'provided':\n case 'system':\n return 'optional';\n default:\n // Unknown scope defaults to production\n return 'production';\n }\n}\n\n/**\n * Extract text content from an XML element by tag name within a given XML fragment.\n * Returns undefined if the element is not found.\n */\nfunction extractXmlElement(xml: string, tagName: string): string | undefined {\n const regex = new RegExp(`<${tagName}>([^<]*)</${tagName}>`);\n const match = xml.match(regex);\n return match ? match[1].trim() : undefined;\n}\n\n/**\n * Parse a Maven pom.xml manifest and extract dependency declarations\n * with groupId, artifactId, version constraints, and scopes.\n *\n * @param manifestPath - Absolute path to the pom.xml file\n * @param projectRoot - Absolute path to the project root directory\n * @returns Array of parsed dependencies with version and scope information\n */\nexport async function parseMavenManifest(\n manifestPath: string,\n projectRoot: string,\n): Promise<ParsedDependency[]> {\n const relativeManifest = path.relative(projectRoot, manifestPath).replace(/\\\\/g, '/');\n\n // Read the pom.xml content\n let content: string;\n try {\n content = fs.readFileSync(manifestPath, 'utf8');\n } catch (err) {\n logWarn(`[sec:maven] Failed to read ${relativeManifest}: ${err instanceof Error ? err.message : String(err)}`);\n return [];\n }\n\n // Validate basic XML structure\n if (!content.includes('<project')) {\n logWarn(`[sec:maven] Invalid pom.xml structure at ${relativeManifest} \u2014 missing <project> element`);\n return [];\n }\n\n // Extract all <dependency> blocks from the <dependencies> sections\n const dependencies: ParsedDependency[] = [];\n\n // <dependencyManagement> entries (BOM imports, version overrides for child\n // modules) are not themselves project dependencies \u2014 strip that block before\n // scanning so they don't get misread as declared dependencies.\n const dependenciesOnlyContent = content.replace(/<dependencyManagement>[\\s\\S]*?<\\/dependencyManagement>/g, '');\n\n // Match all <dependency>...</dependency> blocks\n const dependencyBlocks = dependenciesOnlyContent.matchAll(/<dependency>([\\s\\S]*?)<\\/dependency>/g);\n\n for (const block of dependencyBlocks) {\n const depXml = block[1];\n\n const groupId = extractXmlElement(depXml, 'groupId');\n const artifactId = extractXmlElement(depXml, 'artifactId');\n const version = extractXmlElement(depXml, 'version');\n const scope = extractXmlElement(depXml, 'scope');\n\n // Validate required fields\n if (!groupId || groupId.trim() === '') {\n logWarn(`[sec:maven] Missing groupId in dependency at ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n if (!artifactId || artifactId.trim() === '') {\n logWarn(`[sec:maven] Missing artifactId in dependency at ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n // Skip dependencies with unresolved Maven properties (e.g. ${project.version})\n if (groupId.includes('${') || artifactId.includes('${')) {\n logWarn(`[sec:maven] Unresolved property in dependency ${groupId}:${artifactId} at ${relativeManifest} \u2014 skipping`);\n continue;\n }\n\n // Build the dependency name as groupId:artifactId (Maven convention)\n const name = `${groupId}:${artifactId}`;\n\n // Version constraint \u2014 may be absent (managed by parent POM or BOM)\n let declaredConstraint: string;\n if (version && version.trim() !== '' && !version.includes('${')) {\n declaredConstraint = version.trim();\n } else {\n // No version or unresolved property \u2014 use empty string to indicate managed externally\n declaredConstraint = version && !version.includes('${') ? version.trim() : '';\n }\n\n const mappedScope = mapMavenScope(scope);\n\n dependencies.push({\n name,\n declaredConstraint,\n resolvedVersion: declaredConstraint || undefined,\n scope: mappedScope,\n ecosystem: 'maven',\n sourceManifest: relativeManifest,\n });\n }\n\n // Maven has no lock file, so a <dependency> with no explicit <version> (the\n // common case under a BOM parent like spring-boot-starter-parent) never gets\n // a resolvedVersion above, and packages pulled in only transitively (e.g.\n // tomcat-embed-core via spring-boot-starter-web) never appear in pom.xml at\n // all. If the project has a pre-generated `mvn dependency:tree` text file\n // (the closest Maven equivalent of a lock file), use it to fill in resolved\n // versions and capture transitive-only packages, mirroring how the npm\n // plugin layers package-lock.json on top of package.json.\n const manifestDir = path.dirname(manifestPath);\n const treeFile = findMavenDependencyTreeFile(manifestDir);\n\n if (treeFile) {\n const treeRelativePath = path.relative(projectRoot, treeFile.path).replace(/\\\\/g, '/');\n let treeContent: string;\n try {\n treeContent = fs.readFileSync(treeFile.path, 'utf8');\n } catch (err) {\n logWarn(`[sec:maven] Failed to read ${treeRelativePath}: ${err instanceof Error ? err.message : String(err)}`);\n applyLocalMavenLicenses(dependencies);\n return dependencies;\n }\n\n const treeEntries = parseMavenDependencyTree(treeContent);\n\n for (const dep of dependencies) {\n if (dep.resolvedVersion) continue;\n const treeEntry = treeEntries.get(dep.name);\n if (treeEntry) {\n dep.resolvedVersion = treeEntry.version;\n }\n }\n\n const directNames = new Set(dependencies.map(d => d.name));\n for (const [name, entry] of treeEntries) {\n if (directNames.has(name)) continue;\n dependencies.push({\n name,\n // No declared range exists at the project level for a transitive\n // package \u2014 the resolved version from the tree is the only constraint we have.\n declaredConstraint: entry.version,\n resolvedVersion: entry.version,\n scope: mapMavenScope(entry.scope),\n ecosystem: 'maven',\n sourceManifest: treeRelativePath,\n });\n }\n }\n\n applyLocalMavenLicenses(dependencies);\n return dependencies;\n}\n\n/**\n * Look for a pre-generated Maven dependency-tree text file next to pom.xml,\n * e.g. produced with:\n * mvn dependency:tree -DoutputFile=dependency-tree.txt\n * Checked locations, in order: `dependency-tree.txt` next to pom.xml, then\n * `target/dependency-tree.txt` (Maven's default build output directory).\n */\nfunction findMavenDependencyTreeFile(manifestDir: string): { path: string } | undefined {\n const candidates = ['dependency-tree.txt', path.join('target', 'dependency-tree.txt')];\n for (const candidate of candidates) {\n const candidatePath = path.join(manifestDir, candidate);\n if (fs.existsSync(candidatePath)) {\n return { path: candidatePath };\n }\n }\n return undefined;\n}\n\n/**\n * Parse `mvn dependency:tree` text output into a map of `groupId:artifactId`\n * to its resolved version and scope.\n *\n * Handles both:\n * groupId:artifactId:type:version:scope\n * groupId:artifactId:type:classifier:version:scope\n *\n * Skips the root project line (no scope segment) and parenthesized entries\n * like `(commons-logging:commons-logging:jar:1.1.1:compile - omitted for conflict with 1.2)`\n * \u2014 Maven's conflict resolution means only the winning version ends up on the\n * classpath, so omitted entries aren't actually present in the build.\n */\nexport function parseMavenDependencyTree(content: string): Map<string, { version: string; scope: string }> {\n const result = new Map<string, { version: string; scope: string }>();\n\n for (const rawLine of content.split(/\\r?\\n/)) {\n if (!rawLine.trim()) continue;\n\n // Strip Maven's ASCII tree-drawing prefix (e.g. \"| +- \", \" \\- \")\n const stripped = rawLine.replace(/^[\\s|+\\\\-]+/, '').trim();\n if (!stripped || stripped.startsWith('(')) continue;\n\n const parts = stripped.split(':');\n if (parts.length < 5) continue; // root project line (\"groupId:artifactId:version\") or malformed\n\n const groupId = parts[0];\n const artifactId = parts[1];\n const version = parts[parts.length - 2];\n const scope = parts[parts.length - 1];\n if (!groupId || !artifactId || !version) continue;\n\n const name = `${groupId}:${artifactId}`;\n if (!result.has(name)) {\n result.set(name, { version, scope });\n }\n }\n\n return result;\n}\n\n/**\n * Extract the first license name from a pom.xml `<licenses>` block.\n * Looks for: <licenses><license><name>...</name></license></licenses>\n * Works on any pom.xml content \u2014 the project's own, or a dependency's own\n * POM read from the local Maven repository.\n */\nfunction extractMavenLicense(content: string): string | undefined {\n const licensesMatch = content.match(/<licenses\\b[^>]*>([\\s\\S]*?)<\\/licenses>/i);\n if (!licensesMatch) return undefined;\n const licenseBlock = licensesMatch[1];\n const nameMatch = licenseBlock.match(/<name>([^<]+)<\\/name>/i);\n if (nameMatch && nameMatch[1].trim() !== '') {\n return nameMatch[1].trim();\n }\n return undefined;\n}\n\n/**\n * Resolve the local Maven repository root: `~/.m2/settings.xml`'s\n * `<localRepository>` override if set, else the conventional `~/.m2/repository`.\n */\nfunction resolveMavenLocalRepoRoot(): string {\n const settingsPath = path.join(os.homedir(), '.m2', 'settings.xml');\n try {\n const settings = fs.readFileSync(settingsPath, 'utf8');\n const match = settings.match(/<localRepository>([^<]+)<\\/localRepository>/i);\n if (match && match[1].trim() !== '') return match[1].trim();\n } catch {\n // No settings.xml, or unreadable \u2014 fall through to the default.\n }\n return path.join(os.homedir(), '.m2', 'repository');\n}\n\n/**\n * Each dependency's own license lives in its own POM, not the consuming\n * project's \u2014 Maven has no lock file to carry that data, but a dependency\n * that has ever been built or downloaded locally has its POM cached at the\n * standard local-repository layout:\n * <repoRoot>/<groupId with . -> />/<artifactId>/<version>/<artifactId>-<version>.pom\n * Local file only, no network call. Returns undefined when the dependency\n * has never been resolved locally (e.g. CI running from a clean cache), same\n * as before this fix \u2014 this fills in real data on top of an \"unknown\" default,\n * it never invents one.\n */\nfunction findLocalMavenPomLicense(repoRoot: string, groupId: string, artifactId: string, version: string): string | undefined {\n const pomPath = path.join(repoRoot, ...groupId.split('.'), artifactId, version, `${artifactId}-${version}.pom`);\n try {\n const content = fs.readFileSync(pomPath, 'utf8');\n return extractMavenLicense(content);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Fill in `license` for every dependency that has a resolved version, by\n * reading that exact groupId:artifactId:version's own POM from the local\n * Maven repository. Mutates `dependencies` in place.\n */\nfunction applyLocalMavenLicenses(dependencies: ParsedDependency[]): void {\n const repoRoot = resolveMavenLocalRepoRoot();\n for (const dep of dependencies) {\n if (!dep.resolvedVersion) continue;\n const [groupId, artifactId] = dep.name.split(':');\n if (!groupId || !artifactId) continue;\n const license = findLocalMavenPomLicense(repoRoot, groupId, artifactId, dep.resolvedVersion);\n if (license !== undefined) dep.license = license;\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -57,7 +57,7 @@ async function parseNpmManifest(manifestPath, projectRoot) {
57
57
  return [];
58
58
  }
59
59
  const resolvedVersions = loadResolvedVersions(manifestDir);
60
- const license = extractNpmLicense(pkg);
60
+ const licenses = loadNpmLicenses(manifestDir);
61
61
  const dependencies = [];
62
62
  const warnings = [];
63
63
  for (const { field, scope } of SCOPE_FIELDS) {
@@ -80,6 +80,7 @@ async function parseNpmManifest(manifestPath, projectRoot) {
80
80
  continue;
81
81
  }
82
82
  const resolvedVersion = resolvedVersions.get(name);
83
+ const license = licenses.get(name);
83
84
  dependencies.push({
84
85
  name,
85
86
  declaredConstraint: constraint,
@@ -108,6 +109,7 @@ async function parseNpmManifest(manifestPath, projectRoot) {
108
109
  const alreadyDeclared = dependencies.some((d) => d.name === name);
109
110
  if (!alreadyDeclared) {
110
111
  const resolvedVersion = resolvedVersions.get(name);
112
+ const license = licenses.get(name);
111
113
  dependencies.push({
112
114
  name,
113
115
  declaredConstraint: constraint,
@@ -193,6 +195,7 @@ function extractTransitiveNpmDependencies(manifestDir, lockRelativePath, directN
193
195
  const version = pkg.version;
194
196
  if (typeof version !== "string") continue;
195
197
  seen.add(name);
198
+ const license = normalizeLicenseField(pkg);
196
199
  transitive.push({
197
200
  name,
198
201
  // No real semver range exists at the project level for a transitive
@@ -201,7 +204,8 @@ function extractTransitiveNpmDependencies(manifestDir, lockRelativePath, directN
201
204
  resolvedVersion: version,
202
205
  scope: pkg.dev === true ? "development" : "production",
203
206
  ecosystem: "npm",
204
- sourceManifest: lockRelativePath
207
+ sourceManifest: lockRelativePath,
208
+ ...license !== void 0 ? { license } : {}
205
209
  });
206
210
  }
207
211
  return transitive;
@@ -241,6 +245,31 @@ function loadResolvedVersions(manifestDir) {
241
245
  }
242
246
  return map;
243
247
  }
248
+ function loadNpmLicenses(manifestDir) {
249
+ const map = /* @__PURE__ */ new Map();
250
+ const lockPath = path.join(manifestDir, "package-lock.json");
251
+ if (!fs.existsSync(lockPath)) return map;
252
+ let lockData;
253
+ try {
254
+ lockData = JSON.parse(fs.readFileSync(lockPath, "utf8"));
255
+ } catch (err) {
256
+ (0, import_errors.logWarn)(`[sec:npm] Failed to parse package-lock.json for license data: ${err instanceof Error ? err.message : String(err)}`);
257
+ return map;
258
+ }
259
+ if (typeof lockData !== "object" || lockData === null) return map;
260
+ const packages = lockData.packages;
261
+ if (typeof packages !== "object" || packages === null) return map;
262
+ for (const [key, value] of Object.entries(packages)) {
263
+ if (key === "") continue;
264
+ if (typeof value !== "object" || value === null) continue;
265
+ const pkg = value;
266
+ const name = key.replace(/^.*node_modules\//, "");
267
+ if (!name || map.has(name)) continue;
268
+ const license = normalizeLicenseField(pkg);
269
+ if (license !== void 0) map.set(name, license);
270
+ }
271
+ return map;
272
+ }
244
273
  function extractFromPnpmLock(content, map) {
245
274
  for (const m of content.matchAll(/^[\s]*\/?(@?[a-zA-Z0-9._-]+(?:\/[a-zA-Z0-9._-]+)?)[@/]([0-9][a-zA-Z0-9._-]*)(?:\(.*?\))?:/gm)) {
246
275
  const name = m[1].replace(/^\//, "");
@@ -324,7 +353,7 @@ function extractPackageNameFromYarnHeader(header) {
324
353
  }
325
354
  return cleaned.slice(0, lastAtIndex);
326
355
  }
327
- function extractNpmLicense(pkg) {
356
+ function normalizeLicenseField(pkg) {
328
357
  const raw = pkg["license"];
329
358
  if (typeof raw === "string" && raw.trim() !== "") {
330
359
  return raw.trim();