@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
@@ -6,7 +6,25 @@ import { generateImageSBOM, parseImageRef } from "./oci_image/utils.js";
6
6
  import { addProxyAgent, getCustom, getTokenHeaders, TRUSTIFY_DA_OPERATION_TYPE_HEADER, TRUSTIFY_DA_PACKAGE_MANAGER_HEADER } from "./tools.js";
7
7
  /** Media type for CycloneDX JSON batch payloads (batch-analysis API). */
8
8
  export const CYCLONEDX_JSON_MEDIA_TYPE = 'application/vnd.cyclonedx+json';
9
- export default { requestComponent, requestStack, requestStackBatch, requestImages, validateToken };
9
+ export default { requestComponent, requestStack, requestStackBatch, requestImages, validateToken, appendAnalysisQueryParams };
10
+ /**
11
+ * @overload
12
+ * @param {import('./provider').Provider} provider
13
+ * @param {string} manifest
14
+ * @param {string} url
15
+ * @param {true} [html]
16
+ * @param {import("index.js").Options} [opts={}]
17
+ * @returns {Promise<string>}
18
+ */
19
+ /**
20
+ * @overload
21
+ * @param {import('./provider').Provider} provider
22
+ * @param {string} manifest
23
+ * @param {string} url
24
+ * @param {false} [html]
25
+ * @param {import("index.js").Options} [opts={}]
26
+ * @returns {Promise<import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts').AnalysisReport>>}
27
+ */
10
28
  /**
11
29
  * Send a stack analysis request and get the report as 'text/html' or 'application/json'.
12
30
  * @param {import('./provider').Provider} provider - the provided data for constructing the request
@@ -14,12 +32,15 @@ export default { requestComponent, requestStack, requestStackBatch, requestImage
14
32
  * @param {string} url - the backend url to send the request to
15
33
  * @param {boolean} [html=false] - true will return 'text/html', false will return 'application/json'
16
34
  * @param {import("index.js").Options} [opts={}] - optional various options to pass along the application
17
- * @returns {Promise<string|import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>}
35
+ * @returns {Promise<string|import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts').AnalysisReport>}
18
36
  */
19
37
  async function requestStack(provider, manifest, url, html = false, opts = {}) {
20
38
  opts["source-manifest"] = Buffer.from(fs.readFileSync(manifest).toString()).toString('base64');
21
39
  opts["manifest-type"] = path.parse(manifest).base;
22
40
  let provided = await provider.provideStack(manifest, opts); // throws error if content providing failed
41
+ if (provided.batch) {
42
+ return requestStackBatch(JSON.parse(provided.content), url, html, opts);
43
+ }
23
44
  opts["source-manifest"] = "";
24
45
  opts[TRUSTIFY_DA_OPERATION_TYPE_HEADER.toUpperCase().replaceAll("-", "_")] = "stack-analysis";
25
46
  let startTime = new Date();
@@ -38,9 +59,7 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
38
59
  body: provided.content
39
60
  }, opts);
40
61
  const finalUrl = new URL(`${url}/api/v5/analysis`);
41
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
42
- finalUrl.searchParams.append('recommend', 'false');
43
- }
62
+ appendAnalysisQueryParams(finalUrl, opts);
44
63
  let resp = await fetch(finalUrl, fetchOptions);
45
64
  let result;
46
65
  if (resp.status === 200) {
@@ -74,11 +93,14 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
74
93
  * @param {string} manifest - path for the manifest
75
94
  * @param {string} url - the backend url to send the request to
76
95
  * @param {import("index.js").Options} [opts={}] - optional various options to pass along the application
77
- * @returns {Promise<import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>}
96
+ * @returns {Promise<import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts').AnalysisReport>}
78
97
  */
79
98
  async function requestComponent(provider, manifest, url, opts = {}) {
80
99
  opts["source-manifest"] = Buffer.from(fs.readFileSync(manifest).toString()).toString('base64');
81
100
  let provided = await provider.provideComponent(manifest, opts); // throws error if content providing failed
101
+ if (provided.batch) {
102
+ return requestStackBatch(JSON.parse(provided.content), url, false, opts);
103
+ }
82
104
  opts["source-manifest"] = "";
83
105
  opts[TRUSTIFY_DA_OPERATION_TYPE_HEADER.toUpperCase().replaceAll("-", "_")] = "component-analysis";
84
106
  if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
@@ -95,9 +117,7 @@ async function requestComponent(provider, manifest, url, opts = {}) {
95
117
  body: provided.content
96
118
  }, opts);
97
119
  const finalUrl = new URL(`${url}/api/v5/analysis`);
98
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
99
- finalUrl.searchParams.append('recommend', 'false');
100
- }
120
+ appendAnalysisQueryParams(finalUrl, opts);
101
121
  let resp = await fetch(finalUrl, fetchOptions);
102
122
  let result;
103
123
  if (resp.status === 200) {
@@ -111,7 +131,9 @@ async function requestComponent(provider, manifest, url, opts = {}) {
111
131
  console.log(JSON.stringify(result, null, 4));
112
132
  console.log("Ending time of sending component analysis request to Trustify DA backend server= " + new Date());
113
133
  }
114
- const licenseCheckEnabled = getCustom('TRUSTIFY_DA_LICENSE_CHECK', 'true', opts) !== 'false' && opts.licenseCheck !== false;
134
+ const licenseCheckEnabled = typeof opts.licenseCheck === 'boolean'
135
+ ? opts.licenseCheck
136
+ : getCustom('TRUSTIFY_DA_LICENSE_CHECK', 'true', opts) !== 'false';
115
137
  if (licenseCheckEnabled) {
116
138
  try {
117
139
  result.licenseSummary = await runLicenseCheck(provided.content, manifest, url, opts, result);
@@ -126,19 +148,33 @@ async function requestComponent(provider, manifest, url, opts = {}) {
126
148
  }
127
149
  return Promise.resolve(result);
128
150
  }
151
+ /**
152
+ * @overload
153
+ * @param {Object.<string, object>} sbomByPurl
154
+ * @param {string} url
155
+ * @param {true} [html=false]
156
+ * @param {import("index.js").Options} [opts={}]
157
+ * @returns {Promise<string>}
158
+ */
159
+ /**
160
+ * @overload
161
+ * @param {Object.<string, object>} sbomByPurl
162
+ * @param {string} url
163
+ * @param {false} [html=false]
164
+ * @param {import("index.js").Options} [opts={}]
165
+ * @returns {Promise<Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts').AnalysisReport>>}
166
+ */
129
167
  /**
130
168
  * Send a batch stack analysis request for multiple manifests (SBOMs keyed by purl).
131
169
  * @param {Object.<string, object>} sbomByPurl - Map of root purl to CycloneDX SBOM object
132
170
  * @param {string} url - the backend url
133
171
  * @param {boolean} [html=false] - true returns HTML, false returns JSON
134
172
  * @param {import("index.js").Options} [opts={}]
135
- * @returns {Promise<string|Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>>}
173
+ * @returns {Promise<string|Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts').AnalysisReport>>}
136
174
  */
137
175
  async function requestStackBatch(sbomByPurl, url, html = false, opts = {}) {
138
176
  const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
139
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
140
- finalUrl.searchParams.append('recommend', 'false');
141
- }
177
+ appendAnalysisQueryParams(finalUrl, opts);
142
178
  const fetchOptions = addProxyAgent({
143
179
  method: 'POST',
144
180
  headers: {
@@ -177,7 +213,7 @@ async function requestStackBatch(sbomByPurl, url, html = false, opts = {}) {
177
213
  * @param {Array<string>} imageRefs
178
214
  * @param {string} url
179
215
  * @param {import("index.js").Options} [opts={}] - optional various options to pass along the application
180
- * @returns {Promise<string|Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>>}
216
+ * @returns {Promise<string|Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts').AnalysisReport>>}
181
217
  */
182
218
  async function requestImages(imageRefs, url, html = false, opts = {}) {
183
219
  const imageSboms = {};
@@ -186,9 +222,7 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
186
222
  imageSboms[parsedImageRef.getPackageURL().toString()] = generateImageSBOM(parsedImageRef, opts);
187
223
  }
188
224
  const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
189
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
190
- finalUrl.searchParams.append('recommend', 'false');
191
- }
225
+ appendAnalysisQueryParams(finalUrl, opts);
192
226
  const fetchOptions = addProxyAgent({
193
227
  method: 'POST',
194
228
  headers: {
@@ -222,6 +256,24 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
222
256
  throw new Error(`Got error response from Trustify DA backend - http return code : ${resp.status}, ex-request-id: ${resp.headers.get("ex-request-id")} error message => ${await resp.text()}`);
223
257
  }
224
258
  }
259
+ /**
260
+ * Append common analysis query parameters (recommend, providers, sources) to a URL.
261
+ * @param {URL} url - the URL object to append query parameters to
262
+ * @param {import("index.js").Options} [opts={}] - options containing parameter overrides
263
+ */
264
+ function appendAnalysisQueryParams(url, opts = {}) {
265
+ if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
266
+ url.searchParams.append('recommend', 'false');
267
+ }
268
+ const providers = getCustom('TRUSTIFY_DA_PROVIDERS', null, opts);
269
+ if (providers) {
270
+ url.searchParams.append('providers', providers);
271
+ }
272
+ const sources = getCustom('TRUSTIFY_DA_SOURCES', null, opts);
273
+ if (sources) {
274
+ url.searchParams.append('sources', sources);
275
+ }
276
+ }
225
277
  /**
226
278
  *
227
279
  * @param url the backend url to send the request to
package/dist/src/cli.js CHANGED
@@ -3,8 +3,45 @@ import fs from 'node:fs';
3
3
  import * as path from "path";
4
4
  import yargs from 'yargs';
5
5
  import { hideBin } from 'yargs/helpers';
6
+ import { resolveConfig } from './config.js';
6
7
  import { getProjectLicense, getLicenseDetails } from './license/index.js';
8
+ import { runRemediation } from './remediate.js';
9
+ import { generateReport } from './remediation_report.js';
7
10
  import client, { selectTrustifyDABackend, generateSbom } from './index.js';
11
+ /**
12
+ * Builds a yargs middleware that loads `.trustify-da.yml` (discovered by walking
13
+ * up from the command's target path, or cwd when no path is available) and fills `--providers`, `--sources`, and
14
+ * `--group-by` with config file values when they were not supplied on the CLI or
15
+ * via environment variables. Precedence: CLI flag > env var > config file > default.
16
+ * @param {string} [pathKey] - positional argument holding the target path; defaults to the current working directory
17
+ * @param {{ groupBy?: boolean }} [options={}] - set `groupBy` when the command exposes `--group-by`
18
+ * @returns {(args: object) => object} yargs middleware
19
+ */
20
+ function configMiddleware(pathKey, options = {}) {
21
+ return args => {
22
+ const startPath = pathKey === undefined ? process.cwd() : args[pathKey];
23
+ const envVars = options.groupBy
24
+ ? process.env
25
+ : Object.fromEntries(Object.entries(process.env).filter(([key]) => key !== 'TRUSTIFY_DA_GROUP_BY'));
26
+ const merged = resolveConfig(startPath, { backendUrl: args.backendUrl, providers: args.providers, sources: args.sources, groupBy: args['group-by'] }, envVars);
27
+ if (merged.backendUrlSource === 'file' && process.env.TRUSTIFY_DA_TOKEN) {
28
+ throw new Error('Refusing to send TRUSTIFY_DA_TOKEN to a backend selected by project configuration. Set --backend-url or TRUSTIFY_DA_BACKEND_URL to explicitly trust it.');
29
+ }
30
+ if (args.providers !== undefined || merged.providers.length) {
31
+ args.providers = merged.providers.join(',');
32
+ }
33
+ if (args.sources !== undefined || merged.sources.length) {
34
+ args.sources = merged.sources.join(',');
35
+ }
36
+ if (merged.backendUrl != null) {
37
+ args.backendUrl = merged.backendUrl;
38
+ }
39
+ if (options.groupBy) {
40
+ args['group-by'] = merged.groupBy;
41
+ }
42
+ return args;
43
+ };
44
+ }
8
45
  // command for component analysis take manifest type and content
9
46
  const component = {
10
47
  command: 'component </path/to/manifest>',
@@ -19,11 +56,32 @@ const component = {
19
56
  desc: 'Workspace root directory (for monorepos; lock file is expected here)',
20
57
  type: 'string',
21
58
  normalize: true,
59
+ },
60
+ backendUrl: {
61
+ desc: 'Trustify DA backend URL (env: TRUSTIFY_DA_BACKEND_URL)',
62
+ type: 'string',
63
+ },
64
+ providers: {
65
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
66
+ type: 'string',
67
+ },
68
+ sources: {
69
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
70
+ type: 'string',
22
71
  }
23
- }),
72
+ }).middleware(configMiddleware('/path/to/manifest')),
24
73
  handler: async (args) => {
25
74
  let manifestName = args['/path/to/manifest'];
26
75
  const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
76
+ if (args.backendUrl !== undefined) {
77
+ opts.TRUSTIFY_DA_BACKEND_URL = args.backendUrl;
78
+ }
79
+ if (args.providers !== undefined) {
80
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
81
+ }
82
+ if (args.sources !== undefined) {
83
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
84
+ }
27
85
  let res = await client.componentAnalysis(manifestName, opts);
28
86
  console.log(JSON.stringify(res, null, 2));
29
87
  }
@@ -72,8 +130,20 @@ const image = {
72
130
  desc: 'For JSON report, get only the \'summary\'',
73
131
  type: 'boolean',
74
132
  conflicts: 'html'
133
+ },
134
+ backendUrl: {
135
+ desc: 'Trustify DA backend URL (env: TRUSTIFY_DA_BACKEND_URL)',
136
+ type: 'string',
137
+ },
138
+ providers: {
139
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
140
+ type: 'string',
141
+ },
142
+ sources: {
143
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
144
+ type: 'string',
75
145
  }
76
- }),
146
+ }).middleware(configMiddleware()),
77
147
  handler: async (args) => {
78
148
  let imageRefs = args['image-refs'];
79
149
  if (!Array.isArray(imageRefs)) {
@@ -81,7 +151,17 @@ const image = {
81
151
  }
82
152
  let html = args['html'];
83
153
  let summary = args['summary'];
84
- let res = await client.imageAnalysis(imageRefs, html);
154
+ const opts = {};
155
+ if (args.backendUrl !== undefined) {
156
+ opts.TRUSTIFY_DA_BACKEND_URL = args.backendUrl;
157
+ }
158
+ if (args.providers !== undefined) {
159
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
160
+ }
161
+ if (args.sources !== undefined) {
162
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
163
+ }
164
+ let res = await client.imageAnalysis(imageRefs, html, opts);
85
165
  if (summary && !html) {
86
166
  let summaries = {};
87
167
  for (let [imageRef, report] of Object.entries(res)) {
@@ -132,13 +212,34 @@ const stack = {
132
212
  desc: 'Workspace root directory (for monorepos; lock file is expected here)',
133
213
  type: 'string',
134
214
  normalize: true,
215
+ },
216
+ backendUrl: {
217
+ desc: 'Trustify DA backend URL (env: TRUSTIFY_DA_BACKEND_URL)',
218
+ type: 'string',
219
+ },
220
+ providers: {
221
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
222
+ type: 'string',
223
+ },
224
+ sources: {
225
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
226
+ type: 'string',
135
227
  }
136
- }),
228
+ }).middleware(configMiddleware('/path/to/manifest')),
137
229
  handler: async (args) => {
138
230
  let manifest = args['/path/to/manifest'];
139
231
  let html = args['html'];
140
232
  let summary = args['summary'];
141
233
  const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
234
+ if (args.backendUrl !== undefined) {
235
+ opts.TRUSTIFY_DA_BACKEND_URL = args.backendUrl;
236
+ }
237
+ if (args.providers !== undefined) {
238
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
239
+ }
240
+ if (args.sources !== undefined) {
241
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
242
+ }
142
243
  let theProvidersSummary = new Map();
143
244
  let theProvidersObject = {};
144
245
  let res = await client.stackAnalysis(manifest, html, opts);
@@ -201,13 +302,28 @@ const stackBatch = {
201
302
  desc: 'Stop on first invalid package.json or SBOM error (env: TRUSTIFY_DA_CONTINUE_ON_ERROR=false)',
202
303
  type: 'boolean',
203
304
  default: false,
305
+ },
306
+ backendUrl: {
307
+ desc: 'Trustify DA backend URL (env: TRUSTIFY_DA_BACKEND_URL)',
308
+ type: 'string',
309
+ },
310
+ providers: {
311
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
312
+ type: 'string',
313
+ },
314
+ sources: {
315
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
316
+ type: 'string',
204
317
  }
205
- }),
318
+ }).middleware(configMiddleware('/path/to/workspace-root')),
206
319
  handler: async (args) => {
207
320
  const workspaceRoot = args['/path/to/workspace-root'];
208
321
  const html = args['html'];
209
322
  const summary = args['summary'];
210
323
  const opts = {};
324
+ if (args.backendUrl !== undefined) {
325
+ opts.TRUSTIFY_DA_BACKEND_URL = args.backendUrl;
326
+ }
211
327
  if (args.concurrency != null) {
212
328
  opts.batchConcurrency = args.concurrency;
213
329
  }
@@ -221,6 +337,12 @@ const stackBatch = {
221
337
  if (args.failFast) {
222
338
  opts.continueOnError = false;
223
339
  }
340
+ if (args.providers !== undefined) {
341
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
342
+ }
343
+ if (args.sources !== undefined) {
344
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
345
+ }
224
346
  let res = await client.stackAnalysisBatch(workspaceRoot, html, opts);
225
347
  const batchAnalysis = res && typeof res === 'object' && res != null && 'analysis' in res ? res.analysis : res;
226
348
  if (summary && !html && typeof batchAnalysis === 'object') {
@@ -373,9 +495,68 @@ const sbom = {
373
495
  }
374
496
  }
375
497
  };
498
+ const remediate = {
499
+ command: 'remediate <path>',
500
+ desc: 'Scan and apply vulnerability remediations to manifest files',
501
+ builder: yargs => yargs.positional('path', {
502
+ desc: 'Path to manifest file or directory',
503
+ type: 'string',
504
+ normalize: true,
505
+ }).options({
506
+ 'dry-run': {
507
+ alias: 'd',
508
+ type: 'boolean',
509
+ desc: 'Preview changes without modifying files',
510
+ },
511
+ providers: {
512
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
513
+ type: 'string',
514
+ },
515
+ sources: {
516
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
517
+ type: 'string',
518
+ },
519
+ 'group-by': {
520
+ type: 'string',
521
+ choices: ['dependency', 'bundle'],
522
+ desc: 'Report grouping strategy (default: dependency)',
523
+ },
524
+ backendUrl: {
525
+ desc: 'Trustify DA backend URL (env: TRUSTIFY_DA_BACKEND_URL)',
526
+ type: 'string',
527
+ },
528
+ }).middleware(configMiddleware('path', { groupBy: true })),
529
+ handler: async (args) => {
530
+ try {
531
+ const result = await runRemediation(args.path, {
532
+ dryRun: args['dry-run'],
533
+ providers: args.providers,
534
+ sources: args.sources,
535
+ backendUrl: args.backendUrl,
536
+ });
537
+ if (result.remediations.length === 0 && result.manifests.length === 0) {
538
+ console.log('No supported manifest files found.');
539
+ process.exit(result.exitCode);
540
+ }
541
+ if (!args['dry-run'] && result.appliedFiles.length > 0) {
542
+ console.log(`Updated ${result.appliedFiles.length} file(s):`);
543
+ for (const file of result.appliedFiles) {
544
+ console.log(` ${file}`);
545
+ }
546
+ console.log('');
547
+ }
548
+ console.log(generateReport(result.remediations, { groupBy: args['group-by'], dryRun: args['dry-run'] }));
549
+ process.exit(result.exitCode);
550
+ }
551
+ catch (err) {
552
+ console.error(err.message);
553
+ process.exit(1);
554
+ }
555
+ }
556
+ };
376
557
  // parse and invoke the command
377
558
  yargs(hideBin(process.argv))
378
- .usage(`Usage: ${process.argv[0].includes("node") ? path.parse(process.argv[1]).base : path.parse(process.argv[0]).base} {component|stack|stack-batch|image|validate-token|license|sbom}`)
559
+ .usage(`Usage: ${process.argv[0].includes("node") ? path.parse(process.argv[1]).base : path.parse(process.argv[0]).base} {component|stack|stack-batch|image|validate-token|license|sbom|remediate}`)
379
560
  .command(stack)
380
561
  .command(stackBatch)
381
562
  .command(component)
@@ -383,6 +564,7 @@ yargs(hideBin(process.argv))
383
564
  .command(validateToken)
384
565
  .command(license)
385
566
  .command(sbom)
567
+ .command(remediate)
386
568
  .scriptName('')
387
569
  .version(false)
388
570
  .demandCommand(1)
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Discovers and parses `.trustify-da.yml` by walking up from `startPath`.
3
+ * A missing config file is not an error — an empty object is returned so callers
4
+ * can fall back to defaults.
5
+ * @param {string} [startPath] - manifest file or directory to start the search from
6
+ * @returns {FileConfig} the parsed config object, or `{}` when no file is found
7
+ * @throws {Error} when the config file exists but contains malformed YAML
8
+ */
9
+ export function loadConfig(startPath?: string): FileConfig;
10
+ /**
11
+ * Merges config sources with precedence: CLI flag > environment variable
12
+ * (`TRUSTIFY_DA_*`) > config file > hardcoded default. Providers and sources are
13
+ * normalized to arrays regardless of whether the source used a YAML array or a
14
+ * comma-separated string.
15
+ * @param {FileConfig} [fileConfig={}] - values parsed from `.trustify-da.yml`
16
+ * @param {{ providers?: string, sources?: string, groupBy?: string, backendUrl?: string }} [cliFlags={}]
17
+ * @param {{ [key: string]: string | undefined }} [envVars={}] - typically `process.env`
18
+ * @returns {ResolvedConfig} the resolved, typed config object
19
+ */
20
+ export function mergeConfig(fileConfig?: FileConfig, cliFlags?: {
21
+ providers?: string;
22
+ sources?: string;
23
+ groupBy?: string;
24
+ backendUrl?: string;
25
+ }, envVars?: {
26
+ [key: string]: string | undefined;
27
+ }): ResolvedConfig;
28
+ /**
29
+ * Loads and merges the project configuration for a target path.
30
+ * @param {string} [startPath] - manifest file or directory to start discovery from
31
+ * @param {{ providers?: string, sources?: string, groupBy?: string, backendUrl?: string }} [cliFlags={}] - explicit options
32
+ * @param {{ [key: string]: string | undefined }} [envVars={}] - typically `process.env`
33
+ * @returns {ResolvedConfig} the resolved, typed config object
34
+ */
35
+ export function resolveConfig(startPath?: string, cliFlags?: {
36
+ providers?: string;
37
+ sources?: string;
38
+ groupBy?: string;
39
+ backendUrl?: string;
40
+ }, envVars?: {
41
+ [key: string]: string | undefined;
42
+ }): ResolvedConfig;
43
+ /**
44
+ * @typedef {{
45
+ * 'group-by'?: 'dependency' | 'bundle',
46
+ * exclude?: string[],
47
+ * labels?: string[],
48
+ * 'branch-prefix'?: string,
49
+ * [key: string]: unknown
50
+ * }} RemediationConfig
51
+ */
52
+ /** @typedef {{ critical?: number, high?: number, 'license-conflicts'?: number, [key: string]: unknown }} FailOnConfig */
53
+ /** @typedef {{ 'fail-on'?: FailOnConfig, [key: string]: unknown }} CheckConfig */
54
+ /** @typedef {{ format?: string, targets?: string[], [key: string]: unknown }} SbomConfig */
55
+ /**
56
+ * @typedef {{
57
+ * 'backend-url'?: string,
58
+ * providers?: string[] | string,
59
+ * sources?: string[] | string,
60
+ * remediation?: RemediationConfig,
61
+ * check?: CheckConfig,
62
+ * sbom?: SbomConfig,
63
+ * [key: string]: unknown
64
+ * }} FileConfig
65
+ */
66
+ /**
67
+ * @typedef {{
68
+ * backendUrl: string | null,
69
+ * backendUrlSource: 'cli' | 'environment' | 'file' | 'default',
70
+ * providers: string[],
71
+ * sources: string[],
72
+ * groupBy: string,
73
+ * remediation: object,
74
+ * check: object,
75
+ * sbom: object
76
+ * }} ResolvedConfig
77
+ */
78
+ /** Config file names discovered by walking up the directory tree, in precedence order. */
79
+ export const CONFIG_FILENAMES: string[];
80
+ export type RemediationConfig = {
81
+ "group-by"?: "dependency" | "bundle";
82
+ exclude?: string[];
83
+ labels?: string[];
84
+ "branch-prefix"?: string;
85
+ [key: string]: unknown;
86
+ };
87
+ export type FailOnConfig = {
88
+ critical?: number;
89
+ high?: number;
90
+ "license-conflicts"?: number;
91
+ [key: string]: unknown;
92
+ };
93
+ export type CheckConfig = {
94
+ "fail-on"?: FailOnConfig;
95
+ [key: string]: unknown;
96
+ };
97
+ export type SbomConfig = {
98
+ format?: string;
99
+ targets?: string[];
100
+ [key: string]: unknown;
101
+ };
102
+ export type FileConfig = {
103
+ "backend-url"?: string;
104
+ providers?: string[] | string;
105
+ sources?: string[] | string;
106
+ remediation?: RemediationConfig;
107
+ check?: CheckConfig;
108
+ sbom?: SbomConfig;
109
+ [key: string]: unknown;
110
+ };
111
+ export type ResolvedConfig = {
112
+ backendUrl: string | null;
113
+ backendUrlSource: "cli" | "environment" | "file" | "default";
114
+ providers: string[];
115
+ sources: string[];
116
+ groupBy: string;
117
+ remediation: object;
118
+ check: object;
119
+ sbom: object;
120
+ };