-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathjava_gradle.js
More file actions
586 lines (507 loc) · 19.7 KB
/
Copy pathjava_gradle.js
File metadata and controls
586 lines (507 loc) · 19.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
import crypto from 'node:crypto'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { EOL } from 'os'
import { parse as parseToml } from 'smol-toml'
import { readLicenseFile } from '../license/license_utils.js'
import Sbom from '../sbom.js'
import { invokeCommand } from '../tools.js'
import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js'
import Base_java, { ecosystem_gradle } from "./base_java.js";
/** @typedef {import('../provider.js').Provider} */
/** @typedef {import('../provider.js').Provided} Provided */
const ROOT_PROJECT_KEY_NAME = "root-project";
const TRUSTIFY_DA_IGNORE_REGEX_LINE = /.*\s?exhortignore\s*$/g
const TRUSTIFY_DA_IGNORE_REGEX = /\/\/\s?exhortignore/
/**
* Check if the dependency marked for exclusion has libs notation , so if it's true the rest of coordinates( GAV) should be fetched from TOML file.
* @param {string} depToBeIgnored
* @return {boolean} returns if the dependency type has library notation or not
*/
function depHasLibsNotation(depToBeIgnored) {
const regex = new RegExp(":", "g");
return (depToBeIgnored.trim().startsWith("library(") || depToBeIgnored.trim().includes("libs."))
&& (depToBeIgnored.match(regex) || []).length <= 1
}
function stripString(depPart) {
return depPart.replaceAll(/["']/g,"")
}
/**
* This class provides common functionality for Groovy and Kotlin DSL files.
*/
export default class Java_gradle extends Base_java {
constructor() {
super('gradle', 'gradlew' + (process.platform === 'win32' ? '.bat' : ''))
}
_getManifestName() {
throw new Error('implement getManifestName method')
}
_parseAliasForLibsNotation() {
throw new Error('implement parseAliasForLibsNotation method')
}
_extractDepToBeIgnored() {
throw new Error('implement extractDepToBeIgnored method')
}
/**
* @param {string} manifestName - the subject manifest name-type
* @returns {boolean} - return true if the manifest name-type is the supported type (example build.gradle)
*/
isSupported(manifestName) {
return this._getManifestName() === manifestName
}
/**
* @param {string} manifestDir - the directory where the manifest lies
*/
validateLockFile() { return true; }
/**
* Gradle manifests (build.gradle, build.gradle.kts) have no standard license field.
* @param {string} manifestPath - path to manifest
* @returns {null}
*/
// eslint-disable-next-line no-unused-vars
readLicenseFromManifest(manifestPath) { return readLicenseFile(manifestPath); }
/**
* Provide content and content type for stack analysis.
* @param {string} manifest - the manifest path or name
* @param {{}} [opts={}] - optional various options to pass along the application
* @returns {Provided}
*/
provideStack(manifest, opts = {}) {
return {
ecosystem: ecosystem_gradle,
content: this.#createSbomStackAnalysis(manifest, opts),
contentType: 'application/vnd.cyclonedx+json'
}
}
/**
* Provide content and content type for maven-maven component analysis.
* @param {string} manifest - path to pom.xml for component report
* @param {{}} [opts={}] - optional various options to pass along the application
* @returns {Provided}
*/
provideComponent(manifest, opts = {}) {
return {
ecosystem: ecosystem_gradle,
content: this.#getSbomForComponentAnalysis(manifest, opts),
contentType: 'application/vnd.cyclonedx+json'
}
}
/**
* @param {string} line - the line to parse
* @returns {number} the depth of the dependency in the tree starting from 1. -1 if the line is not a dependency.
* @private
*/
#getIndentationLevel(line) {
// If it is level 1
let match = line.match(/^[\\+-]/);
if (match) {
return 1;
}
// Count the groups of 4 spaces preceded by a pipe or 5 spaces
match = line.match(/\| {4}| {5}/g);
if (!match) {return -1;}
return match.length + 1;
}
#prepareLinesForParsingDependencyTree(lines) {
return lines
.filter(dep => dep.trim() !== "" && !dep.endsWith(" FAILED"))
.map(dependency => {
// Calculate depth from original line
const depth = this.#getIndentationLevel(dependency);
// Now process the dependency line
let processedLine = dependency.replaceAll("|", "");
processedLine = processedLine.replaceAll(/\\---|\+---/g, "");
processedLine = processedLine.replaceAll(/:(.*):(.*) -> (.*)$/g, ":$1:$3");
processedLine = processedLine.replaceAll(/:(.*)\W*->\W*(.*)$/g, ":$1:$2");
processedLine = processedLine.replaceAll(/(.*):(.*):(.*)$/g, "$1:$2:jar:$3");
processedLine = processedLine.replaceAll(/(n)$/g, "");
processedLine = processedLine.replace(/\s*\(\*\)$/, '').trim();
// Return both the processed line and its depth
return {
line: `${processedLine}:compile`,
depth: depth
};
});
}
/**
* Process the dependency tree and add dependencies to the SBOM
* @param {string[]} config - the configuration lines to process
* @param {Object} parentPurl - the parent package URL
* @param {Sbom} sbom - the SBOM object to add dependencies to
* @param {Set} processedDeps - set of already processed dependencies
* @param {string} scope - the dependency scope
* @private
*/
#processDependencyTree(config, parentPurl, sbom, processedDeps, scope) {
const processedLines = this.#prepareLinesForParsingDependencyTree(config);
let parentStack = [parentPurl];
for (const {line, depth} of processedLines) {
if (line) {
const lastDepth = parentStack.length - 1;
if (depth <= lastDepth) {
// Going up - pop parents until we reach the correct level
parentStack = parentStack.slice(0, depth);
}
const currentParent = parentStack[depth - 1];
const purl = this.parseDep(line);
purl.scope = scope;
// Create a unique key for this dependency
const depKey = `${currentParent.namespace}:${currentParent.name}:${currentParent.version}->${purl.namespace}:${purl.name}:${purl.version}`;
// Add dependency to SBOM if not already processed
if (!processedDeps.has(depKey)) {
processedDeps.add(depKey);
sbom.addDependency(currentParent, purl, scope);
}
parentStack.push(purl);
}
}
}
/**
* Create a Dot Graph dependency tree for a manifest path.
* @param {string} manifest - path for pom.xml
* @param {{}} [opts={}] - optional various options to pass along the application
* @returns {string} the Dot Graph content
* @private
*/
#buildSbom(content, properties, manifestPath, opts = {}) {
let sbom = new Sbom();
let root = `${properties.group}:${properties[ROOT_PROJECT_KEY_NAME].match(/Root project '(.+)'/)[1]}:jar:${properties.version}`
let rootPurl = this.parseDep(root)
const license = this.readLicenseFromManifest(manifestPath);
sbom.addRoot(rootPurl, license)
let ignoredDeps = this.#getIgnoredDeps(manifestPath);
const [runtimeConfig, compileConfig] = this.#extractConfigurations(content);
const processedDeps = new Set();
this.#processDependencyTree(runtimeConfig, rootPurl, sbom, processedDeps, 'required');
this.#processDependencyTree(compileConfig, rootPurl, sbom, processedDeps, 'optional');
return sbom.filterIgnoredDepsIncludingVersion(ignoredDeps).getAsJsonString(opts);
}
/**
* Create a Dot Graph dependency tree for a manifest path.
* @param {string} manifest - path for pom.xml
* @param {{}} [opts={}] - optional various options to pass along the application
* @returns {string} the Dot Graph content
* @private
*/
#createSbomStackAnalysis(manifest, opts = {}) {
let content = this.#getDependencies(manifest, opts)
let properties = this.#extractProperties(manifest, opts)
// read dependency tree from temp file
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
console.log("Dependency tree that will be used as input for creating the BOM =>" + EOL + EOL + content)
}
let sbom = this.#buildSbom(content, properties, manifest, opts)
return sbom
}
/**
*
* @param {string} manifestPath - path to build.gradle.
* @param {Object} opts - contains various options settings from client.
* @return {{Object}} an object that contains all gradle properties
*/
#extractProperties(manifestPath, opts) {
let properties = {}
let propertiesContent = this.#getProperties(manifestPath, opts)
let regExpMatchArray = propertiesContent.match(/([^\n:]+):[\t ]*(.*)/g);
for (let i = 0; i < regExpMatchArray.length - 1; i++) {
let parts = regExpMatchArray[i].split(":");
properties[parts[0].trim()] = parts[1].trim()
}
let regExpMatchArray1 = propertiesContent.match(/Root project '(.+)'/);
if (regExpMatchArray1[0]) {
properties[ROOT_PROJECT_KEY_NAME] = regExpMatchArray1[0]
}
return properties;
}
/**
*
* @param manifestPath - path to build.gradle
* @param {Object} opts - contains various options settings from client.
* @return {string} string content of the properties
*/
#getProperties(manifestPath, opts) {
let gradle = this.selectToolBinary(manifestPath, opts)
try {
let properties = this._invokeCommand(gradle, ['properties'], {cwd: path.dirname(manifestPath)})
return properties.toString()
} catch (error) {
throw new Error(`Couldn't get properties of ${this._getManifestName()} file , Error message returned from gradle binary => ${EOL} ${error.message}`)
}
}
/**
* Create a dependency list for a manifest content.
* @param {{}} [opts={}] - optional various options to pass along the application
* @returns {string} - sbom string of the direct dependencies of build.gradle
* @private
*/
#getSbomForComponentAnalysis(manifestPath, opts = {}) {
let content = this.#getDependencies(manifestPath, opts)
let properties = this.#extractProperties(manifestPath, opts)
let sbom = this.#buildDirectDependenciesSbom(content, properties, manifestPath, opts)
return sbom
}
/**
* Get a list of dependencies from gradle dependencies command.
* @param {string} manifest - path for build.gradle
* @returns {string} Multi-line string contain all dependencies from gradle dependencies command
* @private
*/
#getDependencies(manifest, opts={}) {
const gradle = this.selectToolBinary(manifest, opts)
try {
const commandResult = this._invokeCommand(gradle, ['dependencies'], {cwd: path.dirname(manifest)})
return commandResult.toString()
} catch (error) {
throw new Error(`Couldn't run gradle dependencies command, error message returned from gradle binary => ${EOL} ${error.message}`)
}
}
/**
* Extracts runtime and compile configurations from the dependency tree
* @param {string} content - the dependency tree content
* @returns {[string[], string[]]} tuple of [runtimeConfig, compileConfig]
* @private
*/
#extractConfigurations(content) {
const lines = content.split(EOL);
const configs = {
runtimeClasspath: [],
compileClasspath: []
};
let currentConfig = null;
let collecting = false;
for (const line of lines) {
// Check for configuration start
if (line.startsWith('runtimeClasspath')) {
currentConfig = 'runtimeClasspath';
collecting = true;
continue;
} else if (line.startsWith('compileClasspath')) {
currentConfig = 'compileClasspath';
collecting = true;
continue;
}
// If we're not collecting or no config is set, skip
if (!collecting || !currentConfig) {continue;}
// Check for end of configuration
if (line.trim() === '') {
collecting = false;
currentConfig = null;
continue;
}
// Add line to current configuration
configs[currentConfig].push(line);
}
return [configs.runtimeClasspath, configs.compileClasspath];
}
/**
*
* @param content {string} - content of the dependency tree received from gradle dependencies command
* @param properties {Object} - properties of the gradle project.
* @return {string} return sbom json string of the build.gradle manifest file
*/
#buildDirectDependenciesSbom(content, properties, manifestPath, opts = {}) {
let sbom = new Sbom();
let root = `${properties.group}:${properties[ROOT_PROJECT_KEY_NAME].match(/Root project '(.+)'/)[1]}:jar:${properties.version}`
let rootPurl = this.parseDep(root)
const license = this.readLicenseFromManifest(manifestPath);
sbom.addRoot(rootPurl, license)
let ignoredDeps = this.#getIgnoredDeps(manifestPath);
const [runtimeConfig, compileConfig] = this.#extractConfigurations(content);
let directDependencies = new Map();
this.#processDirectDependencies(runtimeConfig, directDependencies, 'required');
this.#processDirectDependencies(compileConfig, directDependencies, 'optional');
directDependencies.forEach((scope, dep) => {
const purl = this.parseDep(dep);
purl.scope = scope;
sbom.addDependency(rootPurl, purl, scope);
});
return sbom.filterIgnoredDepsIncludingVersion(ignoredDeps).getAsJsonString(opts);
}
#processDirectDependencies(config, directDependencies, scope) {
const lines = this.#prepareLinesForParsingDependencyTree(config);
lines.forEach(({line, depth}) => {
if (depth === 1 && !directDependencies.has(line)) {
directDependencies.set(line, scope);
}
});
}
/**
* This method gets build.gradle manifest, and extracts from it all artifacts marks for exclusion using an //exhortignore comment.
* @param {string} manifestPath the build.gradle manifest path
* @return {string[]} an array with all dependencies to ignore - contains 'stringified' purls as elements
* @private
*/
#getIgnoredDeps(manifestPath) {
let buildGradleLines = fs.readFileSync(manifestPath).toString().split(EOL)
let ignored =
buildGradleLines.filter(line => line && line.match(TRUSTIFY_DA_IGNORE_REGEX_LINE))
.map(line => line.indexOf("/*") === -1 ? line : line.substring(0, line.indexOf("/*")))
.map(line => line.trim().substring(0, line.trim().search(TRUSTIFY_DA_IGNORE_REGEX)))
let depsToIgnore = new Array
ignored.forEach(depToBeIgnored => {
let ignoredDepInfo
if (depHasLibsNotation(depToBeIgnored)) {
ignoredDepInfo = this.#getDepFromLibsNotation(depToBeIgnored, manifestPath);
} else {
ignoredDepInfo = this.#getDependencyFromStringOrMapNotation(depToBeIgnored)
}
if (ignoredDepInfo) {
depsToIgnore.push(ignoredDepInfo)
}
})
return depsToIgnore
}
#getDepFromLibsNotation(depToBeIgnored, manifestPath) {
// Extract everything after "libs."
let alias = depToBeIgnored.substring(depToBeIgnored.indexOf("libs.") + "libs.".length).trim()
alias = this._parseAliasForLibsNotation(alias)
// Read and parse the TOML file
let pathOfToml = path.join(path.dirname(manifestPath),"gradle","libs.versions.toml");
const tomlString = fs.readFileSync(pathOfToml).toString()
let tomlObject = parseToml(tomlString)
let groupPlusArtifactObject = tomlObject.libraries[alias]
let parts = groupPlusArtifactObject.module.split(":");
let groupId = parts[0]
let artifactId = parts[1]
let versionRef = groupPlusArtifactObject.version.ref
let version = tomlObject.versions[versionRef]
return groupId && artifactId && version ? this.toPurl(groupId,artifactId,version).toString() : undefined
}
/**
* Gets a dependency line of type string/map notation from build.gradle, extract the coordinates from it and returns string purl
* @param depToBeIgnored
* @return {string|undefined} string of a purl format of the extracted coordinates.
*/
#getDependencyFromStringOrMapNotation(depToBeIgnored) {
// dependency line is of form MapNotation
if (depToBeIgnored.includes("group:") && depToBeIgnored.includes("name:") && depToBeIgnored.includes("version:")) {
let matchedKeyValues = depToBeIgnored.match(/(group|name|version):\s*['"](.*?)['"]/g)
let coordinates = {}
for (let coordinatePairIndex in matchedKeyValues) {
let keyValue = matchedKeyValues[coordinatePairIndex].split(":");
coordinates[keyValue[0].trim()] = stripString(keyValue[1].trim())
}
return this.toPurl(coordinates.group,coordinates.name,coordinates.version).toString()
// Dependency line is of form String Notation
} else {
let depParts
const depToBeIgnoredMatch = this._extractDepToBeIgnored(depToBeIgnored)
if(depToBeIgnoredMatch) {
depParts = depToBeIgnoredMatch.split(":");
} else {
depParts = depToBeIgnored.split(":");
}
if(depParts.length === 3) {
let groupId = stripString(depParts[0])
let artifactId = stripString(depParts[1])
let version = stripString(depParts[2])
return this.toPurl(groupId,artifactId,version).toString()
}
}
return undefined
}
}
const DEFAULT_GRADLE_DISCOVERY_IGNORE = [
'**/build/**',
'**/.gradle/**',
]
/** Gradle init script that emits structured project listing. */
const GRADLE_INIT_SCRIPT = `allprojects {
task daListProjects {
doLast {
println "::DA_PROJECT::\${project.path}::\${project.projectDir}"
}
}
}
`
/**
* Discover all build.gradle[.kts] manifest paths in a Gradle multi-project build.
* Uses a custom init script to get structured project listing.
*
* @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain settings.gradle[.kts])
* @param {import('../index.js').Options} [opts={}]
* @returns {Promise<string[]>} Paths to build.gradle[.kts] files (absolute)
*/
export async function discoverGradleSubprojects(workspaceRoot, opts = {}) {
const root = path.resolve(workspaceRoot)
const hasSettings = fs.existsSync(path.join(root, 'settings.gradle'))
|| fs.existsSync(path.join(root, 'settings.gradle.kts'))
if (!hasSettings) {
return []
}
const manifestPaths = []
const rootBuildKts = path.join(root, 'build.gradle.kts')
const rootBuild = path.join(root, 'build.gradle')
const rootManifest = fs.existsSync(rootBuildKts) ? rootBuildKts : fs.existsSync(rootBuild) ? rootBuild : null
if (rootManifest) {
manifestPaths.push(rootManifest)
}
let gradleBin
try {
gradleBin = new Java_gradle().selectToolBinary(rootManifest || rootBuild, opts)
} catch {
const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_GRADLE_DISCOVERY_IGNORE]
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
}
const initScriptPath = path.join(os.tmpdir(), `da-list-projects-${crypto.randomUUID()}.gradle`)
try {
fs.writeFileSync(initScriptPath, GRADLE_INIT_SCRIPT)
let output
try {
output = invokeCommand(gradleBin, [
'-q', '--no-daemon',
'--init-script', initScriptPath,
'daListProjects',
], { cwd: root })
} catch {
const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_GRADLE_DISCOVERY_IGNORE]
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
}
const projects = parseGradleInitScriptOutput(output.toString())
for (const proj of projects) {
if (proj.path === ':') {
continue
}
const projDir = path.resolve(proj.dir)
const buildKts = path.join(projDir, 'build.gradle.kts')
const buildGroovy = path.join(projDir, 'build.gradle')
if (fs.existsSync(buildKts)) {
manifestPaths.push(buildKts)
} else if (fs.existsSync(buildGroovy)) {
manifestPaths.push(buildGroovy)
}
}
} finally {
try { fs.unlinkSync(initScriptPath) } catch { /* ignore */ }
}
const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_GRADLE_DISCOVERY_IGNORE]
return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns)
}
/**
* Parse the structured output from the Gradle init script.
*
* @param {string} raw - Raw stdout from gradle
* @returns {{ path: string, dir: string }[]}
*/
export function parseGradleInitScriptOutput(raw) {
const projects = []
for (const rawLine of raw.split('\n')) {
const line = rawLine.trimEnd()
if (!line.startsWith('::DA_PROJECT::')) {
continue
}
const prefix = '::DA_PROJECT::'
const remainder = line.substring(prefix.length)
const lastSep = remainder.lastIndexOf('::')
if (lastSep < 0) {
continue
}
const projPath = remainder.substring(0, lastSep)
const dir = remainder.substring(lastSep + 2)
if (projPath && dir) {
projects.push({ path: projPath, dir })
}
}
return projects
}