@trustify-da/trustify-da-javascript-client 0.3.0-ea.e12bc82 → 0.3.0-ea.e258fba

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +367 -13
  2. package/dist/package.json +48 -28
  3. package/dist/src/analysis.d.ts +28 -5
  4. package/dist/src/analysis.js +99 -89
  5. package/dist/src/batch_opts.d.ts +24 -0
  6. package/dist/src/batch_opts.js +35 -0
  7. package/dist/src/cli.js +346 -9
  8. package/dist/src/cyclone_dx_sbom.d.ts +30 -3
  9. package/dist/src/cyclone_dx_sbom.js +95 -9
  10. package/dist/src/index.d.ts +202 -12
  11. package/dist/src/index.js +365 -30
  12. package/dist/src/license/index.d.ts +28 -0
  13. package/dist/src/license/index.js +103 -0
  14. package/dist/src/license/license_utils.d.ts +40 -0
  15. package/dist/src/license/license_utils.js +137 -0
  16. package/dist/src/license/licenses_api.d.ts +34 -0
  17. package/dist/src/license/licenses_api.js +98 -0
  18. package/dist/src/license/project_license.d.ts +20 -0
  19. package/dist/src/license/project_license.js +62 -0
  20. package/dist/src/oci_image/images.d.ts +4 -5
  21. package/dist/src/oci_image/images.js +15 -1
  22. package/dist/src/oci_image/utils.d.ts +7 -7
  23. package/dist/src/oci_image/utils.js +11 -2
  24. package/dist/src/package_version.d.ts +8 -0
  25. package/dist/src/package_version.js +31 -0
  26. package/dist/src/provider.d.ts +18 -5
  27. package/dist/src/provider.js +33 -5
  28. package/dist/src/providers/base_java.d.ts +68 -15
  29. package/dist/src/providers/base_java.js +82 -61
  30. package/dist/src/providers/base_javascript.d.ts +99 -12
  31. package/dist/src/providers/base_javascript.js +243 -27
  32. package/dist/src/providers/base_pyproject.d.ts +158 -0
  33. package/dist/src/providers/base_pyproject.js +322 -0
  34. package/dist/src/providers/containerfile_parser.d.ts +5 -0
  35. package/dist/src/providers/containerfile_parser.js +20 -0
  36. package/dist/src/providers/golang_gomodules.d.ts +30 -13
  37. package/dist/src/providers/golang_gomodules.js +219 -122
  38. package/dist/src/providers/gomod_parser.d.ts +4 -0
  39. package/dist/src/providers/gomod_parser.js +16 -0
  40. package/dist/src/providers/java_gradle.d.ts +76 -3
  41. package/dist/src/providers/java_gradle.js +323 -21
  42. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  43. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  44. package/dist/src/providers/java_maven.d.ts +45 -10
  45. package/dist/src/providers/java_maven.js +300 -14
  46. package/dist/src/providers/javascript_bun.d.ts +22 -0
  47. package/dist/src/providers/javascript_bun.js +141 -0
  48. package/dist/src/providers/javascript_npm.d.ts +19 -0
  49. package/dist/src/providers/javascript_npm.js +61 -1
  50. package/dist/src/providers/javascript_pnpm.d.ts +14 -1
  51. package/dist/src/providers/javascript_pnpm.js +55 -5
  52. package/dist/src/providers/javascript_yarn.d.ts +12 -0
  53. package/dist/src/providers/javascript_yarn.js +75 -2
  54. package/dist/src/providers/manifest.d.ts +2 -0
  55. package/dist/src/providers/manifest.js +28 -7
  56. package/dist/src/providers/marker_evaluator.d.ts +14 -0
  57. package/dist/src/providers/marker_evaluator.js +191 -0
  58. package/dist/src/providers/oci_dockerfile.d.ts +51 -0
  59. package/dist/src/providers/oci_dockerfile.js +177 -0
  60. package/dist/src/providers/processors/yarn_berry_processor.d.ts +6 -2
  61. package/dist/src/providers/processors/yarn_berry_processor.js +91 -7
  62. package/dist/src/providers/processors/yarn_classic_processor.d.ts +6 -2
  63. package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
  64. package/dist/src/providers/python_controller.d.ts +10 -3
  65. package/dist/src/providers/python_controller.js +122 -63
  66. package/dist/src/providers/python_pip.d.ts +16 -4
  67. package/dist/src/providers/python_pip.js +51 -58
  68. package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
  69. package/dist/src/providers/python_pip_pyproject.js +146 -0
  70. package/dist/src/providers/python_poetry.d.ts +98 -0
  71. package/dist/src/providers/python_poetry.js +286 -0
  72. package/dist/src/providers/python_uv.d.ts +46 -0
  73. package/dist/src/providers/python_uv.js +269 -0
  74. package/dist/src/providers/requirements_parser.d.ts +6 -0
  75. package/dist/src/providers/requirements_parser.js +24 -0
  76. package/dist/src/providers/rust_cargo.d.ts +56 -0
  77. package/dist/src/providers/rust_cargo.js +682 -0
  78. package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
  79. package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
  80. package/dist/src/providers/tree-sitter-requirements.wasm +0 -0
  81. package/dist/src/remediate.d.ts +21 -0
  82. package/dist/src/remediate.js +149 -0
  83. package/dist/src/remediation.d.ts +57 -0
  84. package/dist/src/remediation.js +436 -0
  85. package/dist/src/remediation_report.d.ts +40 -0
  86. package/dist/src/remediation_report.js +159 -0
  87. package/dist/src/sbom.d.ts +28 -2
  88. package/dist/src/sbom.js +26 -4
  89. package/dist/src/tools.d.ts +48 -6
  90. package/dist/src/tools.js +138 -2
  91. package/dist/src/updaters/maven_updater.d.ts +26 -0
  92. package/dist/src/updaters/maven_updater.js +322 -0
  93. package/dist/src/updaters/toml_updater.d.ts +33 -0
  94. package/dist/src/updaters/toml_updater.js +244 -0
  95. package/dist/src/workspace.d.ts +70 -0
  96. package/dist/src/workspace.js +256 -0
  97. package/package.json +49 -29
package/dist/src/cli.js CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs';
2
3
  import * as path from "path";
3
4
  import yargs from 'yargs';
4
5
  import { hideBin } from 'yargs/helpers';
5
- import client from './index.js';
6
+ import { getProjectLicense, getLicenseDetails } from './license/index.js';
7
+ import { runRemediation } from './remediate.js';
8
+ import client, { selectTrustifyDABackend, generateSbom } from './index.js';
6
9
  // command for component analysis take manifest type and content
7
10
  const component = {
8
11
  command: 'component </path/to/manifest>',
@@ -11,10 +14,32 @@ const component = {
11
14
  desc: 'manifest path for analyzing',
12
15
  type: 'string',
13
16
  normalize: true,
17
+ }).options({
18
+ workspaceDir: {
19
+ alias: 'w',
20
+ desc: 'Workspace root directory (for monorepos; lock file is expected here)',
21
+ type: 'string',
22
+ normalize: true,
23
+ },
24
+ providers: {
25
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
26
+ type: 'string',
27
+ },
28
+ sources: {
29
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
30
+ type: 'string',
31
+ }
14
32
  }),
15
33
  handler: async (args) => {
16
34
  let manifestName = args['/path/to/manifest'];
17
- let res = await client.componentAnalysis(manifestName);
35
+ const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
36
+ if (args.providers) {
37
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
38
+ }
39
+ if (args.sources) {
40
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
41
+ }
42
+ let res = await client.componentAnalysis(manifestName, opts);
18
43
  console.log(JSON.stringify(res, null, 2));
19
44
  }
20
45
  };
@@ -22,9 +47,8 @@ const validateToken = {
22
47
  command: 'validate-token <token-provider> [--token-value thevalue]',
23
48
  desc: 'Validates input token if authentic and authorized',
24
49
  builder: yargs => yargs.positional('token-provider', {
25
- desc: 'the token provider',
26
- type: 'string',
27
- choices: ['snyk', 'oss-index'],
50
+ desc: 'the token provider name',
51
+ type: 'string'
28
52
  }).options({
29
53
  tokenValue: {
30
54
  alias: 'value',
@@ -37,7 +61,7 @@ const validateToken = {
37
61
  let opts = {};
38
62
  if (args['tokenValue'] !== undefined && args['tokenValue'].trim() !== "") {
39
63
  let tokenValue = args['tokenValue'].trim();
40
- opts[`TRUSTIFY_DA_${tokenProvider}_TOKEN`] = tokenValue;
64
+ opts[`TRUSTIFY_DA_PROVIDER_${tokenProvider}_TOKEN`] = tokenValue;
41
65
  }
42
66
  let res = await client.validateToken(opts);
43
67
  console.log(res);
@@ -63,6 +87,14 @@ const image = {
63
87
  desc: 'For JSON report, get only the \'summary\'',
64
88
  type: 'boolean',
65
89
  conflicts: 'html'
90
+ },
91
+ providers: {
92
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
93
+ type: 'string',
94
+ },
95
+ sources: {
96
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
97
+ type: 'string',
66
98
  }
67
99
  }),
68
100
  handler: async (args) => {
@@ -72,7 +104,14 @@ const image = {
72
104
  }
73
105
  let html = args['html'];
74
106
  let summary = args['summary'];
75
- let res = await client.imageAnalysis(imageRefs, html);
107
+ const opts = {};
108
+ if (args.providers) {
109
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
110
+ }
111
+ if (args.sources) {
112
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
113
+ }
114
+ let res = await client.imageAnalysis(imageRefs, html, opts);
76
115
  if (summary && !html) {
77
116
  let summaries = {};
78
117
  for (let [imageRef, report] of Object.entries(res)) {
@@ -117,15 +156,36 @@ const stack = {
117
156
  desc: 'For JSON report, get only the \'summary\'',
118
157
  type: 'boolean',
119
158
  conflicts: 'html'
159
+ },
160
+ workspaceDir: {
161
+ alias: 'w',
162
+ desc: 'Workspace root directory (for monorepos; lock file is expected here)',
163
+ type: 'string',
164
+ normalize: true,
165
+ },
166
+ providers: {
167
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
168
+ type: 'string',
169
+ },
170
+ sources: {
171
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
172
+ type: 'string',
120
173
  }
121
174
  }),
122
175
  handler: async (args) => {
123
176
  let manifest = args['/path/to/manifest'];
124
177
  let html = args['html'];
125
178
  let summary = args['summary'];
179
+ const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
180
+ if (args.providers) {
181
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
182
+ }
183
+ if (args.sources) {
184
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
185
+ }
126
186
  let theProvidersSummary = new Map();
127
187
  let theProvidersObject = {};
128
- let res = await client.stackAnalysis(manifest, html);
188
+ let res = await client.stackAnalysis(manifest, html, opts);
129
189
  if (summary) {
130
190
  for (let provider in res.providers) {
131
191
  if (res.providers[provider].sources !== undefined) {
@@ -143,13 +203,290 @@ const stack = {
143
203
  console.log(html ? res : JSON.stringify(!html && summary ? theProvidersObject : res, null, 2));
144
204
  }
145
205
  };
206
+ // command for batch stack analysis (workspace)
207
+ const stackBatch = {
208
+ command: 'stack-batch </path/to/workspace-root> [--html|--summary] [--concurrency <n>] [--ignore <pattern>...] [--metadata] [--fail-fast]',
209
+ desc: 'produce stack report for all packages/crates in a workspace (Cargo or JS/TS)',
210
+ builder: yargs => yargs.positional('/path/to/workspace-root', {
211
+ desc: 'workspace root directory (containing Cargo.toml+Cargo.lock or package.json+lock file)',
212
+ type: 'string',
213
+ normalize: true,
214
+ }).options({
215
+ html: {
216
+ alias: 'r',
217
+ desc: 'Get the report as HTML instead of JSON',
218
+ type: 'boolean',
219
+ conflicts: 'summary'
220
+ },
221
+ summary: {
222
+ alias: 's',
223
+ desc: 'For JSON report, get only the \'summary\' per package',
224
+ type: 'boolean',
225
+ conflicts: 'html'
226
+ },
227
+ concurrency: {
228
+ alias: 'c',
229
+ desc: 'Max parallel SBOM generations (default: 10, env: TRUSTIFY_DA_BATCH_CONCURRENCY)',
230
+ type: 'number',
231
+ },
232
+ ignore: {
233
+ alias: 'i',
234
+ desc: 'Extra glob patterns excluded from workspace discovery (merged with defaults). Repeat flag per pattern. Env: TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE (comma-separated)',
235
+ type: 'string',
236
+ array: true,
237
+ },
238
+ metadata: {
239
+ alias: 'm',
240
+ desc: 'Return { analysis, metadata } with per-manifest errors (env: TRUSTIFY_DA_BATCH_METADATA=true)',
241
+ type: 'boolean',
242
+ default: false,
243
+ },
244
+ failFast: {
245
+ desc: 'Stop on first invalid package.json or SBOM error (env: TRUSTIFY_DA_CONTINUE_ON_ERROR=false)',
246
+ type: 'boolean',
247
+ default: false,
248
+ },
249
+ providers: {
250
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
251
+ type: 'string',
252
+ },
253
+ sources: {
254
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
255
+ type: 'string',
256
+ }
257
+ }),
258
+ handler: async (args) => {
259
+ const workspaceRoot = args['/path/to/workspace-root'];
260
+ const html = args['html'];
261
+ const summary = args['summary'];
262
+ const opts = {};
263
+ if (args.concurrency != null) {
264
+ opts.batchConcurrency = args.concurrency;
265
+ }
266
+ const extraIgnores = Array.isArray(args.ignore) ? args.ignore.filter(p => p != null && String(p).trim()) : [];
267
+ if (extraIgnores.length > 0) {
268
+ opts.workspaceDiscoveryIgnore = extraIgnores;
269
+ }
270
+ if (args.metadata) {
271
+ opts.batchMetadata = true;
272
+ }
273
+ if (args.failFast) {
274
+ opts.continueOnError = false;
275
+ }
276
+ if (args.providers) {
277
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
278
+ }
279
+ if (args.sources) {
280
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
281
+ }
282
+ let res = await client.stackAnalysisBatch(workspaceRoot, html, opts);
283
+ const batchAnalysis = res && typeof res === 'object' && res != null && 'analysis' in res ? res.analysis : res;
284
+ if (summary && !html && typeof batchAnalysis === 'object') {
285
+ const summaries = {};
286
+ for (const [purl, report] of Object.entries(batchAnalysis)) {
287
+ if (report?.providers) {
288
+ for (const provider of Object.keys(report.providers)) {
289
+ const sources = report.providers[provider]?.sources;
290
+ if (sources) {
291
+ for (const [source, data] of Object.entries(sources)) {
292
+ if (data?.summary) {
293
+ if (!summaries[purl]) {
294
+ summaries[purl] = {};
295
+ }
296
+ if (!summaries[purl][provider]) {
297
+ summaries[purl][provider] = {};
298
+ }
299
+ summaries[purl][provider][source] = data.summary;
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+ if (res && typeof res === 'object' && res != null && 'metadata' in res) {
307
+ res = { analysis: summaries, metadata: res.metadata };
308
+ }
309
+ else {
310
+ res = summaries;
311
+ }
312
+ }
313
+ if (html) {
314
+ const htmlContent = res && typeof res === 'object' && 'analysis' in res ? res.analysis : res;
315
+ console.log(htmlContent);
316
+ }
317
+ else {
318
+ console.log(JSON.stringify(res, null, 2));
319
+ }
320
+ }
321
+ };
322
+ // command for license checking
323
+ const license = {
324
+ command: 'license </path/to/manifest>',
325
+ desc: 'Display project license information from manifest and LICENSE file in JSON format',
326
+ builder: yargs => yargs.positional('/path/to/manifest', {
327
+ desc: 'manifest path for license analysis',
328
+ type: 'string',
329
+ normalize: true,
330
+ }),
331
+ handler: async (args) => {
332
+ let manifestPath = args['/path/to/manifest'];
333
+ const opts = {}; // CLI options can be extended in the future
334
+ try {
335
+ selectTrustifyDABackend(opts);
336
+ }
337
+ catch (err) {
338
+ console.error(JSON.stringify({ error: err.message }, null, 2));
339
+ process.exit(1);
340
+ }
341
+ let localResult;
342
+ try {
343
+ localResult = getProjectLicense(manifestPath);
344
+ }
345
+ catch (err) {
346
+ console.error(JSON.stringify({ error: `Failed to read manifest: ${err.message}` }, null, 2));
347
+ process.exit(1);
348
+ }
349
+ const errors = [];
350
+ // Build LicenseInfo objects
351
+ const buildLicenseInfo = async (spdxId) => {
352
+ if (!spdxId) {
353
+ return null;
354
+ }
355
+ const licenseInfo = { spdxId };
356
+ try {
357
+ const details = await getLicenseDetails(spdxId, opts);
358
+ if (details) {
359
+ // Check if backend recognized the license as valid
360
+ if (details.category === 'UNKNOWN') {
361
+ errors.push(`"${spdxId}" is not a valid SPDX license identifier. Please use a valid SPDX expression (e.g., "Apache-2.0", "MIT"). See https://spdx.org/licenses/`);
362
+ }
363
+ else {
364
+ Object.assign(licenseInfo, details);
365
+ }
366
+ }
367
+ else {
368
+ errors.push(`No license details found for ${spdxId}`);
369
+ }
370
+ }
371
+ catch (err) {
372
+ errors.push(`Failed to fetch details for ${spdxId}: ${err.message}`);
373
+ }
374
+ return licenseInfo;
375
+ };
376
+ const output = {
377
+ manifestLicense: await buildLicenseInfo(localResult.fromManifest),
378
+ fileLicense: await buildLicenseInfo(localResult.fromFile),
379
+ mismatch: localResult.mismatch
380
+ };
381
+ if (errors.length > 0) {
382
+ output.errors = errors;
383
+ }
384
+ console.log(JSON.stringify(output, null, 2));
385
+ }
386
+ };
387
+ const sbom = {
388
+ command: 'sbom </path/to/manifest> [--output]',
389
+ desc: 'generate a CycloneDX SBOM from a manifest file',
390
+ builder: yargs => yargs.positional('/path/to/manifest', {
391
+ desc: 'manifest path for SBOM generation',
392
+ type: 'string',
393
+ normalize: true,
394
+ }).options({
395
+ output: {
396
+ alias: 'o',
397
+ desc: 'Write SBOM JSON to a file instead of stdout',
398
+ type: 'string',
399
+ normalize: true,
400
+ },
401
+ workspaceDir: {
402
+ alias: 'w',
403
+ desc: 'Workspace root directory (for monorepos; lock file is expected here)',
404
+ type: 'string',
405
+ normalize: true,
406
+ }
407
+ }),
408
+ handler: async (args) => {
409
+ let manifest = args['/path/to/manifest'];
410
+ const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
411
+ let result;
412
+ try {
413
+ result = await generateSbom(manifest, opts);
414
+ }
415
+ catch (err) {
416
+ console.error(JSON.stringify({ error: `Failed to generate SBOM: ${err.message}` }, null, 2));
417
+ process.exit(1);
418
+ }
419
+ const json = JSON.stringify(result, null, 2);
420
+ if (args.output) {
421
+ try {
422
+ fs.writeFileSync(args.output, json);
423
+ }
424
+ catch (err) {
425
+ console.error(JSON.stringify({ error: `Failed to write output file: ${err.message}` }, null, 2));
426
+ process.exit(1);
427
+ }
428
+ }
429
+ else {
430
+ console.log(json);
431
+ }
432
+ }
433
+ };
434
+ const remediate = {
435
+ command: 'remediate <path>',
436
+ desc: 'Scan and apply vulnerability remediations to manifest files',
437
+ builder: yargs => yargs.positional('path', {
438
+ desc: 'Path to manifest file or directory',
439
+ type: 'string',
440
+ normalize: true,
441
+ }).options({
442
+ 'dry-run': {
443
+ alias: 'd',
444
+ type: 'boolean',
445
+ desc: 'Preview changes without modifying files',
446
+ },
447
+ providers: {
448
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
449
+ type: 'string',
450
+ },
451
+ sources: {
452
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
453
+ type: 'string',
454
+ },
455
+ 'group-by': {
456
+ type: 'string',
457
+ choices: ['dependency', 'bundle'],
458
+ default: 'dependency',
459
+ desc: 'Report grouping strategy',
460
+ },
461
+ }),
462
+ handler: async (args) => {
463
+ try {
464
+ const result = await runRemediation(args.path, {
465
+ dryRun: args['dry-run'],
466
+ providers: args.providers,
467
+ sources: args.sources,
468
+ groupBy: args['group-by'],
469
+ });
470
+ console.log(result.output);
471
+ process.exit(result.exitCode);
472
+ }
473
+ catch (err) {
474
+ console.error(err.message);
475
+ process.exit(1);
476
+ }
477
+ }
478
+ };
146
479
  // parse and invoke the command
147
480
  yargs(hideBin(process.argv))
148
- .usage(`Usage: ${process.argv[0].includes("node") ? path.parse(process.argv[1]).base : path.parse(process.argv[0]).base} {component|stack|image|validate-token}`)
481
+ .usage(`Usage: ${process.argv[0].includes("node") ? path.parse(process.argv[1]).base : path.parse(process.argv[0]).base} {component|stack|stack-batch|image|validate-token|license|sbom|remediate}`)
149
482
  .command(stack)
483
+ .command(stackBatch)
150
484
  .command(component)
151
485
  .command(image)
152
486
  .command(validateToken)
487
+ .command(license)
488
+ .command(sbom)
489
+ .command(remediate)
153
490
  .scriptName('')
154
491
  .version(false)
155
492
  .demandCommand(1)
@@ -1,4 +1,3 @@
1
- /// <reference types="packageurl-js/src/package-url" />
2
1
  export default class CycloneDxSbom {
3
2
  sbomObject: any;
4
3
  rootComponent: any;
@@ -7,9 +6,10 @@ export default class CycloneDxSbom {
7
6
  sourceManifestForAuditTrail: any;
8
7
  /**
9
8
  * @param {PackageURL} root - add main/root component for sbom
9
+ * @param {string|Array} [licenses] - optional license(s) for the root component
10
10
  * @return {CycloneDxSbom} the CycloneDxSbom Sbom Object
11
11
  */
12
- addRoot(root: PackageURL): CycloneDxSbom;
12
+ addRoot(root: PackageURL, licenses?: string | any[]): CycloneDxSbom;
13
13
  /**
14
14
  * @return {{{"bom-ref": string, name, purl: string, type, version}}} root component of sbom.
15
15
  */
@@ -18,9 +18,27 @@ export default class CycloneDxSbom {
18
18
  * Adds a dependency relationship between two components in the SBOM
19
19
  * @param {PackageURL} sourceRef - The source component (parent)
20
20
  * @param {PackageURL} targetRef - The target component (dependency)
21
+ * @param {string} [scope] - Scope of the dependency
22
+ * @param {Array<{alg: string, content: string}>} [targetHashes] - Optional hashes for the target component
21
23
  * @return {CycloneDxSbom} The updated SBOM
22
24
  */
23
- addDependency(sourceRef: PackageURL, targetRef: PackageURL, scope: any): CycloneDxSbom;
25
+ addDependency(sourceRef: PackageURL, targetRef: PackageURL, scope?: string, targetHashes?: Array<{
26
+ alg: string;
27
+ content: string;
28
+ }>): CycloneDxSbom;
29
+ /**
30
+ * Attach hashes to already-added components by matching their PURL. This is a
31
+ * post-processing step so ecosystem-specific hash sources (e.g. Maven reading
32
+ * the local .m2 cache) can enrich the SBOM without threading their concern
33
+ * through the shared dependency-tree parser. Components without a matching
34
+ * entry, or whose hashes are already set, are left untouched.
35
+ * @param {Map<string, Array<{alg: string, content: string}>>} hashMap - PURL→hashes map
36
+ * @return {CycloneDxSbom} the updated SBOM
37
+ */
38
+ attachHashes(hashMap: Map<string, Array<{
39
+ alg: string;
40
+ content: string;
41
+ }>>): CycloneDxSbom;
24
42
  /** @param {{}} opts - various options, settings and configuration of application.
25
43
  * @return String CycloneDx Sbom json object in a string format
26
44
  */
@@ -49,6 +67,8 @@ export default class CycloneDxSbom {
49
67
  type: any;
50
68
  version: any;
51
69
  scope: any;
70
+ licenses?: any;
71
+ hashes?: any;
52
72
  };
53
73
  /**
54
74
  * This method gets an array of dependencies to be ignored, and remove all of them from CycloneDx Sbom
@@ -69,6 +89,13 @@ export default class CycloneDxSbom {
69
89
  * @return {boolean}
70
90
  */
71
91
  checkIfPackageInsideDependsOnList(component: any, name: string): boolean;
92
+ /**
93
+ * Checks if any entry in the dependsOn list of sourceRef starts with the given purl prefix.
94
+ * @param {PackageURL} sourceRef - The source component
95
+ * @param {string} purlPrefix - The purl prefix to match (e.g. "pkg:npm/minimist@")
96
+ * @return {boolean}
97
+ */
98
+ checkDependsOnByPurlPrefix(sourceRef: PackageURL, purlPrefix: string): boolean;
72
99
  /** Removes the root component from the sbom
73
100
  */
74
101
  removeRootComponent(): void;
@@ -1,14 +1,23 @@
1
1
  import { EOL } from "os";
2
2
  import { PackageURL } from "packageurl-js";
3
+ import { getPackageVersion } from "./package_version.js";
4
+ /**
5
+ * This client's version, read once at module load, used to populate
6
+ * metadata.tools.components[].version in the generated SBOM.
7
+ * @type {string|undefined}
8
+ */
9
+ const PACKAGE_VERSION = getPackageVersion();
3
10
  /**
4
11
  *
5
12
  * @param component {PackageURL}
6
13
  * @param type type of package - application or library
7
14
  * @param scope scope of the component - runtime or compile
8
- * @return {{"bom-ref": string, name, purl: string, type, version, scope}}
15
+ * @param licenses optional license string or array of licenses for the component
16
+ * @param hashes optional array of hash objects for the component, e.g. [{alg: "SHA-256", content: "..."}]
17
+ * @return {{"bom-ref": string, name, purl: string, type, version, scope, licenses?, hashes?}}
9
18
  * @private
10
19
  */
11
- function getComponent(component, type, scope) {
20
+ function getComponent(component, type, scope, licenses, hashes) {
12
21
  let componentObject;
13
22
  if (component instanceof PackageURL) {
14
23
  if (component.namespace) {
@@ -36,6 +45,28 @@ function getComponent(component, type, scope) {
36
45
  else {
37
46
  componentObject = component;
38
47
  }
48
+ // Add licenses if provided (CycloneDX format). Callers must provide valid SPDX identifiers.
49
+ if (licenses) {
50
+ const licenseArray = Array.isArray(licenses) ? licenses : [licenses];
51
+ const validLicenses = licenseArray
52
+ .map(lic => {
53
+ if (typeof lic === 'string') {
54
+ return { license: { id: lic } };
55
+ }
56
+ if (typeof lic === 'object' && lic !== null && ('license' in lic || 'expression' in lic)) {
57
+ return lic;
58
+ }
59
+ return null;
60
+ })
61
+ .filter(Boolean);
62
+ if (validLicenses.length > 0) {
63
+ componentObject.licenses = validLicenses;
64
+ }
65
+ }
66
+ // Add hashes if provided (CycloneDX 1.4 format).
67
+ if (hashes && hashes.length > 0) {
68
+ componentObject.hashes = hashes;
69
+ }
39
70
  return componentObject;
40
71
  }
41
72
  function createDependency(dependency) {
@@ -56,11 +87,12 @@ export default class CycloneDxSbom {
56
87
  }
57
88
  /**
58
89
  * @param {PackageURL} root - add main/root component for sbom
90
+ * @param {string|Array} [licenses] - optional license(s) for the root component
59
91
  * @return {CycloneDxSbom} the CycloneDxSbom Sbom Object
60
92
  */
61
- addRoot(root) {
93
+ addRoot(root, licenses) {
62
94
  this.rootComponent =
63
- getComponent(root, "application");
95
+ getComponent(root, "application", undefined, licenses);
64
96
  this.components.push(this.rootComponent);
65
97
  return this;
66
98
  }
@@ -74,16 +106,24 @@ export default class CycloneDxSbom {
74
106
  * Adds a dependency relationship between two components in the SBOM
75
107
  * @param {PackageURL} sourceRef - The source component (parent)
76
108
  * @param {PackageURL} targetRef - The target component (dependency)
109
+ * @param {string} [scope] - Scope of the dependency
110
+ * @param {Array<{alg: string, content: string}>} [targetHashes] - Optional hashes for the target component
77
111
  * @return {CycloneDxSbom} The updated SBOM
78
112
  */
79
- addDependency(sourceRef, targetRef, scope) {
113
+ addDependency(sourceRef, targetRef, scope, targetHashes) {
80
114
  const sourcePurl = sourceRef.toString();
81
115
  const targetPurl = targetRef.toString();
82
116
  // Ensure both components exist in the components list
83
117
  [sourceRef, targetRef].forEach((ref, index) => {
84
118
  const purl = index === 0 ? sourcePurl : targetPurl;
85
- if (this.getComponentIndex(purl) < 0) {
86
- this.components.push(getComponent(ref, "library", scope));
119
+ const existingIndex = this.getComponentIndex(purl);
120
+ if (existingIndex < 0) {
121
+ const hashes = index === 1 ? targetHashes : undefined;
122
+ this.components.push(getComponent(ref, "library", scope, undefined, hashes));
123
+ }
124
+ else if (index === 1 && targetHashes && targetHashes.length > 0 && !this.components[existingIndex].hashes) {
125
+ // Update hashes if the component was first seen without them (e.g. as a source)
126
+ this.components[existingIndex].hashes = targetHashes;
87
127
  }
88
128
  });
89
129
  // Ensure source dependency exists
@@ -102,22 +142,54 @@ export default class CycloneDxSbom {
102
142
  }
103
143
  return this;
104
144
  }
145
+ /**
146
+ * Attach hashes to already-added components by matching their PURL. This is a
147
+ * post-processing step so ecosystem-specific hash sources (e.g. Maven reading
148
+ * the local .m2 cache) can enrich the SBOM without threading their concern
149
+ * through the shared dependency-tree parser. Components without a matching
150
+ * entry, or whose hashes are already set, are left untouched.
151
+ * @param {Map<string, Array<{alg: string, content: string}>>} hashMap - PURL→hashes map
152
+ * @return {CycloneDxSbom} the updated SBOM
153
+ */
154
+ attachHashes(hashMap) {
155
+ if (!hashMap || hashMap.size === 0) {
156
+ return this;
157
+ }
158
+ for (const component of this.components) {
159
+ if (component.hashes) {
160
+ continue;
161
+ }
162
+ const hashes = hashMap.get(component.purl);
163
+ if (hashes && hashes.length > 0) {
164
+ component.hashes = hashes;
165
+ }
166
+ }
167
+ return this;
168
+ }
105
169
  /** @param {{}} opts - various options, settings and configuration of application.
106
170
  * @return String CycloneDx Sbom json object in a string format
107
171
  */
108
172
  getAsJsonString(opts) {
109
173
  let manifestType = opts["manifest-type"];
110
174
  this.setSourceManifest(opts["source-manifest"]);
175
+ const rootPurl = this.rootComponent?.purl;
111
176
  this.sbomObject = {
112
177
  "bomFormat": "CycloneDX",
113
- "specVersion": "1.4",
178
+ "specVersion": "1.6",
114
179
  "version": 1,
115
180
  "metadata": {
116
181
  "timestamp": new Date(),
117
182
  "component": this.rootComponent,
183
+ "tools": {
184
+ "components": [{
185
+ "type": "application",
186
+ "name": "trustify-da-javascript-client",
187
+ "version": PACKAGE_VERSION
188
+ }]
189
+ },
118
190
  "properties": new Array()
119
191
  },
120
- "components": this.components,
192
+ "components": this.components.filter(c => c.purl !== rootPurl),
121
193
  "dependencies": this.dependencies
122
194
  };
123
195
  if (this.rootComponent === undefined) {
@@ -229,6 +301,20 @@ export default class CycloneDxSbom {
229
301
  return false;
230
302
  }
231
303
  }
304
+ /**
305
+ * Checks if any entry in the dependsOn list of sourceRef starts with the given purl prefix.
306
+ * @param {PackageURL} sourceRef - The source component
307
+ * @param {string} purlPrefix - The purl prefix to match (e.g. "pkg:npm/minimist@")
308
+ * @return {boolean}
309
+ */
310
+ checkDependsOnByPurlPrefix(sourceRef, purlPrefix) {
311
+ const sourcePurl = sourceRef.toString();
312
+ const depIndex = this.getDependencyIndex(sourcePurl);
313
+ if (depIndex < 0) {
314
+ return false;
315
+ }
316
+ return this.dependencies[depIndex].dependsOn.some(dep => dep.startsWith(purlPrefix));
317
+ }
232
318
  /** Removes the root component from the sbom
233
319
  */
234
320
  removeRootComponent() {