@trustify-da/trustify-da-javascript-client 0.3.0-ea.608d6fa → 0.3.0-ea.61444d4

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 (85) hide show
  1. package/README.md +304 -13
  2. package/dist/package.json +31 -23
  3. package/dist/src/analysis.d.ts +28 -5
  4. package/dist/src/analysis.js +78 -13
  5. package/dist/src/batch_opts.d.ts +24 -0
  6. package/dist/src/batch_opts.js +35 -0
  7. package/dist/src/cli.js +276 -5
  8. package/dist/src/cyclone_dx_sbom.d.ts +14 -1
  9. package/dist/src/cyclone_dx_sbom.js +45 -9
  10. package/dist/src/index.d.ts +142 -7
  11. package/dist/src/index.js +355 -6
  12. package/dist/src/license/index.d.ts +3 -3
  13. package/dist/src/license/index.js +4 -1
  14. package/dist/src/license/license_utils.js +4 -1
  15. package/dist/src/license/licenses_api.d.ts +2 -2
  16. package/dist/src/oci_image/images.d.ts +1 -1
  17. package/dist/src/oci_image/images.js +15 -1
  18. package/dist/src/oci_image/utils.d.ts +5 -5
  19. package/dist/src/oci_image/utils.js +11 -2
  20. package/dist/src/provider.d.ts +8 -4
  21. package/dist/src/provider.js +16 -5
  22. package/dist/src/providers/base_java.d.ts +14 -12
  23. package/dist/src/providers/base_java.js +18 -44
  24. package/dist/src/providers/base_javascript.d.ts +37 -10
  25. package/dist/src/providers/base_javascript.js +108 -20
  26. package/dist/src/providers/base_pyproject.d.ts +158 -0
  27. package/dist/src/providers/base_pyproject.js +322 -0
  28. package/dist/src/providers/containerfile_parser.d.ts +5 -0
  29. package/dist/src/providers/containerfile_parser.js +20 -0
  30. package/dist/src/providers/golang_gomodules.d.ts +23 -13
  31. package/dist/src/providers/golang_gomodules.js +207 -120
  32. package/dist/src/providers/gomod_parser.d.ts +4 -0
  33. package/dist/src/providers/gomod_parser.js +16 -0
  34. package/dist/src/providers/java_gradle.d.ts +19 -0
  35. package/dist/src/providers/java_gradle.js +116 -2
  36. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  37. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  38. package/dist/src/providers/java_maven.d.ts +10 -2
  39. package/dist/src/providers/java_maven.js +165 -1
  40. package/dist/src/providers/javascript_bun.d.ts +10 -0
  41. package/dist/src/providers/javascript_bun.js +100 -0
  42. package/dist/src/providers/javascript_npm.d.ts +6 -0
  43. package/dist/src/providers/javascript_npm.js +21 -0
  44. package/dist/src/providers/javascript_pnpm.d.ts +1 -1
  45. package/dist/src/providers/javascript_pnpm.js +8 -4
  46. package/dist/src/providers/manifest.d.ts +2 -0
  47. package/dist/src/providers/manifest.js +22 -4
  48. package/dist/src/providers/marker_evaluator.d.ts +14 -0
  49. package/dist/src/providers/marker_evaluator.js +191 -0
  50. package/dist/src/providers/oci_dockerfile.d.ts +51 -0
  51. package/dist/src/providers/oci_dockerfile.js +177 -0
  52. package/dist/src/providers/processors/yarn_berry_processor.d.ts +2 -2
  53. package/dist/src/providers/processors/yarn_berry_processor.js +88 -5
  54. package/dist/src/providers/processors/yarn_classic_processor.d.ts +2 -2
  55. package/dist/src/providers/python_controller.d.ts +5 -1
  56. package/dist/src/providers/python_controller.js +69 -8
  57. package/dist/src/providers/python_pip.d.ts +5 -0
  58. package/dist/src/providers/python_pip.js +6 -6
  59. package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
  60. package/dist/src/providers/python_pip_pyproject.js +146 -0
  61. package/dist/src/providers/python_poetry.d.ts +98 -0
  62. package/dist/src/providers/python_poetry.js +286 -0
  63. package/dist/src/providers/python_uv.d.ts +46 -0
  64. package/dist/src/providers/python_uv.js +269 -0
  65. package/dist/src/providers/rust_cargo.d.ts +12 -3
  66. package/dist/src/providers/rust_cargo.js +115 -21
  67. package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
  68. package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
  69. package/dist/src/remediate.d.ts +21 -0
  70. package/dist/src/remediate.js +149 -0
  71. package/dist/src/remediation.d.ts +57 -0
  72. package/dist/src/remediation.js +436 -0
  73. package/dist/src/remediation_report.d.ts +40 -0
  74. package/dist/src/remediation_report.js +159 -0
  75. package/dist/src/sbom.d.ts +14 -1
  76. package/dist/src/sbom.js +13 -2
  77. package/dist/src/tools.d.ts +28 -2
  78. package/dist/src/tools.js +58 -0
  79. package/dist/src/updaters/maven_updater.d.ts +26 -0
  80. package/dist/src/updaters/maven_updater.js +322 -0
  81. package/dist/src/updaters/toml_updater.d.ts +33 -0
  82. package/dist/src/updaters/toml_updater.js +244 -0
  83. package/dist/src/workspace.d.ts +70 -0
  84. package/dist/src/workspace.js +256 -0
  85. package/package.json +32 -24
@@ -4,7 +4,9 @@ import { EOL } from "os";
4
4
  import { runLicenseCheck } from "./license/index.js";
5
5
  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
- export default { requestComponent, requestStack, requestImages, validateToken };
7
+ /** Media type for CycloneDX JSON batch payloads (batch-analysis API). */
8
+ export const CYCLONEDX_JSON_MEDIA_TYPE = 'application/vnd.cyclonedx+json';
9
+ export default { requestComponent, requestStack, requestStackBatch, requestImages, validateToken, appendAnalysisQueryParams };
8
10
  /**
9
11
  * Send a stack analysis request and get the report as 'text/html' or 'application/json'.
10
12
  * @param {import('./provider').Provider} provider - the provided data for constructing the request
@@ -18,6 +20,9 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
18
20
  opts["source-manifest"] = Buffer.from(fs.readFileSync(manifest).toString()).toString('base64');
19
21
  opts["manifest-type"] = path.parse(manifest).base;
20
22
  let provided = await provider.provideStack(manifest, opts); // throws error if content providing failed
23
+ if (provided.batch) {
24
+ return requestStackBatch(JSON.parse(provided.content), url, html, opts);
25
+ }
21
26
  opts["source-manifest"] = "";
22
27
  opts[TRUSTIFY_DA_OPERATION_TYPE_HEADER.toUpperCase().replaceAll("-", "_")] = "stack-analysis";
23
28
  let startTime = new Date();
@@ -36,9 +41,7 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
36
41
  body: provided.content
37
42
  }, opts);
38
43
  const finalUrl = new URL(`${url}/api/v5/analysis`);
39
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
40
- finalUrl.searchParams.append('recommend', 'false');
41
- }
44
+ appendAnalysisQueryParams(finalUrl, opts);
42
45
  let resp = await fetch(finalUrl, fetchOptions);
43
46
  let result;
44
47
  if (resp.status === 200) {
@@ -77,6 +80,9 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
77
80
  async function requestComponent(provider, manifest, url, opts = {}) {
78
81
  opts["source-manifest"] = Buffer.from(fs.readFileSync(manifest).toString()).toString('base64');
79
82
  let provided = await provider.provideComponent(manifest, opts); // throws error if content providing failed
83
+ if (provided.batch) {
84
+ return requestStackBatch(JSON.parse(provided.content), url, false, opts);
85
+ }
80
86
  opts["source-manifest"] = "";
81
87
  opts[TRUSTIFY_DA_OPERATION_TYPE_HEADER.toUpperCase().replaceAll("-", "_")] = "component-analysis";
82
88
  if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
@@ -93,9 +99,7 @@ async function requestComponent(provider, manifest, url, opts = {}) {
93
99
  body: provided.content
94
100
  }, opts);
95
101
  const finalUrl = new URL(`${url}/api/v5/analysis`);
96
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
97
- finalUrl.searchParams.append('recommend', 'false');
98
- }
102
+ appendAnalysisQueryParams(finalUrl, opts);
99
103
  let resp = await fetch(finalUrl, fetchOptions);
100
104
  let result;
101
105
  if (resp.status === 200) {
@@ -124,6 +128,50 @@ async function requestComponent(provider, manifest, url, opts = {}) {
124
128
  }
125
129
  return Promise.resolve(result);
126
130
  }
131
+ /**
132
+ * Send a batch stack analysis request for multiple manifests (SBOMs keyed by purl).
133
+ * @param {Object.<string, object>} sbomByPurl - Map of root purl to CycloneDX SBOM object
134
+ * @param {string} url - the backend url
135
+ * @param {boolean} [html=false] - true returns HTML, false returns JSON
136
+ * @param {import("index.js").Options} [opts={}]
137
+ * @returns {Promise<string|Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>>}
138
+ */
139
+ async function requestStackBatch(sbomByPurl, url, html = false, opts = {}) {
140
+ const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
141
+ appendAnalysisQueryParams(finalUrl, opts);
142
+ const fetchOptions = addProxyAgent({
143
+ method: 'POST',
144
+ headers: {
145
+ 'Accept': html ? 'text/html' : 'application/json',
146
+ 'Content-Type': CYCLONEDX_JSON_MEDIA_TYPE,
147
+ ...getTokenHeaders(opts)
148
+ },
149
+ body: JSON.stringify(sbomByPurl)
150
+ }, opts);
151
+ const resp = await fetch(finalUrl, fetchOptions);
152
+ if (resp.status === 200) {
153
+ let result;
154
+ if (!html) {
155
+ result = await resp.json();
156
+ }
157
+ else {
158
+ result = await resp.text();
159
+ }
160
+ if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
161
+ const exRequestId = resp.headers.get("ex-request-id");
162
+ if (exRequestId) {
163
+ console.log("Unique Identifier associated with this request - ex-request-id=" + exRequestId);
164
+ }
165
+ console.log("Response body received from Trustify DA backend server : " + EOL + EOL);
166
+ console.log(JSON.stringify(result, null, 4));
167
+ console.log("Ending time of sending batch stack analysis request to Trustify DA backend server= " + new Date());
168
+ }
169
+ return result;
170
+ }
171
+ else {
172
+ 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()}`);
173
+ }
174
+ }
127
175
  /**
128
176
  *
129
177
  * @param {Array<string>} imageRefs
@@ -138,18 +186,17 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
138
186
  imageSboms[parsedImageRef.getPackageURL().toString()] = generateImageSBOM(parsedImageRef, opts);
139
187
  }
140
188
  const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
141
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
142
- finalUrl.searchParams.append('recommend', 'false');
143
- }
144
- const resp = await fetch(finalUrl, {
189
+ appendAnalysisQueryParams(finalUrl, opts);
190
+ const fetchOptions = addProxyAgent({
145
191
  method: 'POST',
146
192
  headers: {
147
193
  'Accept': html ? 'text/html' : 'application/json',
148
- 'Content-Type': 'application/vnd.cyclonedx+json',
194
+ 'Content-Type': CYCLONEDX_JSON_MEDIA_TYPE,
149
195
  ...getTokenHeaders(opts)
150
196
  },
151
197
  body: JSON.stringify(imageSboms),
152
- });
198
+ }, opts);
199
+ const resp = await fetch(finalUrl, fetchOptions);
153
200
  if (resp.status === 200) {
154
201
  let result;
155
202
  if (!html) {
@@ -173,6 +220,24 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
173
220
  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()}`);
174
221
  }
175
222
  }
223
+ /**
224
+ * Append common analysis query parameters (recommend, providers, sources) to a URL.
225
+ * @param {URL} url - the URL object to append query parameters to
226
+ * @param {import("index.js").Options} [opts={}] - options containing parameter overrides
227
+ */
228
+ function appendAnalysisQueryParams(url, opts = {}) {
229
+ if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
230
+ url.searchParams.append('recommend', 'false');
231
+ }
232
+ const providers = getCustom('TRUSTIFY_DA_PROVIDERS', null, opts);
233
+ if (providers) {
234
+ url.searchParams.append('providers', providers);
235
+ }
236
+ const sources = getCustom('TRUSTIFY_DA_SOURCES', null, opts);
237
+ if (sources) {
238
+ url.searchParams.append('sources', sources);
239
+ }
240
+ }
176
241
  /**
177
242
  *
178
243
  * @param url the backend url to send the request to
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Whether to skip failed manifests and continue (default), or fail on first SBOM/validation error.
3
+ * `opts.continueOnError` overrides; env `TRUSTIFY_DA_CONTINUE_ON_ERROR=false` disables continuation.
4
+ *
5
+ * @param {{ continueOnError?: boolean, TRUSTIFY_DA_CONTINUE_ON_ERROR?: string, [key: string]: unknown }} [opts={}]
6
+ * @returns {boolean} true = collect errors (default), false = fail-fast
7
+ */
8
+ export function resolveContinueOnError(opts?: {
9
+ continueOnError?: boolean;
10
+ TRUSTIFY_DA_CONTINUE_ON_ERROR?: string;
11
+ [key: string]: unknown;
12
+ }): boolean;
13
+ /**
14
+ * When true, `stackAnalysisBatch` returns `{ analysis, metadata }` instead of the backend response only.
15
+ * `opts.batchMetadata` overrides; env `TRUSTIFY_DA_BATCH_METADATA=true` enables.
16
+ *
17
+ * @param {{ batchMetadata?: boolean, TRUSTIFY_DA_BATCH_METADATA?: string, [key: string]: unknown }} [opts={}]
18
+ * @returns {boolean}
19
+ */
20
+ export function resolveBatchMetadata(opts?: {
21
+ batchMetadata?: boolean;
22
+ TRUSTIFY_DA_BATCH_METADATA?: string;
23
+ [key: string]: unknown;
24
+ }): boolean;
@@ -0,0 +1,35 @@
1
+ import { getCustom } from './tools.js';
2
+ /**
3
+ * Whether to skip failed manifests and continue (default), or fail on first SBOM/validation error.
4
+ * `opts.continueOnError` overrides; env `TRUSTIFY_DA_CONTINUE_ON_ERROR=false` disables continuation.
5
+ *
6
+ * @param {{ continueOnError?: boolean, TRUSTIFY_DA_CONTINUE_ON_ERROR?: string, [key: string]: unknown }} [opts={}]
7
+ * @returns {boolean} true = collect errors (default), false = fail-fast
8
+ */
9
+ export function resolveContinueOnError(opts = {}) {
10
+ if (typeof opts.continueOnError === 'boolean') {
11
+ return opts.continueOnError;
12
+ }
13
+ const v = getCustom('TRUSTIFY_DA_CONTINUE_ON_ERROR', null, opts);
14
+ if (v != null && String(v).trim() !== '') {
15
+ return String(v).toLowerCase() !== 'false';
16
+ }
17
+ return true;
18
+ }
19
+ /**
20
+ * When true, `stackAnalysisBatch` returns `{ analysis, metadata }` instead of the backend response only.
21
+ * `opts.batchMetadata` overrides; env `TRUSTIFY_DA_BATCH_METADATA=true` enables.
22
+ *
23
+ * @param {{ batchMetadata?: boolean, TRUSTIFY_DA_BATCH_METADATA?: string, [key: string]: unknown }} [opts={}]
24
+ * @returns {boolean}
25
+ */
26
+ export function resolveBatchMetadata(opts = {}) {
27
+ if (typeof opts.batchMetadata === 'boolean') {
28
+ return opts.batchMetadata;
29
+ }
30
+ const v = getCustom('TRUSTIFY_DA_BATCH_METADATA', null, opts);
31
+ if (v != null && String(v).trim() !== '') {
32
+ return String(v).toLowerCase() === 'true';
33
+ }
34
+ return false;
35
+ }
package/dist/src/cli.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs';
2
3
  import * as path from "path";
3
4
  import yargs from 'yargs';
4
5
  import { hideBin } from 'yargs/helpers';
5
6
  import { getProjectLicense, getLicenseDetails } from './license/index.js';
6
- import client, { selectTrustifyDABackend } from './index.js';
7
+ import { runRemediation } from './remediate.js';
8
+ import client, { selectTrustifyDABackend, generateSbom } from './index.js';
7
9
  // command for component analysis take manifest type and content
8
10
  const component = {
9
11
  command: 'component </path/to/manifest>',
@@ -12,10 +14,32 @@ const component = {
12
14
  desc: 'manifest path for analyzing',
13
15
  type: 'string',
14
16
  normalize: true,
17
+ }).options({
18
+ workspaceDir: {
19
+ alias: 'w',
20
+ desc: 'Workspace root directory (for monorepos; lock file is expected here)',
21
+ type: 'string',
22
+ normalize: true,
23
+ },
24
+ providers: {
25
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
26
+ type: 'string',
27
+ },
28
+ sources: {
29
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
30
+ type: 'string',
31
+ }
15
32
  }),
16
33
  handler: async (args) => {
17
34
  let manifestName = args['/path/to/manifest'];
18
- let res = await client.componentAnalysis(manifestName);
35
+ const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
36
+ if (args.providers) {
37
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
38
+ }
39
+ if (args.sources) {
40
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
41
+ }
42
+ let res = await client.componentAnalysis(manifestName, opts);
19
43
  console.log(JSON.stringify(res, null, 2));
20
44
  }
21
45
  };
@@ -63,6 +87,14 @@ const image = {
63
87
  desc: 'For JSON report, get only the \'summary\'',
64
88
  type: 'boolean',
65
89
  conflicts: 'html'
90
+ },
91
+ providers: {
92
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
93
+ type: 'string',
94
+ },
95
+ sources: {
96
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
97
+ type: 'string',
66
98
  }
67
99
  }),
68
100
  handler: async (args) => {
@@ -72,7 +104,14 @@ const image = {
72
104
  }
73
105
  let html = args['html'];
74
106
  let summary = args['summary'];
75
- let res = await client.imageAnalysis(imageRefs, html);
107
+ const opts = {};
108
+ if (args.providers) {
109
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
110
+ }
111
+ if (args.sources) {
112
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
113
+ }
114
+ let res = await client.imageAnalysis(imageRefs, html, opts);
76
115
  if (summary && !html) {
77
116
  let summaries = {};
78
117
  for (let [imageRef, report] of Object.entries(res)) {
@@ -117,15 +156,36 @@ const stack = {
117
156
  desc: 'For JSON report, get only the \'summary\'',
118
157
  type: 'boolean',
119
158
  conflicts: 'html'
159
+ },
160
+ workspaceDir: {
161
+ alias: 'w',
162
+ desc: 'Workspace root directory (for monorepos; lock file is expected here)',
163
+ type: 'string',
164
+ normalize: true,
165
+ },
166
+ providers: {
167
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
168
+ type: 'string',
169
+ },
170
+ sources: {
171
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
172
+ type: 'string',
120
173
  }
121
174
  }),
122
175
  handler: async (args) => {
123
176
  let manifest = args['/path/to/manifest'];
124
177
  let html = args['html'];
125
178
  let summary = args['summary'];
179
+ const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
180
+ if (args.providers) {
181
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
182
+ }
183
+ if (args.sources) {
184
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
185
+ }
126
186
  let theProvidersSummary = new Map();
127
187
  let theProvidersObject = {};
128
- let res = await client.stackAnalysis(manifest, html);
188
+ let res = await client.stackAnalysis(manifest, html, opts);
129
189
  if (summary) {
130
190
  for (let provider in res.providers) {
131
191
  if (res.providers[provider].sources !== undefined) {
@@ -143,6 +203,122 @@ const stack = {
143
203
  console.log(html ? res : JSON.stringify(!html && summary ? theProvidersObject : res, null, 2));
144
204
  }
145
205
  };
206
+ // command for batch stack analysis (workspace)
207
+ const stackBatch = {
208
+ command: 'stack-batch </path/to/workspace-root> [--html|--summary] [--concurrency <n>] [--ignore <pattern>...] [--metadata] [--fail-fast]',
209
+ desc: 'produce stack report for all packages/crates in a workspace (Cargo or JS/TS)',
210
+ builder: yargs => yargs.positional('/path/to/workspace-root', {
211
+ desc: 'workspace root directory (containing Cargo.toml+Cargo.lock or package.json+lock file)',
212
+ type: 'string',
213
+ normalize: true,
214
+ }).options({
215
+ html: {
216
+ alias: 'r',
217
+ desc: 'Get the report as HTML instead of JSON',
218
+ type: 'boolean',
219
+ conflicts: 'summary'
220
+ },
221
+ summary: {
222
+ alias: 's',
223
+ desc: 'For JSON report, get only the \'summary\' per package',
224
+ type: 'boolean',
225
+ conflicts: 'html'
226
+ },
227
+ concurrency: {
228
+ alias: 'c',
229
+ desc: 'Max parallel SBOM generations (default: 10, env: TRUSTIFY_DA_BATCH_CONCURRENCY)',
230
+ type: 'number',
231
+ },
232
+ ignore: {
233
+ alias: 'i',
234
+ desc: 'Extra glob patterns excluded from workspace discovery (merged with defaults). Repeat flag per pattern. Env: TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE (comma-separated)',
235
+ type: 'string',
236
+ array: true,
237
+ },
238
+ metadata: {
239
+ alias: 'm',
240
+ desc: 'Return { analysis, metadata } with per-manifest errors (env: TRUSTIFY_DA_BATCH_METADATA=true)',
241
+ type: 'boolean',
242
+ default: false,
243
+ },
244
+ failFast: {
245
+ desc: 'Stop on first invalid package.json or SBOM error (env: TRUSTIFY_DA_CONTINUE_ON_ERROR=false)',
246
+ type: 'boolean',
247
+ default: false,
248
+ },
249
+ providers: {
250
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
251
+ type: 'string',
252
+ },
253
+ sources: {
254
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
255
+ type: 'string',
256
+ }
257
+ }),
258
+ handler: async (args) => {
259
+ const workspaceRoot = args['/path/to/workspace-root'];
260
+ const html = args['html'];
261
+ const summary = args['summary'];
262
+ const opts = {};
263
+ if (args.concurrency != null) {
264
+ opts.batchConcurrency = args.concurrency;
265
+ }
266
+ const extraIgnores = Array.isArray(args.ignore) ? args.ignore.filter(p => p != null && String(p).trim()) : [];
267
+ if (extraIgnores.length > 0) {
268
+ opts.workspaceDiscoveryIgnore = extraIgnores;
269
+ }
270
+ if (args.metadata) {
271
+ opts.batchMetadata = true;
272
+ }
273
+ if (args.failFast) {
274
+ opts.continueOnError = false;
275
+ }
276
+ if (args.providers) {
277
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
278
+ }
279
+ if (args.sources) {
280
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
281
+ }
282
+ let res = await client.stackAnalysisBatch(workspaceRoot, html, opts);
283
+ const batchAnalysis = res && typeof res === 'object' && res != null && 'analysis' in res ? res.analysis : res;
284
+ if (summary && !html && typeof batchAnalysis === 'object') {
285
+ const summaries = {};
286
+ for (const [purl, report] of Object.entries(batchAnalysis)) {
287
+ if (report?.providers) {
288
+ for (const provider of Object.keys(report.providers)) {
289
+ const sources = report.providers[provider]?.sources;
290
+ if (sources) {
291
+ for (const [source, data] of Object.entries(sources)) {
292
+ if (data?.summary) {
293
+ if (!summaries[purl]) {
294
+ summaries[purl] = {};
295
+ }
296
+ if (!summaries[purl][provider]) {
297
+ summaries[purl][provider] = {};
298
+ }
299
+ summaries[purl][provider][source] = data.summary;
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+ if (res && typeof res === 'object' && res != null && 'metadata' in res) {
307
+ res = { analysis: summaries, metadata: res.metadata };
308
+ }
309
+ else {
310
+ res = summaries;
311
+ }
312
+ }
313
+ if (html) {
314
+ const htmlContent = res && typeof res === 'object' && 'analysis' in res ? res.analysis : res;
315
+ console.log(htmlContent);
316
+ }
317
+ else {
318
+ console.log(JSON.stringify(res, null, 2));
319
+ }
320
+ }
321
+ };
146
322
  // command for license checking
147
323
  const license = {
148
324
  command: 'license </path/to/manifest>',
@@ -208,14 +384,109 @@ const license = {
208
384
  console.log(JSON.stringify(output, null, 2));
209
385
  }
210
386
  };
387
+ const sbom = {
388
+ command: 'sbom </path/to/manifest> [--output]',
389
+ desc: 'generate a CycloneDX SBOM from a manifest file',
390
+ builder: yargs => yargs.positional('/path/to/manifest', {
391
+ desc: 'manifest path for SBOM generation',
392
+ type: 'string',
393
+ normalize: true,
394
+ }).options({
395
+ output: {
396
+ alias: 'o',
397
+ desc: 'Write SBOM JSON to a file instead of stdout',
398
+ type: 'string',
399
+ normalize: true,
400
+ },
401
+ workspaceDir: {
402
+ alias: 'w',
403
+ desc: 'Workspace root directory (for monorepos; lock file is expected here)',
404
+ type: 'string',
405
+ normalize: true,
406
+ }
407
+ }),
408
+ handler: async (args) => {
409
+ let manifest = args['/path/to/manifest'];
410
+ const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
411
+ let result;
412
+ try {
413
+ result = await generateSbom(manifest, opts);
414
+ }
415
+ catch (err) {
416
+ console.error(JSON.stringify({ error: `Failed to generate SBOM: ${err.message}` }, null, 2));
417
+ process.exit(1);
418
+ }
419
+ const json = JSON.stringify(result, null, 2);
420
+ if (args.output) {
421
+ try {
422
+ fs.writeFileSync(args.output, json);
423
+ }
424
+ catch (err) {
425
+ console.error(JSON.stringify({ error: `Failed to write output file: ${err.message}` }, null, 2));
426
+ process.exit(1);
427
+ }
428
+ }
429
+ else {
430
+ console.log(json);
431
+ }
432
+ }
433
+ };
434
+ const remediate = {
435
+ command: 'remediate <path>',
436
+ desc: 'Scan and apply vulnerability remediations to manifest files',
437
+ builder: yargs => yargs.positional('path', {
438
+ desc: 'Path to manifest file or directory',
439
+ type: 'string',
440
+ normalize: true,
441
+ }).options({
442
+ 'dry-run': {
443
+ alias: 'd',
444
+ type: 'boolean',
445
+ desc: 'Preview changes without modifying files',
446
+ },
447
+ providers: {
448
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
449
+ type: 'string',
450
+ },
451
+ sources: {
452
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
453
+ type: 'string',
454
+ },
455
+ 'group-by': {
456
+ type: 'string',
457
+ choices: ['dependency', 'bundle'],
458
+ default: 'dependency',
459
+ desc: 'Report grouping strategy',
460
+ },
461
+ }),
462
+ handler: async (args) => {
463
+ try {
464
+ const result = await runRemediation(args.path, {
465
+ dryRun: args['dry-run'],
466
+ providers: args.providers,
467
+ sources: args.sources,
468
+ groupBy: args['group-by'],
469
+ });
470
+ console.log(result.output);
471
+ process.exit(result.exitCode);
472
+ }
473
+ catch (err) {
474
+ console.error(err.message);
475
+ process.exit(1);
476
+ }
477
+ }
478
+ };
211
479
  // parse and invoke the command
212
480
  yargs(hideBin(process.argv))
213
- .usage(`Usage: ${process.argv[0].includes("node") ? path.parse(process.argv[1]).base : path.parse(process.argv[0]).base} {component|stack|image|validate-token|license}`)
481
+ .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}`)
214
482
  .command(stack)
483
+ .command(stackBatch)
215
484
  .command(component)
216
485
  .command(image)
217
486
  .command(validateToken)
218
487
  .command(license)
488
+ .command(sbom)
489
+ .command(remediate)
219
490
  .scriptName('')
220
491
  .version(false)
221
492
  .demandCommand(1)
@@ -18,9 +18,14 @@ export default class CycloneDxSbom {
18
18
  * Adds a dependency relationship between two components in the SBOM
19
19
  * @param {PackageURL} sourceRef - The source component (parent)
20
20
  * @param {PackageURL} targetRef - The target component (dependency)
21
+ * @param {string} [scope] - Scope of the dependency
22
+ * @param {Array<{alg: string, content: string}>} [targetHashes] - Optional hashes for the target component
21
23
  * @return {CycloneDxSbom} The updated SBOM
22
24
  */
23
- addDependency(sourceRef: PackageURL, targetRef: PackageURL, scope: any): CycloneDxSbom;
25
+ addDependency(sourceRef: PackageURL, targetRef: PackageURL, scope?: string, targetHashes?: Array<{
26
+ alg: string;
27
+ content: string;
28
+ }>): CycloneDxSbom;
24
29
  /** @param {{}} opts - various options, settings and configuration of application.
25
30
  * @return String CycloneDx Sbom json object in a string format
26
31
  */
@@ -50,6 +55,7 @@ export default class CycloneDxSbom {
50
55
  version: any;
51
56
  scope: any;
52
57
  licenses?: any;
58
+ hashes?: any;
53
59
  };
54
60
  /**
55
61
  * This method gets an array of dependencies to be ignored, and remove all of them from CycloneDx Sbom
@@ -70,6 +76,13 @@ export default class CycloneDxSbom {
70
76
  * @return {boolean}
71
77
  */
72
78
  checkIfPackageInsideDependsOnList(component: any, name: string): boolean;
79
+ /**
80
+ * Checks if any entry in the dependsOn list of sourceRef starts with the given purl prefix.
81
+ * @param {PackageURL} sourceRef - The source component
82
+ * @param {string} purlPrefix - The purl prefix to match (e.g. "pkg:npm/minimist@")
83
+ * @return {boolean}
84
+ */
85
+ checkDependsOnByPurlPrefix(sourceRef: PackageURL, purlPrefix: string): boolean;
73
86
  /** Removes the root component from the sbom
74
87
  */
75
88
  removeRootComponent(): void;