@trustify-da/trustify-da-javascript-client 0.3.0-ea.6549d2a → 0.3.0-ea.68905d3

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 (98) hide show
  1. package/README.md +368 -14
  2. package/dist/package.json +39 -26
  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 +287 -5
  8. package/dist/src/cyclone_dx_sbom.d.ts +27 -1
  9. package/dist/src/cyclone_dx_sbom.js +84 -10
  10. package/dist/src/index.d.ts +145 -8
  11. package/dist/src/index.js +362 -28
  12. package/dist/src/license/index.d.ts +5 -5
  13. package/dist/src/license/index.js +8 -5
  14. package/dist/src/license/license_utils.d.ts +40 -0
  15. package/dist/src/license/license_utils.js +137 -0
  16. package/dist/src/license/licenses_api.d.ts +2 -2
  17. package/dist/src/license/licenses_api.js +9 -2
  18. package/dist/src/license/project_license.d.ts +1 -6
  19. package/dist/src/license/project_license.js +4 -81
  20. package/dist/src/oci_image/images.d.ts +1 -1
  21. package/dist/src/oci_image/images.js +15 -1
  22. package/dist/src/oci_image/utils.d.ts +5 -5
  23. package/dist/src/oci_image/utils.js +11 -2
  24. package/dist/src/package_version.d.ts +8 -0
  25. package/dist/src/package_version.js +31 -0
  26. package/dist/src/provider.d.ts +8 -4
  27. package/dist/src/provider.js +18 -5
  28. package/dist/src/providers/base_java.d.ts +67 -12
  29. package/dist/src/providers/base_java.js +82 -61
  30. package/dist/src/providers/base_javascript.d.ts +91 -10
  31. package/dist/src/providers/base_javascript.js +220 -28
  32. package/dist/src/providers/base_pyproject.d.ts +158 -0
  33. package/dist/src/providers/base_pyproject.js +322 -0
  34. package/dist/src/providers/containerfile_parser.d.ts +5 -0
  35. package/dist/src/providers/containerfile_parser.js +20 -0
  36. package/dist/src/providers/golang_gomodules.d.ts +23 -13
  37. package/dist/src/providers/golang_gomodules.js +210 -121
  38. package/dist/src/providers/gomod_parser.d.ts +4 -0
  39. package/dist/src/providers/gomod_parser.js +16 -0
  40. package/dist/src/providers/java_gradle.d.ts +67 -0
  41. package/dist/src/providers/java_gradle.js +314 -21
  42. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  43. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  44. package/dist/src/providers/java_maven.d.ts +38 -10
  45. package/dist/src/providers/java_maven.js +279 -20
  46. package/dist/src/providers/javascript_bun.d.ts +22 -0
  47. package/dist/src/providers/javascript_bun.js +141 -0
  48. package/dist/src/providers/javascript_npm.d.ts +19 -0
  49. package/dist/src/providers/javascript_npm.js +61 -1
  50. package/dist/src/providers/javascript_pnpm.d.ts +14 -1
  51. package/dist/src/providers/javascript_pnpm.js +55 -5
  52. package/dist/src/providers/javascript_yarn.d.ts +12 -0
  53. package/dist/src/providers/javascript_yarn.js +75 -2
  54. package/dist/src/providers/manifest.d.ts +2 -0
  55. package/dist/src/providers/manifest.js +28 -7
  56. package/dist/src/providers/marker_evaluator.d.ts +14 -0
  57. package/dist/src/providers/marker_evaluator.js +191 -0
  58. package/dist/src/providers/oci_dockerfile.d.ts +51 -0
  59. package/dist/src/providers/oci_dockerfile.js +177 -0
  60. package/dist/src/providers/processors/yarn_berry_processor.d.ts +6 -2
  61. package/dist/src/providers/processors/yarn_berry_processor.js +91 -7
  62. package/dist/src/providers/processors/yarn_classic_processor.d.ts +6 -2
  63. package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
  64. package/dist/src/providers/python_controller.d.ts +5 -1
  65. package/dist/src/providers/python_controller.js +69 -8
  66. package/dist/src/providers/python_pip.d.ts +5 -0
  67. package/dist/src/providers/python_pip.js +8 -7
  68. package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
  69. package/dist/src/providers/python_pip_pyproject.js +146 -0
  70. package/dist/src/providers/python_poetry.d.ts +98 -0
  71. package/dist/src/providers/python_poetry.js +286 -0
  72. package/dist/src/providers/python_uv.d.ts +46 -0
  73. package/dist/src/providers/python_uv.js +269 -0
  74. package/dist/src/providers/requirements_parser.js +5 -4
  75. package/dist/src/providers/rust_cargo.d.ts +56 -0
  76. package/dist/src/providers/rust_cargo.js +682 -0
  77. package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
  78. package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
  79. package/dist/src/providers/tree-sitter-requirements.wasm +0 -0
  80. package/dist/src/remediate.d.ts +117 -0
  81. package/dist/src/remediate.js +249 -0
  82. package/dist/src/remediation.d.ts +57 -0
  83. package/dist/src/remediation.js +436 -0
  84. package/dist/src/remediation_report.d.ts +40 -0
  85. package/dist/src/remediation_report.js +159 -0
  86. package/dist/src/sbom.d.ts +25 -1
  87. package/dist/src/sbom.js +23 -2
  88. package/dist/src/tools.d.ts +28 -2
  89. package/dist/src/tools.js +82 -1
  90. package/dist/src/updaters/maven_updater.d.ts +58 -0
  91. package/dist/src/updaters/maven_updater.js +345 -0
  92. package/dist/src/updaters/toml_updater.d.ts +62 -0
  93. package/dist/src/updaters/toml_updater.js +276 -0
  94. package/dist/src/workspace.d.ts +70 -0
  95. package/dist/src/workspace.js +256 -0
  96. package/package.json +40 -27
  97. package/dist/src/license/compatibility.d.ts +0 -18
  98. package/dist/src/license/compatibility.js +0 -45
@@ -1,8 +1,12 @@
1
+ /** Media type for CycloneDX JSON batch payloads (batch-analysis API). */
2
+ export const CYCLONEDX_JSON_MEDIA_TYPE: "application/vnd.cyclonedx+json";
1
3
  declare namespace _default {
2
4
  export { requestComponent };
3
5
  export { requestStack };
6
+ export { requestStackBatch };
4
7
  export { requestImages };
5
8
  export { validateToken };
9
+ export { appendAnalysisQueryParams };
6
10
  }
7
11
  export default _default;
8
12
  /**
@@ -13,7 +17,7 @@ export default _default;
13
17
  * @param {import("index.js").Options} [opts={}] - optional various options to pass along the application
14
18
  * @returns {Promise<import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>}
15
19
  */
16
- declare function requestComponent(provider: import("./provider").Provider, manifest: string, url: string, opts?: import("index.js").Options): Promise<import("@trustify-da/trustify-da-api-model/model/v5/AnalysisReport").AnalysisReport>;
20
+ declare function requestComponent(provider: any, manifest: string, url: string, opts?: any): Promise<any>;
17
21
  /**
18
22
  * Send a stack analysis request and get the report as 'text/html' or 'application/json'.
19
23
  * @param {import('./provider').Provider} provider - the provided data for constructing the request
@@ -23,7 +27,20 @@ declare function requestComponent(provider: import("./provider").Provider, manif
23
27
  * @param {import("index.js").Options} [opts={}] - optional various options to pass along the application
24
28
  * @returns {Promise<string|import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>}
25
29
  */
26
- declare function requestStack(provider: import("./provider").Provider, manifest: string, url: string, html?: boolean, opts?: import("index.js").Options): Promise<string | import("@trustify-da/trustify-da-api-model/model/v5/AnalysisReport").AnalysisReport>;
30
+ declare function requestStack(provider: any, manifest: string, url: string, html?: boolean, opts?: any): Promise<string | any>;
31
+ /**
32
+ * Send a batch stack analysis request for multiple manifests (SBOMs keyed by purl).
33
+ * @param {Object.<string, object>} sbomByPurl - Map of root purl to CycloneDX SBOM object
34
+ * @param {string} url - the backend url
35
+ * @param {boolean} [html=false] - true returns HTML, false returns JSON
36
+ * @param {import("index.js").Options} [opts={}]
37
+ * @returns {Promise<string|Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>>}
38
+ */
39
+ declare function requestStackBatch(sbomByPurl: {
40
+ [x: string]: object;
41
+ }, url: string, html?: boolean, opts?: any): Promise<string | {
42
+ [x: string]: any;
43
+ }>;
27
44
  /**
28
45
  *
29
46
  * @param {Array<string>} imageRefs
@@ -31,8 +48,8 @@ declare function requestStack(provider: import("./provider").Provider, manifest:
31
48
  * @param {import("index.js").Options} [opts={}] - optional various options to pass along the application
32
49
  * @returns {Promise<string|Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>>}
33
50
  */
34
- declare function requestImages(imageRefs: Array<string>, url: string, html?: boolean, opts?: import("index.js").Options): Promise<string | {
35
- [x: string]: import("@trustify-da/trustify-da-api-model/model/v5/AnalysisReport").AnalysisReport;
51
+ declare function requestImages(imageRefs: Array<string>, url: string, html?: boolean, opts?: any): Promise<string | {
52
+ [x: string]: any;
36
53
  }>;
37
54
  /**
38
55
  *
@@ -40,4 +57,10 @@ declare function requestImages(imageRefs: Array<string>, url: string, html?: boo
40
57
  * @param {import("index.js").Options} [opts={}] - optional various options to pass headers for t he validateToken Request
41
58
  * @return {Promise<number>} return the HTTP status Code of the response from the validate token request.
42
59
  */
43
- declare function validateToken(url: any, opts?: import("index.js").Options): Promise<number>;
60
+ declare function validateToken(url: any, opts?: any): Promise<number>;
61
+ /**
62
+ * Append common analysis query parameters (recommend, providers, sources) to a URL.
63
+ * @param {URL} url - the URL object to append query parameters to
64
+ * @param {import("index.js").Options} [opts={}] - options containing parameter overrides
65
+ */
66
+ declare function appendAnalysisQueryParams(url: URL, opts?: any): void;
@@ -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,12 @@
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 { generateReport } from './remediation_report.js';
9
+ import client, { selectTrustifyDABackend, generateSbom } from './index.js';
7
10
  // command for component analysis take manifest type and content
8
11
  const component = {
9
12
  command: 'component </path/to/manifest>',
@@ -12,10 +15,32 @@ const component = {
12
15
  desc: 'manifest path for analyzing',
13
16
  type: 'string',
14
17
  normalize: true,
18
+ }).options({
19
+ workspaceDir: {
20
+ alias: 'w',
21
+ desc: 'Workspace root directory (for monorepos; lock file is expected here)',
22
+ type: 'string',
23
+ normalize: true,
24
+ },
25
+ providers: {
26
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
27
+ type: 'string',
28
+ },
29
+ sources: {
30
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
31
+ type: 'string',
32
+ }
15
33
  }),
16
34
  handler: async (args) => {
17
35
  let manifestName = args['/path/to/manifest'];
18
- let res = await client.componentAnalysis(manifestName);
36
+ const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
37
+ if (args.providers) {
38
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
39
+ }
40
+ if (args.sources) {
41
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
42
+ }
43
+ let res = await client.componentAnalysis(manifestName, opts);
19
44
  console.log(JSON.stringify(res, null, 2));
20
45
  }
21
46
  };
@@ -63,6 +88,14 @@ const image = {
63
88
  desc: 'For JSON report, get only the \'summary\'',
64
89
  type: 'boolean',
65
90
  conflicts: 'html'
91
+ },
92
+ providers: {
93
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
94
+ type: 'string',
95
+ },
96
+ sources: {
97
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
98
+ type: 'string',
66
99
  }
67
100
  }),
68
101
  handler: async (args) => {
@@ -72,7 +105,14 @@ const image = {
72
105
  }
73
106
  let html = args['html'];
74
107
  let summary = args['summary'];
75
- let res = await client.imageAnalysis(imageRefs, html);
108
+ const opts = {};
109
+ if (args.providers) {
110
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
111
+ }
112
+ if (args.sources) {
113
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
114
+ }
115
+ let res = await client.imageAnalysis(imageRefs, html, opts);
76
116
  if (summary && !html) {
77
117
  let summaries = {};
78
118
  for (let [imageRef, report] of Object.entries(res)) {
@@ -117,15 +157,36 @@ const stack = {
117
157
  desc: 'For JSON report, get only the \'summary\'',
118
158
  type: 'boolean',
119
159
  conflicts: 'html'
160
+ },
161
+ workspaceDir: {
162
+ alias: 'w',
163
+ desc: 'Workspace root directory (for monorepos; lock file is expected here)',
164
+ type: 'string',
165
+ normalize: true,
166
+ },
167
+ providers: {
168
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
169
+ type: 'string',
170
+ },
171
+ sources: {
172
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
173
+ type: 'string',
120
174
  }
121
175
  }),
122
176
  handler: async (args) => {
123
177
  let manifest = args['/path/to/manifest'];
124
178
  let html = args['html'];
125
179
  let summary = args['summary'];
180
+ const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
181
+ if (args.providers) {
182
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
183
+ }
184
+ if (args.sources) {
185
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
186
+ }
126
187
  let theProvidersSummary = new Map();
127
188
  let theProvidersObject = {};
128
- let res = await client.stackAnalysis(manifest, html);
189
+ let res = await client.stackAnalysis(manifest, html, opts);
129
190
  if (summary) {
130
191
  for (let provider in res.providers) {
131
192
  if (res.providers[provider].sources !== undefined) {
@@ -143,6 +204,122 @@ const stack = {
143
204
  console.log(html ? res : JSON.stringify(!html && summary ? theProvidersObject : res, null, 2));
144
205
  }
145
206
  };
207
+ // command for batch stack analysis (workspace)
208
+ const stackBatch = {
209
+ command: 'stack-batch </path/to/workspace-root> [--html|--summary] [--concurrency <n>] [--ignore <pattern>...] [--metadata] [--fail-fast]',
210
+ desc: 'produce stack report for all packages/crates in a workspace (Cargo or JS/TS)',
211
+ builder: yargs => yargs.positional('/path/to/workspace-root', {
212
+ desc: 'workspace root directory (containing Cargo.toml+Cargo.lock or package.json+lock file)',
213
+ type: 'string',
214
+ normalize: true,
215
+ }).options({
216
+ html: {
217
+ alias: 'r',
218
+ desc: 'Get the report as HTML instead of JSON',
219
+ type: 'boolean',
220
+ conflicts: 'summary'
221
+ },
222
+ summary: {
223
+ alias: 's',
224
+ desc: 'For JSON report, get only the \'summary\' per package',
225
+ type: 'boolean',
226
+ conflicts: 'html'
227
+ },
228
+ concurrency: {
229
+ alias: 'c',
230
+ desc: 'Max parallel SBOM generations (default: 10, env: TRUSTIFY_DA_BATCH_CONCURRENCY)',
231
+ type: 'number',
232
+ },
233
+ ignore: {
234
+ alias: 'i',
235
+ desc: 'Extra glob patterns excluded from workspace discovery (merged with defaults). Repeat flag per pattern. Env: TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE (comma-separated)',
236
+ type: 'string',
237
+ array: true,
238
+ },
239
+ metadata: {
240
+ alias: 'm',
241
+ desc: 'Return { analysis, metadata } with per-manifest errors (env: TRUSTIFY_DA_BATCH_METADATA=true)',
242
+ type: 'boolean',
243
+ default: false,
244
+ },
245
+ failFast: {
246
+ desc: 'Stop on first invalid package.json or SBOM error (env: TRUSTIFY_DA_CONTINUE_ON_ERROR=false)',
247
+ type: 'boolean',
248
+ default: false,
249
+ },
250
+ providers: {
251
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
252
+ type: 'string',
253
+ },
254
+ sources: {
255
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
256
+ type: 'string',
257
+ }
258
+ }),
259
+ handler: async (args) => {
260
+ const workspaceRoot = args['/path/to/workspace-root'];
261
+ const html = args['html'];
262
+ const summary = args['summary'];
263
+ const opts = {};
264
+ if (args.concurrency != null) {
265
+ opts.batchConcurrency = args.concurrency;
266
+ }
267
+ const extraIgnores = Array.isArray(args.ignore) ? args.ignore.filter(p => p != null && String(p).trim()) : [];
268
+ if (extraIgnores.length > 0) {
269
+ opts.workspaceDiscoveryIgnore = extraIgnores;
270
+ }
271
+ if (args.metadata) {
272
+ opts.batchMetadata = true;
273
+ }
274
+ if (args.failFast) {
275
+ opts.continueOnError = false;
276
+ }
277
+ if (args.providers) {
278
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
279
+ }
280
+ if (args.sources) {
281
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
282
+ }
283
+ let res = await client.stackAnalysisBatch(workspaceRoot, html, opts);
284
+ const batchAnalysis = res && typeof res === 'object' && res != null && 'analysis' in res ? res.analysis : res;
285
+ if (summary && !html && typeof batchAnalysis === 'object') {
286
+ const summaries = {};
287
+ for (const [purl, report] of Object.entries(batchAnalysis)) {
288
+ if (report?.providers) {
289
+ for (const provider of Object.keys(report.providers)) {
290
+ const sources = report.providers[provider]?.sources;
291
+ if (sources) {
292
+ for (const [source, data] of Object.entries(sources)) {
293
+ if (data?.summary) {
294
+ if (!summaries[purl]) {
295
+ summaries[purl] = {};
296
+ }
297
+ if (!summaries[purl][provider]) {
298
+ summaries[purl][provider] = {};
299
+ }
300
+ summaries[purl][provider][source] = data.summary;
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+ }
307
+ if (res && typeof res === 'object' && res != null && 'metadata' in res) {
308
+ res = { analysis: summaries, metadata: res.metadata };
309
+ }
310
+ else {
311
+ res = summaries;
312
+ }
313
+ }
314
+ if (html) {
315
+ const htmlContent = res && typeof res === 'object' && 'analysis' in res ? res.analysis : res;
316
+ console.log(htmlContent);
317
+ }
318
+ else {
319
+ console.log(JSON.stringify(res, null, 2));
320
+ }
321
+ }
322
+ };
146
323
  // command for license checking
147
324
  const license = {
148
325
  command: 'license </path/to/manifest>',
@@ -208,14 +385,119 @@ const license = {
208
385
  console.log(JSON.stringify(output, null, 2));
209
386
  }
210
387
  };
388
+ const sbom = {
389
+ command: 'sbom </path/to/manifest> [--output]',
390
+ desc: 'generate a CycloneDX SBOM from a manifest file',
391
+ builder: yargs => yargs.positional('/path/to/manifest', {
392
+ desc: 'manifest path for SBOM generation',
393
+ type: 'string',
394
+ normalize: true,
395
+ }).options({
396
+ output: {
397
+ alias: 'o',
398
+ desc: 'Write SBOM JSON to a file instead of stdout',
399
+ type: 'string',
400
+ normalize: true,
401
+ },
402
+ workspaceDir: {
403
+ alias: 'w',
404
+ desc: 'Workspace root directory (for monorepos; lock file is expected here)',
405
+ type: 'string',
406
+ normalize: true,
407
+ }
408
+ }),
409
+ handler: async (args) => {
410
+ let manifest = args['/path/to/manifest'];
411
+ const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
412
+ let result;
413
+ try {
414
+ result = await generateSbom(manifest, opts);
415
+ }
416
+ catch (err) {
417
+ console.error(JSON.stringify({ error: `Failed to generate SBOM: ${err.message}` }, null, 2));
418
+ process.exit(1);
419
+ }
420
+ const json = JSON.stringify(result, null, 2);
421
+ if (args.output) {
422
+ try {
423
+ fs.writeFileSync(args.output, json);
424
+ }
425
+ catch (err) {
426
+ console.error(JSON.stringify({ error: `Failed to write output file: ${err.message}` }, null, 2));
427
+ process.exit(1);
428
+ }
429
+ }
430
+ else {
431
+ console.log(json);
432
+ }
433
+ }
434
+ };
435
+ const remediate = {
436
+ command: 'remediate <path>',
437
+ desc: 'Scan and apply vulnerability remediations to manifest files',
438
+ builder: yargs => yargs.positional('path', {
439
+ desc: 'Path to manifest file or directory',
440
+ type: 'string',
441
+ normalize: true,
442
+ }).options({
443
+ 'dry-run': {
444
+ alias: 'd',
445
+ type: 'boolean',
446
+ desc: 'Preview changes without modifying files',
447
+ },
448
+ providers: {
449
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
450
+ type: 'string',
451
+ },
452
+ sources: {
453
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
454
+ type: 'string',
455
+ },
456
+ 'group-by': {
457
+ type: 'string',
458
+ choices: ['dependency', 'bundle'],
459
+ default: 'dependency',
460
+ desc: 'Report grouping strategy',
461
+ },
462
+ }),
463
+ handler: async (args) => {
464
+ try {
465
+ const result = await runRemediation(args.path, {
466
+ dryRun: args['dry-run'],
467
+ providers: args.providers,
468
+ sources: args.sources,
469
+ });
470
+ if (result.remediations.length === 0 && result.manifests.length === 0) {
471
+ console.log('No supported manifest files found.');
472
+ process.exit(result.exitCode);
473
+ }
474
+ if (!args['dry-run'] && result.appliedFiles.length > 0) {
475
+ console.log(`Updated ${result.appliedFiles.length} file(s):`);
476
+ for (const file of result.appliedFiles) {
477
+ console.log(` ${file}`);
478
+ }
479
+ console.log('');
480
+ }
481
+ console.log(generateReport(result.remediations, { groupBy: args['group-by'], dryRun: args['dry-run'] }));
482
+ process.exit(result.exitCode);
483
+ }
484
+ catch (err) {
485
+ console.error(err.message);
486
+ process.exit(1);
487
+ }
488
+ }
489
+ };
211
490
  // parse and invoke the command
212
491
  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}`)
492
+ .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
493
  .command(stack)
494
+ .command(stackBatch)
215
495
  .command(component)
216
496
  .command(image)
217
497
  .command(validateToken)
218
498
  .command(license)
499
+ .command(sbom)
500
+ .command(remediate)
219
501
  .scriptName('')
220
502
  .version(false)
221
503
  .demandCommand(1)