@trustify-da/trustify-da-javascript-client 0.3.0-ea.243eaef → 0.3.0-ea.24dd325

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.
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Resolves a target path to the supported manifest files it contains: the single file
3
+ * if `targetPath` is a supported manifest, or every supported manifest discovered
4
+ * recursively if it's a directory. This is the single source of truth for what
5
+ * `runRemediation` will scan, so callers (e.g. the CLI) can reuse it to tell
6
+ * "no manifests here" apart from "manifests, but nothing to fix".
7
+ * @param {string} targetPath - path to a manifest file or directory
8
+ * @returns {string[]} absolute paths to supported manifests (empty for an empty directory)
9
+ * @throws if the path does not exist, or is a file of an unsupported manifest type
10
+ */
11
+ export function findManifests(targetPath: string): string[];
1
12
  /**
2
13
  * Orchestrates the full remediation pipeline for a single manifest or directory:
3
14
  * discover manifests → scan via DA backend → extract remediations → apply or preview.
@@ -7,15 +18,103 @@
7
18
  * @param {boolean} [options.dryRun=false] - preview changes without modifying files (applies by default)
8
19
  * @param {string} [options.providers] - comma-separated provider list
9
20
  * @param {string} [options.sources] - comma-separated source list
10
- * @param {'dependency'|'bundle'} [options.groupBy='dependency'] - report grouping strategy
11
- * @returns {Promise<{exitCode: number, output: string}>}
21
+ * @param {string} [options.backendUrl] - Trustify DA backend URL
22
+ * @param {boolean} [options.perDependencyChanges=false] - when true, each remediation is populated with
23
+ * a `changes` array describing the isolated, single-dependency edit (see {@link DependencyFix}). This lets
24
+ * callers create one commit/PR per dependency without attributing diff hunks themselves.
25
+ * @returns {Promise<{exitCode: number, output: string, remediations: Remediation[], manifests: string[], appliedFiles: string[]}>}
26
+ * exitCode is 2 for a dry-run that found remediations (nothing written), 0 otherwise. `remediations`
27
+ * is the structured, per-manifest list of applicable updates — each entry carries the originating
28
+ * manifest path(s) in `files` so callers can group and create per-dependency changes. `appliedFiles`
29
+ * lists only the manifests actually written to disk (empty on a dry-run), so callers can report a
30
+ * truthful "updated N files" count without conflating "had remediations" with "was written".
12
31
  */
13
32
  export function runRemediation(targetPath: string, options?: {
14
33
  dryRun?: boolean | undefined;
15
34
  providers?: string | undefined;
16
35
  sources?: string | undefined;
17
- groupBy?: "bundle" | "dependency" | undefined;
36
+ backendUrl?: string | undefined;
37
+ perDependencyChanges?: boolean | undefined;
18
38
  }): Promise<{
19
39
  exitCode: number;
20
40
  output: string;
41
+ remediations: Remediation[];
42
+ manifests: string[];
43
+ appliedFiles: string[];
21
44
  }>;
45
+ /**
46
+ * A single version change requested from an updater: bump `groupId:artifactId` to `newVersion`.
47
+ * The input side of every updater; each updater's output side (its `applied` entries) is its own
48
+ * type — see {@link AppliedChange}.
49
+ */
50
+ export type VersionChangeRequest = {
51
+ groupId: string;
52
+ artifactId: string;
53
+ newVersion: string;
54
+ };
55
+ /**
56
+ * One entry from an updater's `applied` list, describing where and how a version was changed.
57
+ * Owned by the updater layer: the union of each updater's applied-entry shape. `changeKey`
58
+ * (per {@link ManifestType}) turns one of these into a stable edit-site identifier.
59
+ */
60
+ export type AppliedChange = import("./updaters/maven_updater.js").MavenAppliedChange | import("./updaters/toml_updater.js").TomlAppliedChange;
61
+ /**
62
+ * The result of running an updater over a manifest's raw content.
63
+ */
64
+ export type UpdaterResult = {
65
+ content: string;
66
+ applied: AppliedChange[];
67
+ skipped: Array<{
68
+ groupId: string;
69
+ artifactId: string;
70
+ newVersion: string;
71
+ reason: string;
72
+ }>;
73
+ };
74
+ /**
75
+ * A supported manifest type and the operations that act on it.
76
+ * `changeKey` builds a stable edit-site key from a single `applied` entry, so callers can detect
77
+ * inseparable remediations (same key => same commit/PR).
78
+ */
79
+ export type ManifestType = {
80
+ test: (basename: string) => boolean;
81
+ updater: (content: string, versionChanges: VersionChangeRequest[]) => UpdaterResult;
82
+ label: ("maven" | "toml");
83
+ changeKey: (manifestPath: string, applied: AppliedChange) => string;
84
+ };
85
+ /**
86
+ * An isolated, single-dependency edit to one manifest file.
87
+ * - `after` is the *original* manifest content with only this dependency's fix applied, so a caller
88
+ * can create an isolated commit by writing `after` to `path` on a branch cut from the base.
89
+ * - `changeKey` is a stable identifier for the underlying edit site. Two remediations that share a
90
+ * `changeKey` are inseparable (e.g. two Maven deps whose versions resolve to the same `${property}`,
91
+ * or two Gradle libraries sharing one `version.ref`) and MUST land in the same commit/PR — the
92
+ * caller should union their CVEs/advisories.
93
+ */
94
+ export type DependencyFix = {
95
+ path: string;
96
+ after: string;
97
+ changeKey: string;
98
+ };
99
+ /**
100
+ * A single applicable remediation, as produced by `extractRemediations` and enriched by
101
+ * `runRemediation` with the originating manifest path(s) and (optionally) per-dependency changes.
102
+ */
103
+ export type Remediation = {
104
+ purl: string;
105
+ groupId: string;
106
+ artifactId: string;
107
+ currentVersion: string;
108
+ fixedInVersion: string;
109
+ fixedInPurl: string;
110
+ provider: string;
111
+ source: string;
112
+ advisories: Array<{
113
+ id: string;
114
+ url: string;
115
+ }>;
116
+ severity: string;
117
+ cves: string[];
118
+ files: string[];
119
+ changes?: DependencyFix[];
120
+ };
@@ -3,34 +3,122 @@ import path from 'node:path';
3
3
  import analysis from './analysis.js';
4
4
  import { availableProviders, match } from './provider.js';
5
5
  import { extractRemediations } from './remediation.js';
6
- import { generateReport } from './remediation_report.js';
7
- import { updateMavenVersions } from './updaters/maven_updater.js';
8
- import { updateTomlVersions } from './updaters/toml_updater.js';
6
+ import { mavenChangeKey, updateMavenVersions } from './updaters/maven_updater.js';
7
+ import { tomlChangeKey, updateTomlVersions } from './updaters/toml_updater.js';
9
8
  import { selectTrustifyDABackend } from './index.js';
10
9
  // Mirrors DEFAULT_WORKSPACE_DISCOVERY_IGNORE in workspace.js
11
10
  const SKIP_DIRS = new Set(['node_modules', '.git']);
11
+ /**
12
+ * A single version change requested from an updater: bump `groupId:artifactId` to `newVersion`.
13
+ * The input side of every updater; each updater's output side (its `applied` entries) is its own
14
+ * type — see {@link AppliedChange}.
15
+ * @typedef {{groupId: string, artifactId: string, newVersion: string}} VersionChangeRequest
16
+ */
17
+ /**
18
+ * One entry from an updater's `applied` list, describing where and how a version was changed.
19
+ * Owned by the updater layer: the union of each updater's applied-entry shape. `changeKey`
20
+ * (per {@link ManifestType}) turns one of these into a stable edit-site identifier.
21
+ * @typedef {import('./updaters/maven_updater.js').MavenAppliedChange
22
+ * | import('./updaters/toml_updater.js').TomlAppliedChange} AppliedChange
23
+ */
24
+ /**
25
+ * The result of running an updater over a manifest's raw content.
26
+ * @typedef {{content: string, applied: AppliedChange[], skipped: Array<{groupId: string, artifactId: string, newVersion: string, reason: string}>}} UpdaterResult
27
+ */
28
+ /**
29
+ * A supported manifest type and the operations that act on it.
30
+ * `changeKey` builds a stable edit-site key from a single `applied` entry, so callers can detect
31
+ * inseparable remediations (same key => same commit/PR).
32
+ * @typedef {{
33
+ * test: (basename: string) => boolean,
34
+ * updater: (content: string, versionChanges: VersionChangeRequest[]) => UpdaterResult,
35
+ * label: ('maven'|'toml'),
36
+ * changeKey: (manifestPath: string, applied: AppliedChange) => string
37
+ * }} ManifestType
38
+ */
39
+ /**
40
+ * An isolated, single-dependency edit to one manifest file.
41
+ * - `after` is the *original* manifest content with only this dependency's fix applied, so a caller
42
+ * can create an isolated commit by writing `after` to `path` on a branch cut from the base.
43
+ * - `changeKey` is a stable identifier for the underlying edit site. Two remediations that share a
44
+ * `changeKey` are inseparable (e.g. two Maven deps whose versions resolve to the same `${property}`,
45
+ * or two Gradle libraries sharing one `version.ref`) and MUST land in the same commit/PR — the
46
+ * caller should union their CVEs/advisories.
47
+ * @typedef {{ path: string, after: string, changeKey: string }} DependencyFix
48
+ */
49
+ /**
50
+ * A single applicable remediation, as produced by `extractRemediations` and enriched by
51
+ * `runRemediation` with the originating manifest path(s) and (optionally) per-dependency changes.
52
+ * @typedef {{
53
+ * purl: string,
54
+ * groupId: string,
55
+ * artifactId: string,
56
+ * currentVersion: string,
57
+ * fixedInVersion: string,
58
+ * fixedInPurl: string,
59
+ * provider: string,
60
+ * source: string,
61
+ * advisories: Array<{id: string, url: string}>,
62
+ * severity: string,
63
+ * cves: string[],
64
+ * files: string[],
65
+ * changes?: DependencyFix[]
66
+ * }} Remediation
67
+ */
68
+ /** @type {ManifestType[]} */
12
69
  const MANIFEST_TYPES = [
13
70
  {
14
71
  test: (basename) => basename === 'pom.xml',
15
72
  updater: updateMavenVersions,
16
73
  label: 'maven',
74
+ changeKey: mavenChangeKey
17
75
  },
18
76
  {
19
77
  test: (basename) => basename.endsWith('.versions.toml') || basename === 'libs.versions.toml',
20
78
  updater: updateTomlVersions,
21
79
  label: 'toml',
80
+ changeKey: tomlChangeKey
22
81
  },
23
82
  ];
24
83
  /**
25
- * Discovers supported manifest files recursively within a directory.
26
- * @param {string} dirPath - absolute path to the directory
27
- * @returns {string[]} array of absolute paths to supported manifest files
84
+ * Returns the manifest type descriptor for a given filename, or null if unsupported.
85
+ * @param {string} basename - the file name to check
86
+ * @returns {ManifestType|null}
87
+ */
88
+ function getManifestType(basename) {
89
+ return MANIFEST_TYPES.find(t => t.test(basename)) || null;
90
+ }
91
+ /**
92
+ * Resolves a target path to the supported manifest files it contains: the single file
93
+ * if `targetPath` is a supported manifest, or every supported manifest discovered
94
+ * recursively if it's a directory. This is the single source of truth for what
95
+ * `runRemediation` will scan, so callers (e.g. the CLI) can reuse it to tell
96
+ * "no manifests here" apart from "manifests, but nothing to fix".
97
+ * @param {string} targetPath - path to a manifest file or directory
98
+ * @returns {string[]} absolute paths to supported manifests (empty for an empty directory)
99
+ * @throws if the path does not exist, or is a file of an unsupported manifest type
28
100
  */
29
- function discoverManifests(dirPath) {
101
+ export function findManifests(targetPath) {
102
+ const resolvedPath = path.resolve(targetPath);
103
+ let stat;
104
+ try {
105
+ stat = fs.statSync(resolvedPath);
106
+ }
107
+ catch {
108
+ throw new Error(`Path not found: ${resolvedPath}`);
109
+ }
110
+ // A single file must itself be a supported manifest.
111
+ if (!stat.isDirectory()) {
112
+ const basename = path.basename(resolvedPath);
113
+ if (!getManifestType(basename)) {
114
+ throw new Error(`Unsupported manifest type: ${basename}`);
115
+ }
116
+ return [resolvedPath];
117
+ }
118
+ // A directory yields every supported manifest found recursively beneath it.
30
119
  const manifests = [];
31
120
  function walk(dir) {
32
- const entries = fs.readdirSync(dir, { withFileTypes: true });
33
- for (const entry of entries) {
121
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
34
122
  if (SKIP_DIRS.has(entry.name)) {
35
123
  continue;
36
124
  }
@@ -43,17 +131,9 @@ function discoverManifests(dirPath) {
43
131
  }
44
132
  }
45
133
  }
46
- walk(dirPath);
134
+ walk(resolvedPath);
47
135
  return manifests;
48
136
  }
49
- /**
50
- * Returns the manifest type descriptor for a given filename, or null if unsupported.
51
- * @param {string} basename - the file name to check
52
- * @returns {{test: function, updater: function, label: string}|null}
53
- */
54
- function getManifestType(basename) {
55
- return MANIFEST_TYPES.find(t => t.test(basename)) || null;
56
- }
57
137
  /**
58
138
  * Orchestrates the full remediation pipeline for a single manifest or directory:
59
139
  * discover manifests → scan via DA backend → extract remediations → apply or preview.
@@ -63,38 +143,31 @@ function getManifestType(basename) {
63
143
  * @param {boolean} [options.dryRun=false] - preview changes without modifying files (applies by default)
64
144
  * @param {string} [options.providers] - comma-separated provider list
65
145
  * @param {string} [options.sources] - comma-separated source list
66
- * @param {'dependency'|'bundle'} [options.groupBy='dependency'] - report grouping strategy
67
- * @returns {Promise<{exitCode: number, output: string}>}
146
+ * @param {string} [options.backendUrl] - Trustify DA backend URL
147
+ * @param {boolean} [options.perDependencyChanges=false] - when true, each remediation is populated with
148
+ * a `changes` array describing the isolated, single-dependency edit (see {@link DependencyFix}). This lets
149
+ * callers create one commit/PR per dependency without attributing diff hunks themselves.
150
+ * @returns {Promise<{exitCode: number, output: string, remediations: Remediation[], manifests: string[], appliedFiles: string[]}>}
151
+ * exitCode is 2 for a dry-run that found remediations (nothing written), 0 otherwise. `remediations`
152
+ * is the structured, per-manifest list of applicable updates — each entry carries the originating
153
+ * manifest path(s) in `files` so callers can group and create per-dependency changes. `appliedFiles`
154
+ * lists only the manifests actually written to disk (empty on a dry-run), so callers can report a
155
+ * truthful "updated N files" count without conflating "had remediations" with "was written".
68
156
  */
69
157
  export async function runRemediation(targetPath, options = {}) {
70
- const { dryRun = false, providers, sources, groupBy = 'dependency' } = options;
71
- const resolvedPath = path.resolve(targetPath);
72
- let manifestPaths;
73
- let stat;
74
- try {
75
- stat = fs.statSync(resolvedPath);
76
- }
77
- catch {
78
- throw new Error(`Path not found: ${resolvedPath}`);
79
- }
80
- if (stat.isDirectory()) {
81
- manifestPaths = discoverManifests(resolvedPath);
82
- if (manifestPaths.length === 0) {
83
- return { exitCode: 0, output: 'No supported manifest files found.' };
84
- }
85
- }
86
- else {
87
- const basename = path.basename(resolvedPath);
88
- if (!getManifestType(basename)) {
89
- throw new Error(`Unsupported manifest type: ${basename}`);
90
- }
91
- manifestPaths = [resolvedPath];
158
+ const { dryRun = false, providers, sources, perDependencyChanges = false, backendUrl } = options;
159
+ const manifestPaths = findManifests(targetPath);
160
+ if (manifestPaths.length === 0) {
161
+ return { exitCode: 0, remediations: [], manifests: manifestPaths, appliedFiles: [] };
92
162
  }
93
163
  const opts = {};
94
- if (providers) {
164
+ if (backendUrl !== undefined) {
165
+ opts.TRUSTIFY_DA_BACKEND_URL = backendUrl;
166
+ }
167
+ if (providers !== undefined) {
95
168
  opts.TRUSTIFY_DA_PROVIDERS = providers;
96
169
  }
97
- if (sources) {
170
+ if (sources !== undefined) {
98
171
  opts.TRUSTIFY_DA_SOURCES = sources;
99
172
  }
100
173
  const url = selectTrustifyDABackend(opts);
@@ -120,15 +193,52 @@ export async function runRemediation(targetPath, options = {}) {
120
193
  if (remediations.length === 0) {
121
194
  continue;
122
195
  }
123
- allRemediations.push(...remediations);
196
+ // Tag each remediation with the manifest it came from so callers can group
197
+ // changes per dependency across a multi-manifest workspace.
198
+ for (const remediation of remediations) {
199
+ remediation.files = [manifestPath];
200
+ }
201
+ // Read the pristine manifest once. Both the atomic apply and the per-dependency
202
+ // change computation must diff against the *original* content.
203
+ const needsContent = perDependencyChanges || !dryRun;
204
+ const originalContent = needsContent ? fs.readFileSync(manifestPath, 'utf-8') : null;
205
+ if (perDependencyChanges) {
206
+ // With per-dependency changes every returned remediation must carry an isolated
207
+ // edit. A dependency the updater cannot locate in this manifest — e.g. a vulnerable
208
+ // *transitive* dependency surfaced by analysis but not declared here — yields no
209
+ // applied change, so it is dropped rather than returned with an absent `changes`
210
+ // array (which would crash callers that iterate `remediation.changes` to build PRs).
211
+ const applicable = [];
212
+ for (const remediation of remediations) {
213
+ const isolated = manifestType.updater(originalContent, [{
214
+ groupId: remediation.groupId,
215
+ artifactId: remediation.artifactId,
216
+ newVersion: remediation.fixedInVersion,
217
+ }]);
218
+ if (isolated.applied.length === 0) {
219
+ continue;
220
+ }
221
+ remediation.changes = [{
222
+ path: manifestPath,
223
+ after: isolated.content,
224
+ changeKey: manifestType.changeKey(manifestPath, isolated.applied[0])
225
+ }];
226
+ applicable.push(remediation);
227
+ }
228
+ allRemediations.push(...applicable);
229
+ }
230
+ else {
231
+ allRemediations.push(...remediations);
232
+ }
124
233
  if (!dryRun) {
125
- const content = fs.readFileSync(manifestPath, 'utf-8');
126
- const versionChanges = remediations.map(r => ({
234
+ // Disk receives the union of every dependency's fix. When
235
+ // perDependencyChanges is also set, each remediation's `changes[].after`
236
+ // deliberately stays isolated (single-dep) for branch-per-dep workflows.
237
+ const result = manifestType.updater(originalContent, remediations.map(r => ({
127
238
  groupId: r.groupId,
128
239
  artifactId: r.artifactId,
129
240
  newVersion: r.fixedInVersion,
130
- }));
131
- const result = manifestType.updater(content, versionChanges);
241
+ })));
132
242
  if (result.applied.length > 0) {
133
243
  fs.writeFileSync(manifestPath, result.content, 'utf-8');
134
244
  appliedFiles.push(manifestPath);
@@ -136,14 +246,8 @@ export async function runRemediation(targetPath, options = {}) {
136
246
  }
137
247
  }
138
248
  if (allRemediations.length === 0) {
139
- return { exitCode: 0, output: 'No remediations found.' };
140
- }
141
- const report = generateReport(allRemediations, { groupBy });
142
- if (dryRun) {
143
- return { exitCode: 2, output: report };
249
+ return { exitCode: 0, remediations: [], manifests: manifestPaths, appliedFiles };
144
250
  }
145
- const summary = appliedFiles.length > 0
146
- ? `Updated ${appliedFiles.length} file(s):\n${appliedFiles.map(f => ` ${f}`).join('\n')}\n\n${report}`
147
- : report;
148
- return { exitCode: 0, output: summary };
251
+ // Dry-run signals "changes available but not written" via exit code 2.
252
+ return { exitCode: dryRun ? 2 : 0, remediations: allRemediations, manifests: manifestPaths, appliedFiles };
149
253
  }
@@ -6,18 +6,18 @@
6
6
  * version selection strategy to resolve conflicts. Dependencies with no remediation
7
7
  * data are skipped.
8
8
  *
9
- * @param {object} analysisReport - raw DA AnalysisReport JSON response
9
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts').AnalysisReport} analysisReport - raw DA AnalysisReport JSON response
10
10
  * @param {object} [options] - extraction options
11
11
  * @param {string[]} [options.providerPriority] - provider names in descending priority order.
12
12
  * The first entry has the highest priority. Providers not listed share the lowest priority.
13
13
  * When omitted or empty, all providers are treated equally and the highest fix version wins.
14
- * @param {object} [options.versionStrategy] - version selection strategy with selectVersion
14
+ * @param {VersionStrategy} [options.versionStrategy] - version selection strategy with selectVersion
15
15
  * and resolveConflict methods. Defaults to closestCoverageStrategy.
16
16
  * @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[]}>}
17
17
  */
18
- export function extractRemediations(analysisReport: object, options?: {
18
+ export function extractRemediations(analysisReport: import("@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts").AnalysisReport, options?: {
19
19
  providerPriority?: string[] | undefined;
20
- versionStrategy?: object | undefined;
20
+ versionStrategy?: VersionStrategy | undefined;
21
21
  }): Array<{
22
22
  purl: string;
23
23
  groupId: string;
@@ -34,24 +34,25 @@ export function extractRemediations(analysisReport: object, options?: {
34
34
  severity: string;
35
35
  cves: string[];
36
36
  }>;
37
+ /**
38
+ * @typedef {{selectVersion: (fixedInVersions: string[], currentVersion: string) => string, resolveConflict: (existing: ConflictCandidate, candidate: ConflictCandidate) => 'existing'|'candidate'}} VersionStrategy
39
+ */
37
40
  /**
38
41
  * Version selection strategy that prefers the closest compatible version
39
42
  * within the same major version stream. Falls back to the lowest cross-major
40
43
  * version when no same-major option exists.
41
- * @type {{selectVersion: function(string[], string): string, resolveConflict: function(object, object): string}}
44
+ * @type {VersionStrategy}
42
45
  */
43
- export const closestCoverageStrategy: {
44
- selectVersion: (arg0: string[], arg1: string) => string;
45
- resolveConflict: (arg0: object, arg1: object) => string;
46
- };
46
+ export const closestCoverageStrategy: VersionStrategy;
47
47
  /**
48
48
  * Version selection strategy that always picks the highest version regardless
49
49
  * of major version distance. Guarantees maximum CVE coverage but may produce
50
50
  * large version jumps. This is the original behavior before pluggable strategies.
51
- * @type {{selectVersion: function(string[], string): string, resolveConflict: function(object, object): string}}
51
+ * @type {VersionStrategy}
52
52
  */
53
- export const highestStrategy: {
54
- selectVersion: (arg0: string[], arg1: string) => string;
55
- resolveConflict: (arg0: object, arg1: object) => string;
56
- };
53
+ export const highestStrategy: VersionStrategy;
57
54
  export const SEVERITY_ORDER: string[];
55
+ export type VersionStrategy = {
56
+ selectVersion: (fixedInVersions: string[], currentVersion: string) => string;
57
+ resolveConflict: (existing: ConflictCandidate, candidate: ConflictCandidate) => "existing" | "candidate";
58
+ };
@@ -7,11 +7,14 @@ import { PackageURL } from 'packageurl-js';
7
7
  function getMajorVersion(version) {
8
8
  return version.split('.')[0] || '';
9
9
  }
10
+ /**
11
+ * @typedef {{selectVersion: (fixedInVersions: string[], currentVersion: string) => string, resolveConflict: (existing: ConflictCandidate, candidate: ConflictCandidate) => 'existing'|'candidate'}} VersionStrategy
12
+ */
10
13
  /**
11
14
  * Version selection strategy that prefers the closest compatible version
12
15
  * within the same major version stream. Falls back to the lowest cross-major
13
16
  * version when no same-major option exists.
14
- * @type {{selectVersion: function(string[], string): string, resolveConflict: function(object, object): string}}
17
+ * @type {VersionStrategy}
15
18
  */
16
19
  export const closestCoverageStrategy = {
17
20
  selectVersion(fixedInVersions, currentVersion) {
@@ -47,7 +50,7 @@ export const closestCoverageStrategy = {
47
50
  * Version selection strategy that always picks the highest version regardless
48
51
  * of major version distance. Guarantees maximum CVE coverage but may produce
49
52
  * large version jumps. This is the original behavior before pluggable strategies.
50
- * @type {{selectVersion: function(string[], string): string, resolveConflict: function(object, object): string}}
53
+ * @type {VersionStrategy}
51
54
  */
52
55
  export const highestStrategy = {
53
56
  selectVersion(fixedInVersions) {
@@ -73,12 +76,12 @@ export const highestStrategy = {
73
76
  * version selection strategy to resolve conflicts. Dependencies with no remediation
74
77
  * data are skipped.
75
78
  *
76
- * @param {object} analysisReport - raw DA AnalysisReport JSON response
79
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts').AnalysisReport} analysisReport - raw DA AnalysisReport JSON response
77
80
  * @param {object} [options] - extraction options
78
81
  * @param {string[]} [options.providerPriority] - provider names in descending priority order.
79
82
  * The first entry has the highest priority. Providers not listed share the lowest priority.
80
83
  * 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
84
+ * @param {VersionStrategy} [options.versionStrategy] - version selection strategy with selectVersion
82
85
  * and resolveConflict methods. Defaults to closestCoverageStrategy.
83
86
  * @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[]}>}
84
87
  */
@@ -88,6 +91,7 @@ export function extractRemediations(analysisReport, options = {}) {
88
91
  }
89
92
  const priorityMap = buildPriorityMap(options.providerPriority);
90
93
  const strategy = options.versionStrategy || closestCoverageStrategy;
94
+ /** @type {Map<string, Remediation & { _fromTrustedContent?: boolean}>} */
91
95
  const remediationsByDep = new Map();
92
96
  const rankByDep = new Map();
93
97
  for (const [providerName, providerReport] of Object.entries(analysisReport.providers)) {
@@ -120,12 +124,12 @@ function buildPriorityMap(providerPriority) {
120
124
  }
121
125
  /**
122
126
  * Extracts remediations from the sources/dependencies/issues tree of a provider report.
123
- * @param {object} providerReport
127
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/ProviderReport.js').ProviderReport} providerReport
124
128
  * @param {string} providerName
125
129
  * @param {number} providerRank - numeric priority rank for this provider
126
- * @param {Map<string, object>} remediationsByDep - accumulator keyed by dependency PURL
130
+ * @param {Map<string, Remediation & { _fromTrustedContent?: boolean }>} remediationsByDep - accumulator keyed by dependency PURL
127
131
  * @param {Map<string, number>} rankByDep - tracks current winning rank per dependency
128
- * @param {object} strategy - version selection strategy
132
+ * @param {VersionStrategy} strategy - version selection strategy
129
133
  */
130
134
  function extractFromSources(providerReport, providerName, providerRank, remediationsByDep, rankByDep, strategy) {
131
135
  if (!providerReport.sources) {
@@ -147,14 +151,14 @@ function extractFromSources(providerReport, providerName, providerRank, remediat
147
151
  }
148
152
  /**
149
153
  * 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
154
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/Issue.js').Issue} issue - issue object containing remediation and CVE data
155
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/DependencyReport.js').DependencyReport} dep - dependency object containing the ref PURL
152
156
  * @param {string} providerName
153
157
  * @param {string} sourceName
154
158
  * @param {number} providerRank
155
- * @param {Map<string, object>} remediationsByDep
159
+ * @param {Map<string, Remediation & { _fromTrustedContent?: boolean }>} remediationsByDep
156
160
  * @param {Map<string, number>} rankByDep
157
- * @param {object} strategy - version selection strategy
161
+ * @param {VersionStrategy} strategy - version selection strategy
158
162
  */
159
163
  function processIssueRemediation(issue, dep, providerName, sourceName, providerRank, remediationsByDep, rankByDep, strategy) {
160
164
  const depPurl = dep.ref;
@@ -185,7 +189,7 @@ function processIssueRemediation(issue, dep, providerName, sourceName, providerR
185
189
  if (!fixedInVersion) {
186
190
  return;
187
191
  }
188
- const cveId = issue.id || issue.cve;
192
+ const cveId = issue.id;
189
193
  const severity = issue.severity || 'UNKNOWN';
190
194
  const advisories = extractAdvisories(issue);
191
195
  const existing = remediationsByDep.get(depPurl);
@@ -236,10 +240,10 @@ function processIssueRemediation(issue, dep, providerName, sourceName, providerR
236
240
  /**
237
241
  * Extracts remediations from the recommendations section of a provider report.
238
242
  * Merges CVEs and advisories into existing entries when present.
239
- * @param {object} providerReport
243
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/ProviderReport.js').ProviderReport} providerReport
240
244
  * @param {string} providerName
241
245
  * @param {number} providerRank
242
- * @param {Map<string, object>} remediationsByDep
246
+ * @param {Map<string, Remediation & { _fromTrustedContent?: boolean }>} remediationsByDep
243
247
  * @param {Map<string, number>} rankByDep
244
248
  */
245
249
  function extractFromRecommendations(providerReport, providerName, providerRank, remediationsByDep, rankByDep) {
@@ -308,9 +312,9 @@ function extractFromRecommendations(providerReport, providerName, providerRank,
308
312
  * Gets the fixedIn PURL from an issue's remediation, preferring trustedContent.
309
313
  * When fixedIn is an array of version strings (not PURLs), uses the strategy's
310
314
  * selectVersion to pick the best candidate and constructs a PURL from the dependency ref.
311
- * @param {object} issue
315
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/Issue.js').Issue} issue
312
316
  * @param {string} depPurl - the dependency PURL, used to construct fixedIn PURLs from version strings
313
- * @param {object} strategy - version selection strategy
317
+ * @param {VersionStrategy} strategy - version selection strategy
314
318
  * @param {string} currentVersion - the dependency's current version
315
319
  * @returns {string|undefined}
316
320
  */
@@ -318,17 +322,11 @@ function getFixedInPurl(issue, depPurl, strategy, currentVersion) {
318
322
  if (!issue.remediation) {
319
323
  return undefined;
320
324
  }
321
- if (issue.remediation.trustedContent && issue.remediation.trustedContent.ref) {
325
+ if (issue.remediation.trustedContent?.ref) {
322
326
  return issue.remediation.trustedContent.ref;
323
327
  }
324
328
  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) {
329
+ if (fixedIn?.length > 0) {
332
330
  const version = fixedIn.length > 1
333
331
  ? strategy.selectVersion(fixedIn, currentVersion)
334
332
  : fixedIn[0];
@@ -350,25 +348,16 @@ function getFixedInPurl(issue, depPurl, strategy, currentVersion) {
350
348
  }
351
349
  /**
352
350
  * Extracts advisory objects from an issue.
353
- * @param {object} issue
351
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/Issue.js').Issue} issue
354
352
  * @returns {Array<{id: string, url: string}>}
355
353
  */
356
354
  function extractAdvisories(issue) {
357
355
  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) {
356
+ for (const advisory of issue.remediation?.advisories ?? []) {
357
+ if (advisory.advisory?.id) {
369
358
  advisories.push({
370
- id: adv.id || adv,
371
- url: adv.url || '',
359
+ id: advisory.advisory.id,
360
+ url: advisory.advisory.url || '',
372
361
  });
373
362
  }
374
363
  }