@trustify-da/trustify-da-javascript-client 0.3.0-ea.61444d4 → 0.3.0-ea.62b88e5

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 (59) hide show
  1. package/README.md +22 -8
  2. package/dist/package.json +6 -5
  3. package/dist/src/analysis.d.ts +40 -19
  4. package/dist/src/analysis.js +41 -5
  5. package/dist/src/cli.js +95 -17
  6. package/dist/src/config.d.ts +120 -0
  7. package/dist/src/config.js +260 -0
  8. package/dist/src/cyclone_dx_sbom.d.ts +13 -0
  9. package/dist/src/cyclone_dx_sbom.js +39 -1
  10. package/dist/src/index.d.ts +23 -23
  11. package/dist/src/index.js +23 -39
  12. package/dist/src/license/index.d.ts +2 -2
  13. package/dist/src/license/index.js +9 -6
  14. package/dist/src/license/licenses_api.d.ts +2 -2
  15. package/dist/src/license/licenses_api.js +1 -1
  16. package/dist/src/package_version.d.ts +8 -0
  17. package/dist/src/package_version.js +31 -0
  18. package/dist/src/providers/base_java.d.ts +53 -0
  19. package/dist/src/providers/base_java.js +64 -17
  20. package/dist/src/providers/base_javascript.d.ts +54 -0
  21. package/dist/src/providers/base_javascript.js +106 -4
  22. package/dist/src/providers/golang_gomodules.js +6 -5
  23. package/dist/src/providers/java_gradle.d.ts +48 -0
  24. package/dist/src/providers/java_gradle.js +204 -20
  25. package/dist/src/providers/java_maven.d.ts +28 -8
  26. package/dist/src/providers/java_maven.js +104 -10
  27. package/dist/src/providers/javascript_bun.d.ts +12 -0
  28. package/dist/src/providers/javascript_bun.js +42 -1
  29. package/dist/src/providers/javascript_npm.d.ts +13 -0
  30. package/dist/src/providers/javascript_npm.js +40 -1
  31. package/dist/src/providers/javascript_pnpm.d.ts +13 -0
  32. package/dist/src/providers/javascript_pnpm.js +47 -1
  33. package/dist/src/providers/javascript_yarn.d.ts +12 -0
  34. package/dist/src/providers/javascript_yarn.js +75 -2
  35. package/dist/src/providers/manifest.js +6 -3
  36. package/dist/src/providers/processors/yarn_berry_processor.d.ts +5 -1
  37. package/dist/src/providers/processors/yarn_berry_processor.js +3 -2
  38. package/dist/src/providers/processors/yarn_classic_processor.d.ts +5 -1
  39. package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
  40. package/dist/src/providers/python_uv.js +6 -0
  41. package/dist/src/providers/requirements_parser.js +1 -1
  42. package/dist/src/providers/rust_cargo.js +34 -52
  43. package/dist/src/remediate.d.ts +90 -4
  44. package/dist/src/remediate.js +153 -58
  45. package/dist/src/remediation.d.ts +54 -30
  46. package/dist/src/remediation.js +92 -53
  47. package/dist/src/remediation_report.d.ts +3 -20
  48. package/dist/src/remediation_report.js +37 -16
  49. package/dist/src/sbom.d.ts +11 -0
  50. package/dist/src/sbom.js +10 -0
  51. package/dist/src/tools.d.ts +9 -11
  52. package/dist/src/tools.js +34 -13
  53. package/dist/src/updaters/maven_updater.d.ts +40 -8
  54. package/dist/src/updaters/maven_updater.js +26 -3
  55. package/dist/src/updaters/toml_updater.d.ts +42 -13
  56. package/dist/src/updaters/toml_updater.js +36 -4
  57. package/dist/src/workspace.d.ts +2 -1
  58. package/dist/src/workspace.js +2 -1
  59. package/package.json +7 -6
@@ -1,4 +1,24 @@
1
1
  import { PackageURL } from 'packageurl-js';
2
+ /**
3
+ * A single per-CVE vulnerability carried by a remediation: one CVE with its own severity and the
4
+ * advisories attributed to it.
5
+ * @typedef {{id: string, severity: string, advisories: Array<{id: string, url: string}>}} Vulnerability
6
+ */
7
+ /**
8
+ * A single applicable remediation as produced by {@link extractRemediations}: a dependency, the
9
+ * version that fixes it, and the per-CVE `vulnerabilities` it resolves.
10
+ * @typedef {{
11
+ * purl: string,
12
+ * groupId: string,
13
+ * artifactId: string,
14
+ * currentVersion: string,
15
+ * fixedInVersion: string,
16
+ * fixedInPurl: string,
17
+ * provider: string,
18
+ * source: string,
19
+ * vulnerabilities: Vulnerability[]
20
+ * }} Remediation
21
+ */
2
22
  /**
3
23
  * Extracts the major version segment from a version string.
4
24
  * @param {string} version
@@ -7,11 +27,14 @@ import { PackageURL } from 'packageurl-js';
7
27
  function getMajorVersion(version) {
8
28
  return version.split('.')[0] || '';
9
29
  }
30
+ /**
31
+ * @typedef {{selectVersion: (fixedInVersions: string[], currentVersion: string) => string, resolveConflict: (existing: ConflictCandidate, candidate: ConflictCandidate) => 'existing'|'candidate'}} VersionStrategy
32
+ */
10
33
  /**
11
34
  * Version selection strategy that prefers the closest compatible version
12
35
  * within the same major version stream. Falls back to the lowest cross-major
13
36
  * version when no same-major option exists.
14
- * @type {{selectVersion: function(string[], string): string, resolveConflict: function(object, object): string}}
37
+ * @type {VersionStrategy}
15
38
  */
16
39
  export const closestCoverageStrategy = {
17
40
  selectVersion(fixedInVersions, currentVersion) {
@@ -47,7 +70,7 @@ export const closestCoverageStrategy = {
47
70
  * Version selection strategy that always picks the highest version regardless
48
71
  * of major version distance. Guarantees maximum CVE coverage but may produce
49
72
  * large version jumps. This is the original behavior before pluggable strategies.
50
- * @type {{selectVersion: function(string[], string): string, resolveConflict: function(object, object): string}}
73
+ * @type {VersionStrategy}
51
74
  */
52
75
  export const highestStrategy = {
53
76
  selectVersion(fixedInVersions) {
@@ -73,14 +96,16 @@ export const highestStrategy = {
73
96
  * version selection strategy to resolve conflicts. Dependencies with no remediation
74
97
  * data are skipped.
75
98
  *
76
- * @param {object} analysisReport - raw DA AnalysisReport JSON response
99
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts').AnalysisReport} analysisReport - raw DA AnalysisReport JSON response
77
100
  * @param {object} [options] - extraction options
78
101
  * @param {string[]} [options.providerPriority] - provider names in descending priority order.
79
102
  * The first entry has the highest priority. Providers not listed share the lowest priority.
80
103
  * When omitted or empty, all providers are treated equally and the highest fix version wins.
81
- * @param {object} [options.versionStrategy] - version selection strategy with selectVersion
104
+ * @param {VersionStrategy} [options.versionStrategy] - version selection strategy with selectVersion
82
105
  * and resolveConflict methods. Defaults to closestCoverageStrategy.
83
- * @returns {Array<{purl: string, groupId: string, artifactId: string, currentVersion: string, fixedInVersion: string, fixedInPurl: string, provider: string, source: string, advisories: Array<{id: string, url: string}>, severity: string, cves: string[]}>}
106
+ * @returns {Remediation[]} `vulnerabilities` is the sole source of vulnerability data — each entry
107
+ * holds one CVE with its own severity and advisories. Use {@link maxSeverity} to derive a
108
+ * dependency-level severity.
84
109
  */
85
110
  export function extractRemediations(analysisReport, options = {}) {
86
111
  if (!analysisReport || !analysisReport.providers) {
@@ -88,6 +113,7 @@ export function extractRemediations(analysisReport, options = {}) {
88
113
  }
89
114
  const priorityMap = buildPriorityMap(options.providerPriority);
90
115
  const strategy = options.versionStrategy || closestCoverageStrategy;
116
+ /** @type {Map<string, Remediation & { _fromTrustedContent?: boolean}>} */
91
117
  const remediationsByDep = new Map();
92
118
  const rankByDep = new Map();
93
119
  for (const [providerName, providerReport] of Object.entries(analysisReport.providers)) {
@@ -120,12 +146,12 @@ function buildPriorityMap(providerPriority) {
120
146
  }
121
147
  /**
122
148
  * Extracts remediations from the sources/dependencies/issues tree of a provider report.
123
- * @param {object} providerReport
149
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/ProviderReport.js').ProviderReport} providerReport
124
150
  * @param {string} providerName
125
151
  * @param {number} providerRank - numeric priority rank for this provider
126
- * @param {Map<string, object>} remediationsByDep - accumulator keyed by dependency PURL
152
+ * @param {Map<string, Remediation & { _fromTrustedContent?: boolean }>} remediationsByDep - accumulator keyed by dependency PURL
127
153
  * @param {Map<string, number>} rankByDep - tracks current winning rank per dependency
128
- * @param {object} strategy - version selection strategy
154
+ * @param {VersionStrategy} strategy - version selection strategy
129
155
  */
130
156
  function extractFromSources(providerReport, providerName, providerRank, remediationsByDep, rankByDep, strategy) {
131
157
  if (!providerReport.sources) {
@@ -147,14 +173,14 @@ function extractFromSources(providerReport, providerName, providerRank, remediat
147
173
  }
148
174
  /**
149
175
  * Processes a single issue's remediation data and merges it into the accumulator.
150
- * @param {object} issue - issue object containing remediation and CVE data
151
- * @param {object} dep - dependency object containing the ref PURL
176
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/Issue.js').Issue} issue - issue object containing remediation and CVE data
177
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/DependencyReport.js').DependencyReport} dep - dependency object containing the ref PURL
152
178
  * @param {string} providerName
153
179
  * @param {string} sourceName
154
180
  * @param {number} providerRank
155
- * @param {Map<string, object>} remediationsByDep
181
+ * @param {Map<string, Remediation & { _fromTrustedContent?: boolean }>} remediationsByDep
156
182
  * @param {Map<string, number>} rankByDep
157
- * @param {object} strategy - version selection strategy
183
+ * @param {VersionStrategy} strategy - version selection strategy
158
184
  */
159
185
  function processIssueRemediation(issue, dep, providerName, sourceName, providerRank, remediationsByDep, rankByDep, strategy) {
160
186
  const depPurl = dep.ref;
@@ -185,12 +211,12 @@ function processIssueRemediation(issue, dep, providerName, sourceName, providerR
185
211
  if (!fixedInVersion) {
186
212
  return;
187
213
  }
188
- const cveId = issue.id || issue.cve;
214
+ const cveId = issue.id;
189
215
  const severity = issue.severity || 'UNKNOWN';
190
216
  const advisories = extractAdvisories(issue);
191
217
  const existing = remediationsByDep.get(depPurl);
192
218
  if (!existing) {
193
- remediationsByDep.set(depPurl, {
219
+ const entry = {
194
220
  purl: depPurl,
195
221
  groupId: parsedDep.namespace || '',
196
222
  artifactId: parsedDep.name,
@@ -199,25 +225,21 @@ function processIssueRemediation(issue, dep, providerName, sourceName, providerR
199
225
  fixedInPurl,
200
226
  provider: providerName,
201
227
  source: sourceName,
202
- advisories,
203
- severity: severity.toUpperCase(),
204
- cves: cveId ? [cveId] : [],
228
+ vulnerabilities: [],
205
229
  _fromTrustedContent: isTrustedContent,
206
- });
230
+ };
231
+ addVulnerability(entry, cveId, severity, advisories);
232
+ remediationsByDep.set(depPurl, entry);
207
233
  rankByDep.set(depPurl, providerRank);
208
234
  return;
209
235
  }
210
- if (cveId && !existing.cves.includes(cveId)) {
211
- existing.cves.push(cveId);
212
- }
213
- mergeAdvisories(existing.advisories, advisories);
236
+ addVulnerability(existing, cveId, severity, advisories);
214
237
  const existingRank = rankByDep.get(depPurl);
215
238
  if (providerRank > existingRank) {
216
239
  existing.fixedInVersion = fixedInVersion;
217
240
  existing.fixedInPurl = fixedInPurl;
218
241
  existing.provider = providerName;
219
242
  existing.source = sourceName;
220
- existing.severity = higherSeverity(existing.severity, severity);
221
243
  existing._fromTrustedContent = isTrustedContent;
222
244
  rankByDep.set(depPurl, providerRank);
223
245
  }
@@ -230,16 +252,15 @@ function processIssueRemediation(issue, dep, providerName, sourceName, providerR
230
252
  existing.source = sourceName;
231
253
  existing._fromTrustedContent = isTrustedContent;
232
254
  }
233
- existing.severity = higherSeverity(existing.severity, severity);
234
255
  }
235
256
  }
236
257
  /**
237
258
  * Extracts remediations from the recommendations section of a provider report.
238
259
  * Merges CVEs and advisories into existing entries when present.
239
- * @param {object} providerReport
260
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/ProviderReport.js').ProviderReport} providerReport
240
261
  * @param {string} providerName
241
262
  * @param {number} providerRank
242
- * @param {Map<string, object>} remediationsByDep
263
+ * @param {Map<string, Remediation & { _fromTrustedContent?: boolean }>} remediationsByDep
243
264
  * @param {Map<string, number>} rankByDep
244
265
  */
245
266
  function extractFromRecommendations(providerReport, providerName, providerRank, remediationsByDep, rankByDep) {
@@ -279,9 +300,7 @@ function extractFromRecommendations(providerReport, providerName, providerRank,
279
300
  fixedInPurl: recommendedPurl,
280
301
  provider: providerName,
281
302
  source: 'recommendation',
282
- advisories: [],
283
- severity: 'UNKNOWN',
284
- cves: [],
303
+ vulnerabilities: [],
285
304
  });
286
305
  rankByDep.set(depPurl, providerRank);
287
306
  continue;
@@ -308,9 +327,9 @@ function extractFromRecommendations(providerReport, providerName, providerRank,
308
327
  * Gets the fixedIn PURL from an issue's remediation, preferring trustedContent.
309
328
  * When fixedIn is an array of version strings (not PURLs), uses the strategy's
310
329
  * selectVersion to pick the best candidate and constructs a PURL from the dependency ref.
311
- * @param {object} issue
330
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/Issue.js').Issue} issue
312
331
  * @param {string} depPurl - the dependency PURL, used to construct fixedIn PURLs from version strings
313
- * @param {object} strategy - version selection strategy
332
+ * @param {VersionStrategy} strategy - version selection strategy
314
333
  * @param {string} currentVersion - the dependency's current version
315
334
  * @returns {string|undefined}
316
335
  */
@@ -318,17 +337,11 @@ function getFixedInPurl(issue, depPurl, strategy, currentVersion) {
318
337
  if (!issue.remediation) {
319
338
  return undefined;
320
339
  }
321
- if (issue.remediation.trustedContent && issue.remediation.trustedContent.ref) {
340
+ if (issue.remediation.trustedContent?.ref) {
322
341
  return issue.remediation.trustedContent.ref;
323
342
  }
324
343
  const fixedIn = issue.remediation.fixedIn;
325
- if (!fixedIn) {
326
- return undefined;
327
- }
328
- if (typeof fixedIn === 'string') {
329
- return fixedIn;
330
- }
331
- if (Array.isArray(fixedIn) && fixedIn.length > 0) {
344
+ if (fixedIn?.length > 0) {
332
345
  const version = fixedIn.length > 1
333
346
  ? strategy.selectVersion(fixedIn, currentVersion)
334
347
  : fixedIn[0];
@@ -348,27 +361,44 @@ function getFixedInPurl(issue, depPurl, strategy, currentVersion) {
348
361
  }
349
362
  return undefined;
350
363
  }
364
+ /**
365
+ * Adds a per-CVE vulnerability entry to a remediation, deduplicating by CVE id. When the
366
+ * CVE is already present, the higher severity is kept and its advisories are merged.
367
+ * Issues without a CVE id contribute no vulnerability entry.
368
+ * @param {object} entry - remediation accumulator entry with a `vulnerabilities` array
369
+ * @param {string|undefined} cveId - the CVE identifier for this issue
370
+ * @param {string} severity - the issue's severity
371
+ * @param {Array<{id: string, url: string}>} advisories - advisories attributed to this issue
372
+ */
373
+ function addVulnerability(entry, cveId, severity, advisories) {
374
+ if (!cveId) {
375
+ return;
376
+ }
377
+ const normalizedSeverity = (severity || 'UNKNOWN').toUpperCase();
378
+ const existingVuln = entry.vulnerabilities.find(v => v.id === cveId);
379
+ if (existingVuln) {
380
+ existingVuln.severity = higherSeverity(existingVuln.severity, normalizedSeverity);
381
+ mergeAdvisories(existingVuln.advisories, advisories);
382
+ return;
383
+ }
384
+ entry.vulnerabilities.push({
385
+ id: cveId,
386
+ severity: normalizedSeverity,
387
+ advisories: [...advisories],
388
+ });
389
+ }
351
390
  /**
352
391
  * Extracts advisory objects from an issue.
353
- * @param {object} issue
392
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/Issue.js').Issue} issue
354
393
  * @returns {Array<{id: string, url: string}>}
355
394
  */
356
395
  function extractAdvisories(issue) {
357
396
  const advisories = [];
358
- if (issue.remediation && issue.remediation.trustedContent) {
359
- const tc = issue.remediation.trustedContent;
360
- if (tc.advisory) {
361
- advisories.push({
362
- id: tc.advisory.id || tc.advisory,
363
- url: tc.advisory.url || '',
364
- });
365
- }
366
- }
367
- if (issue.advisories) {
368
- for (const adv of issue.advisories) {
397
+ for (const advisory of issue.remediation?.advisories ?? []) {
398
+ if (advisory.advisory?.id) {
369
399
  advisories.push({
370
- id: adv.id || adv,
371
- url: adv.url || '',
400
+ id: advisory.advisory.id,
401
+ url: advisory.advisory.url || '',
372
402
  });
373
403
  }
374
404
  }
@@ -434,3 +464,12 @@ function higherSeverity(a, b) {
434
464
  const indexB = SEVERITY_ORDER.indexOf(upperB);
435
465
  return indexA >= indexB ? upperA : upperB;
436
466
  }
467
+ /**
468
+ * Derives a dependency-level severity as the max across a list of vulnerabilities.
469
+ * Returns 'UNKNOWN' for an empty or missing list.
470
+ * @param {Array<{severity: string}>} [vulnerabilities]
471
+ * @returns {string}
472
+ */
473
+ export function maxSeverity(vulnerabilities) {
474
+ return (vulnerabilities || []).reduce((acc, v) => higherSeverity(acc, v.severity), 'UNKNOWN');
475
+ }
@@ -1,32 +1,15 @@
1
1
  /**
2
2
  * Generates a formatted report from an array of remediation entries.
3
3
  *
4
- * @param {Array<{purl: string, groupId: string, artifactId: string, currentVersion: string,
5
- * fixedInVersion: string, fixedInPurl: string, provider: string, source: string,
6
- * advisories: Array<{id: string, url: string}>, severity: string, cves: string[]}>} remediations
4
+ * @param {import('./remediation.js').Remediation[]} remediations
7
5
  * @param {object} [options]
8
6
  * @param {'dependency'|'bundle'} [options.groupBy='dependency'] - grouping strategy
9
7
  * @param {'markdown'|'json'} [options.format='markdown'] - output format
10
8
  * @param {boolean} [options.dryRun=false] - when true, produces tabular summary
11
9
  * @returns {string}
12
10
  */
13
- export function generateReport(remediations: Array<{
14
- purl: string;
15
- groupId: string;
16
- artifactId: string;
17
- currentVersion: string;
18
- fixedInVersion: string;
19
- fixedInPurl: string;
20
- provider: string;
21
- source: string;
22
- advisories: Array<{
23
- id: string;
24
- url: string;
25
- }>;
26
- severity: string;
27
- cves: string[];
28
- }>, options?: {
29
- groupBy?: "dependency" | "bundle" | undefined;
11
+ export function generateReport(remediations: import("./remediation.js").Remediation[], options?: {
12
+ groupBy?: "bundle" | "dependency" | undefined;
30
13
  format?: "markdown" | "json" | undefined;
31
14
  dryRun?: boolean | undefined;
32
15
  }): string;
@@ -2,13 +2,11 @@
2
2
  * Report generator that transforms remediation extractor output into structured
3
3
  * markdown for PR bodies, CLI dry-run output, and JSON.
4
4
  */
5
- import { SEVERITY_ORDER } from './remediation.js';
5
+ import { SEVERITY_ORDER, maxSeverity } from './remediation.js';
6
6
  /**
7
7
  * Generates a formatted report from an array of remediation entries.
8
8
  *
9
- * @param {Array<{purl: string, groupId: string, artifactId: string, currentVersion: string,
10
- * fixedInVersion: string, fixedInPurl: string, provider: string, source: string,
11
- * advisories: Array<{id: string, url: string}>, severity: string, cves: string[]}>} remediations
9
+ * @param {import('./remediation.js').Remediation[]} remediations
12
10
  * @param {object} [options]
13
11
  * @param {'dependency'|'bundle'} [options.groupBy='dependency'] - grouping strategy
14
12
  * @param {'markdown'|'json'} [options.format='markdown'] - output format
@@ -33,7 +31,11 @@ export function generateReport(remediations, options = {}) {
33
31
  }
34
32
  /**
35
33
  * Generates a per-dependency markdown report with one section per remediation entry.
36
- * @param {Array<object>} remediations
34
+ *
35
+ * Each vulnerability row is rendered from its own per-CVE severity and advisories
36
+ * (from `rem.vulnerabilities`), so a Moderate CVE is no longer inflated to the
37
+ * dependency's max severity.
38
+ * @param {import('./remediation.js').Remediation[]} remediations
37
39
  * @returns {string}
38
40
  */
39
41
  function generatePerDependencyReport(remediations) {
@@ -47,14 +49,14 @@ function generatePerDependencyReport(remediations) {
47
49
  `**Provider:** ${rem.provider} | **Source:** ${rem.source}`,
48
50
  '',
49
51
  ];
50
- if (rem.cves && rem.cves.length > 0) {
52
+ const vulnerabilities = rem.vulnerabilities || [];
53
+ if (vulnerabilities.length > 0) {
51
54
  lines.push('### Vulnerabilities resolved');
52
55
  lines.push('');
53
56
  lines.push('| CVE | Severity | Advisory |');
54
57
  lines.push('| --- | --- | --- |');
55
- const advisoryLinks = formatAdvisoryLinks(rem.advisories);
56
- for (const cve of rem.cves) {
57
- lines.push(`| ${cve} | ${rem.severity} | ${advisoryLinks} |`);
58
+ for (const v of vulnerabilities) {
59
+ lines.push(`| ${v.id} | ${v.severity} | ${formatAdvisoryLinks(v.advisories)} |`);
58
60
  }
59
61
  }
60
62
  return lines.join('\n');
@@ -63,7 +65,7 @@ function generatePerDependencyReport(remediations) {
63
65
  }
64
66
  /**
65
67
  * Generates a bundled markdown report grouping all remediations by severity.
66
- * @param {Array<object>} remediations
68
+ * @param {import('./remediation.js').Remediation[]} remediations
67
69
  * @returns {string}
68
70
  */
69
71
  function generateBundledReport(remediations) {
@@ -82,8 +84,9 @@ function generateBundledReport(remediations) {
82
84
  const depName = rem.groupId
83
85
  ? `${rem.groupId}:${rem.artifactId}`
84
86
  : rem.artifactId;
85
- const cves = (rem.cves || []).join(', ');
86
- const advisoryLinks = formatAdvisoryLinks(rem.advisories);
87
+ const vulnerabilities = rem.vulnerabilities || [];
88
+ const cves = vulnerabilities.map(v => v.id).join(', ');
89
+ const advisoryLinks = formatAdvisoryLinks(collectAdvisories(vulnerabilities));
87
90
  lines.push(`| ${depName} | ${rem.currentVersion} | ${rem.fixedInVersion}`
88
91
  + ` | ${rem.provider} | ${cves} | ${advisoryLinks} |`);
89
92
  }
@@ -93,7 +96,7 @@ function generateBundledReport(remediations) {
93
96
  }
94
97
  /**
95
98
  * Generates a tabular dry-run summary of proposed changes.
96
- * @param {Array<object>} remediations
99
+ * @param {import('./remediation.js').Remediation[]} remediations
97
100
  * @returns {string}
98
101
  */
99
102
  function generateDryRunReport(remediations) {
@@ -108,13 +111,13 @@ function generateDryRunReport(remediations) {
108
111
  ? `${rem.groupId}:${rem.artifactId}`
109
112
  : rem.artifactId;
110
113
  lines.push(`| ${depName} | ${rem.currentVersion} | ${rem.fixedInVersion}`
111
- + ` | ${rem.severity} | ${rem.provider} |`);
114
+ + ` | ${maxSeverity(rem.vulnerabilities)} | ${rem.provider} |`);
112
115
  }
113
116
  return lines.join('\n');
114
117
  }
115
118
  /**
116
119
  * Groups remediations by their severity.
117
- * @param {Array<object>} remediations
120
+ * @param {import('./remediation.js').Remediation[]} remediations
118
121
  * @returns {Map<string, Array<object>>}
119
122
  */
120
123
  function groupBySeverity(remediations) {
@@ -123,7 +126,7 @@ function groupBySeverity(remediations) {
123
126
  map.set(severity, []);
124
127
  }
125
128
  for (const rem of remediations) {
126
- const sev = rem.severity || 'UNKNOWN';
129
+ const sev = maxSeverity(rem.vulnerabilities);
127
130
  if (!map.has(sev)) {
128
131
  map.set(sev, []);
129
132
  }
@@ -131,6 +134,24 @@ function groupBySeverity(remediations) {
131
134
  }
132
135
  return map;
133
136
  }
137
+ /**
138
+ * Collects the de-duplicated union of advisories across a list of vulnerabilities.
139
+ * @param {Array<{advisories: Array<{id: string, url: string}>}>} vulnerabilities
140
+ * @returns {Array<{id: string, url: string}>}
141
+ */
142
+ function collectAdvisories(vulnerabilities) {
143
+ const merged = [];
144
+ const seen = new Set();
145
+ for (const v of vulnerabilities) {
146
+ for (const adv of v.advisories || []) {
147
+ if (!seen.has(adv.id)) {
148
+ seen.add(adv.id);
149
+ merged.push(adv);
150
+ }
151
+ }
152
+ }
153
+ return merged;
154
+ }
134
155
  /**
135
156
  * Formats advisory entries into markdown links or plain text.
136
157
  * @param {Array<{id: string, url: string}>} advisories
@@ -33,6 +33,17 @@ export default class Sbom {
33
33
  alg: string;
34
34
  content: string;
35
35
  }>): CycloneDxSbom;
36
+ /**
37
+ * Attach hashes to existing components by matching their PURL. Post-processing
38
+ * step used by ecosystem-specific providers (e.g. Maven) to enrich the SBOM
39
+ * with artifact hashes without leaking their concern into the shared parser.
40
+ * @param {Map<string, Array<{alg: string, content: string}>>} hashMap - PURL→hashes map
41
+ * @return {Sbom}
42
+ */
43
+ attachHashes(hashMap: Map<string, Array<{
44
+ alg: string;
45
+ content: string;
46
+ }>>): Sbom;
36
47
  /**
37
48
  * @return String sbom json in a string format
38
49
  */
package/dist/src/sbom.js CHANGED
@@ -50,6 +50,16 @@ export default class Sbom {
50
50
  addDependency(sourceRef, targetRef, scope, targetHashes) {
51
51
  return this.sbomModel.addDependency(sourceRef, targetRef, scope, targetHashes);
52
52
  }
53
+ /**
54
+ * Attach hashes to existing components by matching their PURL. Post-processing
55
+ * step used by ecosystem-specific providers (e.g. Maven) to enrich the SBOM
56
+ * with artifact hashes without leaking their concern into the shared parser.
57
+ * @param {Map<string, Array<{alg: string, content: string}>>} hashMap - PURL→hashes map
58
+ * @return {Sbom}
59
+ */
60
+ attachHashes(hashMap) {
61
+ return this.sbomModel.attachHashes(hashMap);
62
+ }
53
63
  /**
54
64
  * @return String sbom json in a string format
55
65
  */
@@ -6,24 +6,22 @@
6
6
  */
7
7
  export function logValueFromObjects(key: string, opts?: {}, defValue: string): void;
8
8
  /**
9
- * Utility function will return the value for key from the environment variables,
10
- * if not present will return the value for key from the opts objects only if it's a string,
11
- * if not present, or not string will return the default value supplied which default to null.
9
+ * Utility function returns the value for key from opts, then environment variables,
10
+ * then the supplied default. Values from opts are used only if they are strings.
12
11
  * @param {string} key the key to look for in the environment variables and the opts object
13
12
  * @param {string|null} [def=null] the value to return if nothing else found
14
- * @param {{}} [opts={}] the options object to look for the key in if not found in environment
15
- * @returns {string|null} the value of the key found in the environment, options object, or the
13
+ * @param {{}} [opts={}] the options object to check before the environment
14
+ * @returns {string|null} the value of the key found in the options object, environment, or the
16
15
  * default supplied
17
16
  */
18
17
  export function getCustom(key: string, def?: string | null, opts?: {}): string | null;
19
18
  /**
20
19
  * Utility function for looking up custom variable for a binary path.
21
- * Will look in the environment variables (1) or in opts (2) for a key with TRUSTIFY_DA_x_PATH, x is an
22
- * uppercase version of passed name to look for. The name will also be returned if nothing else was
23
- * found.
20
+ * Looks in opts, then environment variables, for a key with TRUSTIFY_DA_x_PATH, where x is an
21
+ * uppercase version of the supplied name. The name is returned if neither contains the key.
24
22
  * @param name the binary name to look for, will be returned as value in nothing else found
25
- * @param {{}} [opts={}] the options object to look for the key in if not found in environment
26
- * @returns {string|null} the value of the key found in the environment, options object, or the
23
+ * @param {{}} [opts={}] the options object to check before the environment
24
+ * @returns {string|null} the value of the key found in the options object, environment, or the
27
25
  * original name supplied
28
26
  */
29
27
  export function getCustomPath(name: any, opts?: {}): string | null;
@@ -31,7 +29,7 @@ export function getCustomPath(name: any, opts?: {}): string | null;
31
29
  * Utility function for determining whether wrappers for build tools such as gradlew/mvnw should be
32
30
  * preferred over invoking the binary directly.
33
31
  * @param {string} name - binary for which to search for its wrapper
34
- * @param {{}} opts - the options object to look for the key in if not found in environment
32
+ * @param {{}} opts - the options object to check before the environment
35
33
  * @returns {boolean} whether to prefer the wrapper if exists or not
36
34
  */
37
35
  export function getWrapperPreference(name: string, opts?: {}): boolean;
package/dist/src/tools.js CHANGED
@@ -27,39 +27,60 @@ export function logValueFromObjects(key, opts, defValue) {
27
27
  console.log(`default value for ${key} = ${defValue} ${EOL}`);
28
28
  }
29
29
  /**
30
- * Utility function will return the value for key from the environment variables,
31
- * if not present will return the value for key from the opts objects only if it's a string,
32
- * if not present, or not string will return the default value supplied which default to null.
30
+ * Utility function returns the value for key from opts, then environment variables,
31
+ * then the supplied default. Values from opts are used only if they are strings.
33
32
  * @param {string} key the key to look for in the environment variables and the opts object
34
33
  * @param {string|null} [def=null] the value to return if nothing else found
35
- * @param {{}} [opts={}] the options object to look for the key in if not found in environment
36
- * @returns {string|null} the value of the key found in the environment, options object, or the
34
+ * @param {{}} [opts={}] the options object to check before the environment
35
+ * @returns {string|null} the value of the key found in the options object, environment, or the
37
36
  * default supplied
38
37
  */
39
38
  export function getCustom(key, def = null, opts = {}) {
40
39
  if (process.env["TRUSTIFY_DA_DEBUG"] === "true" && !key.match(RegexNotToBeLogged)) {
41
40
  logValueFromObjects(key, opts, def);
42
41
  }
43
- return key in process.env ? process.env[key] : key in opts && typeof opts[key] === 'string' ? opts[key] : def;
42
+ return key in opts && typeof opts[key] === 'string' ? opts[key] : key in process.env ? process.env[key] : def;
43
+ }
44
+ /**
45
+ * Validates that an executable path does not use directory traversal or relative segments.
46
+ * @param {string} binPath - The executable path to validate.
47
+ * @returns {string} The validated path.
48
+ * @throws {Error} If the path contains '..' segments or is a relative path with separators.
49
+ */
50
+ function validateExecutablePath(binPath) {
51
+ if (typeof binPath !== 'string' || binPath.length === 0) {
52
+ throw new Error('Executable path rejected: expected a non-empty string');
53
+ }
54
+ if (binPath.startsWith('./') || binPath.startsWith('.\\')) {
55
+ throw new Error(`Executable path rejected: relative paths starting with './' are not allowed: ${binPath}`);
56
+ }
57
+ const segments = binPath.split(/[/\\]/);
58
+ if (segments.includes('..')) {
59
+ throw new Error(`Executable path rejected: path contains directory traversal segment (..): ${binPath}`);
60
+ }
61
+ if ((binPath.includes('/') || binPath.includes('\\')) && !path.isAbsolute(binPath)) {
62
+ throw new Error(`Executable path rejected: relative paths are not allowed, use an absolute path or a bare command name: ${binPath}`);
63
+ }
64
+ return binPath;
44
65
  }
45
66
  /**
46
67
  * Utility function for looking up custom variable for a binary path.
47
- * Will look in the environment variables (1) or in opts (2) for a key with TRUSTIFY_DA_x_PATH, x is an
48
- * uppercase version of passed name to look for. The name will also be returned if nothing else was
49
- * found.
68
+ * Looks in opts, then environment variables, for a key with TRUSTIFY_DA_x_PATH, where x is an
69
+ * uppercase version of the supplied name. The name is returned if neither contains the key.
50
70
  * @param name the binary name to look for, will be returned as value in nothing else found
51
- * @param {{}} [opts={}] the options object to look for the key in if not found in environment
52
- * @returns {string|null} the value of the key found in the environment, options object, or the
71
+ * @param {{}} [opts={}] the options object to check before the environment
72
+ * @returns {string|null} the value of the key found in the options object, environment, or the
53
73
  * original name supplied
54
74
  */
55
75
  export function getCustomPath(name, opts = {}) {
56
- return getCustom(`TRUSTIFY_DA_${name.toUpperCase()}_PATH`, name, opts);
76
+ const resolvedPath = getCustom(`TRUSTIFY_DA_${name.toUpperCase()}_PATH`, name, opts);
77
+ return validateExecutablePath(resolvedPath);
57
78
  }
58
79
  /**
59
80
  * Utility function for determining whether wrappers for build tools such as gradlew/mvnw should be
60
81
  * preferred over invoking the binary directly.
61
82
  * @param {string} name - binary for which to search for its wrapper
62
- * @param {{}} opts - the options object to look for the key in if not found in environment
83
+ * @param {{}} opts - the options object to check before the environment
63
84
  * @returns {boolean} whether to prefer the wrapper if exists or not
64
85
  */
65
86
  export function getWrapperPreference(name, opts = {}) {