@trustify-da/trustify-da-javascript-client 0.3.0-ea.8adb67b → 0.3.0-ea.8eab29b

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