@trustify-da/trustify-da-javascript-client 0.3.0-ea.6549d2a → 0.3.0-ea.7281b26

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 (54) hide show
  1. package/README.md +179 -11
  2. package/dist/package.json +12 -3
  3. package/dist/src/analysis.d.ts +16 -0
  4. package/dist/src/analysis.js +53 -4
  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 +171 -4
  8. package/dist/src/cyclone_dx_sbom.d.ts +7 -0
  9. package/dist/src/cyclone_dx_sbom.js +16 -1
  10. package/dist/src/index.d.ts +75 -2
  11. package/dist/src/index.js +284 -5
  12. package/dist/src/license/index.d.ts +2 -2
  13. package/dist/src/license/index.js +4 -4
  14. package/dist/src/license/license_utils.d.ts +40 -0
  15. package/dist/src/license/license_utils.js +134 -0
  16. package/dist/src/license/licenses_api.js +9 -2
  17. package/dist/src/license/project_license.d.ts +1 -6
  18. package/dist/src/license/project_license.js +4 -81
  19. package/dist/src/oci_image/utils.js +11 -2
  20. package/dist/src/provider.d.ts +6 -3
  21. package/dist/src/provider.js +12 -5
  22. package/dist/src/providers/base_javascript.d.ts +19 -3
  23. package/dist/src/providers/base_javascript.js +106 -23
  24. package/dist/src/providers/base_pyproject.d.ts +170 -0
  25. package/dist/src/providers/base_pyproject.js +338 -0
  26. package/dist/src/providers/golang_gomodules.d.ts +12 -12
  27. package/dist/src/providers/golang_gomodules.js +102 -112
  28. package/dist/src/providers/gomod_parser.d.ts +4 -0
  29. package/dist/src/providers/gomod_parser.js +16 -0
  30. package/dist/src/providers/java_gradle.js +2 -1
  31. package/dist/src/providers/java_maven.d.ts +1 -1
  32. package/dist/src/providers/java_maven.js +10 -9
  33. package/dist/src/providers/javascript_pnpm.d.ts +1 -1
  34. package/dist/src/providers/javascript_pnpm.js +2 -2
  35. package/dist/src/providers/manifest.d.ts +2 -0
  36. package/dist/src/providers/manifest.js +22 -4
  37. package/dist/src/providers/processors/yarn_berry_processor.js +82 -3
  38. package/dist/src/providers/python_pip.js +3 -2
  39. package/dist/src/providers/python_poetry.d.ts +42 -0
  40. package/dist/src/providers/python_poetry.js +169 -0
  41. package/dist/src/providers/python_uv.d.ts +27 -0
  42. package/dist/src/providers/python_uv.js +146 -0
  43. package/dist/src/providers/requirements_parser.js +4 -3
  44. package/dist/src/providers/rust_cargo.d.ts +52 -0
  45. package/dist/src/providers/rust_cargo.js +614 -0
  46. package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
  47. package/dist/src/providers/tree-sitter-requirements.wasm +0 -0
  48. package/dist/src/sbom.d.ts +7 -0
  49. package/dist/src/sbom.js +9 -0
  50. package/dist/src/workspace.d.ts +61 -0
  51. package/dist/src/workspace.js +256 -0
  52. package/package.json +13 -4
  53. package/dist/src/license/compatibility.d.ts +0 -18
  54. package/dist/src/license/compatibility.js +0 -45
@@ -1,9 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { EOL } from "os";
4
3
  import { PackageURL } from 'packageurl-js';
4
+ import { readLicenseFile } from '../license/license_utils.js';
5
5
  import Sbom from '../sbom.js';
6
6
  import { getCustom, getCustomPath, invokeCommand } from "../tools.js";
7
+ import { getParser, getRequireQuery } from './gomod_parser.js';
7
8
  export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest };
8
9
  /** @typedef {import('../provider').Provider} */
9
10
  /** @typedef {import('../provider').Provided} Provided */
@@ -16,46 +17,46 @@ export default { isSupported, validateLockFile, provideComponent, provideStack,
16
17
  const ecosystem = 'golang';
17
18
  const defaultMainModuleVersion = "v0.0.0";
18
19
  /**
19
- * @param {string} manifestName - the subject manifest name-type
20
- * @returns {boolean} - return true if `pom.xml` is the manifest name-type
20
+ * @param {string} manifestName the subject manifest name-type
21
+ * @returns {boolean} return true if `pom.xml` is the manifest name-type
21
22
  */
22
23
  function isSupported(manifestName) {
23
24
  return 'go.mod' === manifestName;
24
25
  }
25
26
  /**
26
27
  * Go modules have no standard license field in go.mod
27
- * @param {string} manifestPath - path to go.mod
28
+ * @param {string} manifestPath path to go.mod
28
29
  * @returns {string|null}
29
30
  */
30
31
  // eslint-disable-next-line no-unused-vars
31
- function readLicenseFromManifest(manifestPath) { return null; }
32
+ function readLicenseFromManifest(manifestPath) { return readLicenseFile(manifestPath); }
32
33
  /**
33
- * @param {string} manifestDir - the directory where the manifest lies
34
+ * @param {string} manifestDir the directory where the manifest lies
34
35
  */
35
36
  function validateLockFile() { return true; }
36
37
  /**
37
38
  * Provide content and content type for maven-maven stack analysis.
38
- * @param {string} manifest - the manifest path or name
39
- * @param {{}} [opts={}] - optional various options to pass along the application
40
- * @returns {Provided}
39
+ * @param {string} manifest the manifest path or name
40
+ * @param {{}} [opts={}] optional various options to pass along the application
41
+ * @returns {Promise<Provided>}
41
42
  */
42
- function provideStack(manifest, opts = {}) {
43
+ async function provideStack(manifest, opts = {}) {
43
44
  return {
44
45
  ecosystem,
45
- content: getSBOM(manifest, opts, true),
46
+ content: await getSBOM(manifest, opts, true),
46
47
  contentType: 'application/vnd.cyclonedx+json'
47
48
  };
48
49
  }
49
50
  /**
50
51
  * Provide content and content type for maven-maven component analysis.
51
- * @param {string} manifest - path to go.mod for component report
52
- * @param {{}} [opts={}] - optional various options to pass along the application
53
- * @returns {Provided}
52
+ * @param {string} manifest path to go.mod for component report
53
+ * @param {{}} [opts={}] optional various options to pass along the application
54
+ * @returns {Promise<Provided>}
54
55
  */
55
- function provideComponent(manifest, opts = {}) {
56
+ async function provideComponent(manifest, opts = {}) {
56
57
  return {
57
58
  ecosystem,
58
- content: getSBOM(manifest, opts, false),
59
+ content: await getSBOM(manifest, opts, false),
59
60
  contentType: 'application/vnd.cyclonedx+json'
60
61
  };
61
62
  }
@@ -76,50 +77,52 @@ function getChildVertexFromEdge(edge) {
76
77
  return edge.split(" ")[1];
77
78
  }
78
79
  /**
79
- *
80
- * @param line one row from go.mod file
81
- * @return {boolean} whether line from go.mod should be considered as ignored or not
80
+ * Check whether a require_spec has a valid exhortignore marker.
81
+ * For direct dependencies: `//exhortignore` or `// exhortignore`
82
+ * For indirect dependencies: `// indirect; exhortignore` (semicolon-separated)
83
+ * @param {import('web-tree-sitter').SyntaxNode} specNode
84
+ * @return {boolean}
82
85
  */
83
- function ignoredLine(line) {
84
- let result = false;
85
- if (line.match(".*exhortignore.*")) {
86
- if (line.match(".+//\\s*exhortignore") || line.match(".+//\\sindirect (//)?\\s*exhortignore")) {
87
- let trimmedRow = line.trim();
88
- if (!trimmedRow.startsWith("module ") && !trimmedRow.startsWith("go ") && !trimmedRow.startsWith("require (") && !trimmedRow.startsWith("require(")
89
- && !trimmedRow.startsWith("exclude ") && !trimmedRow.startsWith("replace ") && !trimmedRow.startsWith("retract ") && !trimmedRow.startsWith("use ")
90
- && !trimmedRow.includes("=>")) {
91
- if (trimmedRow.startsWith("require ") || trimmedRow.match("^[a-z.0-9/-]+\\s{1,2}[vV][0-9]\\.[0-9](\\.[0-9]){0,2}.*")) {
92
- result = true;
93
- }
94
- }
86
+ function hasExhortIgnore(specNode) {
87
+ // Ideally this would be the following tree-sitter query instead, but for some
88
+ // reason it throws an error here but not in the playground.
89
+ // (require_spec) ((module_path) @path (version) (comment) @comment (#match? @comment "^//.*exhortignore"))
90
+ // QueryError: Bad pattern structure at offset 53: '(comment) @comment (#match? @comment "^//.*exhortignore")) @spec'...
91
+ let comments = specNode.children.filter(c => c.type === 'comment');
92
+ for (let comment of comments) {
93
+ let text = comment.text;
94
+ if (/^\/\/\s*indirect;\s*exhortignore/.test(text)) {
95
+ return true;
96
+ }
97
+ if (/^\/\/\s*exhortignore/.test(text)) {
98
+ return true;
95
99
  }
96
100
  }
97
- return result;
98
- }
99
- /**
100
- * extract package name from go.mod line that contains exhortignore comment.
101
- * @param line a row contains exhortignore as part of a comment
102
- * @return {string} the full package name + group/namespace + version
103
- * @private
104
- */
105
- function extractPackageName(line) {
106
- let trimmedRow = line.trim();
107
- let firstRemarkNotationOccurrence = trimmedRow.indexOf("//");
108
- return trimmedRow.substring(0, firstRemarkNotationOccurrence).trim();
101
+ return false;
109
102
  }
110
103
  /**
111
104
  *
112
- * @param {string } manifest - path to manifest
113
- * @return {[PackageURL]} list of ignored dependencies d
105
+ * @param {string} manifestContent go.mod file contents
106
+ * @param {import('web-tree-sitter').Parser} parser
107
+ * @param {import('web-tree-sitter').Query} requireQuery
108
+ * @return {PackageURL[]} list of ignored dependencies
114
109
  */
115
- function getIgnoredDeps(manifest) {
116
- let goMod = fs.readFileSync(manifest).toString().trim();
117
- let lines = goMod.split(getLineSeparatorGolang());
118
- return lines.filter(line => ignoredLine(line)).map(line => extractPackageName(line)).map(dep => toPurl(dep, /[ ]{1,3}/));
110
+ function getIgnoredDeps(manifestContent, parser, requireQuery) {
111
+ let tree = parser.parse(manifestContent);
112
+ return requireQuery.matches(tree.rootNode)
113
+ .filter(match => {
114
+ let specNode = match.captures.find(c => c.name === 'spec').node;
115
+ return hasExhortIgnore(specNode);
116
+ })
117
+ .map(match => {
118
+ let name = match.captures.find(c => c.name === 'name').node.text;
119
+ let version = match.captures.find(c => c.name === 'version').node.text;
120
+ return toPurl(`${name} ${version}`, /[ ]{1,3}/);
121
+ });
119
122
  }
120
123
  /**
121
124
  *
122
- * @param {[PackageURL]}allIgnoredDeps - list of purls of all dependencies that should be ignored
125
+ * @param {PackageURL[]} allIgnoredDeps list of purls of all dependencies that should be ignored
123
126
  * @param {PackageURL} purl object to be checked if needed to be ignored
124
127
  * @return {boolean}
125
128
  */
@@ -138,59 +141,29 @@ function enforceRemovingIgnoredDepsInCaseOfAutomaticVersionUpdate(ignoredDeps, s
138
141
  }
139
142
  /**
140
143
  *
141
- * @param {[string]} lines - array of lines of go.mod manifest
142
- * @param {string} goMod - content of go.mod manifest
143
- * @return {[string]} all dependencies from go.mod file as array
144
+ * @param {string} manifestContent go.mod file contents
145
+ * @param {import('web-tree-sitter').Parser} parser
146
+ * @param {import('web-tree-sitter').Query} requireQuery
147
+ * @return {string[]} all dependencies from go.mod file as "name version" strings
144
148
  */
145
- function collectAllDepsFromManifest(lines, goMod) {
146
- let result;
147
- // collect all deps that starts with require keyword
148
- result = lines.filter((line) => line.trim().startsWith("require") && !line.includes("(")).map((dep) => dep.substring("require".length).trim());
149
- // collect all deps that are inside `require` blocks
150
- let currentSegmentOfGoMod = goMod;
151
- let requirePositionObject = decideRequireBlockIndex(currentSegmentOfGoMod);
152
- while (requirePositionObject.index > -1) {
153
- let depsInsideRequirementsBlock = currentSegmentOfGoMod.substring(requirePositionObject.index + requirePositionObject.startingOffeset).trim();
154
- let endOfBlockIndex = depsInsideRequirementsBlock.indexOf(")");
155
- let currentIndex = 0;
156
- while (currentIndex < endOfBlockIndex) {
157
- let endOfLinePosition = depsInsideRequirementsBlock.indexOf(EOL, currentIndex);
158
- let dependency = depsInsideRequirementsBlock.substring(currentIndex, endOfLinePosition);
159
- result.push(dependency.trim());
160
- currentIndex = endOfLinePosition + 1;
161
- }
162
- currentSegmentOfGoMod = currentSegmentOfGoMod.substring(endOfBlockIndex + 1).trim();
163
- requirePositionObject = decideRequireBlockIndex(currentSegmentOfGoMod);
164
- }
165
- function decideRequireBlockIndex(goMod) {
166
- let object = {};
167
- let index = goMod.indexOf("require(");
168
- object.startingOffeset = "require(".length;
169
- if (index === -1) {
170
- index = goMod.indexOf("require (");
171
- object.startingOffeset = "require (".length;
172
- if (index === -1) {
173
- index = goMod.indexOf("require (");
174
- object.startingOffeset = "require (".length;
175
- }
176
- }
177
- object.index = index;
178
- return object;
179
- }
180
- return result;
149
+ function collectAllDepsFromManifest(manifestContent, parser, requireQuery) {
150
+ let tree = parser.parse(manifestContent);
151
+ return requireQuery.matches(tree.rootNode).map(match => {
152
+ let name = match.captures.find(c => c.name === 'name').node.text;
153
+ let version = match.captures.find(c => c.name === 'version').node.text;
154
+ return `${name} ${version}`;
155
+ });
181
156
  }
182
157
  /**
183
158
  *
184
159
  * @param {string} rootElementName the rootElementName element of go mod graph, to compare only direct deps from go mod graph against go.mod manifest
185
- * @param{[string]} goModGraphOutputRows the goModGraphOutputRows from go mod graph' output
186
- * @param {string }manifest path to go.mod manifest on file system
160
+ * @param {string[]} goModGraphOutputRows the goModGraphOutputRows from go mod graph' output
161
+ * @param {string} manifestContent go.mod file contents
187
162
  * @private
188
163
  */
189
- function performManifestVersionsCheck(rootElementName, goModGraphOutputRows, manifest) {
190
- let goMod = fs.readFileSync(manifest).toString().trim();
191
- let lines = goMod.split(getLineSeparatorGolang());
164
+ function performManifestVersionsCheck(rootElementName, goModGraphOutputRows, manifestContent, parser, requireQuery) {
192
165
  let comparisonLines = goModGraphOutputRows.filter((line) => line.startsWith(rootElementName)).map((line) => getChildVertexFromEdge(line));
193
- let manifestDeps = collectAllDepsFromManifest(lines, goMod);
166
+ let manifestDeps = collectAllDepsFromManifest(manifestContent, parser, requireQuery);
194
167
  try {
195
168
  comparisonLines.forEach((dependency) => {
196
169
  let parts = dependency.split("@");
@@ -202,7 +175,7 @@ function performManifestVersionsCheck(rootElementName, goModGraphOutputRows, man
202
175
  let currentVersion = components[1];
203
176
  if (currentDepName === depName) {
204
177
  if (currentVersion !== version) {
205
- throw new Error(`versions mismatch for dependency name ${depName}, manifest version=${currentVersion}, installed Version=${version}, if you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting - MATCH_MANIFEST_VERSIONS=false`);
178
+ throw new Error(`version mismatch for dependency "${depName}", manifest version=${currentVersion}, installed version=${version}, if you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting MATCH_MANIFEST_VERSIONS=false`);
206
179
  }
207
180
  }
208
181
  });
@@ -218,10 +191,10 @@ function performManifestVersionsCheck(rootElementName, goModGraphOutputRows, man
218
191
  * @param {string} manifest - path for go.mod
219
192
  * @param {{}} [opts={}] - optional various options to pass along the application
220
193
  * @param {boolean} includeTransitive - whether the sbom should contain transitive dependencies of the main module or not.
221
- * @returns {string} the SBOM json content
194
+ * @returns {Promise<string>} the SBOM json content
222
195
  * @private
223
196
  */
224
- function getSBOM(manifest, opts = {}, includeTransitive) {
197
+ async function getSBOM(manifest, opts = {}, includeTransitive) {
225
198
  // get custom goBin path
226
199
  let goBin = getCustomPath('go', opts);
227
200
  // verify goBin is accessible
@@ -247,14 +220,25 @@ function getSBOM(manifest, opts = {}, includeTransitive) {
247
220
  catch (error) {
248
221
  throw new Error('failed to determine root module name', { cause: error });
249
222
  }
250
- let ignoredDeps = getIgnoredDeps(manifest);
223
+ let manifestContent = fs.readFileSync(manifest).toString();
224
+ let [parser, requireQuery] = await Promise.all([getParser(), getRequireQuery()]);
225
+ let ignoredDeps = getIgnoredDeps(manifestContent, parser, requireQuery);
251
226
  let allIgnoredDeps = ignoredDeps.map((dep) => dep.toString());
252
227
  let sbom = new Sbom();
253
228
  let rows = goGraphOutput.split(getLineSeparatorGolang()).filter(line => !line.includes(' go@'));
254
- let root = getParentVertexFromEdge(goModEditOutput['Module']['Path']);
229
+ let root = goModEditOutput['Module']['Path'];
230
+ // Build set of direct dependency paths from go mod edit -json
231
+ let directDepPaths = new Set();
232
+ if (goModEditOutput['Require']) {
233
+ goModEditOutput['Require'].forEach(req => {
234
+ if (!req['Indirect']) {
235
+ directDepPaths.add(req['Path']);
236
+ }
237
+ });
238
+ }
255
239
  let matchManifestVersions = getCustom("MATCH_MANIFEST_VERSIONS", "false", opts);
256
240
  if (matchManifestVersions === "true") {
257
- performManifestVersionsCheck(root, rows, manifest);
241
+ performManifestVersionsCheck(root, rows, manifestContent, parser, requireQuery);
258
242
  }
259
243
  const mainModule = toPurl(root, "@");
260
244
  const license = readLicenseFromManifest(manifest);
@@ -272,7 +256,11 @@ function getSBOM(manifest, opts = {}, includeTransitive) {
272
256
  currentParent = getParentVertexFromEdge(row);
273
257
  source = toPurl(currentParent, "@");
274
258
  }
275
- let target = toPurl(getChildVertexFromEdge(row), "@");
259
+ let child = getChildVertexFromEdge(row);
260
+ let target = toPurl(child, "@");
261
+ if (getParentVertexFromEdge(row) === root && !directDepPaths.has(getPackageName(child))) {
262
+ return;
263
+ }
276
264
  sbom.addDependency(source, target);
277
265
  });
278
266
  // at the end, filter out all ignored dependencies including versions.
@@ -282,10 +270,12 @@ function getSBOM(manifest, opts = {}, includeTransitive) {
282
270
  else {
283
271
  let directDependencies = rows.filter(row => row.startsWith(root));
284
272
  directDependencies.forEach(pair => {
285
- let dependency = getChildVertexFromEdge(pair);
286
- let depPurl = toPurl(dependency, "@");
287
- if (dependencyNotIgnored(ignoredDeps, depPurl)) {
288
- sbom.addDependency(mainModule, depPurl);
273
+ let child = getChildVertexFromEdge(pair);
274
+ let target = toPurl(child, "@");
275
+ if (dependencyNotIgnored(ignoredDeps, target)) {
276
+ if (directDepPaths.has(getPackageName(child))) {
277
+ sbom.addDependency(mainModule, target);
278
+ }
289
279
  }
290
280
  });
291
281
  enforceRemovingIgnoredDepsInCaseOfAutomaticVersionUpdate(ignoredDeps, sbom);
@@ -295,7 +285,7 @@ function getSBOM(manifest, opts = {}, includeTransitive) {
295
285
  /**
296
286
  * Utility function for creating Purl String
297
287
 
298
- * @param {string }dependency the name of the artifact, can include a namespace(group) or not - namespace/artifactName.
288
+ * @param {string} dependency the name of the artifact, can include a namespace(group) or not - namespace/artifactName.
299
289
  * @param {RegExp} delimiter delimiter between name of dependency and version
300
290
  * @private
301
291
  * @returns {PackageURL|null} PackageUrl Object ready to be used in SBOM
@@ -320,12 +310,12 @@ function toPurl(dependency, delimiter) {
320
310
  }
321
311
  return pkg;
322
312
  }
323
- /** This function gets rows from go mod graph , and go.mod graph, and selecting for all
313
+ /** This function gets rows from go mod graph, and go.mod graph, and selecting for all
324
314
  * packages the has more than one minor the final versions as selected by golang MVS algorithm.
325
- * @param {[string]}rows all the rows from go modules dependency tree
315
+ * @param {string[]} rows all the rows from go modules dependency tree
326
316
  * @param {string} manifestPath the path of the go.mod file
327
317
  * @param {string} path to go binary
328
- * @return {[string]} rows that contains final versions.
318
+ * @return {string[]} rows that contains final versions.
329
319
  */
330
320
  function getFinalPackagesVersionsForModule(rows, manifestPath, goBin) {
331
321
  let manifestDir = path.dirname(manifestPath);
@@ -380,7 +370,7 @@ function getFinalPackagesVersionsForModule(rows, manifestPath, goBin) {
380
370
  /**
381
371
  *
382
372
  * @param {string} fullPackage - full package with its name and version
383
- * @return -{string} package name only
373
+ * @return {string} package name only
384
374
  * @private
385
375
  */
386
376
  function getPackageName(fullPackage) {
@@ -398,7 +388,7 @@ function isSpecialGoModule(moduleName) {
398
388
  /**
399
389
  *
400
390
  * @param {string} fullPackage - full package with its name and version
401
- * @return -{string} package version only
391
+ * @return {string|undefined} package version only
402
392
  * @private
403
393
  */
404
394
  function getVersionOfPackage(fullPackage) {
@@ -0,0 +1,4 @@
1
+ export function getParser(): Promise<Parser>;
2
+ export function getRequireQuery(): Promise<Query>;
3
+ import { Parser } from 'web-tree-sitter';
4
+ import { Query } from 'web-tree-sitter';
@@ -0,0 +1,16 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { Language, Parser, Query } from 'web-tree-sitter';
3
+ const wasmUrl = new URL('./tree-sitter-gomod.wasm', import.meta.url);
4
+ async function init() {
5
+ await Parser.init();
6
+ const wasmBytes = new Uint8Array(await readFile(wasmUrl));
7
+ return await Language.load(wasmBytes);
8
+ }
9
+ export async function getParser() {
10
+ const language = await init();
11
+ return new Parser().setLanguage(language);
12
+ }
13
+ export async function getRequireQuery() {
14
+ const language = await init();
15
+ return new Query(language, '(require_spec (module_path) @name (version) @version) @spec');
16
+ }
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { EOL } from 'os';
4
4
  import TOML from 'fast-toml';
5
+ import { readLicenseFile } from '../license/license_utils.js';
5
6
  import Sbom from '../sbom.js';
6
7
  import Base_java, { ecosystem_gradle } from "./base_java.js";
7
8
  /** @typedef {import('../provider.js').Provider} */
@@ -55,7 +56,7 @@ export default class Java_gradle extends Base_java {
55
56
  * @returns {null}
56
57
  */
57
58
  // eslint-disable-next-line no-unused-vars
58
- readLicenseFromManifest(manifestPath) { return null; }
59
+ readLicenseFromManifest(manifestPath) { return readLicenseFile(manifestPath); }
59
60
  /**
60
61
  * Provide content and content type for stack analysis.
61
62
  * @param {string} manifest - the manifest path or name
@@ -28,7 +28,7 @@ export default class Java_maven extends Base_java {
28
28
  */
29
29
  provideComponent(manifest: string, opts?: {}): Provided;
30
30
  /**
31
- * Read license from pom.xml manifest
31
+ * Read license from pom.xml manifest, with fallback to LICENSE file
32
32
  * @param {string} manifestPath - path to pom.xml
33
33
  * @returns {string|null}
34
34
  */
@@ -3,6 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { EOL } from 'os';
5
5
  import { XMLParser } from 'fast-xml-parser';
6
+ import { getLicense } from '../license/license_utils.js';
6
7
  import Sbom from '../sbom.js';
7
8
  import { getCustom } from '../tools.js';
8
9
  import Base_java, { ecosystem_maven } from "./base_java.js";
@@ -52,28 +53,28 @@ export default class Java_maven extends Base_java {
52
53
  };
53
54
  }
54
55
  /**
55
- * Read license from pom.xml manifest
56
+ * Read license from pom.xml manifest, with fallback to LICENSE file
56
57
  * @param {string} manifestPath - path to pom.xml
57
58
  * @returns {string|null}
58
59
  */
59
60
  readLicenseFromManifest(manifestPath) {
61
+ let fromPom = null;
60
62
  try {
61
63
  const xml = fs.readFileSync(manifestPath, 'utf-8');
62
64
  const parser = new XMLParser({ ignoreAttributes: false });
63
65
  const obj = parser.parse(xml);
64
66
  const project = obj?.project;
65
- if (!project?.licenses?.license) {
66
- return null;
67
+ if (project?.licenses?.license) {
68
+ const license = Array.isArray(project.licenses.license)
69
+ ? project.licenses.license[0]
70
+ : project.licenses.license;
71
+ fromPom = (license?.name && license.name.trim()) || null;
67
72
  }
68
- const license = Array.isArray(project.licenses.license)
69
- ? project.licenses.license[0]
70
- : project.licenses.license;
71
- const name = (license?.name && license.name.trim()) || null;
72
- return name || null;
73
73
  }
74
74
  catch {
75
- return null;
75
+ // leave fromPom as null
76
76
  }
77
+ return getLicense(fromPom, manifestPath);
77
78
  }
78
79
  /**
79
80
  * Create a Dot Graph dependency tree for a manifest path.
@@ -1,5 +1,5 @@
1
1
  export default class Javascript_pnpm extends Base_javascript {
2
2
  _listCmdArgs(includeTransitive: any): string[];
3
- _buildDependencyTree(includeTransitive: any, manifest: any): any;
3
+ _buildDependencyTree(includeTransitive: any, opts?: {}): any;
4
4
  }
5
5
  import Base_javascript from './base_javascript.js';
@@ -12,8 +12,8 @@ export default class Javascript_pnpm extends Base_javascript {
12
12
  _updateLockFileCmdArgs() {
13
13
  return ['install', '--frozen-lockfile'];
14
14
  }
15
- _buildDependencyTree(includeTransitive, manifest) {
16
- const tree = super._buildDependencyTree(includeTransitive, manifest);
15
+ _buildDependencyTree(includeTransitive, opts = {}) {
16
+ const tree = super._buildDependencyTree(includeTransitive, opts);
17
17
  if (Array.isArray(tree) && tree.length > 0) {
18
18
  return tree[0];
19
19
  }
@@ -2,6 +2,8 @@ export default class Manifest {
2
2
  constructor(manifestPath: any);
3
3
  manifestPath: any;
4
4
  dependencies: any[];
5
+ peerDependencies: any;
6
+ optionalDependencies: any;
5
7
  name: any;
6
8
  version: any;
7
9
  ignored: any[];
@@ -9,6 +9,8 @@ export default class Manifest {
9
9
  this.manifestPath = manifestPath;
10
10
  const content = this.loadManifest();
11
11
  this.dependencies = this.loadDependencies(content);
12
+ this.peerDependencies = content.peerDependencies || {};
13
+ this.optionalDependencies = content.optionalDependencies || {};
12
14
  this.name = content.name;
13
15
  this.version = content.version || DEFAULT_VERSION;
14
16
  this.ignored = this.loadIgnored(content);
@@ -27,11 +29,27 @@ export default class Manifest {
27
29
  }
28
30
  loadDependencies(content) {
29
31
  let deps = [];
30
- if (!content.dependencies) {
31
- return deps;
32
+ const depSources = [
33
+ content.dependencies,
34
+ content.peerDependencies,
35
+ content.optionalDependencies,
36
+ ];
37
+ for (const source of depSources) {
38
+ if (source) {
39
+ for (let dep in source) {
40
+ if (!deps.includes(dep)) {
41
+ deps.push(dep);
42
+ }
43
+ }
44
+ }
32
45
  }
33
- for (let dep in content.dependencies) {
34
- deps.push(dep);
46
+ // bundledDependencies is an array of package names (subset of dependencies)
47
+ if (Array.isArray(content.bundledDependencies)) {
48
+ for (const dep of content.bundledDependencies) {
49
+ if (!deps.includes(dep)) {
50
+ deps.push(dep);
51
+ }
52
+ }
35
53
  }
36
54
  return deps;
37
55
  }
@@ -48,13 +48,15 @@ export default class Yarn_berry_processor extends Yarn_processor {
48
48
  if (!depTree) {
49
49
  return new Map();
50
50
  }
51
- return new Map(depTree.filter(dep => !this.#isRoot(dep.value)).map(dep => {
51
+ return new Map(depTree.filter(dep => !this.#isRoot(dep.value))
52
+ .map(dep => {
52
53
  const depName = dep.value;
53
54
  const idx = depName.lastIndexOf('@');
54
55
  const name = depName.substring(0, idx);
55
56
  const version = dep.children.Version;
56
57
  return [name, toPurl(purlType, name, version)];
57
- }));
58
+ })
59
+ .filter(([name]) => this._manifest.dependencies.includes(name)));
58
60
  }
59
61
  /**
60
62
  * Checks if a dependency is the root package
@@ -77,14 +79,58 @@ export default class Yarn_berry_processor extends Yarn_processor {
77
79
  if (!depTree) {
78
80
  return;
79
81
  }
82
+ // Build index of nodes by their value for quick lookup
83
+ const nodeIndex = new Map();
84
+ depTree.forEach(n => nodeIndex.set(n.value, n));
85
+ // Determine the set of node values reachable from root via production deps
86
+ const prodDeps = new Set(this._manifest.dependencies);
87
+ const reachable = new Set();
88
+ const queue = [];
89
+ // Seed with root's production dependencies
90
+ const rootNode = depTree.find(n => this.#isRoot(n.value));
91
+ if (rootNode?.children?.Dependencies) {
92
+ for (const d of rootNode.children.Dependencies) {
93
+ const to = this.#purlFromLocator(d.locator);
94
+ if (to) {
95
+ const fullName = to.namespace ? `${to.namespace}/${to.name}` : to.name;
96
+ if (prodDeps.has(fullName)) {
97
+ queue.push(d.locator);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ // BFS to find all transitively reachable packages
103
+ while (queue.length > 0) {
104
+ const locator = queue.shift();
105
+ if (reachable.has(locator)) {
106
+ continue;
107
+ }
108
+ reachable.add(locator);
109
+ const node = nodeIndex.get(this.#nodeValueFromLocator(locator));
110
+ if (node?.children?.Dependencies) {
111
+ for (const d of node.children.Dependencies) {
112
+ if (!reachable.has(d.locator)) {
113
+ queue.push(d.locator);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ // Only emit edges for root and reachable nodes
80
119
  depTree.forEach(n => {
81
120
  const depName = n.value;
82
- const from = this.#isRoot(depName) ? toPurlFromString(sbom.getRoot().purl) : this.#purlFromNode(depName, n);
121
+ const isRoot = this.#isRoot(depName);
122
+ if (!isRoot && !this.#isReachableNode(depName, reachable)) {
123
+ return;
124
+ }
125
+ const from = isRoot ? toPurlFromString(sbom.getRoot().purl) : this.#purlFromNode(depName, n);
83
126
  const deps = n.children?.Dependencies;
84
127
  if (!deps) {
85
128
  return;
86
129
  }
87
130
  deps.forEach(d => {
131
+ if (!reachable.has(d.locator)) {
132
+ return;
133
+ }
88
134
  const to = this.#purlFromLocator(d.locator);
89
135
  if (to) {
90
136
  sbom.addDependency(from, to);
@@ -92,6 +138,39 @@ export default class Yarn_berry_processor extends Yarn_processor {
92
138
  });
93
139
  });
94
140
  }
141
+ /**
142
+ * Converts a locator to the node value format used in yarn info output
143
+ * @param {string} locator - e.g. "express@npm:4.17.1"
144
+ * @returns {string} The node value, same as locator for non-virtual
145
+ * @private
146
+ */
147
+ #nodeValueFromLocator(locator) {
148
+ // Virtual locators: "@scope/name@virtual:hash#npm:version" → "@scope/name@npm:version"
149
+ const virtualMatch = Yarn_berry_processor.VIRTUAL_LOCATOR_PATTERN.exec(locator);
150
+ if (virtualMatch) {
151
+ return `${virtualMatch[1]}@npm:${virtualMatch[2]}`;
152
+ }
153
+ return locator;
154
+ }
155
+ /**
156
+ * Checks if a node is in the reachable set by matching its value against reachable locators
157
+ * @param {string} depName - The node value (e.g. "express@npm:4.17.1")
158
+ * @param {Set<string>} reachable - Set of reachable locators
159
+ * @returns {boolean}
160
+ * @private
161
+ */
162
+ #isReachableNode(depName, reachable) {
163
+ if (reachable.has(depName)) {
164
+ return true;
165
+ }
166
+ // Check if any reachable locator resolves to this node value
167
+ for (const locator of reachable) {
168
+ if (this.#nodeValueFromLocator(locator) === depName) {
169
+ return true;
170
+ }
171
+ }
172
+ return false;
173
+ }
95
174
  /**
96
175
  * Creates a PackageURL from a dependency locator
97
176
  * @param {string} locator - The dependency locator