@trustify-da/trustify-da-javascript-client 0.3.0-ea.904e9f2 → 0.3.0-ea.9988076

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 (46) hide show
  1. package/README.md +65 -2
  2. package/dist/package.json +17 -9
  3. package/dist/src/analysis.d.ts +5 -5
  4. package/dist/src/analysis.js +21 -76
  5. package/dist/src/cli.js +72 -6
  6. package/dist/src/cyclone_dx_sbom.d.ts +3 -2
  7. package/dist/src/cyclone_dx_sbom.js +18 -5
  8. package/dist/src/index.d.ts +66 -11
  9. package/dist/src/index.js +6 -3
  10. package/dist/src/license/index.d.ts +28 -0
  11. package/dist/src/license/index.js +100 -0
  12. package/dist/src/license/license_utils.d.ts +40 -0
  13. package/dist/src/license/license_utils.js +134 -0
  14. package/dist/src/license/licenses_api.d.ts +34 -0
  15. package/dist/src/license/licenses_api.js +98 -0
  16. package/dist/src/license/project_license.d.ts +20 -0
  17. package/dist/src/license/project_license.js +62 -0
  18. package/dist/src/oci_image/images.d.ts +4 -5
  19. package/dist/src/oci_image/utils.d.ts +4 -4
  20. package/dist/src/provider.d.ts +12 -3
  21. package/dist/src/provider.js +19 -2
  22. package/dist/src/providers/base_java.d.ts +3 -5
  23. package/dist/src/providers/base_javascript.d.ts +10 -4
  24. package/dist/src/providers/base_javascript.js +30 -4
  25. package/dist/src/providers/golang_gomodules.d.ts +11 -4
  26. package/dist/src/providers/golang_gomodules.js +13 -4
  27. package/dist/src/providers/java_gradle.d.ts +9 -3
  28. package/dist/src/providers/java_gradle.js +12 -2
  29. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  30. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  31. package/dist/src/providers/java_maven.d.ts +12 -5
  32. package/dist/src/providers/java_maven.js +33 -5
  33. package/dist/src/providers/python_controller.d.ts +5 -2
  34. package/dist/src/providers/python_controller.js +56 -58
  35. package/dist/src/providers/python_pip.d.ts +11 -4
  36. package/dist/src/providers/python_pip.js +46 -53
  37. package/dist/src/providers/requirements_parser.d.ts +6 -0
  38. package/dist/src/providers/requirements_parser.js +24 -0
  39. package/dist/src/providers/rust_cargo.d.ts +47 -0
  40. package/dist/src/providers/rust_cargo.js +606 -0
  41. package/dist/src/providers/tree-sitter-requirements.wasm +0 -0
  42. package/dist/src/sbom.d.ts +3 -1
  43. package/dist/src/sbom.js +3 -2
  44. package/dist/src/tools.d.ts +22 -6
  45. package/dist/src/tools.js +56 -1
  46. package/package.json +18 -10
@@ -0,0 +1,606 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { PackageURL } from 'packageurl-js';
4
+ import { parse as parseToml } from 'smol-toml';
5
+ import { getLicense } from '../license/license_utils.js';
6
+ import Sbom from '../sbom.js';
7
+ import { getCustomPath, invokeCommand } from '../tools.js';
8
+ export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest };
9
+ /** @typedef {import('../provider').Provider} */
10
+ /** @typedef {import('../provider').Provided} Provided */
11
+ /**
12
+ * @type {string} ecosystem identifier for cargo/crates packages
13
+ * @private
14
+ */
15
+ const ecosystem = 'cargo';
16
+ /**
17
+ * Ignore markers recognised in Cargo.toml comments.
18
+ * Supports both the legacy `exhortignore` marker and the new `trustify-da-ignore` marker.
19
+ * @type {string[]}
20
+ * @private
21
+ */
22
+ const IGNORE_MARKERS = ['exhortignore', 'trustify-da-ignore'];
23
+ /**
24
+ * Checks whether a line contains any of the recognised ignore markers.
25
+ * @param {string} line - the line to check
26
+ * @returns {boolean} true if the line contains at least one ignore marker
27
+ * @private
28
+ */
29
+ function hasIgnoreMarker(line) {
30
+ return IGNORE_MARKERS.some(marker => line.includes(marker));
31
+ }
32
+ /**
33
+ * Checks whether a dependency name is in the ignored set, accounting for
34
+ * Cargo's underscore/hyphen equivalence
35
+ * @param {string} name - the dependency name to check
36
+ * @param {Set<string>} ignoredDeps - set of ignored dependency names
37
+ * @returns {boolean} true if the dependency should be ignored
38
+ * @private
39
+ */
40
+ function isDepIgnored(name, ignoredDeps) {
41
+ if (ignoredDeps.has(name)) {
42
+ return true;
43
+ }
44
+ let normalized = name.replace(/_/g, '-');
45
+ if (normalized !== name && ignoredDeps.has(normalized)) {
46
+ return true;
47
+ }
48
+ normalized = name.replace(/-/g, '_');
49
+ if (normalized !== name && ignoredDeps.has(normalized)) {
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+ /**
55
+ * Enum-like constants for Cargo project types.
56
+ * @private
57
+ */
58
+ const CrateType = {
59
+ SINGLE_CRATE: 'SINGLE_CRATE',
60
+ WORKSPACE_VIRTUAL: 'WORKSPACE_VIRTUAL',
61
+ WORKSPACE_WITH_ROOT_CRATE: 'WORKSPACE_WITH_ROOT_CRATE'
62
+ };
63
+ /**
64
+ * @param {string} manifestName - the subject manifest name-type
65
+ * @returns {boolean} - return true if `Cargo.toml` is the manifest name-type
66
+ */
67
+ function isSupported(manifestName) {
68
+ return 'Cargo.toml' === manifestName;
69
+ }
70
+ /**
71
+ * Read project license from Cargo.toml, with fallback to LICENSE file.
72
+ * Supports the `license` field under `[package]` (single crate / workspace
73
+ * with root) and under `[workspace.package]` (virtual workspaces).
74
+ * @param {string} manifestPath - path to Cargo.toml
75
+ * @returns {string|null} SPDX identifier or null
76
+ */
77
+ function readLicenseFromManifest(manifestPath) {
78
+ let fromManifest = null;
79
+ try {
80
+ let content = fs.readFileSync(manifestPath, 'utf-8');
81
+ let parsed = parseToml(content);
82
+ fromManifest = parsed.package?.license
83
+ || parsed.workspace?.package?.license
84
+ || null;
85
+ }
86
+ catch (_) {
87
+ // leave fromManifest as null
88
+ }
89
+ return getLicense(fromManifest, manifestPath);
90
+ }
91
+ /**
92
+ * Validates that Cargo.lock exists in the manifest directory or in a parent
93
+ * workspace root directory. In Cargo workspaces the lock file always lives at
94
+ * the workspace root, so when a member crate's Cargo.toml is provided we walk
95
+ * up the directory tree looking for Cargo.lock (stopping when we find a
96
+ * Cargo.toml that contains a [workspace] section, or when we reach the
97
+ * filesystem root).
98
+ * @param {string} manifestDir - the directory where the manifest lies
99
+ * @returns {boolean} true if Cargo.lock is found
100
+ */
101
+ function validateLockFile(manifestDir) {
102
+ let dir = path.resolve(manifestDir);
103
+ let parent = dir;
104
+ do {
105
+ dir = parent;
106
+ if (fs.existsSync(path.join(dir, 'Cargo.lock'))) {
107
+ return true;
108
+ }
109
+ // If this directory has a Cargo.toml with [workspace], the lock file
110
+ // should have been here — stop searching.
111
+ let cargoToml = path.join(dir, 'Cargo.toml');
112
+ if (fs.existsSync(cargoToml)) {
113
+ try {
114
+ let content = fs.readFileSync(cargoToml, 'utf-8');
115
+ if (/\[workspace\]/.test(content)) {
116
+ return false;
117
+ }
118
+ }
119
+ catch (_) {
120
+ // ignore read errors, keep searching
121
+ }
122
+ }
123
+ parent = path.dirname(dir);
124
+ } while (parent !== dir);
125
+ return false;
126
+ }
127
+ /**
128
+ * Provide content and content type for Cargo stack analysis.
129
+ * @param {string} manifest - the manifest path
130
+ * @param {{}} [opts={}] - optional various options to pass along the application
131
+ * @returns {Provided}
132
+ */
133
+ function provideStack(manifest, opts = {}) {
134
+ return {
135
+ ecosystem,
136
+ content: getSBOM(manifest, opts, true),
137
+ contentType: 'application/vnd.cyclonedx+json'
138
+ };
139
+ }
140
+ /**
141
+ * Provide content and content type for Cargo component analysis.
142
+ * @param {string} manifest - path to Cargo.toml for component report
143
+ * @param {{}} [opts={}] - optional various options to pass along the application
144
+ * @returns {Provided}
145
+ */
146
+ function provideComponent(manifest, opts = {}) {
147
+ return {
148
+ ecosystem,
149
+ content: getSBOM(manifest, opts, false),
150
+ contentType: 'application/vnd.cyclonedx+json'
151
+ };
152
+ }
153
+ /**
154
+ * Create SBOM json string for a Cargo project.
155
+ * @param {string} manifest - path to Cargo.toml
156
+ * @param {{}} [opts={}] - optional various options to pass along the application
157
+ * @param {boolean} includeTransitive - whether the sbom should contain transitive dependencies
158
+ * @returns {string} the SBOM json content
159
+ * @private
160
+ */
161
+ function getSBOM(manifest, opts = {}, includeTransitive) {
162
+ let cargoBin = getCustomPath('cargo', opts);
163
+ verifyCargoAccessible(cargoBin);
164
+ let manifestDir = path.dirname(manifest);
165
+ let metadata = executeCargoMetadata(cargoBin, manifestDir);
166
+ let ignoredDeps = getIgnoredDeps(manifest, metadata);
167
+ let crateType = detectCrateType(metadata);
168
+ let license = readLicenseFromManifest(manifest);
169
+ let sbom;
170
+ if (crateType === CrateType.WORKSPACE_VIRTUAL) {
171
+ sbom = handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransitive, opts, license);
172
+ }
173
+ else {
174
+ sbom = handleSingleCrate(metadata, ignoredDeps, includeTransitive, opts, license);
175
+ }
176
+ return sbom;
177
+ }
178
+ /**
179
+ * Verifies that the cargo binary is accessible.
180
+ * @param {string} cargoBin - path to cargo binary
181
+ * @throws {Error} if cargo is not accessible
182
+ * @private
183
+ */
184
+ function verifyCargoAccessible(cargoBin) {
185
+ try {
186
+ invokeCommand(cargoBin, ['--version']);
187
+ }
188
+ catch (error) {
189
+ if (error.code === 'ENOENT') {
190
+ throw new Error(`cargo binary is not accessible at "${cargoBin}"`);
191
+ }
192
+ throw new Error('failed to check for cargo binary', { cause: error });
193
+ }
194
+ }
195
+ /**
196
+ * Executes `cargo metadata` and returns the parsed JSON.
197
+ * @param {string} cargoBin - path to cargo binary
198
+ * @param {string} manifestDir - directory containing Cargo.toml
199
+ * @returns {object} parsed cargo metadata JSON
200
+ * @throws {Error} if cargo metadata fails
201
+ * @private
202
+ */
203
+ function executeCargoMetadata(cargoBin, manifestDir) {
204
+ try {
205
+ let output = invokeCommand(cargoBin, ['metadata', '--format-version', '1'], { cwd: manifestDir });
206
+ return JSON.parse(output.toString().trim());
207
+ }
208
+ catch (error) {
209
+ throw new Error('failed to execute cargo metadata', { cause: error });
210
+ }
211
+ }
212
+ /**
213
+ * Detects the type of Cargo project from metadata.
214
+ * @param {object} metadata - parsed cargo metadata
215
+ * @returns {string} one of CrateType values
216
+ * @private
217
+ */
218
+ function detectCrateType(metadata) {
219
+ let rootPackageId = metadata.resolve?.root;
220
+ let workspaceMembers = metadata.workspace_members || [];
221
+ if (!rootPackageId && workspaceMembers.length > 0) {
222
+ return CrateType.WORKSPACE_VIRTUAL;
223
+ }
224
+ if (rootPackageId && workspaceMembers.length > 1) {
225
+ return CrateType.WORKSPACE_WITH_ROOT_CRATE;
226
+ }
227
+ return CrateType.SINGLE_CRATE;
228
+ }
229
+ /**
230
+ * Handles SBOM generation for single crate and workspace-with-root-crate projects.
231
+ * For workspace-with-root-crate, workspace members are only included if they
232
+ * appear in the root crate's dependency graph from cargo metadata. We don't
233
+ * automatically add all members as dependencies since most workspace members
234
+ * (examples, tools, benchmarks) depend ON the root crate, not the other way around.
235
+ * @param {object} metadata - parsed cargo metadata
236
+ * @param {Set<string>} ignoredDeps - set of ignored dependency names
237
+ * @param {boolean} includeTransitive - whether to include transitive dependencies
238
+ * @param {{}} opts - options
239
+ * @param {string|null} license - SPDX license identifier for the root component
240
+ * @returns {string} SBOM json string
241
+ * @private
242
+ */
243
+ function handleSingleCrate(metadata, ignoredDeps, includeTransitive, opts, license) {
244
+ let rootPackageId = metadata.resolve.root;
245
+ let rootPackage = findPackageById(metadata, rootPackageId);
246
+ let rootPurl = toPurl(rootPackage.name, rootPackage.version);
247
+ let sbom = new Sbom();
248
+ sbom.addRoot(rootPurl, license);
249
+ let resolveNode = findResolveNode(metadata, rootPackageId);
250
+ if (!resolveNode) {
251
+ return sbom.getAsJsonString(opts);
252
+ }
253
+ if (includeTransitive) {
254
+ addTransitiveDeps(sbom, metadata, rootPackageId, ignoredDeps, new Set(), rootPurl);
255
+ }
256
+ else {
257
+ addDirectDeps(sbom, metadata, rootPackageId, rootPurl, ignoredDeps);
258
+ }
259
+ return sbom.getAsJsonString(opts);
260
+ }
261
+ /**
262
+ * Handles SBOM generation for virtual workspace projects.
263
+ *
264
+ * For stack analysis (includeTransitive=true):
265
+ * Iterates all workspace members and walks their full dependency trees.
266
+ *
267
+ * For component analysis (includeTransitive=false):
268
+ * Only includes dependencies explicitly listed in [workspace.dependencies]
269
+ * of the root Cargo.toml. If that section is absent the SBOM contains only
270
+ * the synthetic workspace root with no dependencies — this matches the Java
271
+ * client behaviour where CA "just analyzes direct dependencies defined in
272
+ * Cargo.toml".
273
+ *
274
+ * @param {string} manifest - path to the root Cargo.toml
275
+ * @param {object} metadata - parsed cargo metadata
276
+ * @param {Set<string>} ignoredDeps - set of ignored dependency names
277
+ * @param {boolean} includeTransitive - whether to include transitive dependencies
278
+ * @param {{}} opts - options
279
+ * @param {string|null} license - SPDX license identifier for the root component
280
+ * @returns {string} SBOM json string
281
+ * @private
282
+ */
283
+ function handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransitive, opts, license) {
284
+ let workspaceRoot = metadata.workspace_root;
285
+ let rootName = path.basename(workspaceRoot);
286
+ let workspaceVersion = getWorkspaceVersion(metadata);
287
+ let rootPurl = toPurl(rootName, workspaceVersion);
288
+ let sbom = new Sbom();
289
+ sbom.addRoot(rootPurl, license);
290
+ if (includeTransitive) {
291
+ // Stack analysis: walk all members and their full dependency trees
292
+ let workspaceMembers = metadata.workspace_members || [];
293
+ for (let memberId of workspaceMembers) {
294
+ let memberPackage = findPackageById(metadata, memberId);
295
+ if (!memberPackage) {
296
+ continue;
297
+ }
298
+ let memberPurl = memberPackage.source == null
299
+ ? toPathDepPurl(memberPackage.name, memberPackage.version)
300
+ : toPurl(memberPackage.name, memberPackage.version);
301
+ sbom.addDependency(rootPurl, memberPurl);
302
+ addTransitiveDeps(sbom, metadata, memberId, ignoredDeps, new Set(), memberPurl);
303
+ }
304
+ }
305
+ else {
306
+ // Component analysis: only [workspace.dependencies] from root Cargo.toml
307
+ let workspaceDeps = getWorkspaceDepsFromManifest(manifest);
308
+ for (let depName of workspaceDeps) {
309
+ if (isDepIgnored(depName, ignoredDeps)) {
310
+ continue;
311
+ }
312
+ let pkg = metadata.packages.find(p => p.name === depName);
313
+ if (!pkg) {
314
+ let altName = depName.replace(/-/g, '_');
315
+ pkg = metadata.packages.find(p => p.name === altName);
316
+ }
317
+ if (!pkg) {
318
+ continue;
319
+ }
320
+ let depPurl = pkg.source == null
321
+ ? toPathDepPurl(pkg.name, pkg.version)
322
+ : toPurl(pkg.name, pkg.version);
323
+ sbom.addDependency(rootPurl, depPurl);
324
+ }
325
+ }
326
+ return sbom.getAsJsonString(opts);
327
+ }
328
+ /**
329
+ * Recursively adds transitive dependencies to the SBOM.
330
+ * Path dependencies (source == null) are included with a
331
+ * {@code repository_url=local} qualifier so the backend can skip
332
+ * vulnerability checks while still showing them in the dependency tree.
333
+ * @param {Sbom} sbom - the SBOM to add dependencies to
334
+ * @param {object} metadata - parsed cargo metadata
335
+ * @param {string} packageId - the package ID to resolve dependencies for
336
+ * @param {Set<string>} ignoredDeps - set of ignored dependency names
337
+ * @param {Set<string>} visited - set of already-visited package IDs to prevent cycles
338
+ * @param {PackageURL} [startingPurl] - purl to use for the starting package,
339
+ * so callers can ensure it matches the purl already added to the SBOM
340
+ * @private
341
+ */
342
+ function addTransitiveDeps(sbom, metadata, packageId, ignoredDeps, visited, startingPurl) {
343
+ if (visited.has(packageId)) {
344
+ return;
345
+ }
346
+ visited.add(packageId);
347
+ let resolveNode = findResolveNode(metadata, packageId);
348
+ if (!resolveNode) {
349
+ return;
350
+ }
351
+ let sourcePackage = findPackageById(metadata, packageId);
352
+ if (!sourcePackage) {
353
+ return;
354
+ }
355
+ let sourcePurl = startingPurl || (sourcePackage.source == null
356
+ ? toPathDepPurl(sourcePackage.name, sourcePackage.version)
357
+ : toPurl(sourcePackage.name, sourcePackage.version));
358
+ let runtimeDeps = filterRuntimeDeps(resolveNode);
359
+ for (let depId of runtimeDeps) {
360
+ let depPackage = findPackageById(metadata, depId);
361
+ if (!depPackage) {
362
+ continue;
363
+ }
364
+ if (isDepIgnored(depPackage.name, ignoredDeps)) {
365
+ continue;
366
+ }
367
+ let depPurl = depPackage.source == null
368
+ ? toPathDepPurl(depPackage.name, depPackage.version)
369
+ : toPurl(depPackage.name, depPackage.version);
370
+ sbom.addDependency(sourcePurl, depPurl);
371
+ addTransitiveDeps(sbom, metadata, depId, ignoredDeps, visited);
372
+ }
373
+ }
374
+ /**
375
+ * Adds only direct (non-transitive) dependencies to the SBOM.
376
+ * Path dependencies are included with a {@code repository_url=local} qualifier.
377
+ * @param {Sbom} sbom - the SBOM to add dependencies to
378
+ * @param {object} metadata - parsed cargo metadata
379
+ * @param {string} packageId - the package ID to resolve dependencies for
380
+ * @param {PackageURL} parentPurl - the parent purl to attach dependencies to
381
+ * @param {Set<string>} ignoredDeps - set of ignored dependency names
382
+ * @private
383
+ */
384
+ function addDirectDeps(sbom, metadata, packageId, parentPurl, ignoredDeps) {
385
+ let resolveNode = findResolveNode(metadata, packageId);
386
+ if (!resolveNode) {
387
+ return;
388
+ }
389
+ let runtimeDeps = filterRuntimeDeps(resolveNode);
390
+ for (let depId of runtimeDeps) {
391
+ let depPackage = findPackageById(metadata, depId);
392
+ if (!depPackage) {
393
+ continue;
394
+ }
395
+ if (isDepIgnored(depPackage.name, ignoredDeps)) {
396
+ continue;
397
+ }
398
+ let depPurl = depPackage.source == null
399
+ ? toPathDepPurl(depPackage.name, depPackage.version)
400
+ : toPurl(depPackage.name, depPackage.version);
401
+ sbom.addDependency(parentPurl, depPurl);
402
+ }
403
+ }
404
+ /**
405
+ * Filters the deps of a resolve node to only include runtime (normal) dependencies.
406
+ * @param {object} resolveNode - a node from metadata.resolve.nodes
407
+ * @returns {string[]} array of package IDs for runtime dependencies
408
+ * @private
409
+ */
410
+ function filterRuntimeDeps(resolveNode) {
411
+ if (!resolveNode.deps) {
412
+ return [];
413
+ }
414
+ return resolveNode.deps
415
+ .filter(dep => {
416
+ if (!dep.dep_kinds || dep.dep_kinds.length === 0) {
417
+ return true;
418
+ }
419
+ return dep.dep_kinds.some(dk => dk.kind == null || dk.kind === 'normal');
420
+ })
421
+ .map(dep => dep.pkg);
422
+ }
423
+ /**
424
+ * Finds a package in the metadata by its ID.
425
+ * @param {object} metadata - parsed cargo metadata
426
+ * @param {string} packageId - the package ID to find
427
+ * @returns {object|undefined} the found package or undefined
428
+ * @private
429
+ */
430
+ function findPackageById(metadata, packageId) {
431
+ return metadata.packages.find(pkg => pkg.id === packageId);
432
+ }
433
+ /**
434
+ * Finds a resolve node by package ID.
435
+ * @param {object} metadata - parsed cargo metadata
436
+ * @param {string} packageId - the package ID to find
437
+ * @returns {object|undefined} the found resolve node or undefined
438
+ * @private
439
+ */
440
+ function findResolveNode(metadata, packageId) {
441
+ return metadata.resolve.nodes.find(node => node.id === packageId);
442
+ }
443
+ /**
444
+ * Parses the root Cargo.toml and returns the dependency names listed in the
445
+ * [workspace.dependencies] section. Returns an empty array when the section
446
+ * does not exist.
447
+ * @param {string} manifest - path to the root Cargo.toml
448
+ * @returns {string[]} list of dependency names
449
+ * @private
450
+ */
451
+ function getWorkspaceDepsFromManifest(manifest) {
452
+ let content = fs.readFileSync(manifest, 'utf-8');
453
+ let parsed = parseToml(content);
454
+ return Object.keys(parsed.workspace?.dependencies || {});
455
+ }
456
+ /**
457
+ * Extracts the workspace version from metadata if defined.
458
+ * @param {object} metadata - parsed cargo metadata
459
+ * @returns {string} the workspace version or a placeholder
460
+ * @private
461
+ */
462
+ function getWorkspaceVersion(metadata) {
463
+ let workspaceRoot = metadata.workspace_root;
464
+ let cargoTomlPath = path.join(workspaceRoot, 'Cargo.toml');
465
+ try {
466
+ let content = fs.readFileSync(cargoTomlPath, 'utf-8');
467
+ let parsed = parseToml(content);
468
+ return parsed.workspace?.package?.version || '0.0.0';
469
+ }
470
+ catch (_) {
471
+ return '0.0.0';
472
+ }
473
+ }
474
+ /**
475
+ * Parses Cargo.toml for dependencies annotated with an ignore marker.
476
+ * Supports both `exhortignore` and `trustify-da-ignore` markers.
477
+ * Supports inline deps, table-based deps, and workspace-level dependency sections.
478
+ * Uses cargo metadata to discover workspace members (which already handles glob
479
+ * expansion and exclude filtering) instead of parsing workspace member paths ourselves.
480
+ * @param {string} manifest - path to Cargo.toml
481
+ * @param {object} metadata - parsed cargo metadata
482
+ * @returns {Set<string>} set of dependency names to ignore
483
+ * @private
484
+ */
485
+ function getIgnoredDeps(manifest, metadata) {
486
+ let ignored = new Set();
487
+ scanManifestForIgnored(manifest, ignored);
488
+ // Scan workspace member manifests using metadata
489
+ let manifestDir = path.dirname(manifest);
490
+ let workspaceRoot = metadata.workspace_root;
491
+ let workspaceMembers = metadata.workspace_members || [];
492
+ for (let memberId of workspaceMembers) {
493
+ let memberRelDir = getMemberRelativeDir(memberId, workspaceRoot);
494
+ if (memberRelDir == null) {
495
+ continue;
496
+ }
497
+ let memberManifest = path.join(manifestDir, memberRelDir, 'Cargo.toml');
498
+ if (path.resolve(memberManifest) === path.resolve(manifest)) {
499
+ continue;
500
+ }
501
+ if (fs.existsSync(memberManifest)) {
502
+ scanManifestForIgnored(memberManifest, ignored);
503
+ }
504
+ }
505
+ return ignored;
506
+ }
507
+ /**
508
+ * Extracts a workspace member's directory path relative to the workspace root
509
+ * from its cargo package ID. Workspace member IDs use the `path+file://` scheme,
510
+ * e.g. `path+file:///workspace/root/crates/member#0.1.0`.
511
+ * @param {string} packageId - the cargo package ID
512
+ * @param {string} workspaceRoot - the workspace root path from cargo metadata
513
+ * @returns {string|null} relative directory path, or null if it cannot be determined
514
+ * @private
515
+ */
516
+ function getMemberRelativeDir(packageId, workspaceRoot) {
517
+ let pathMatch = packageId.match(/path\+file:\/\/(.+?)#/);
518
+ if (!pathMatch) {
519
+ return null;
520
+ }
521
+ let pkgPath = pathMatch[1];
522
+ if (!pkgPath.startsWith(workspaceRoot)) {
523
+ return null;
524
+ }
525
+ let relPath = pkgPath.substring(workspaceRoot.length);
526
+ if (relPath.startsWith('/')) {
527
+ relPath = relPath.substring(1);
528
+ }
529
+ return relPath || null;
530
+ }
531
+ /**
532
+ * Scans a single Cargo.toml for ignored dependencies and adds them to the set.
533
+ * @param {string} manifest - path to Cargo.toml
534
+ * @param {Set<string>} ignored - the set to add ignored dependency names to
535
+ * @private
536
+ */
537
+ function scanManifestForIgnored(manifest, ignored) {
538
+ let content = fs.readFileSync(manifest, 'utf-8');
539
+ let lines = content.split(/\r?\n/);
540
+ let currentSection = '';
541
+ let currentDepName = null;
542
+ for (let line of lines) {
543
+ let trimmed = line.trim();
544
+ let sectionMatch = trimmed.match(/^\[([^\]]+)\]\s*(?:#.*)?$/);
545
+ if (sectionMatch) {
546
+ currentSection = sectionMatch[1];
547
+ currentDepName = null;
548
+ let tableDep = currentSection.match(/^(?:dependencies|dev-dependencies|build-dependencies|workspace\.dependencies)\.(.+)$/);
549
+ if (tableDep) {
550
+ currentDepName = tableDep[1];
551
+ // If the section header line itself carries an ignore marker,
552
+ // immediately add the dep
553
+ if (hasIgnoreMarker(line)) {
554
+ ignored.add(currentDepName);
555
+ }
556
+ }
557
+ continue;
558
+ }
559
+ let isIgnored = hasIgnoreMarker(line);
560
+ if (isIgnored && isInDependencySection(currentSection)) {
561
+ let inlineMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=/);
562
+ if (inlineMatch) {
563
+ ignored.add(inlineMatch[1]);
564
+ continue;
565
+ }
566
+ }
567
+ if (isIgnored && currentDepName) {
568
+ ignored.add(currentDepName);
569
+ continue;
570
+ }
571
+ }
572
+ }
573
+ /**
574
+ * Checks if a section name is a dependency section.
575
+ * @param {string} section - the TOML section name
576
+ * @returns {boolean}
577
+ * @private
578
+ */
579
+ function isInDependencySection(section) {
580
+ return section === 'dependencies' ||
581
+ section === 'dev-dependencies' ||
582
+ section === 'build-dependencies' ||
583
+ section === 'workspace.dependencies';
584
+ }
585
+ /**
586
+ * Creates a PackageURL for a Cargo/crates.io package.
587
+ * @param {string} name - the crate name
588
+ * @param {string} version - the crate version
589
+ * @returns {PackageURL} the package URL
590
+ * @private
591
+ */
592
+ function toPurl(name, version) {
593
+ return new PackageURL(ecosystem, undefined, name, version, undefined, undefined);
594
+ }
595
+ /**
596
+ * Creates a PackageURL for a local path dependency, marked with a
597
+ * {@code repository_url=local} qualifier so the backend can distinguish
598
+ * it from registry packages and skip vulnerability checks.
599
+ * @param {string} name - the crate name
600
+ * @param {string} version - the crate version
601
+ * @returns {PackageURL} the package URL with local qualifier
602
+ * @private
603
+ */
604
+ function toPathDepPurl(name, version) {
605
+ return new PackageURL(ecosystem, undefined, name, version, { repository_url: 'local' }, undefined);
606
+ }
@@ -2,9 +2,10 @@ export default class Sbom {
2
2
  sbomModel: CycloneDxSbom;
3
3
  /**
4
4
  * @param {PackageURL} root - add main/root component for sbom
5
+ * @param {string|Array} [licenses] - optional license(s) for the root component
5
6
  * @return Sbom
6
7
  */
7
- addRoot(root: PackageURL): CycloneDxSbom;
8
+ addRoot(root: PackageURL, licenses?: string | any[]): CycloneDxSbom;
8
9
  /**
9
10
  * @return {{{"bom-ref": string, name, purl: string, type, version}}} root component of sbom.
10
11
  */
@@ -43,6 +44,7 @@ export default class Sbom {
43
44
  type: any;
44
45
  version: any;
45
46
  scope: any;
47
+ licenses?: any;
46
48
  };
47
49
  /** This method gets a component object, and a string name, and checks if the name is a substring of the component' purl.
48
50
  * @param {} component to search in its dependencies
package/dist/src/sbom.js CHANGED
@@ -12,10 +12,11 @@ export default class Sbom {
12
12
  }
13
13
  /**
14
14
  * @param {PackageURL} root - add main/root component for sbom
15
+ * @param {string|Array} [licenses] - optional license(s) for the root component
15
16
  * @return Sbom
16
17
  */
17
- addRoot(root) {
18
- return this.sbomModel.addRoot(root);
18
+ addRoot(root, licenses) {
19
+ return this.sbomModel.addRoot(root, licenses);
19
20
  }
20
21
  /**
21
22
  * @return {{{"bom-ref": string, name, purl: string, type, version}}} root component of sbom.