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