@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.
- package/README.md +304 -13
- package/dist/package.json +31 -23
- package/dist/src/analysis.d.ts +28 -5
- package/dist/src/analysis.js +78 -13
- package/dist/src/batch_opts.d.ts +24 -0
- package/dist/src/batch_opts.js +35 -0
- package/dist/src/cli.js +276 -5
- package/dist/src/cyclone_dx_sbom.d.ts +14 -1
- package/dist/src/cyclone_dx_sbom.js +45 -9
- package/dist/src/index.d.ts +142 -7
- package/dist/src/index.js +355 -6
- package/dist/src/license/index.d.ts +3 -3
- package/dist/src/license/index.js +4 -1
- package/dist/src/license/license_utils.js +4 -1
- package/dist/src/license/licenses_api.d.ts +2 -2
- package/dist/src/oci_image/images.d.ts +1 -1
- package/dist/src/oci_image/images.js +15 -1
- package/dist/src/oci_image/utils.d.ts +5 -5
- package/dist/src/oci_image/utils.js +11 -2
- package/dist/src/provider.d.ts +8 -4
- package/dist/src/provider.js +16 -5
- package/dist/src/providers/base_java.d.ts +14 -12
- package/dist/src/providers/base_java.js +18 -44
- package/dist/src/providers/base_javascript.d.ts +37 -10
- package/dist/src/providers/base_javascript.js +108 -20
- package/dist/src/providers/base_pyproject.d.ts +158 -0
- package/dist/src/providers/base_pyproject.js +322 -0
- package/dist/src/providers/containerfile_parser.d.ts +5 -0
- package/dist/src/providers/containerfile_parser.js +20 -0
- package/dist/src/providers/golang_gomodules.d.ts +23 -13
- package/dist/src/providers/golang_gomodules.js +207 -120
- package/dist/src/providers/gomod_parser.d.ts +4 -0
- package/dist/src/providers/gomod_parser.js +16 -0
- package/dist/src/providers/java_gradle.d.ts +19 -0
- package/dist/src/providers/java_gradle.js +116 -2
- package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
- package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
- package/dist/src/providers/java_maven.d.ts +10 -2
- package/dist/src/providers/java_maven.js +165 -1
- package/dist/src/providers/javascript_bun.d.ts +10 -0
- package/dist/src/providers/javascript_bun.js +100 -0
- package/dist/src/providers/javascript_npm.d.ts +6 -0
- package/dist/src/providers/javascript_npm.js +21 -0
- package/dist/src/providers/javascript_pnpm.d.ts +1 -1
- package/dist/src/providers/javascript_pnpm.js +8 -4
- package/dist/src/providers/manifest.d.ts +2 -0
- package/dist/src/providers/manifest.js +22 -4
- package/dist/src/providers/marker_evaluator.d.ts +14 -0
- package/dist/src/providers/marker_evaluator.js +191 -0
- package/dist/src/providers/oci_dockerfile.d.ts +51 -0
- package/dist/src/providers/oci_dockerfile.js +177 -0
- package/dist/src/providers/processors/yarn_berry_processor.d.ts +2 -2
- package/dist/src/providers/processors/yarn_berry_processor.js +88 -5
- package/dist/src/providers/processors/yarn_classic_processor.d.ts +2 -2
- package/dist/src/providers/python_controller.d.ts +5 -1
- package/dist/src/providers/python_controller.js +69 -8
- package/dist/src/providers/python_pip.d.ts +5 -0
- package/dist/src/providers/python_pip.js +6 -6
- package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
- package/dist/src/providers/python_pip_pyproject.js +146 -0
- package/dist/src/providers/python_poetry.d.ts +98 -0
- package/dist/src/providers/python_poetry.js +286 -0
- package/dist/src/providers/python_uv.d.ts +46 -0
- package/dist/src/providers/python_uv.js +269 -0
- package/dist/src/providers/rust_cargo.d.ts +12 -3
- package/dist/src/providers/rust_cargo.js +115 -21
- package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
- package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
- package/dist/src/remediate.d.ts +21 -0
- package/dist/src/remediate.js +149 -0
- package/dist/src/remediation.d.ts +57 -0
- package/dist/src/remediation.js +436 -0
- package/dist/src/remediation_report.d.ts +40 -0
- package/dist/src/remediation_report.js +159 -0
- package/dist/src/sbom.d.ts +14 -1
- package/dist/src/sbom.js +13 -2
- package/dist/src/tools.d.ts +28 -2
- package/dist/src/tools.js +58 -0
- package/dist/src/updaters/maven_updater.d.ts +26 -0
- package/dist/src/updaters/maven_updater.js +322 -0
- package/dist/src/updaters/toml_updater.d.ts +33 -0
- package/dist/src/updaters/toml_updater.js +244 -0
- package/dist/src/workspace.d.ts +70 -0
- package/dist/src/workspace.js +256 -0
- package/package.json +32 -24
package/dist/src/analysis.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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':
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|