@vyuhlabs/dxkit 2.4.2 → 2.4.4

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 (90) hide show
  1. package/CHANGELOG.md +296 -0
  2. package/README.md +9 -1
  3. package/dist/analyzers/health.js +9 -9
  4. package/dist/analyzers/health.js.map +1 -1
  5. package/dist/analyzers/licenses/gather.js +1 -1
  6. package/dist/analyzers/licenses/gather.js.map +1 -1
  7. package/dist/analyzers/quality/gather.js +2 -2
  8. package/dist/analyzers/quality/gather.js.map +1 -1
  9. package/dist/analyzers/security/gather.d.ts.map +1 -1
  10. package/dist/analyzers/security/gather.js +11 -2
  11. package/dist/analyzers/security/gather.js.map +1 -1
  12. package/dist/analyzers/tests/import-graph.d.ts.map +1 -1
  13. package/dist/analyzers/tests/import-graph.js +5 -0
  14. package/dist/analyzers/tests/import-graph.js.map +1 -1
  15. package/dist/analyzers/tests/index.d.ts.map +1 -1
  16. package/dist/analyzers/tests/index.js +2 -0
  17. package/dist/analyzers/tests/index.js.map +1 -1
  18. package/dist/analyzers/tests/types.d.ts +12 -2
  19. package/dist/analyzers/tests/types.d.ts.map +1 -1
  20. package/dist/analyzers/tools/coverage.d.ts +5 -16
  21. package/dist/analyzers/tools/coverage.d.ts.map +1 -1
  22. package/dist/analyzers/tools/coverage.js +9 -201
  23. package/dist/analyzers/tools/coverage.js.map +1 -1
  24. package/dist/analyzers/tools/generic.d.ts.map +1 -1
  25. package/dist/analyzers/tools/generic.js +21 -4
  26. package/dist/analyzers/tools/generic.js.map +1 -1
  27. package/dist/analyzers/tools/graphify.js +2 -2
  28. package/dist/analyzers/tools/grep-secrets.d.ts.map +1 -1
  29. package/dist/analyzers/tools/grep-secrets.js +10 -1
  30. package/dist/analyzers/tools/grep-secrets.js.map +1 -1
  31. package/dist/analyzers/tools/jscpd.d.ts +8 -7
  32. package/dist/analyzers/tools/jscpd.d.ts.map +1 -1
  33. package/dist/analyzers/tools/jscpd.js +30 -13
  34. package/dist/analyzers/tools/jscpd.js.map +1 -1
  35. package/dist/analyzers/tools/tool-registry.d.ts.map +1 -1
  36. package/dist/analyzers/tools/tool-registry.js +31 -20
  37. package/dist/analyzers/tools/tool-registry.js.map +1 -1
  38. package/dist/constants.d.ts +5 -3
  39. package/dist/constants.d.ts.map +1 -1
  40. package/dist/constants.js +41 -17
  41. package/dist/constants.js.map +1 -1
  42. package/dist/detect.d.ts.map +1 -1
  43. package/dist/detect.js +15 -16
  44. package/dist/detect.js.map +1 -1
  45. package/dist/doctor.d.ts.map +1 -1
  46. package/dist/doctor.js +10 -16
  47. package/dist/doctor.js.map +1 -1
  48. package/dist/generator.d.ts.map +1 -1
  49. package/dist/generator.js +41 -75
  50. package/dist/generator.js.map +1 -1
  51. package/dist/languages/capabilities/index.d.ts +21 -12
  52. package/dist/languages/capabilities/index.d.ts.map +1 -1
  53. package/dist/languages/capabilities/index.js +47 -13
  54. package/dist/languages/capabilities/index.js.map +1 -1
  55. package/dist/languages/csharp.d.ts.map +1 -1
  56. package/dist/languages/csharp.js +18 -0
  57. package/dist/languages/csharp.js.map +1 -1
  58. package/dist/languages/go.d.ts +10 -0
  59. package/dist/languages/go.d.ts.map +1 -1
  60. package/dist/languages/go.js +89 -1
  61. package/dist/languages/go.js.map +1 -1
  62. package/dist/languages/index.d.ts +42 -1
  63. package/dist/languages/index.d.ts.map +1 -1
  64. package/dist/languages/index.js +57 -1
  65. package/dist/languages/index.js.map +1 -1
  66. package/dist/languages/kotlin.d.ts +103 -0
  67. package/dist/languages/kotlin.d.ts.map +1 -0
  68. package/dist/languages/kotlin.js +676 -0
  69. package/dist/languages/kotlin.js.map +1 -0
  70. package/dist/languages/python.d.ts +3 -0
  71. package/dist/languages/python.d.ts.map +1 -1
  72. package/dist/languages/python.js +55 -1
  73. package/dist/languages/python.js.map +1 -1
  74. package/dist/languages/rust.d.ts.map +1 -1
  75. package/dist/languages/rust.js +8 -0
  76. package/dist/languages/rust.js.map +1 -1
  77. package/dist/languages/types.d.ts +69 -1
  78. package/dist/languages/types.d.ts.map +1 -1
  79. package/dist/languages/typescript.d.ts +5 -0
  80. package/dist/languages/typescript.d.ts.map +1 -1
  81. package/dist/languages/typescript.js +72 -2
  82. package/dist/languages/typescript.js.map +1 -1
  83. package/dist/project-yaml.d.ts.map +1 -1
  84. package/dist/project-yaml.js +19 -15
  85. package/dist/project-yaml.js.map +1 -1
  86. package/dist/types.d.ts +42 -15
  87. package/dist/types.d.ts.map +1 -1
  88. package/package.json +2 -1
  89. package/templates/.claude/rules/kotlin.md +11 -0
  90. package/templates/configs/kotlin/README.md +6 -0
@@ -0,0 +1,676 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.kotlin = void 0;
37
+ exports.mapDetektSeverity = mapDetektSeverity;
38
+ exports.parseDetektCheckstyleXml = parseDetektCheckstyleXml;
39
+ exports.parseJaCoCoXml = parseJaCoCoXml;
40
+ exports.parseOsvScannerMavenFindings = parseOsvScannerMavenFindings;
41
+ exports.extractKotlinImportsRaw = extractKotlinImportsRaw;
42
+ const fs = __importStar(require("fs"));
43
+ const os = __importStar(require("os"));
44
+ const path = __importStar(require("path"));
45
+ const coverage_1 = require("../analyzers/tools/coverage");
46
+ const exclusions_1 = require("../analyzers/tools/exclusions");
47
+ const osv_1 = require("../analyzers/tools/osv");
48
+ const runner_1 = require("../analyzers/tools/runner");
49
+ const tool_registry_1 = require("../analyzers/tools/tool-registry");
50
+ // ─── Detection ──────────────────────────────────────────────────────────────
51
+ /**
52
+ * Walk the project tree (bounded depth) looking for a `.kt` or `.kts`
53
+ * source file. Catches Kotlin projects that haven't yet adopted Gradle
54
+ * (rare but possible for libraries vendored as plain `src/` trees) and
55
+ * mixed JVM monorepos where Kotlin sits beside Java/Scala.
56
+ */
57
+ function hasKotlinSourceWithinDepth(cwd, maxDepth = 3) {
58
+ function search(dir, depth) {
59
+ if (depth > maxDepth)
60
+ return false;
61
+ let entries;
62
+ try {
63
+ entries = fs.readdirSync(dir, { withFileTypes: true });
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ for (const e of entries) {
69
+ if (e.name.startsWith('.') ||
70
+ ['node_modules', 'build', '.gradle', 'target'].includes(e.name)) {
71
+ continue;
72
+ }
73
+ if (e.isFile() && (e.name.endsWith('.kt') || e.name.endsWith('.kts')))
74
+ return true;
75
+ if (e.isDirectory() && search(path.join(dir, e.name), depth + 1))
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+ return search(cwd, 0);
81
+ }
82
+ function detectKotlin(cwd) {
83
+ return ((0, runner_1.fileExists)(cwd, 'build.gradle.kts') ||
84
+ (0, runner_1.fileExists)(cwd, 'settings.gradle.kts') ||
85
+ (0, runner_1.fileExists)(cwd, 'build.gradle') ||
86
+ (0, runner_1.fileExists)(cwd, 'settings.gradle') ||
87
+ (0, runner_1.fileExists)(cwd, 'gradlew') ||
88
+ hasKotlinSourceWithinDepth(cwd, 3));
89
+ }
90
+ /**
91
+ * Manifest gate for capability providers. detectKotlin() activates the
92
+ * pack on bare `.kt` source dirs too (no build manifest), but several
93
+ * capabilities (depVulns, coverage) need a build-tool manifest to do
94
+ * anything useful. This helper is the second-line check inside each
95
+ * gather function — independent of detect() so providers fail cleanly
96
+ * even if D010 (inactive-pack pollution) is later closed by stack-aware
97
+ * `providersFor()`.
98
+ */
99
+ function hasKotlinBuildManifest(cwd) {
100
+ return ((0, runner_1.fileExists)(cwd, 'build.gradle.kts') ||
101
+ (0, runner_1.fileExists)(cwd, 'build.gradle') ||
102
+ (0, runner_1.fileExists)(cwd, 'settings.gradle.kts') ||
103
+ (0, runner_1.fileExists)(cwd, 'settings.gradle') ||
104
+ (0, runner_1.fileExists)(cwd, 'pom.xml') ||
105
+ (0, runner_1.fileExists)(cwd, 'gradle.lockfile'));
106
+ }
107
+ // ─── Lint (detekt) ──────────────────────────────────────────────────────────
108
+ /**
109
+ * Map detekt's lowercased Severity enum to dxkit's four-tier scheme.
110
+ * detekt 1.23+ emits `error|warning|info` (the `dev.detekt.api.Severity`
111
+ * enum lowercased — see CheckstyleOutputReportSpec in detekt's repo).
112
+ *
113
+ * Tiering rationale (no source-of-truth from detekt — they don't tier):
114
+ * - error → high (detekt's authors classify as a real defect)
115
+ * - warning → medium (style/maintainability concerns)
116
+ * - info → low (informational signals)
117
+ * - unknown → medium (defensive default — never trust an empty string)
118
+ *
119
+ * detekt's older `Defect`/`Style`/`Maintainability`/etc. taxonomy was
120
+ * collapsed in 1.23; we only need to recognise the lowercased modern
121
+ * names. Any future taxonomy change shows up here as an unknown bucket
122
+ * rather than silently miscounting.
123
+ */
124
+ function mapDetektSeverity(severity) {
125
+ const s = severity.toLowerCase();
126
+ if (s === 'error')
127
+ return 'high';
128
+ if (s === 'warning')
129
+ return 'medium';
130
+ if (s === 'info')
131
+ return 'low';
132
+ return 'medium';
133
+ }
134
+ /**
135
+ * Pure parser for detekt's Checkstyle XML report. The format is fixed
136
+ * (detekt's CheckstyleOutputReport renders verbatim per its 1.23 spec):
137
+ *
138
+ * <?xml version="1.0" encoding="UTF-8"?>
139
+ * <checkstyle version="4.3">
140
+ * <file name="src/main/Sample1.kt">
141
+ * <TAB><error line="11" column="1" severity="error" message="..." source="detekt.style/MagicNumber" />
142
+ * ...
143
+ * </file>
144
+ * </checkstyle>
145
+ *
146
+ * We tally `<error severity="...">` attribute values; we don't need the
147
+ * full file/line index because the lint envelope only carries
148
+ * SeverityCounts. Future enrichment (per-finding paths) would extend
149
+ * the parser without breaking this signature.
150
+ *
151
+ * Exported for unit tests; consumed by `gatherKotlinLintResult`.
152
+ */
153
+ function parseDetektCheckstyleXml(raw) {
154
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
155
+ // Match each <error ...> element's severity attribute. `severity="X"`
156
+ // is mandatory in detekt's renderer; we still default to 'medium' for
157
+ // forward compat with hypothetical future detekt versions.
158
+ const re = /<error\s+[^>]*severity="([^"]*)"/g;
159
+ let m;
160
+ while ((m = re.exec(raw)) !== null) {
161
+ counts[mapDetektSeverity(m[1])]++;
162
+ }
163
+ return counts;
164
+ }
165
+ /**
166
+ * Single source of truth for the kotlin pack's lint gathering.
167
+ * Consumed by `kotlinLintProvider` (capability dispatcher).
168
+ *
169
+ * detekt is invoked with `--input <cwd>` to scan the whole project, and
170
+ * `--report xml:<tmp>` to produce a parseable Checkstyle XML. Default
171
+ * config is implicit; we don't pass `--build-upon-default-config` because
172
+ * 1) it's the default since detekt 1.23, and 2) projects that ship a
173
+ * `detekt.yml` get their own config respected automatically.
174
+ *
175
+ * Exit code: detekt exits 1 when issues are found and 2 on internal
176
+ * errors. We tolerate any non-fatal exit by reading the XML regardless;
177
+ * a missing/unparseable XML is treated as `unavailable`.
178
+ */
179
+ function gatherKotlinLintResult(cwd) {
180
+ if (!hasKotlinBuildManifest(cwd) && !hasKotlinSourceWithinDepth(cwd, 3)) {
181
+ return { kind: 'unavailable', reason: 'no kotlin source' };
182
+ }
183
+ const detekt = (0, tool_registry_1.findTool)(tool_registry_1.TOOL_DEFS.detekt, cwd);
184
+ if (!detekt.available || !detekt.path) {
185
+ return { kind: 'unavailable', reason: 'not installed' };
186
+ }
187
+ // detekt v1.23 cli supports `--report xml:<path>` only — no stdout
188
+ // form. Use a process-unique temp file so concurrent dxkit runs don't
189
+ // race on the same path.
190
+ const tmpFile = path.join(os.tmpdir(), `dxkit-detekt-${process.pid}-${Date.now()}.xml`);
191
+ try {
192
+ (0, runner_1.run)(`${detekt.path} --input . --report xml:${tmpFile} 2>/dev/null`, cwd, 120000);
193
+ let raw;
194
+ try {
195
+ raw = fs.readFileSync(tmpFile, 'utf-8');
196
+ }
197
+ catch {
198
+ return { kind: 'unavailable', reason: 'no detekt output' };
199
+ }
200
+ const counts = parseDetektCheckstyleXml(raw);
201
+ const envelope = { schemaVersion: 1, tool: 'detekt', counts };
202
+ return { kind: 'success', envelope };
203
+ }
204
+ finally {
205
+ try {
206
+ fs.unlinkSync(tmpFile);
207
+ }
208
+ catch {
209
+ /* best-effort cleanup */
210
+ }
211
+ }
212
+ }
213
+ const kotlinLintProvider = {
214
+ source: 'kotlin',
215
+ async gather(cwd) {
216
+ const outcome = gatherKotlinLintResult(cwd);
217
+ return outcome.kind === 'success' ? outcome.envelope : null;
218
+ },
219
+ };
220
+ // ─── Coverage (JaCoCo XML) ──────────────────────────────────────────────────
221
+ /**
222
+ * Pure parser for JaCoCo's XML report (DTD: `report.dtd`). The structure:
223
+ *
224
+ * <report name="...">
225
+ * <package name="com/example">
226
+ * <class name="com/example/Foo" sourcefilename="Foo.kt">...</class>
227
+ * <sourcefile name="Foo.kt">
228
+ * <line nr="N" mi="..." ci="..." mb="..." cb="..."/>
229
+ * <counter type="LINE" missed="X" covered="Y"/>
230
+ * </sourcefile>
231
+ * <counter type="LINE" missed="X" covered="Y"/>
232
+ * </package>
233
+ * <counter type="LINE" missed="X" covered="Y"/>
234
+ * </report>
235
+ *
236
+ * Per-file coverage comes from `<sourcefile>` blocks: their LINE counter
237
+ * holds the file's missed/covered totals. Project-level total comes from
238
+ * the top-level `<counter type="LINE">` (last in the document).
239
+ *
240
+ * Path attribution joins `<package name>` (forward-slashed, JVM-style)
241
+ * with `<sourcefile name>` to produce the canonical relative path the
242
+ * downstream consumers expect (`com/example/Foo.kt`). JVM bytecode
243
+ * namespacing isn't 1:1 with on-disk source paths in multi-module
244
+ * projects — accepted limitation (matches C#'s cobertura attribution).
245
+ *
246
+ * Returns null when no `<counter type="LINE">` exists at the top level
247
+ * — that's JaCoCo's "no coverage data" signal, distinct from "0%
248
+ * coverage" (where the counter exists with covered=0).
249
+ *
250
+ * Exported for unit tests; consumed by `gatherKotlinCoverageResult`.
251
+ */
252
+ function parseJaCoCoXml(raw, sourceFile, _cwd) {
253
+ const files = new Map();
254
+ // Iterate <package> blocks. Each block contains <sourcefile> children
255
+ // we tally and aggregate counters we can ignore (they're sums of the
256
+ // children, redundant given we sum the children ourselves).
257
+ const packageRe = /<package\s+name="([^"]+)">([\s\S]*?)<\/package>/g;
258
+ let pm;
259
+ while ((pm = packageRe.exec(raw)) !== null) {
260
+ const pkgPath = pm[1].replace(/\\/g, '/'); // JVM uses forward-slashes already; defensive
261
+ const pkgInner = pm[2];
262
+ // Within a <package>, <sourcefile> blocks own per-file counters.
263
+ const sourceFileRe = /<sourcefile\s+name="([^"]+)">([\s\S]*?)<\/sourcefile>/g;
264
+ let sm;
265
+ while ((sm = sourceFileRe.exec(pkgInner)) !== null) {
266
+ const fileName = sm[1];
267
+ const sourceInner = sm[2];
268
+ // The LAST <counter type="LINE"> in <sourcefile> is the
269
+ // file-level aggregate; earlier ones are per-method. We pick the
270
+ // last to get the full-file roll-up.
271
+ const counterRe = /<counter\s+type="LINE"\s+missed="(\d+)"\s+covered="(\d+)"\s*\/>/g;
272
+ let lastMissed = 0;
273
+ let lastCovered = 0;
274
+ let cm;
275
+ while ((cm = counterRe.exec(sourceInner)) !== null) {
276
+ lastMissed = parseInt(cm[1], 10);
277
+ lastCovered = parseInt(cm[2], 10);
278
+ }
279
+ const total = lastMissed + lastCovered;
280
+ const rel = pkgPath ? `${pkgPath}/${fileName}` : fileName;
281
+ files.set(rel, {
282
+ path: rel,
283
+ covered: lastCovered,
284
+ total,
285
+ pct: (0, coverage_1.round1)(total > 0 ? (lastCovered / total) * 100 : 0),
286
+ });
287
+ }
288
+ }
289
+ // Top-level project-wide LINE counter — JaCoCo emits it after the
290
+ // last </package>. Use a non-greedy match against the document tail
291
+ // to avoid grabbing per-package counters as project-level.
292
+ const tailMatch = raw.match(/<\/package>\s*<counter\s+type="LINE"\s+missed="(\d+)"\s+covered="(\d+)"\s*\/>\s*<\/report>/);
293
+ let totalMissed = 0;
294
+ let totalCovered = 0;
295
+ if (tailMatch) {
296
+ totalMissed = parseInt(tailMatch[1], 10);
297
+ totalCovered = parseInt(tailMatch[2], 10);
298
+ }
299
+ else {
300
+ // No project-level counter (degenerate report — single package, no
301
+ // explicit roll-up). Sum the per-file totals as the linePercent
302
+ // basis.
303
+ for (const f of files.values()) {
304
+ totalCovered += f.covered;
305
+ totalMissed += f.total - f.covered;
306
+ }
307
+ }
308
+ const grandTotal = totalCovered + totalMissed;
309
+ if (grandTotal === 0 && files.size === 0)
310
+ return null;
311
+ return {
312
+ source: 'jacoco',
313
+ sourceFile,
314
+ linePercent: (0, coverage_1.round1)(grandTotal > 0 ? (totalCovered / grandTotal) * 100 : 0),
315
+ files,
316
+ };
317
+ }
318
+ /**
319
+ * Standard JaCoCo report locations across project layouts:
320
+ * - app/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml
321
+ * (Android default — `app` module, `jacocoTestReport` task)
322
+ * - build/reports/jacoco/test/jacocoTestReport.xml
323
+ * (plain JVM Kotlin via the `jacoco` plugin's default `test` task)
324
+ * - build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml
325
+ * (multi-module aggregate; many builds rename the task)
326
+ * - jacocoTestReport.xml (top-level — fixture / direct path)
327
+ *
328
+ * Multi-module projects emit per-module reports under `<module>/build/...`;
329
+ * the cross-ecosystem fixture in step 5 will calibrate which paths show
330
+ * up reliably. Until then, the list above is conservative-priority: most
331
+ * specific (app-module) first, root fallback last.
332
+ */
333
+ function findJaCoCoReport(cwd) {
334
+ const candidates = [
335
+ 'app/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml',
336
+ 'build/reports/jacoco/test/jacocoTestReport.xml',
337
+ 'build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml',
338
+ 'jacocoTestReport.xml',
339
+ ];
340
+ for (const rel of candidates) {
341
+ const abs = path.join(cwd, rel);
342
+ if (fs.existsSync(abs))
343
+ return rel;
344
+ }
345
+ return null;
346
+ }
347
+ function gatherKotlinCoverageResult(cwd) {
348
+ const reportRel = findJaCoCoReport(cwd);
349
+ if (!reportRel)
350
+ return null;
351
+ let raw;
352
+ try {
353
+ raw = fs.readFileSync(path.join(cwd, reportRel), 'utf-8');
354
+ }
355
+ catch {
356
+ return null;
357
+ }
358
+ const coverage = parseJaCoCoXml(raw, reportRel, cwd);
359
+ if (!coverage)
360
+ return null;
361
+ return { schemaVersion: 1, tool: `coverage:${coverage.source}`, coverage };
362
+ }
363
+ const kotlinCoverageProvider = {
364
+ source: 'kotlin',
365
+ async gather(cwd) {
366
+ return gatherKotlinCoverageResult(cwd);
367
+ },
368
+ };
369
+ /**
370
+ * Pure parser for osv-scanner v2.x JSON output, scoped to Maven
371
+ * findings only. Other ecosystems (npm, PyPI, Go) are filtered out so
372
+ * polyglot repos don't double-count: the typescript pack handles npm,
373
+ * the python pack handles PyPI, etc. The kotlin pack owns Maven.
374
+ *
375
+ * Returns counts + findings + the raw OSV vuln records for downstream
376
+ * CVSS resolution. Exported for unit tests.
377
+ */
378
+ function parseOsvScannerMavenFindings(raw) {
379
+ const counts = { critical: 0, high: 0, medium: 0, low: 0 };
380
+ const findings = [];
381
+ const vulnsForCvss = [];
382
+ let data;
383
+ try {
384
+ data = JSON.parse(raw);
385
+ }
386
+ catch {
387
+ return { counts, findings, vulnsForCvss };
388
+ }
389
+ // Dedup at the source: osv-scanner can list the same advisory twice
390
+ // when a transitive dep is reachable through multiple top-level deps.
391
+ // Same (package, version, id) → same fingerprint, so collapse here.
392
+ const seen = new Set();
393
+ for (const result of data.results ?? []) {
394
+ for (const pkg of result.packages ?? []) {
395
+ if (pkg.package?.ecosystem !== 'Maven')
396
+ continue;
397
+ const pkgName = pkg.package.name ?? 'unknown';
398
+ const pkgVersion = pkg.package.version;
399
+ for (const vuln of pkg.vulnerabilities ?? []) {
400
+ if (!vuln.id)
401
+ continue;
402
+ const dedupKey = `${pkgName}\0${pkgVersion ?? ''}\0${vuln.id}`;
403
+ if (seen.has(dedupKey))
404
+ continue;
405
+ seen.add(dedupKey);
406
+ const sev = (0, osv_1.classifyOsvSeverity)(vuln);
407
+ const tier = sev === 'critical' || sev === 'high' || sev === 'medium' || sev === 'low'
408
+ ? sev
409
+ : 'medium';
410
+ counts[tier]++;
411
+ const cvss = (0, osv_1.extractOsvCvssScore)(vuln);
412
+ const aliases = (vuln.aliases ?? []).filter((a) => a && a.length > 0);
413
+ const finding = {
414
+ id: vuln.id,
415
+ package: pkgName,
416
+ installedVersion: pkgVersion,
417
+ tool: 'osv-scanner',
418
+ severity: tier,
419
+ };
420
+ if (cvss !== null)
421
+ finding.cvssScore = cvss;
422
+ if (aliases.length > 0)
423
+ finding.aliases = aliases;
424
+ if (vuln.summary)
425
+ finding.summary = vuln.summary;
426
+ // OSV.dev hosts a canonical page per id — synthesize when the
427
+ // record's `references[]` is empty, otherwise keep the
428
+ // tool-supplied URLs.
429
+ const refUrls = (vuln.references ?? []).map((r) => r.url).filter((u) => !!u);
430
+ finding.references =
431
+ refUrls.length > 0 ? refUrls : [`https://osv.dev/vulnerability/${vuln.id}`];
432
+ findings.push(finding);
433
+ vulnsForCvss.push({
434
+ primaryId: vuln.id,
435
+ embeddedCvss: cvss,
436
+ aliases,
437
+ });
438
+ }
439
+ }
440
+ }
441
+ return { counts, findings, vulnsForCvss };
442
+ }
443
+ /**
444
+ * Single source of truth for the kotlin pack's dep-vuln gathering.
445
+ * Consumed by `kotlinDepVulnsProvider` (capability dispatcher).
446
+ *
447
+ * Tool choice: osv-scanner is the established multi-ecosystem scanner;
448
+ * no Tier-1 native equivalent exists for Maven/Gradle (CLAUDE.md rule
449
+ * #5). osv-scanner-fix.ts in the typescript pack uses the `fix`
450
+ * subcommand for upgrade planning — different mode, no shared logic.
451
+ *
452
+ * Manifest gating: osv-scanner reads `pom.xml`, `gradle.lockfile`,
453
+ * `gradle/verification-metadata.xml`, and (limited) `build.gradle`. Bare
454
+ * `build.gradle.kts` is NOT a reliable input — gradle.lockfile is
455
+ * preferred. Without any of these, return `tool-missing` (matches
456
+ * python/csharp's manifest-gating pattern).
457
+ */
458
+ async function gatherKotlinDepVulnsResult(cwd) {
459
+ // Find the most reliable manifest. Order matters: lockfile > pom.xml
460
+ // > verification-metadata. We pass it explicitly via --lockfile so
461
+ // osv-scanner doesn't fall back to its (unreliable) build.gradle.kts
462
+ // parser. Multi-module Android projects with per-module lockfiles
463
+ // are not yet handled — first-module-found is the v1 behaviour;
464
+ // future enhancement scoped to 10j.x recipe-gap.
465
+ const manifestCandidates = ['gradle.lockfile', 'pom.xml', 'gradle/verification-metadata.xml'];
466
+ let manifest = null;
467
+ for (const rel of manifestCandidates) {
468
+ if ((0, runner_1.fileExists)(cwd, rel)) {
469
+ manifest = rel;
470
+ break;
471
+ }
472
+ }
473
+ if (!manifest)
474
+ return { kind: 'tool-missing' };
475
+ const scanner = (0, tool_registry_1.findTool)(tool_registry_1.TOOL_DEFS['osv-scanner'], cwd);
476
+ if (!scanner.available || !scanner.path)
477
+ return { kind: 'tool-missing' };
478
+ // `scan source --lockfile <path>` is the v2.x form. JSON output to
479
+ // stdout. Exit code is non-zero when findings exist — we ignore the
480
+ // exit code and parse the JSON regardless (run() already swallows
481
+ // non-zero exits cleanly via execSync's catch).
482
+ const raw = (0, runner_1.run)(`${scanner.path} scan source --lockfile ${manifest} --format json 2>/dev/null`, cwd, 180000);
483
+ if (!raw)
484
+ return { kind: 'no-output' };
485
+ const { counts, findings, vulnsForCvss } = parseOsvScannerMavenFindings(raw);
486
+ // CVSS alias-fallback: osv-scanner ships CVSS vectors when present,
487
+ // but Maven advisories are inconsistent — some carry only
488
+ // database_specific.severity strings. resolveCvssScores looks up via
489
+ // CVE alias when the primary record lacks a vector.
490
+ if (findings.length > 0) {
491
+ const resolved = await (0, osv_1.resolveCvssScores)(vulnsForCvss);
492
+ for (const f of findings) {
493
+ const score = resolved.get(f.id);
494
+ if (score !== null && score !== undefined)
495
+ f.cvssScore = score;
496
+ }
497
+ }
498
+ const envelope = {
499
+ schemaVersion: 1,
500
+ tool: 'osv-scanner',
501
+ enrichment: 'osv.dev',
502
+ counts,
503
+ findings,
504
+ };
505
+ return { kind: 'success', envelope };
506
+ }
507
+ const kotlinDepVulnsProvider = {
508
+ source: 'kotlin',
509
+ async gather(cwd) {
510
+ const outcome = await gatherKotlinDepVulnsResult(cwd);
511
+ return outcome.kind === 'success' ? outcome.envelope : null;
512
+ },
513
+ };
514
+ // ─── Imports (regex extraction, no resolver) ────────────────────────────────
515
+ /**
516
+ * Capture Kotlin import specifiers from source text. Handles both
517
+ * `import com.foo.Bar` (single) and `import com.foo.*` (wildcard) plus
518
+ * the `import com.foo.Bar as Baz` alias form. Comments are stripped
519
+ * conservatively — single-line `//` and inline `/* ... *\/`. Multi-line
520
+ * `/* ... *\/` blocks containing `import` statements are not extracted
521
+ * (acceptable: comment-out-import is intentional non-use).
522
+ *
523
+ * Exported for unit tests; consumed by `gatherKotlinImportsResult`.
524
+ */
525
+ function extractKotlinImportsRaw(content) {
526
+ const out = [];
527
+ // Strip line comments first so `// import foo` doesn't false-match.
528
+ const stripped = content.replace(/\/\/[^\n]*/g, '');
529
+ // `import` must start a statement (preceded only by whitespace at line
530
+ // start). Trailing `as Alias` is captured but discarded.
531
+ const re = /^\s*import\s+([A-Za-z_][\w.*]*)/gm;
532
+ let m;
533
+ while ((m = re.exec(stripped)) !== null) {
534
+ out.push(m[1]);
535
+ }
536
+ return out;
537
+ }
538
+ /**
539
+ * Enumerate `.kt` / `.kts` files under cwd and capture per-file imports.
540
+ * Kotlin packages don't 1:1 map to file paths (a package `com.foo` can
541
+ * span many files in many directories), so we don't produce `edges` —
542
+ * the resolution would be heuristic and is best left to graphify if
543
+ * downstream consumers need it. Mirrors the rust pack's choice.
544
+ */
545
+ function gatherKotlinImportsResult(cwd) {
546
+ const excludes = (0, exclusions_1.getFindExcludeFlags)(cwd);
547
+ const raw = (0, runner_1.run)(`find . -type f \\( -name "*.kt" -o -name "*.kts" \\) ${excludes} 2>/dev/null`, cwd);
548
+ if (!raw)
549
+ return null;
550
+ const extracted = new Map();
551
+ for (const line of raw.split('\n')) {
552
+ const p = line.trim();
553
+ if (!p)
554
+ continue;
555
+ const rel = p.replace(/^\.\//, '');
556
+ let content;
557
+ try {
558
+ content = fs.readFileSync(path.join(cwd, rel), 'utf-8');
559
+ }
560
+ catch {
561
+ continue;
562
+ }
563
+ extracted.set(rel, extractKotlinImportsRaw(content));
564
+ }
565
+ if (extracted.size === 0)
566
+ return null;
567
+ return {
568
+ schemaVersion: 1,
569
+ tool: 'kotlin-imports',
570
+ sourceExtensions: ['.kt', '.kts'],
571
+ extracted,
572
+ edges: new Map(),
573
+ };
574
+ }
575
+ const kotlinImportsProvider = {
576
+ source: 'kotlin',
577
+ async gather(cwd) {
578
+ return gatherKotlinImportsResult(cwd);
579
+ },
580
+ };
581
+ // ─── Test framework (gradle dependency text scan) ───────────────────────────
582
+ /**
583
+ * Detect the test framework by scanning gradle build files for known
584
+ * dependency coordinates. Order of precedence: Kotest → Spek → JUnit
585
+ * (Kotest/Spek typically sit alongside JUnit, and the more specific
586
+ * framework is the "primary" runner).
587
+ *
588
+ * Returns null when no gradle file exists or no known runner is
589
+ * declared — a polyglot Maven-only Kotlin project would skip this
590
+ * cleanly (no false positive on `pom.xml` lookups, until a Kotlin/Maven
591
+ * customer surfaces).
592
+ */
593
+ function gatherKotlinTestFrameworkResult(cwd) {
594
+ const gradleFiles = ['build.gradle.kts', 'build.gradle'];
595
+ let combinedText = '';
596
+ for (const rel of gradleFiles) {
597
+ if (!(0, runner_1.fileExists)(cwd, rel))
598
+ continue;
599
+ try {
600
+ combinedText += fs.readFileSync(path.join(cwd, rel), 'utf-8') + '\n';
601
+ }
602
+ catch {
603
+ /* ignore unreadable */
604
+ }
605
+ }
606
+ if (!combinedText)
607
+ return null;
608
+ if (combinedText.includes('io.kotest:')) {
609
+ return { schemaVersion: 1, tool: 'kotlin', name: 'kotest' };
610
+ }
611
+ if (combinedText.includes('org.spekframework:') || combinedText.includes('spek-')) {
612
+ return { schemaVersion: 1, tool: 'kotlin', name: 'spek' };
613
+ }
614
+ if (combinedText.includes('junit') || combinedText.includes('JUnit')) {
615
+ return { schemaVersion: 1, tool: 'kotlin', name: 'junit' };
616
+ }
617
+ return null;
618
+ }
619
+ const kotlinTestFrameworkProvider = {
620
+ source: 'kotlin',
621
+ async gather(cwd) {
622
+ return gatherKotlinTestFrameworkResult(cwd);
623
+ },
624
+ };
625
+ // ─── Pack export ────────────────────────────────────────────────────────────
626
+ exports.kotlin = {
627
+ id: 'kotlin',
628
+ displayName: 'Kotlin (Android)',
629
+ // `.kts` covers both Gradle build scripts and Kotlin scratch files; the
630
+ // dxkit analyzers treat them as first-class source so coverage and lint
631
+ // reach build logic too. Java files in mixed Kotlin/Java codebases are
632
+ // handled by the Java pack (when added) — not co-mingled here, to keep
633
+ // attribution clean.
634
+ sourceExtensions: ['.kt', '.kts'],
635
+ // JUnit-style (*Test/*Tests) plus Spek/Kotest's `*Spec.kt` convention.
636
+ testFilePatterns: ['*Test.kt', '*Tests.kt', '*Spec.kt'],
637
+ // `build` = Gradle build output (per-project + per-module),
638
+ // `.gradle` = Gradle daemon/cache state,
639
+ // `out` = IntelliJ IDE build output.
640
+ extraExcludes: ['build', '.gradle', 'out'],
641
+ detect: detectKotlin,
642
+ tools: ['detekt', 'osv-scanner'],
643
+ // Semgrep's Kotlin ruleset (`p/kotlin`) is sparse compared to Python/JS
644
+ // — skipping for now until coverage matures, mirroring the csharp pack.
645
+ semgrepRulesets: [],
646
+ capabilities: {
647
+ depVulns: kotlinDepVulnsProvider,
648
+ lint: kotlinLintProvider,
649
+ coverage: kotlinCoverageProvider,
650
+ imports: kotlinImportsProvider,
651
+ testFramework: kotlinTestFrameworkProvider,
652
+ // licenses: deliberately omitted. No canonical CLI license tool for
653
+ // Maven/Gradle equivalent to pip-licenses or cargo-license. Gradle
654
+ // plugins (jk1.dependency-license-report) require modifying user's
655
+ // build.gradle.kts which violates pack non-intrusiveness. Re-evaluate
656
+ // if a customer surfaces the need.
657
+ },
658
+ mapLintSeverity: mapDetektSeverity,
659
+ // ─── LP-recipe metadata ────────────────────────────────────────────────
660
+ permissions: ['Bash(./gradlew:*)', 'Bash(gradle:*)', 'Bash(detekt:*)'],
661
+ ruleFile: 'kotlin.md',
662
+ templateFiles: [],
663
+ cliBinaries: ['gradle', 'detekt'],
664
+ defaultVersion: '2.0.21',
665
+ projectYamlBlock: ({ config, enabled }) => [
666
+ ` kotlin:`,
667
+ ` enabled: ${enabled}`,
668
+ ` version: "${config.versions.kotlin ?? ''}"`,
669
+ ` quality:`,
670
+ ` coverage: ${config.coverageThreshold}`,
671
+ ` lint: true`,
672
+ ` typecheck: true`,
673
+ ` format: true`,
674
+ ].join('\n'),
675
+ };
676
+ //# sourceMappingURL=kotlin.js.map