@trustify-da/trustify-da-javascript-client 0.3.0-ea.e660b02 → 0.3.0-ea.efdc87d

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 (77) hide show
  1. package/README.md +199 -11
  2. package/dist/package.json +29 -25
  3. package/dist/src/analysis.d.ts +50 -22
  4. package/dist/src/analysis.js +70 -18
  5. package/dist/src/cli.js +188 -6
  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 +27 -22
  11. package/dist/src/index.js +27 -38
  12. package/dist/src/license/index.d.ts +4 -4
  13. package/dist/src/license/index.js +13 -7
  14. package/dist/src/license/license_utils.js +4 -1
  15. package/dist/src/license/licenses_api.d.ts +3 -3
  16. package/dist/src/license/licenses_api.js +1 -1
  17. package/dist/src/oci_image/images.d.ts +1 -1
  18. package/dist/src/oci_image/images.js +15 -1
  19. package/dist/src/oci_image/utils.d.ts +5 -5
  20. package/dist/src/package_version.d.ts +8 -0
  21. package/dist/src/package_version.js +31 -0
  22. package/dist/src/provider.d.ts +2 -2
  23. package/dist/src/provider.js +3 -1
  24. package/dist/src/providers/base_java.d.ts +55 -2
  25. package/dist/src/providers/base_java.js +64 -17
  26. package/dist/src/providers/base_javascript.d.ts +63 -9
  27. package/dist/src/providers/base_javascript.js +106 -4
  28. package/dist/src/providers/base_pyproject.d.ts +5 -5
  29. package/dist/src/providers/containerfile_parser.d.ts +5 -0
  30. package/dist/src/providers/containerfile_parser.js +20 -0
  31. package/dist/src/providers/golang_gomodules.d.ts +1 -1
  32. package/dist/src/providers/golang_gomodules.js +49 -7
  33. package/dist/src/providers/java_gradle.d.ts +48 -0
  34. package/dist/src/providers/java_gradle.js +196 -18
  35. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  36. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  37. package/dist/src/providers/java_maven.d.ts +29 -9
  38. package/dist/src/providers/java_maven.js +104 -10
  39. package/dist/src/providers/javascript_bun.d.ts +12 -0
  40. package/dist/src/providers/javascript_bun.js +42 -1
  41. package/dist/src/providers/javascript_npm.d.ts +19 -1
  42. package/dist/src/providers/javascript_npm.js +40 -1
  43. package/dist/src/providers/javascript_pnpm.d.ts +13 -0
  44. package/dist/src/providers/javascript_pnpm.js +47 -1
  45. package/dist/src/providers/javascript_yarn.d.ts +12 -0
  46. package/dist/src/providers/javascript_yarn.js +75 -2
  47. package/dist/src/providers/manifest.js +6 -3
  48. package/dist/src/providers/oci_dockerfile.d.ts +51 -0
  49. package/dist/src/providers/oci_dockerfile.js +177 -0
  50. package/dist/src/providers/processors/yarn_berry_processor.d.ts +6 -2
  51. package/dist/src/providers/processors/yarn_berry_processor.js +3 -2
  52. package/dist/src/providers/processors/yarn_classic_processor.d.ts +6 -2
  53. package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
  54. package/dist/src/providers/python_controller.js +4 -1
  55. package/dist/src/providers/python_poetry.d.ts +1 -1
  56. package/dist/src/providers/python_uv.d.ts +1 -1
  57. package/dist/src/providers/requirements_parser.js +1 -1
  58. package/dist/src/providers/rust_cargo.d.ts +1 -1
  59. package/dist/src/providers/rust_cargo.js +70 -29
  60. package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
  61. package/dist/src/remediate.d.ts +120 -0
  62. package/dist/src/remediate.js +253 -0
  63. package/dist/src/remediation.d.ts +58 -0
  64. package/dist/src/remediation.js +425 -0
  65. package/dist/src/remediation_report.d.ts +40 -0
  66. package/dist/src/remediation_report.js +159 -0
  67. package/dist/src/sbom.d.ts +11 -0
  68. package/dist/src/sbom.js +10 -0
  69. package/dist/src/tools.d.ts +11 -13
  70. package/dist/src/tools.js +34 -13
  71. package/dist/src/updaters/maven_updater.d.ts +58 -0
  72. package/dist/src/updaters/maven_updater.js +345 -0
  73. package/dist/src/updaters/toml_updater.d.ts +62 -0
  74. package/dist/src/updaters/toml_updater.js +276 -0
  75. package/dist/src/workspace.d.ts +2 -1
  76. package/dist/src/workspace.js +2 -1
  77. package/package.json +30 -26
@@ -0,0 +1,253 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import analysis from './analysis.js';
4
+ import { availableProviders, match } from './provider.js';
5
+ import { extractRemediations } from './remediation.js';
6
+ import { mavenChangeKey, updateMavenVersions } from './updaters/maven_updater.js';
7
+ import { tomlChangeKey, updateTomlVersions } from './updaters/toml_updater.js';
8
+ import { selectTrustifyDABackend } from './index.js';
9
+ // Mirrors DEFAULT_WORKSPACE_DISCOVERY_IGNORE in workspace.js
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[]} */
69
+ const MANIFEST_TYPES = [
70
+ {
71
+ test: (basename) => basename === 'pom.xml',
72
+ updater: updateMavenVersions,
73
+ label: 'maven',
74
+ changeKey: mavenChangeKey
75
+ },
76
+ {
77
+ test: (basename) => basename.endsWith('.versions.toml') || basename === 'libs.versions.toml',
78
+ updater: updateTomlVersions,
79
+ label: 'toml',
80
+ changeKey: tomlChangeKey
81
+ },
82
+ ];
83
+ /**
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
100
+ */
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.
119
+ const manifests = [];
120
+ function walk(dir) {
121
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
122
+ if (SKIP_DIRS.has(entry.name)) {
123
+ continue;
124
+ }
125
+ const fullPath = path.join(dir, entry.name);
126
+ if (entry.isDirectory()) {
127
+ walk(fullPath);
128
+ }
129
+ else if (getManifestType(entry.name)) {
130
+ manifests.push(fullPath);
131
+ }
132
+ }
133
+ }
134
+ walk(resolvedPath);
135
+ return manifests;
136
+ }
137
+ /**
138
+ * Orchestrates the full remediation pipeline for a single manifest or directory:
139
+ * discover manifests → scan via DA backend → extract remediations → apply or preview.
140
+ *
141
+ * @param {string} targetPath - path to a manifest file or directory
142
+ * @param {object} [options]
143
+ * @param {boolean} [options.dryRun=false] - preview changes without modifying files (applies by default)
144
+ * @param {string} [options.providers] - comma-separated provider list
145
+ * @param {string} [options.sources] - comma-separated source list
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".
156
+ */
157
+ export async function runRemediation(targetPath, options = {}) {
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: [] };
162
+ }
163
+ const opts = {};
164
+ if (backendUrl !== undefined) {
165
+ opts.TRUSTIFY_DA_BACKEND_URL = backendUrl;
166
+ }
167
+ if (providers !== undefined) {
168
+ opts.TRUSTIFY_DA_PROVIDERS = providers;
169
+ }
170
+ if (sources !== undefined) {
171
+ opts.TRUSTIFY_DA_SOURCES = sources;
172
+ }
173
+ const url = selectTrustifyDABackend(opts);
174
+ const allRemediations = [];
175
+ const appliedFiles = [];
176
+ for (const manifestPath of manifestPaths) {
177
+ const basename = path.basename(manifestPath);
178
+ const manifestType = getManifestType(basename);
179
+ if (!manifestType) {
180
+ continue;
181
+ }
182
+ let provider;
183
+ try {
184
+ provider = match(manifestPath, availableProviders, opts);
185
+ }
186
+ catch {
187
+ continue;
188
+ }
189
+ const analysisReport = await analysis.requestStack(provider, manifestPath, url, false, opts);
190
+ const remediations = extractRemediations(analysisReport, {
191
+ providerPriority: providers ? providers.split(',').map(p => p.trim()).filter(Boolean) : undefined,
192
+ });
193
+ if (remediations.length === 0) {
194
+ continue;
195
+ }
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
+ }
233
+ if (!dryRun) {
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 => ({
238
+ groupId: r.groupId,
239
+ artifactId: r.artifactId,
240
+ newVersion: r.fixedInVersion,
241
+ })));
242
+ if (result.applied.length > 0) {
243
+ fs.writeFileSync(manifestPath, result.content, 'utf-8');
244
+ appliedFiles.push(manifestPath);
245
+ }
246
+ }
247
+ }
248
+ if (allRemediations.length === 0) {
249
+ return { exitCode: 0, remediations: [], manifests: manifestPaths, appliedFiles };
250
+ }
251
+ // Dry-run signals "changes available but not written" via exit code 2.
252
+ return { exitCode: dryRun ? 2 : 0, remediations: allRemediations, manifests: manifestPaths, appliedFiles };
253
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Extracts actionable remediation instructions from a DA AnalysisReport response.
3
+ *
4
+ * Walks the provider/source/dependency/issue tree, collects fixedIn and trustedContent
5
+ * remediation data, applies provider priority resolution, and uses the configured
6
+ * version selection strategy to resolve conflicts. Dependencies with no remediation
7
+ * data are skipped.
8
+ *
9
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts').AnalysisReport} analysisReport - raw DA AnalysisReport JSON response
10
+ * @param {object} [options] - extraction options
11
+ * @param {string[]} [options.providerPriority] - provider names in descending priority order.
12
+ * The first entry has the highest priority. Providers not listed share the lowest priority.
13
+ * When omitted or empty, all providers are treated equally and the highest fix version wins.
14
+ * @param {VersionStrategy} [options.versionStrategy] - version selection strategy with selectVersion
15
+ * and resolveConflict methods. Defaults to closestCoverageStrategy.
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
+ */
18
+ export function extractRemediations(analysisReport: import("@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts").AnalysisReport, options?: {
19
+ providerPriority?: string[] | undefined;
20
+ versionStrategy?: VersionStrategy | undefined;
21
+ }): Array<{
22
+ purl: string;
23
+ groupId: string;
24
+ artifactId: string;
25
+ currentVersion: string;
26
+ fixedInVersion: string;
27
+ fixedInPurl: string;
28
+ provider: string;
29
+ source: string;
30
+ advisories: Array<{
31
+ id: string;
32
+ url: string;
33
+ }>;
34
+ severity: string;
35
+ cves: string[];
36
+ }>;
37
+ /**
38
+ * @typedef {{selectVersion: (fixedInVersions: string[], currentVersion: string) => string, resolveConflict: (existing: ConflictCandidate, candidate: ConflictCandidate) => 'existing'|'candidate'}} VersionStrategy
39
+ */
40
+ /**
41
+ * Version selection strategy that prefers the closest compatible version
42
+ * within the same major version stream. Falls back to the lowest cross-major
43
+ * version when no same-major option exists.
44
+ * @type {VersionStrategy}
45
+ */
46
+ export const closestCoverageStrategy: VersionStrategy;
47
+ /**
48
+ * Version selection strategy that always picks the highest version regardless
49
+ * of major version distance. Guarantees maximum CVE coverage but may produce
50
+ * large version jumps. This is the original behavior before pluggable strategies.
51
+ * @type {VersionStrategy}
52
+ */
53
+ export const highestStrategy: VersionStrategy;
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
+ };