@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.
- package/README.md +22 -8
- package/dist/package.json +0 -3
- package/dist/src/analysis.d.ts +40 -19
- package/dist/src/analysis.js +41 -5
- package/dist/src/cli.js +95 -17
- package/dist/src/config.d.ts +120 -0
- package/dist/src/config.js +260 -0
- package/dist/src/index.d.ts +22 -21
- package/dist/src/index.js +15 -13
- package/dist/src/license/index.d.ts +2 -2
- package/dist/src/license/index.js +9 -6
- package/dist/src/license/licenses_api.d.ts +2 -2
- package/dist/src/license/licenses_api.js +1 -1
- package/dist/src/providers/base_javascript.js +1 -1
- package/dist/src/providers/rust_cargo.js +3 -3
- package/dist/src/remediate.d.ts +102 -3
- package/dist/src/remediate.js +162 -58
- package/dist/src/remediation.d.ts +15 -14
- package/dist/src/remediation.js +27 -38
- package/dist/src/tools.d.ts +9 -11
- package/dist/src/tools.js +10 -12
- package/dist/src/updaters/maven_updater.d.ts +40 -8
- package/dist/src/updaters/maven_updater.js +26 -3
- package/dist/src/updaters/toml_updater.d.ts +42 -13
- package/dist/src/updaters/toml_updater.js +36 -4
- package/dist/src/workspace.d.ts +2 -1
- package/dist/src/workspace.js +2 -1
- package/package.json +1 -4
package/dist/src/remediate.d.ts
CHANGED
|
@@ -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 {
|
|
11
|
-
* @
|
|
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
|
-
|
|
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
|
+
};
|
package/dist/src/remediate.js
CHANGED
|
@@ -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 {
|
|
7
|
-
import {
|
|
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
|
-
*
|
|
26
|
-
* @param {string}
|
|
27
|
-
* @returns {
|
|
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
|
|
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
|
|
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(
|
|
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 {
|
|
67
|
-
* @
|
|
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,
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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,
|
|
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
|
-
|
|
146
|
-
|
|
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 {
|
|
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 {
|
|
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:
|
|
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?:
|
|
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 {
|
|
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 {
|
|
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
|
+
};
|
package/dist/src/remediation.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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,
|
|
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 {
|
|
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 {
|
|
151
|
-
* @param {
|
|
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,
|
|
159
|
+
* @param {Map<string, Remediation & { _fromTrustedContent?: boolean }>} remediationsByDep
|
|
156
160
|
* @param {Map<string, number>} rankByDep
|
|
157
|
-
* @param {
|
|
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
|
|
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 {
|
|
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,
|
|
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 {
|
|
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 {
|
|
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
|
|
325
|
+
if (issue.remediation.trustedContent?.ref) {
|
|
322
326
|
return issue.remediation.trustedContent.ref;
|
|
323
327
|
}
|
|
324
328
|
const fixedIn = issue.remediation.fixedIn;
|
|
325
|
-
if (
|
|
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 {
|
|
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
|
-
|
|
359
|
-
|
|
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:
|
|
371
|
-
url:
|
|
359
|
+
id: advisory.advisory.id,
|
|
360
|
+
url: advisory.advisory.url || '',
|
|
372
361
|
});
|
|
373
362
|
}
|
|
374
363
|
}
|