@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.
- package/CHANGELOG.md +296 -0
- package/README.md +9 -1
- package/dist/analyzers/health.js +9 -9
- package/dist/analyzers/health.js.map +1 -1
- package/dist/analyzers/licenses/gather.js +1 -1
- package/dist/analyzers/licenses/gather.js.map +1 -1
- package/dist/analyzers/quality/gather.js +2 -2
- package/dist/analyzers/quality/gather.js.map +1 -1
- package/dist/analyzers/security/gather.d.ts.map +1 -1
- package/dist/analyzers/security/gather.js +11 -2
- package/dist/analyzers/security/gather.js.map +1 -1
- package/dist/analyzers/tests/import-graph.d.ts.map +1 -1
- package/dist/analyzers/tests/import-graph.js +5 -0
- package/dist/analyzers/tests/import-graph.js.map +1 -1
- package/dist/analyzers/tests/index.d.ts.map +1 -1
- package/dist/analyzers/tests/index.js +2 -0
- package/dist/analyzers/tests/index.js.map +1 -1
- package/dist/analyzers/tests/types.d.ts +12 -2
- package/dist/analyzers/tests/types.d.ts.map +1 -1
- package/dist/analyzers/tools/coverage.d.ts +5 -16
- package/dist/analyzers/tools/coverage.d.ts.map +1 -1
- package/dist/analyzers/tools/coverage.js +9 -201
- package/dist/analyzers/tools/coverage.js.map +1 -1
- package/dist/analyzers/tools/generic.d.ts.map +1 -1
- package/dist/analyzers/tools/generic.js +21 -4
- package/dist/analyzers/tools/generic.js.map +1 -1
- package/dist/analyzers/tools/graphify.js +2 -2
- package/dist/analyzers/tools/grep-secrets.d.ts.map +1 -1
- package/dist/analyzers/tools/grep-secrets.js +10 -1
- package/dist/analyzers/tools/grep-secrets.js.map +1 -1
- package/dist/analyzers/tools/jscpd.d.ts +8 -7
- package/dist/analyzers/tools/jscpd.d.ts.map +1 -1
- package/dist/analyzers/tools/jscpd.js +30 -13
- package/dist/analyzers/tools/jscpd.js.map +1 -1
- package/dist/analyzers/tools/tool-registry.d.ts.map +1 -1
- package/dist/analyzers/tools/tool-registry.js +31 -20
- package/dist/analyzers/tools/tool-registry.js.map +1 -1
- package/dist/constants.d.ts +5 -3
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +41 -17
- package/dist/constants.js.map +1 -1
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +15 -16
- package/dist/detect.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +10 -16
- package/dist/doctor.js.map +1 -1
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +41 -75
- package/dist/generator.js.map +1 -1
- package/dist/languages/capabilities/index.d.ts +21 -12
- package/dist/languages/capabilities/index.d.ts.map +1 -1
- package/dist/languages/capabilities/index.js +47 -13
- package/dist/languages/capabilities/index.js.map +1 -1
- package/dist/languages/csharp.d.ts.map +1 -1
- package/dist/languages/csharp.js +18 -0
- package/dist/languages/csharp.js.map +1 -1
- package/dist/languages/go.d.ts +10 -0
- package/dist/languages/go.d.ts.map +1 -1
- package/dist/languages/go.js +89 -1
- package/dist/languages/go.js.map +1 -1
- package/dist/languages/index.d.ts +42 -1
- package/dist/languages/index.d.ts.map +1 -1
- package/dist/languages/index.js +57 -1
- package/dist/languages/index.js.map +1 -1
- package/dist/languages/kotlin.d.ts +103 -0
- package/dist/languages/kotlin.d.ts.map +1 -0
- package/dist/languages/kotlin.js +676 -0
- package/dist/languages/kotlin.js.map +1 -0
- package/dist/languages/python.d.ts +3 -0
- package/dist/languages/python.d.ts.map +1 -1
- package/dist/languages/python.js +55 -1
- package/dist/languages/python.js.map +1 -1
- package/dist/languages/rust.d.ts.map +1 -1
- package/dist/languages/rust.js +8 -0
- package/dist/languages/rust.js.map +1 -1
- package/dist/languages/types.d.ts +69 -1
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/languages/typescript.d.ts +5 -0
- package/dist/languages/typescript.d.ts.map +1 -1
- package/dist/languages/typescript.js +72 -2
- package/dist/languages/typescript.js.map +1 -1
- package/dist/project-yaml.d.ts.map +1 -1
- package/dist/project-yaml.js +19 -15
- package/dist/project-yaml.js.map +1 -1
- package/dist/types.d.ts +42 -15
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/templates/.claude/rules/kotlin.md +11 -0
- 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
|