@trustify-da/trustify-da-javascript-client 0.3.0-ea.9b6856c → 0.3.0-ea.a27acdd

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 (97) hide show
  1. package/README.md +367 -13
  2. package/dist/package.json +48 -28
  3. package/dist/src/analysis.d.ts +28 -5
  4. package/dist/src/analysis.js +99 -89
  5. package/dist/src/batch_opts.d.ts +24 -0
  6. package/dist/src/batch_opts.js +35 -0
  7. package/dist/src/cli.js +346 -9
  8. package/dist/src/cyclone_dx_sbom.d.ts +30 -3
  9. package/dist/src/cyclone_dx_sbom.js +95 -9
  10. package/dist/src/index.d.ts +202 -12
  11. package/dist/src/index.js +365 -30
  12. package/dist/src/license/index.d.ts +28 -0
  13. package/dist/src/license/index.js +103 -0
  14. package/dist/src/license/license_utils.d.ts +40 -0
  15. package/dist/src/license/license_utils.js +137 -0
  16. package/dist/src/license/licenses_api.d.ts +34 -0
  17. package/dist/src/license/licenses_api.js +98 -0
  18. package/dist/src/license/project_license.d.ts +20 -0
  19. package/dist/src/license/project_license.js +62 -0
  20. package/dist/src/oci_image/images.d.ts +4 -5
  21. package/dist/src/oci_image/images.js +15 -1
  22. package/dist/src/oci_image/utils.d.ts +7 -7
  23. package/dist/src/oci_image/utils.js +11 -2
  24. package/dist/src/package_version.d.ts +8 -0
  25. package/dist/src/package_version.js +31 -0
  26. package/dist/src/provider.d.ts +18 -5
  27. package/dist/src/provider.js +33 -5
  28. package/dist/src/providers/base_java.d.ts +68 -15
  29. package/dist/src/providers/base_java.js +82 -61
  30. package/dist/src/providers/base_javascript.d.ts +99 -12
  31. package/dist/src/providers/base_javascript.js +243 -27
  32. package/dist/src/providers/base_pyproject.d.ts +158 -0
  33. package/dist/src/providers/base_pyproject.js +322 -0
  34. package/dist/src/providers/containerfile_parser.d.ts +5 -0
  35. package/dist/src/providers/containerfile_parser.js +20 -0
  36. package/dist/src/providers/golang_gomodules.d.ts +30 -13
  37. package/dist/src/providers/golang_gomodules.js +218 -122
  38. package/dist/src/providers/gomod_parser.d.ts +4 -0
  39. package/dist/src/providers/gomod_parser.js +16 -0
  40. package/dist/src/providers/java_gradle.d.ts +28 -3
  41. package/dist/src/providers/java_gradle.js +128 -4
  42. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  43. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  44. package/dist/src/providers/java_maven.d.ts +46 -6
  45. package/dist/src/providers/java_maven.js +292 -11
  46. package/dist/src/providers/javascript_bun.d.ts +22 -0
  47. package/dist/src/providers/javascript_bun.js +141 -0
  48. package/dist/src/providers/javascript_npm.d.ts +19 -0
  49. package/dist/src/providers/javascript_npm.js +61 -1
  50. package/dist/src/providers/javascript_pnpm.d.ts +14 -1
  51. package/dist/src/providers/javascript_pnpm.js +55 -5
  52. package/dist/src/providers/javascript_yarn.d.ts +12 -0
  53. package/dist/src/providers/javascript_yarn.js +75 -2
  54. package/dist/src/providers/manifest.d.ts +2 -0
  55. package/dist/src/providers/manifest.js +22 -4
  56. package/dist/src/providers/marker_evaluator.d.ts +14 -0
  57. package/dist/src/providers/marker_evaluator.js +191 -0
  58. package/dist/src/providers/oci_dockerfile.d.ts +51 -0
  59. package/dist/src/providers/oci_dockerfile.js +177 -0
  60. package/dist/src/providers/processors/yarn_berry_processor.d.ts +6 -2
  61. package/dist/src/providers/processors/yarn_berry_processor.js +91 -7
  62. package/dist/src/providers/processors/yarn_classic_processor.d.ts +6 -2
  63. package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
  64. package/dist/src/providers/python_controller.d.ts +10 -3
  65. package/dist/src/providers/python_controller.js +122 -63
  66. package/dist/src/providers/python_pip.d.ts +16 -4
  67. package/dist/src/providers/python_pip.js +51 -58
  68. package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
  69. package/dist/src/providers/python_pip_pyproject.js +146 -0
  70. package/dist/src/providers/python_poetry.d.ts +98 -0
  71. package/dist/src/providers/python_poetry.js +286 -0
  72. package/dist/src/providers/python_uv.d.ts +46 -0
  73. package/dist/src/providers/python_uv.js +269 -0
  74. package/dist/src/providers/requirements_parser.d.ts +6 -0
  75. package/dist/src/providers/requirements_parser.js +24 -0
  76. package/dist/src/providers/rust_cargo.d.ts +56 -0
  77. package/dist/src/providers/rust_cargo.js +682 -0
  78. package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
  79. package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
  80. package/dist/src/providers/tree-sitter-requirements.wasm +0 -0
  81. package/dist/src/remediate.d.ts +21 -0
  82. package/dist/src/remediate.js +149 -0
  83. package/dist/src/remediation.d.ts +57 -0
  84. package/dist/src/remediation.js +436 -0
  85. package/dist/src/remediation_report.d.ts +40 -0
  86. package/dist/src/remediation_report.js +159 -0
  87. package/dist/src/sbom.d.ts +28 -2
  88. package/dist/src/sbom.js +26 -4
  89. package/dist/src/tools.d.ts +48 -6
  90. package/dist/src/tools.js +138 -2
  91. package/dist/src/updaters/maven_updater.d.ts +26 -0
  92. package/dist/src/updaters/maven_updater.js +322 -0
  93. package/dist/src/updaters/toml_updater.d.ts +33 -0
  94. package/dist/src/updaters/toml_updater.js +244 -0
  95. package/dist/src/workspace.d.ts +70 -0
  96. package/dist/src/workspace.js +256 -0
  97. package/package.json +49 -29
@@ -1,8 +1,13 @@
1
+ import crypto from 'node:crypto';
1
2
  import fs from 'node:fs';
3
+ import os from 'node:os';
2
4
  import path from 'node:path';
3
5
  import { EOL } from 'os';
4
- import TOML from 'fast-toml';
6
+ import { parse as parseToml } from 'smol-toml';
7
+ import { readLicenseFile } from '../license/license_utils.js';
5
8
  import Sbom from '../sbom.js';
9
+ import { invokeCommand } from '../tools.js';
10
+ import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js';
6
11
  import Base_java, { ecosystem_gradle } from "./base_java.js";
7
12
  /** @typedef {import('../provider.js').Provider} */
8
13
  /** @typedef {import('../provider.js').Provided} Provided */
@@ -49,6 +54,13 @@ export default class Java_gradle extends Base_java {
49
54
  * @param {string} manifestDir - the directory where the manifest lies
50
55
  */
51
56
  validateLockFile() { return true; }
57
+ /**
58
+ * Gradle manifests (build.gradle, build.gradle.kts) have no standard license field.
59
+ * @param {string} manifestPath - path to manifest
60
+ * @returns {null}
61
+ */
62
+ // eslint-disable-next-line no-unused-vars
63
+ readLicenseFromManifest(manifestPath) { return readLicenseFile(manifestPath); }
52
64
  /**
53
65
  * Provide content and content type for stack analysis.
54
66
  * @param {string} manifest - the manifest path or name
@@ -158,7 +170,8 @@ export default class Java_gradle extends Base_java {
158
170
  let sbom = new Sbom();
159
171
  let root = `${properties.group}:${properties[ROOT_PROJECT_KEY_NAME].match(/Root project '(.+)'/)[1]}:jar:${properties.version}`;
160
172
  let rootPurl = this.parseDep(root);
161
- sbom.addRoot(rootPurl);
173
+ const license = this.readLicenseFromManifest(manifestPath);
174
+ sbom.addRoot(rootPurl, license);
162
175
  let ignoredDeps = this.#getIgnoredDeps(manifestPath);
163
176
  const [runtimeConfig, compileConfig] = this.#extractConfigurations(content);
164
177
  const processedDeps = new Set();
@@ -298,7 +311,8 @@ export default class Java_gradle extends Base_java {
298
311
  let sbom = new Sbom();
299
312
  let root = `${properties.group}:${properties[ROOT_PROJECT_KEY_NAME].match(/Root project '(.+)'/)[1]}:jar:${properties.version}`;
300
313
  let rootPurl = this.parseDep(root);
301
- sbom.addRoot(rootPurl);
314
+ const license = this.readLicenseFromManifest(manifestPath);
315
+ sbom.addRoot(rootPurl, license);
302
316
  let ignoredDeps = this.#getIgnoredDeps(manifestPath);
303
317
  const [runtimeConfig, compileConfig] = this.#extractConfigurations(content);
304
318
  let directDependencies = new Map();
@@ -352,7 +366,7 @@ export default class Java_gradle extends Base_java {
352
366
  // Read and parse the TOML file
353
367
  let pathOfToml = path.join(path.dirname(manifestPath), "gradle", "libs.versions.toml");
354
368
  const tomlString = fs.readFileSync(pathOfToml).toString();
355
- let tomlObject = TOML.parse(tomlString);
369
+ let tomlObject = parseToml(tomlString);
356
370
  let groupPlusArtifactObject = tomlObject.libraries[alias];
357
371
  let parts = groupPlusArtifactObject.module.split(":");
358
372
  let groupId = parts[0];
@@ -397,3 +411,113 @@ export default class Java_gradle extends Base_java {
397
411
  return undefined;
398
412
  }
399
413
  }
414
+ const DEFAULT_GRADLE_DISCOVERY_IGNORE = [
415
+ '**/build/**',
416
+ '**/.gradle/**',
417
+ ];
418
+ /** Gradle init script that emits structured project listing. */
419
+ const GRADLE_INIT_SCRIPT = `allprojects {
420
+ task daListProjects {
421
+ doLast {
422
+ println "::DA_PROJECT::\${project.path}::\${project.projectDir}"
423
+ }
424
+ }
425
+ }
426
+ `;
427
+ /**
428
+ * Discover all build.gradle[.kts] manifest paths in a Gradle multi-project build.
429
+ * Uses a custom init script to get structured project listing.
430
+ *
431
+ * @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain settings.gradle[.kts])
432
+ * @param {import('../index.js').Options} [opts={}]
433
+ * @returns {Promise<string[]>} Paths to build.gradle[.kts] files (absolute)
434
+ */
435
+ export async function discoverGradleSubprojects(workspaceRoot, opts = {}) {
436
+ const root = path.resolve(workspaceRoot);
437
+ const hasSettings = fs.existsSync(path.join(root, 'settings.gradle'))
438
+ || fs.existsSync(path.join(root, 'settings.gradle.kts'));
439
+ if (!hasSettings) {
440
+ return [];
441
+ }
442
+ const manifestPaths = [];
443
+ const rootBuildKts = path.join(root, 'build.gradle.kts');
444
+ const rootBuild = path.join(root, 'build.gradle');
445
+ const rootManifest = fs.existsSync(rootBuildKts) ? rootBuildKts : fs.existsSync(rootBuild) ? rootBuild : null;
446
+ if (rootManifest) {
447
+ manifestPaths.push(rootManifest);
448
+ }
449
+ let gradleBin;
450
+ try {
451
+ gradleBin = new Java_gradle().selectToolBinary(rootManifest || rootBuild, opts);
452
+ }
453
+ catch {
454
+ const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_GRADLE_DISCOVERY_IGNORE];
455
+ return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns);
456
+ }
457
+ const initScriptPath = path.join(os.tmpdir(), `da-list-projects-${crypto.randomUUID()}.gradle`);
458
+ try {
459
+ fs.writeFileSync(initScriptPath, GRADLE_INIT_SCRIPT);
460
+ let output;
461
+ try {
462
+ output = invokeCommand(gradleBin, [
463
+ '-q', '--no-daemon',
464
+ '--init-script', initScriptPath,
465
+ 'daListProjects',
466
+ ], { cwd: root });
467
+ }
468
+ catch {
469
+ const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_GRADLE_DISCOVERY_IGNORE];
470
+ return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns);
471
+ }
472
+ const projects = parseGradleInitScriptOutput(output.toString());
473
+ for (const proj of projects) {
474
+ if (proj.path === ':') {
475
+ continue;
476
+ }
477
+ const projDir = path.resolve(proj.dir);
478
+ const buildKts = path.join(projDir, 'build.gradle.kts');
479
+ const buildGroovy = path.join(projDir, 'build.gradle');
480
+ if (fs.existsSync(buildKts)) {
481
+ manifestPaths.push(buildKts);
482
+ }
483
+ else if (fs.existsSync(buildGroovy)) {
484
+ manifestPaths.push(buildGroovy);
485
+ }
486
+ }
487
+ }
488
+ finally {
489
+ try {
490
+ fs.unlinkSync(initScriptPath);
491
+ }
492
+ catch { /* ignore */ }
493
+ }
494
+ const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_GRADLE_DISCOVERY_IGNORE];
495
+ return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns);
496
+ }
497
+ /**
498
+ * Parse the structured output from the Gradle init script.
499
+ *
500
+ * @param {string} raw - Raw stdout from gradle
501
+ * @returns {{ path: string, dir: string }[]}
502
+ */
503
+ export function parseGradleInitScriptOutput(raw) {
504
+ const projects = [];
505
+ for (const rawLine of raw.split('\n')) {
506
+ const line = rawLine.trimEnd();
507
+ if (!line.startsWith('::DA_PROJECT::')) {
508
+ continue;
509
+ }
510
+ const prefix = '::DA_PROJECT::';
511
+ const remainder = line.substring(prefix.length);
512
+ const lastSep = remainder.lastIndexOf('::');
513
+ if (lastSep < 0) {
514
+ continue;
515
+ }
516
+ const projPath = remainder.substring(0, lastSep);
517
+ const dir = remainder.substring(lastSep + 2);
518
+ if (projPath && dir) {
519
+ projects.push({ path: projPath, dir });
520
+ }
521
+ }
522
+ return projects;
523
+ }
@@ -3,5 +3,5 @@ export default class Java_gradle_groovy extends Java_gradle {
3
3
  _parseAliasForLibsNotation(alias: any): any;
4
4
  _extractDepToBeIgnored(dep: any): any;
5
5
  }
6
- export type Provided = import('../provider').Provided;
6
+ export type Provided = any;
7
7
  import Java_gradle from './java_gradle.js';
@@ -7,5 +7,5 @@ export default class Java_gradle_kotlin extends Java_gradle {
7
7
  _parseAliasForLibsNotation(alias: any): any;
8
8
  _extractDepToBeIgnored(dep: any): string | null;
9
9
  }
10
- export type Provided = import('../provider').Provided;
10
+ export type Provided = any;
11
11
  import Java_gradle from './java_gradle.js';
@@ -1,8 +1,20 @@
1
+ /**
2
+ * Discover all pom.xml manifest paths in a Maven multi-module project.
3
+ *
4
+ * @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain pom.xml)
5
+ * @param {object} [opts={}]
6
+ * @returns {Promise<string[]>} Paths to pom.xml files (absolute)
7
+ */
8
+ export function discoverMavenModules(workspaceRoot: string, opts?: object): Promise<string[]>;
1
9
  /** @typedef {import('../provider').Provider} */
2
10
  /** @typedef {import('../provider').Provided} Provided */
3
11
  /** @typedef {{name: string, version: string}} Package */
4
12
  /** @typedef {{groupId: string, artifactId: string, version: string, scope: string, ignore: boolean}} Dependency */
5
13
  export default class Java_maven extends Base_java {
14
+ /** @type {Object<string, string>} Packaging types that produce .jar files despite non-jar packaging names. */
15
+ static PACKAGING_TO_JAR: {
16
+ [x: string]: string;
17
+ };
6
18
  constructor();
7
19
  /**
8
20
  * @param {string} manifestName - the subject manifest name-type
@@ -17,27 +29,55 @@ export default class Java_maven extends Base_java {
17
29
  * Provide content and content type for maven-maven stack analysis.
18
30
  * @param {string} manifest - the manifest path or name
19
31
  * @param {{}} [opts={}] - optional various options to pass along the application
20
- * @returns {Provided}
32
+ * @returns {Promise<Provided>}
21
33
  */
22
- provideStack(manifest: string, opts?: {} | undefined): Provided;
34
+ provideStack(manifest: string, opts?: {}): Promise<Provided>;
23
35
  /**
24
36
  * Provide content and content type for maven-maven component analysis.
25
37
  * @param {string} manifest - path to the manifest file
26
38
  * @param {{}} [opts={}] - optional various options to pass along the application
27
39
  * @returns {Provided}
28
40
  */
29
- provideComponent(manifest: string, opts?: {} | undefined): Provided;
41
+ provideComponent(manifest: string, opts?: {}): Provided;
42
+ /**
43
+ * Read license from pom.xml manifest, with fallback to LICENSE file
44
+ * @param {string} manifestPath - path to pom.xml
45
+ * @returns {string|null}
46
+ */
47
+ readLicenseFromManifest(manifestPath: string): string | null;
30
48
  /**
49
+ * Build a Map of PURL string → CycloneDX hash entries by reading artifact files from the local Maven repository.
31
50
  *
51
+ * Both the artifact file path and the hash-map key are derived from the
52
+ * shared {@link Base_java#parseCoordinate} parser: the file path uses the
53
+ * resolved version and raw classifier, while the key uses the same canonical
54
+ * PURL builder as {@link Base_java#parseDep}. This guarantees the key always
55
+ * matches the PURL that {@link Base_java#parseDependencyTree} looks up.
56
+ *
57
+ * @param {string} depTreeText Raw dependency tree text from mvn dependency:tree
58
+ * @param {{}} [opts={}] Options bag (may contain TRUSTIFY_DA_MVN_REPO)
59
+ * @returns {Promise<Map<string, Array<{alg: string, content: string}>>>}
60
+ */
61
+ _buildMavenHashMap(depTreeText: string, opts?: {}): Promise<Map<string, Array<{
62
+ alg: string;
63
+ content: string;
64
+ }>>>;
65
+ /**
32
66
  * @param {String} textGraphList Text graph String of the manifest
33
67
  * @param {[String]} ignoredDeps List of ignored dependencies to be omitted from sbom
68
+ * @param {{}} opts Options
69
+ * @param {String} manifestPath Path to the pom.xml manifest
70
+ * @param {Map<string, Array<{alg: string, content: string}>>} [hashMap] PURL→hashes map
34
71
  * @return {String} formatted sbom Json String with all dependencies
35
72
  */
36
- createSbomFileFromTextFormat(textGraphList: string, ignoredDeps: [string], opts: any): string;
73
+ createSbomFileFromTextFormat(textGraphList: string, ignoredDeps: [string], opts: {}, manifestPath: string, hashMap?: Map<string, Array<{
74
+ alg: string;
75
+ content: string;
76
+ }>>): string;
37
77
  #private;
38
78
  }
39
- export type Java_maven = import('../provider').Provider;
40
- export type Provided = import('../provider').Provided;
79
+ export type Java_maven = any;
80
+ export type Provided = any;
41
81
  export type Package = {
42
82
  name: string;
43
83
  version: string;
@@ -1,10 +1,13 @@
1
+ import crypto from 'node:crypto';
1
2
  import fs from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import { EOL } from 'os';
5
6
  import { XMLParser } from 'fast-xml-parser';
7
+ import { getLicense } from '../license/license_utils.js';
6
8
  import Sbom from '../sbom.js';
7
- import { getCustom } from '../tools.js';
9
+ import { getCustom, invokeCommand } from '../tools.js';
10
+ import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js';
8
11
  import Base_java, { ecosystem_maven } from "./base_java.js";
9
12
  /** @typedef {import('../provider').Provider} */
10
13
  /** @typedef {import('../provider').Provided} Provided */
@@ -29,12 +32,12 @@ export default class Java_maven extends Base_java {
29
32
  * Provide content and content type for maven-maven stack analysis.
30
33
  * @param {string} manifest - the manifest path or name
31
34
  * @param {{}} [opts={}] - optional various options to pass along the application
32
- * @returns {Provided}
35
+ * @returns {Promise<Provided>}
33
36
  */
34
- provideStack(manifest, opts = {}) {
37
+ async provideStack(manifest, opts = {}) {
35
38
  return {
36
39
  ecosystem: ecosystem_maven,
37
- content: this.#createSbomStackAnalysis(manifest, opts),
40
+ content: await this.#createSbomStackAnalysis(manifest, opts),
38
41
  contentType: 'application/vnd.cyclonedx+json'
39
42
  };
40
43
  }
@@ -51,14 +54,38 @@ export default class Java_maven extends Base_java {
51
54
  contentType: 'application/vnd.cyclonedx+json'
52
55
  };
53
56
  }
57
+ /**
58
+ * Read license from pom.xml manifest, with fallback to LICENSE file
59
+ * @param {string} manifestPath - path to pom.xml
60
+ * @returns {string|null}
61
+ */
62
+ readLicenseFromManifest(manifestPath) {
63
+ let fromPom = null;
64
+ try {
65
+ const xml = fs.readFileSync(manifestPath, 'utf-8');
66
+ const parser = new XMLParser({ ignoreAttributes: false });
67
+ const obj = parser.parse(xml);
68
+ const project = obj?.project;
69
+ if (project?.licenses?.license) {
70
+ const license = Array.isArray(project.licenses.license)
71
+ ? project.licenses.license[0]
72
+ : project.licenses.license;
73
+ fromPom = (license?.name && license.name.trim()) || null;
74
+ }
75
+ }
76
+ catch {
77
+ // leave fromPom as null
78
+ }
79
+ return getLicense(fromPom, manifestPath);
80
+ }
54
81
  /**
55
82
  * Create a Dot Graph dependency tree for a manifest path.
56
83
  * @param {string} manifest - path for pom.xml
57
84
  * @param {{}} [opts={}] - optional various options to pass along the application
58
- * @returns {string} the Dot Graph content
85
+ * @returns {Promise<string>} the Dot Graph content
59
86
  * @private
60
87
  */
61
- #createSbomStackAnalysis(manifest, opts = {}) {
88
+ async #createSbomStackAnalysis(manifest, opts = {}) {
62
89
  const manifestDir = path.dirname(manifest);
63
90
  const mvn = this.selectToolBinary(manifest, opts);
64
91
  const mvnArgs = JSON.parse(getCustom('TRUSTIFY_DA_MVN_ARGS', '[]', opts));
@@ -105,26 +132,116 @@ export default class Java_maven extends Base_java {
105
132
  if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
106
133
  console.error("Dependency tree that will be used as input for creating the BOM =>" + EOL + EOL + content.toString());
107
134
  }
108
- let sbom = this.createSbomFileFromTextFormat(content.toString(), ignoredDeps, opts);
135
+ const depTreeContent = content.toString();
136
+ const hashMap = await this._buildMavenHashMap(depTreeContent, opts);
137
+ let sbom = this.createSbomFileFromTextFormat(depTreeContent, ignoredDeps, opts, manifest, hashMap);
109
138
  // delete temp file and directory
110
139
  fs.rmSync(tmpDir, { recursive: true, force: true });
111
140
  // return dependency graph as string
112
141
  return sbom;
113
142
  }
143
+ /** @type {Object<string, string>} Packaging types that produce .jar files despite non-jar packaging names. */
144
+ static PACKAGING_TO_JAR = { 'bundle': 'jar', 'eclipse-plugin': 'jar' };
114
145
  /**
146
+ * Build a Map of PURL string → CycloneDX hash entries by reading artifact files from the local Maven repository.
147
+ *
148
+ * Both the artifact file path and the hash-map key are derived from the
149
+ * shared {@link Base_java#parseCoordinate} parser: the file path uses the
150
+ * resolved version and raw classifier, while the key uses the same canonical
151
+ * PURL builder as {@link Base_java#parseDep}. This guarantees the key always
152
+ * matches the PURL that {@link Base_java#parseDependencyTree} looks up.
115
153
  *
154
+ * @param {string} depTreeText Raw dependency tree text from mvn dependency:tree
155
+ * @param {{}} [opts={}] Options bag (may contain TRUSTIFY_DA_MVN_REPO)
156
+ * @returns {Promise<Map<string, Array<{alg: string, content: string}>>>}
157
+ */
158
+ async _buildMavenHashMap(depTreeText, opts = {}) {
159
+ const m2Repo = getCustom('TRUSTIFY_DA_MVN_REPO', path.join(os.homedir(), '.m2', 'repository'), opts);
160
+ const hashMap = new Map();
161
+ const lines = depTreeText.split(EOL);
162
+ // Track hash coverage so an incomplete .m2 cache (e.g. ephemeral CI
163
+ // containers or resolve-only phases) surfaces a summary warning instead
164
+ // of silently producing an SBOM with missing hashes.
165
+ let attempted = 0;
166
+ let missed = 0;
167
+ for (const rawLine of lines) {
168
+ const trimmed = rawLine.trim();
169
+ // Strip leading tree-drawing characters (e.g. "\-", "+-", "|") so the
170
+ // parenthesized-line check sees the coordinate itself. Omitted
171
+ // duplicate/conflict lines start with these characters after trim(),
172
+ // then an opening "(" — without stripping, the "(" check never fires.
173
+ const cleaned = trimmed.replace(/^[|+\\\- ]+/, '');
174
+ if (!cleaned || cleaned.startsWith('(')) {
175
+ continue;
176
+ }
177
+ const coord = this.parseCoordinate(rawLine);
178
+ if (!coord.groupId || !coord.artifactId || !coord.packaging) {
179
+ continue;
180
+ }
181
+ if (coord.packaging === 'pom') {
182
+ continue;
183
+ }
184
+ // Key by the exact PURL parseDep() produces, so the lookup in
185
+ // parseDependencyTree (hashMap.get(to.toString())) always hits.
186
+ const purl = this._coordinateToPurl(coord).toString();
187
+ // The same artifact recurs across dependency-tree branches — notably in
188
+ // multi-module reactor builds, where every module re-lists shared deps.
189
+ // The digest is deterministic per PURL, so skip the redundant file read
190
+ // and SHA-256 computation once this PURL is already hashed.
191
+ if (hashMap.has(purl)) {
192
+ continue;
193
+ }
194
+ const ext = Java_maven.PACKAGING_TO_JAR[coord.packaging] || coord.packaging;
195
+ const groupPath = coord.groupId.replaceAll('.', path.sep);
196
+ const fileName = coord.classifier
197
+ ? `${coord.artifactId}-${coord.version}-${coord.classifier}.${ext}`
198
+ : `${coord.artifactId}-${coord.version}.${ext}`;
199
+ const artifactPath = path.join(m2Repo, groupPath, coord.artifactId, coord.version, fileName);
200
+ attempted++;
201
+ try {
202
+ const stream = fs.createReadStream(artifactPath);
203
+ const hash = crypto.createHash('sha256');
204
+ for await (const chunk of stream) {
205
+ hash.update(chunk);
206
+ }
207
+ const digest = hash.digest('hex');
208
+ hashMap.set(purl, [{ alg: 'SHA-256', content: digest }]);
209
+ }
210
+ catch {
211
+ missed++;
212
+ if (process.env['TRUSTIFY_DA_DEBUG'] === 'true') {
213
+ console.error(`Maven hash: artifact not found at ${artifactPath}, omitting hash`);
214
+ }
215
+ }
216
+ }
217
+ // Mirror the pip provider's convention (python_controller.js): surface an
218
+ // unconditional warning when hashes could not be computed, so incomplete
219
+ // hash coverage is visible even without TRUSTIFY_DA_DEBUG.
220
+ if (missed > 0) {
221
+ console.warn(`Maven hash: ${missed} of ${attempted} artifacts could not be read from the local .m2 cache; SBOM will be generated without hashes for those components.`);
222
+ }
223
+ return hashMap;
224
+ }
225
+ /**
116
226
  * @param {String} textGraphList Text graph String of the manifest
117
227
  * @param {[String]} ignoredDeps List of ignored dependencies to be omitted from sbom
228
+ * @param {{}} opts Options
229
+ * @param {String} manifestPath Path to the pom.xml manifest
230
+ * @param {Map<string, Array<{alg: string, content: string}>>} [hashMap] PURL→hashes map
118
231
  * @return {String} formatted sbom Json String with all dependencies
119
232
  */
120
- createSbomFileFromTextFormat(textGraphList, ignoredDeps, opts) {
233
+ createSbomFileFromTextFormat(textGraphList, ignoredDeps, opts, manifestPath, hashMap) {
121
234
  let lines = textGraphList.split(EOL);
122
235
  // get root component
123
236
  let root = lines[0];
124
237
  let rootPurl = this.parseDep(root);
238
+ const license = this.readLicenseFromManifest(manifestPath);
125
239
  let sbom = new Sbom();
126
- sbom.addRoot(rootPurl);
240
+ sbom.addRoot(rootPurl, license);
127
241
  this.parseDependencyTree(root, 0, lines.slice(1), sbom);
242
+ // Attach Maven artifact hashes as a post-processing step, keeping this
243
+ // Maven-specific concern out of the shared dependency-tree parser.
244
+ sbom.attachHashes(hashMap);
128
245
  return sbom.filterIgnoredDeps(ignoredDeps).getAsJsonString(opts);
129
246
  }
130
247
  /**
@@ -153,10 +270,12 @@ export default class Java_maven extends Base_java {
153
270
  /** @type [Dependency] */
154
271
  let dependencies = this.#getDependencies(tmpEffectivePom)
155
272
  .filter(d => !this.#dependencyIn(d, ignored));
273
+ dependencies = this.#resolveVersionRanges(dependencies, manifestPath, opts);
156
274
  let sbom = new Sbom();
157
275
  let rootDependency = this.#getRootFromPom(tmpEffectivePom, manifestPath);
158
276
  let purlRoot = this.toPurl(rootDependency.groupId, rootDependency.artifactId, rootDependency.version);
159
- sbom.addRoot(purlRoot);
277
+ const license = this.readLicenseFromManifest(manifestPath);
278
+ sbom.addRoot(purlRoot, license);
160
279
  dependencies.forEach(dep => {
161
280
  let currentPurl = this.toPurl(dep.groupId, dep.artifactId, dep.version);
162
281
  sbom.addDependency(purlRoot, currentPurl);
@@ -209,7 +328,7 @@ export default class Java_maven extends Base_java {
209
328
  let ignored = [];
210
329
  // build xml parser with options
211
330
  let parser = new XMLParser({
212
- commentPropName: '#comment',
331
+ commentPropName: '#comment', // mark comments with #comment
213
332
  isArray: (_, jpath) => 'project.dependencies.dependency' === jpath,
214
333
  parseTagValue: false
215
334
  });
@@ -260,4 +379,166 @@ export default class Java_maven extends Base_java {
260
379
  #dependencyIn(dep, deps) {
261
380
  return deps.filter(d => dep.artifactId === d.artifactId && dep.groupId === d.groupId && dep.scope === d.scope).length > 0;
262
381
  }
382
+ /**
383
+ * Returns true if the given version string is a Maven version range
384
+ * (starts with '[' or '(').
385
+ * @param {string} version
386
+ * @returns {boolean}
387
+ * @private
388
+ */
389
+ #isVersionRange(version) {
390
+ return typeof version === 'string' && (version.startsWith('[') || version.startsWith('('));
391
+ }
392
+ /**
393
+ * Resolves Maven version ranges in the given dependency list by running
394
+ * maven-dependency-plugin:tree and reading the concrete versions it selects.
395
+ * If no dependency uses a version range, returns the list unchanged.
396
+ * @param {Dependency[]} dependencies
397
+ * @param {string} manifestPath
398
+ * @param {object} opts
399
+ * @returns {Dependency[]}
400
+ * @private
401
+ */
402
+ #resolveVersionRanges(dependencies, manifestPath, opts = {}) {
403
+ // short-circuit if no dependency has a version range
404
+ if (!dependencies.some(dep => this.#isVersionRange(dep.version))) {
405
+ return dependencies;
406
+ }
407
+ const mvn = this.selectToolBinary(manifestPath, opts);
408
+ const mvnArgs = JSON.parse(getCustom('TRUSTIFY_DA_MVN_ARGS', '[]', opts));
409
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trustify_da_range_'));
410
+ const tmpDepTree = path.join(tmpDir, 'mvn_deptree_ranges.txt');
411
+ try {
412
+ this._invokeCommand(mvn, [
413
+ '-q',
414
+ 'org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree',
415
+ '-Dscope=compile',
416
+ '-DoutputType=text',
417
+ `-DoutputFile=${tmpDepTree}`,
418
+ ...mvnArgs
419
+ ], { cwd: path.dirname(manifestPath) });
420
+ const content = fs.readFileSync(tmpDepTree);
421
+ const lines = content.toString().split(EOL).filter(l => l.trim() !== '');
422
+ // Build a map of groupId:artifactId -> resolved version from depth-1 entries
423
+ /** @type {Map<string, string>} */
424
+ const resolvedVersions = new Map();
425
+ for (const line of lines) {
426
+ if (this._getDepth(line) === 1) {
427
+ const purl = this.parseDep(line);
428
+ resolvedVersions.set(`${purl.namespace}:${purl.name}`, purl.version);
429
+ }
430
+ }
431
+ // Replace version ranges with resolved concrete versions
432
+ return dependencies.map(dep => {
433
+ if (this.#isVersionRange(dep.version)) {
434
+ const key = `${dep.groupId}:${dep.artifactId}`;
435
+ const resolved = resolvedVersions.get(key);
436
+ if (resolved) {
437
+ return { ...dep, version: resolved };
438
+ }
439
+ }
440
+ return dep;
441
+ });
442
+ }
443
+ catch (error) {
444
+ if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
445
+ console.error("Failed to resolve Maven version ranges: " + error.message);
446
+ }
447
+ return dependencies;
448
+ }
449
+ finally {
450
+ fs.rmSync(tmpDir, { recursive: true, force: true });
451
+ }
452
+ }
453
+ }
454
+ const DEFAULT_MAVEN_DISCOVERY_IGNORE = [
455
+ '**/target/**',
456
+ ];
457
+ /**
458
+ * Discover all pom.xml manifest paths in a Maven multi-module project.
459
+ *
460
+ * @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain pom.xml)
461
+ * @param {object} [opts={}]
462
+ * @returns {Promise<string[]>} Paths to pom.xml files (absolute)
463
+ */
464
+ export async function discoverMavenModules(workspaceRoot, opts = {}) {
465
+ const root = path.resolve(workspaceRoot);
466
+ const rootPom = path.join(root, 'pom.xml');
467
+ if (!fs.existsSync(rootPom)) {
468
+ return [];
469
+ }
470
+ let mvnBin;
471
+ try {
472
+ mvnBin = new Java_maven().selectToolBinary(rootPom, opts);
473
+ }
474
+ catch {
475
+ return [rootPom];
476
+ }
477
+ const visited = new Set();
478
+ const manifestPaths = [rootPom];
479
+ collectMavenModules(root, mvnBin, visited, manifestPaths);
480
+ const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_MAVEN_DISCOVERY_IGNORE];
481
+ return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns);
482
+ }
483
+ /**
484
+ * @param {string} dir - Absolute path to directory containing pom.xml
485
+ * @param {string} mvnBin - Maven binary path
486
+ * @param {Set<string>} visited - Already-visited directories (cycle guard)
487
+ * @param {string[]} manifestPaths - Accumulator for discovered pom.xml paths
488
+ */
489
+ function collectMavenModules(dir, mvnBin, visited, manifestPaths) {
490
+ const resolvedDir = path.resolve(dir);
491
+ if (visited.has(resolvedDir)) {
492
+ return;
493
+ }
494
+ visited.add(resolvedDir);
495
+ const modules = listMavenModules(resolvedDir, mvnBin);
496
+ for (const mod of modules) {
497
+ const moduleDir = path.resolve(resolvedDir, mod);
498
+ const modulePom = path.join(moduleDir, 'pom.xml');
499
+ if (fs.existsSync(modulePom)) {
500
+ manifestPaths.push(modulePom);
501
+ collectMavenModules(moduleDir, mvnBin, visited, manifestPaths);
502
+ }
503
+ }
504
+ }
505
+ /**
506
+ * @param {string} dir - Directory containing pom.xml
507
+ * @param {string} mvnBin - Maven binary path
508
+ * @returns {string[]} Module directory names (relative to `dir`)
509
+ */
510
+ function listMavenModules(dir, mvnBin) {
511
+ let output;
512
+ try {
513
+ output = invokeCommand(mvnBin, [
514
+ 'help:evaluate',
515
+ '-Dexpression=project.modules',
516
+ '-q',
517
+ '-DforceStdout',
518
+ '-f', path.join(dir, 'pom.xml'),
519
+ '--batch-mode',
520
+ ], { cwd: dir });
521
+ }
522
+ catch {
523
+ return [];
524
+ }
525
+ const raw = output.toString().trim();
526
+ if (!raw || raw.startsWith('<modules')) {
527
+ return [];
528
+ }
529
+ return parseMavenModuleList(raw);
530
+ }
531
+ /**
532
+ * @param {string} raw - Raw stdout from mvn help:evaluate -DforceStdout
533
+ * @returns {string[]}
534
+ */
535
+ function parseMavenModuleList(raw) {
536
+ const parser = new XMLParser();
537
+ const parsed = parser.parse(raw);
538
+ const entries = parsed?.strings?.string;
539
+ if (!entries) {
540
+ return [];
541
+ }
542
+ const list = Array.isArray(entries) ? entries : [entries];
543
+ return list.map(s => String(s).trim()).filter(Boolean);
263
544
  }
@@ -0,0 +1,22 @@
1
+ export default class Javascript_bun extends Base_javascript {
2
+ /**
3
+ * Parses `bun.lock` into a hash map keyed by `name@version`. Each entry in
4
+ * the `packages` section is an array whose first element is the resolved id
5
+ * (`name@version`) and whose fourth element is the SRI integrity string.
6
+ * @param {string} lockDir - Directory containing bun.lock
7
+ * @returns {Map<string, Array<{alg: string, content: string}>>} Hash map
8
+ * @protected
9
+ */
10
+ protected _parseLockFileHashes(lockDir: string): Map<string, Array<{
11
+ alg: string;
12
+ content: string;
13
+ }>>;
14
+ _listCmdArgs(): void;
15
+ _buildDependencyTree(includeTransitive: any, opts?: {}): {
16
+ name: any;
17
+ version: any;
18
+ dependencies: {};
19
+ };
20
+ #private;
21
+ }
22
+ import Base_javascript from './base_javascript.js';