@trustify-da/trustify-da-javascript-client 0.3.0-ea.c2f6c64 → 0.3.0-ea.c300d9f
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 +30 -26
- package/dist/src/analysis.d.ts +14 -7
- package/dist/src/analysis.js +32 -14
- 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 +20 -1
- package/dist/src/cyclone_dx_sbom.js +68 -9
- package/dist/src/index.d.ts +79 -15
- package/dist/src/index.js +90 -34
- 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/package_version.d.ts +8 -0
- package/dist/src/package_version.js +31 -0
- package/dist/src/provider.d.ts +4 -3
- package/dist/src/provider.js +8 -2
- package/dist/src/providers/base_java.d.ts +67 -12
- package/dist/src/providers/base_java.js +82 -61
- package/dist/src/providers/base_javascript.d.ts +74 -9
- package/dist/src/providers/base_javascript.js +116 -7
- package/dist/src/providers/base_pyproject.d.ts +42 -31
- package/dist/src/providers/base_pyproject.js +121 -78
- 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 +11 -1
- package/dist/src/providers/golang_gomodules.js +114 -15
- package/dist/src/providers/java_gradle.d.ts +67 -0
- package/dist/src/providers/java_gradle.js +312 -20
- 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 +37 -9
- package/dist/src/providers/java_maven.js +269 -11
- package/dist/src/providers/javascript_bun.d.ts +22 -0
- package/dist/src/providers/javascript_bun.js +141 -0
- package/dist/src/providers/javascript_npm.d.ts +19 -0
- package/dist/src/providers/javascript_npm.js +61 -1
- package/dist/src/providers/javascript_pnpm.d.ts +13 -0
- package/dist/src/providers/javascript_pnpm.js +53 -3
- 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/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 +6 -2
- package/dist/src/providers/processors/yarn_berry_processor.js +9 -4
- 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.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 +5 -5
- 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 +64 -8
- package/dist/src/providers/python_poetry.js +165 -25
- package/dist/src/providers/python_uv.d.ts +22 -2
- package/dist/src/providers/python_uv.js +157 -6
- package/dist/src/providers/requirements_parser.js +1 -1
- package/dist/src/providers/rust_cargo.d.ts +6 -2
- package/dist/src/providers/rust_cargo.js +101 -33
- 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 +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 +18 -1
- package/dist/src/sbom.js +14 -2
- package/dist/src/tools.d.ts +37 -13
- package/dist/src/tools.js +92 -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 +11 -1
- package/dist/src/workspace.js +3 -2
- package/package.json +31 -27
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
|
+
};
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{
|
|
6
|
+
* 'group-by'?: 'dependency' | 'bundle',
|
|
7
|
+
* exclude?: string[],
|
|
8
|
+
* labels?: string[],
|
|
9
|
+
* 'branch-prefix'?: string,
|
|
10
|
+
* [key: string]: unknown
|
|
11
|
+
* }} RemediationConfig
|
|
12
|
+
*/
|
|
13
|
+
/** @typedef {{ critical?: number, high?: number, 'license-conflicts'?: number, [key: string]: unknown }} FailOnConfig */
|
|
14
|
+
/** @typedef {{ 'fail-on'?: FailOnConfig, [key: string]: unknown }} CheckConfig */
|
|
15
|
+
/** @typedef {{ format?: string, targets?: string[], [key: string]: unknown }} SbomConfig */
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {{
|
|
18
|
+
* 'backend-url'?: string,
|
|
19
|
+
* providers?: string[] | string,
|
|
20
|
+
* sources?: string[] | string,
|
|
21
|
+
* remediation?: RemediationConfig,
|
|
22
|
+
* check?: CheckConfig,
|
|
23
|
+
* sbom?: SbomConfig,
|
|
24
|
+
* [key: string]: unknown
|
|
25
|
+
* }} FileConfig
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {{
|
|
29
|
+
* backendUrl: string | null,
|
|
30
|
+
* backendUrlSource: 'cli' | 'environment' | 'file' | 'default',
|
|
31
|
+
* providers: string[],
|
|
32
|
+
* sources: string[],
|
|
33
|
+
* groupBy: string,
|
|
34
|
+
* remediation: object,
|
|
35
|
+
* check: object,
|
|
36
|
+
* sbom: object
|
|
37
|
+
* }} ResolvedConfig
|
|
38
|
+
*/
|
|
39
|
+
/** Config file names discovered by walking up the directory tree, in precedence order. */
|
|
40
|
+
export const CONFIG_FILENAMES = ['.trustify-da.yml', '.trustify-da.yaml'];
|
|
41
|
+
function isMapping(value) {
|
|
42
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const prototype = Object.getPrototypeOf(value);
|
|
46
|
+
return prototype === Object.prototype || prototype == null;
|
|
47
|
+
}
|
|
48
|
+
function assertMapping(value, field, source) {
|
|
49
|
+
if (!isMapping(value)) {
|
|
50
|
+
throw new Error(`Invalid config file ${source}: ${field} must be a mapping`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function assertString(value, field, source) {
|
|
54
|
+
if (typeof value !== 'string') {
|
|
55
|
+
throw new Error(`Invalid config file ${source}: ${field} must be a string`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function assertStringArray(value, field, source) {
|
|
59
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
|
|
60
|
+
throw new Error(`Invalid config file ${source}: ${field} must be an array of strings`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function assertStringOrArray(value, field, source) {
|
|
64
|
+
if (typeof value !== 'string') {
|
|
65
|
+
assertStringArray(value, field, source);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validates all fields in the initial configuration schema. Unknown fields are preserved.
|
|
70
|
+
* @param {unknown} config
|
|
71
|
+
* @param {string} [source]
|
|
72
|
+
* @returns {FileConfig}
|
|
73
|
+
*/
|
|
74
|
+
function validateConfig(config, source = '<config>') {
|
|
75
|
+
assertMapping(config, 'root', source);
|
|
76
|
+
if (config['backend-url'] !== undefined) {
|
|
77
|
+
assertString(config['backend-url'], 'backend-url', source);
|
|
78
|
+
}
|
|
79
|
+
if (config.providers !== undefined) {
|
|
80
|
+
assertStringOrArray(config.providers, 'providers', source);
|
|
81
|
+
}
|
|
82
|
+
if (config.sources !== undefined) {
|
|
83
|
+
assertStringOrArray(config.sources, 'sources', source);
|
|
84
|
+
}
|
|
85
|
+
if (config.remediation !== undefined) {
|
|
86
|
+
assertMapping(config.remediation, 'remediation', source);
|
|
87
|
+
const remediation = config.remediation;
|
|
88
|
+
if (remediation['group-by'] !== undefined && !['dependency', 'bundle'].includes(remediation['group-by'])) {
|
|
89
|
+
throw new Error(`Invalid config file ${source}: remediation.group-by must be dependency or bundle`);
|
|
90
|
+
}
|
|
91
|
+
if (remediation.exclude !== undefined) {
|
|
92
|
+
assertStringArray(remediation.exclude, 'remediation.exclude', source);
|
|
93
|
+
}
|
|
94
|
+
if (remediation.labels !== undefined) {
|
|
95
|
+
assertStringArray(remediation.labels, 'remediation.labels', source);
|
|
96
|
+
}
|
|
97
|
+
if (remediation['branch-prefix'] !== undefined) {
|
|
98
|
+
assertString(remediation['branch-prefix'], 'remediation.branch-prefix', source);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (config.check !== undefined) {
|
|
102
|
+
assertMapping(config.check, 'check', source);
|
|
103
|
+
if (config.check['fail-on'] !== undefined) {
|
|
104
|
+
assertMapping(config.check['fail-on'], 'check.fail-on', source);
|
|
105
|
+
for (const field of ['critical', 'high', 'license-conflicts']) {
|
|
106
|
+
const value = config.check['fail-on'][field];
|
|
107
|
+
if (value !== undefined && (typeof value !== 'number' || !Number.isFinite(value))) {
|
|
108
|
+
throw new Error(`Invalid config file ${source}: check.fail-on.${field} must be a finite number`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (config.sbom !== undefined) {
|
|
114
|
+
assertMapping(config.sbom, 'sbom', source);
|
|
115
|
+
if (config.sbom.format !== undefined) {
|
|
116
|
+
assertString(config.sbom.format, 'sbom.format', source);
|
|
117
|
+
}
|
|
118
|
+
if (config.sbom.targets !== undefined) {
|
|
119
|
+
assertStringArray(config.sbom.targets, 'sbom.targets', source);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return config;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Walks up from `startPath` looking for a `.trustify-da.yml` (or `.yaml`) file,
|
|
126
|
+
* similar to how `.eslintrc` discovery works. If `startPath` points at a file,
|
|
127
|
+
* discovery begins in its containing directory. Within a directory, `.yml` takes
|
|
128
|
+
* precedence over `.yaml`.
|
|
129
|
+
* @param {string} startPath - manifest file or directory to start the search from
|
|
130
|
+
* @returns {string | null} absolute path to the config file, or null if none found
|
|
131
|
+
*/
|
|
132
|
+
function findConfigFile(startPath) {
|
|
133
|
+
let dir = path.resolve(startPath || '.');
|
|
134
|
+
try {
|
|
135
|
+
if (fs.statSync(dir).isFile()) {
|
|
136
|
+
dir = path.dirname(dir);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// startPath may not exist yet — walk up from its resolved location anyway
|
|
141
|
+
}
|
|
142
|
+
// Walk up until the filesystem root (where dirname(dir) === dir)
|
|
143
|
+
for (;;) {
|
|
144
|
+
for (const name of CONFIG_FILENAMES) {
|
|
145
|
+
const candidate = path.join(dir, name);
|
|
146
|
+
try {
|
|
147
|
+
if (fs.statSync(candidate).isFile()) {
|
|
148
|
+
return candidate;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Ignore inaccessible or disappearing candidates and continue discovery.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const parent = path.dirname(dir);
|
|
156
|
+
if (parent === dir) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
dir = parent;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Discovers and parses `.trustify-da.yml` by walking up from `startPath`.
|
|
164
|
+
* A missing config file is not an error — an empty object is returned so callers
|
|
165
|
+
* can fall back to defaults.
|
|
166
|
+
* @param {string} [startPath] - manifest file or directory to start the search from
|
|
167
|
+
* @returns {FileConfig} the parsed config object, or `{}` when no file is found
|
|
168
|
+
* @throws {Error} when the config file exists but contains malformed YAML
|
|
169
|
+
*/
|
|
170
|
+
export function loadConfig(startPath) {
|
|
171
|
+
const configPath = findConfigFile(startPath);
|
|
172
|
+
if (!configPath) {
|
|
173
|
+
return {};
|
|
174
|
+
}
|
|
175
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
176
|
+
let doc;
|
|
177
|
+
try {
|
|
178
|
+
doc = yamlLoad(content);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
throw new Error(`Failed to parse config file ${configPath}: ${err.message}`);
|
|
182
|
+
}
|
|
183
|
+
return doc == null ? {} : validateConfig(doc, configPath);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Normalizes a providers/sources value to an array of trimmed strings. Accepts
|
|
187
|
+
* either a YAML array (`[redhat, osv]`) or a comma-separated string (`redhat,osv`).
|
|
188
|
+
* @param {string[] | string | undefined | null} value
|
|
189
|
+
* @returns {string[]}
|
|
190
|
+
*/
|
|
191
|
+
function toArray(value) {
|
|
192
|
+
if (Array.isArray(value)) {
|
|
193
|
+
return value.filter(v => typeof v === 'string' && v.trim()).map(v => v.trim());
|
|
194
|
+
}
|
|
195
|
+
if (typeof value === 'string') {
|
|
196
|
+
return value.split(',').map(v => v.trim()).filter(Boolean);
|
|
197
|
+
}
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Returns the first argument that is neither undefined nor null.
|
|
202
|
+
* @param {...unknown} values
|
|
203
|
+
* @returns {unknown}
|
|
204
|
+
*/
|
|
205
|
+
function first(...values) {
|
|
206
|
+
return values.find(v => v != null);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Merges config sources with precedence: CLI flag > environment variable
|
|
210
|
+
* (`TRUSTIFY_DA_*`) > config file > hardcoded default. Providers and sources are
|
|
211
|
+
* normalized to arrays regardless of whether the source used a YAML array or a
|
|
212
|
+
* comma-separated string.
|
|
213
|
+
* @param {FileConfig} [fileConfig={}] - values parsed from `.trustify-da.yml`
|
|
214
|
+
* @param {{ providers?: string, sources?: string, groupBy?: string, backendUrl?: string }} [cliFlags={}]
|
|
215
|
+
* @param {{ [key: string]: string | undefined }} [envVars={}] - typically `process.env`
|
|
216
|
+
* @returns {ResolvedConfig} the resolved, typed config object
|
|
217
|
+
*/
|
|
218
|
+
export function mergeConfig(fileConfig = {}, cliFlags = {}, envVars = {}) {
|
|
219
|
+
const file = validateConfig(fileConfig || {});
|
|
220
|
+
const cli = cliFlags || {};
|
|
221
|
+
const env = envVars || {};
|
|
222
|
+
let backendUrl = null;
|
|
223
|
+
let backendUrlSource = 'default';
|
|
224
|
+
if (cli.backendUrl != null) {
|
|
225
|
+
backendUrl = cli.backendUrl;
|
|
226
|
+
backendUrlSource = 'cli';
|
|
227
|
+
}
|
|
228
|
+
else if (env.TRUSTIFY_DA_BACKEND_URL != null) {
|
|
229
|
+
backendUrl = env.TRUSTIFY_DA_BACKEND_URL;
|
|
230
|
+
backendUrlSource = 'environment';
|
|
231
|
+
}
|
|
232
|
+
else if (file['backend-url'] != null) {
|
|
233
|
+
backendUrl = file['backend-url'];
|
|
234
|
+
backendUrlSource = 'file';
|
|
235
|
+
}
|
|
236
|
+
const groupBy = first(cli.groupBy, env.TRUSTIFY_DA_GROUP_BY, file.remediation?.['group-by']) ?? 'dependency';
|
|
237
|
+
if (!['dependency', 'bundle'].includes(groupBy)) {
|
|
238
|
+
throw new Error(`Invalid group-by value "${groupBy}". Expected dependency or bundle.`);
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
backendUrl,
|
|
242
|
+
backendUrlSource,
|
|
243
|
+
providers: toArray(first(cli.providers, env.TRUSTIFY_DA_PROVIDERS, file.providers)),
|
|
244
|
+
sources: toArray(first(cli.sources, env.TRUSTIFY_DA_SOURCES, file.sources)),
|
|
245
|
+
groupBy,
|
|
246
|
+
remediation: file.remediation ?? {},
|
|
247
|
+
check: file.check ?? {},
|
|
248
|
+
sbom: file.sbom ?? {},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Loads and merges the project configuration for a target path.
|
|
253
|
+
* @param {string} [startPath] - manifest file or directory to start discovery from
|
|
254
|
+
* @param {{ providers?: string, sources?: string, groupBy?: string, backendUrl?: string }} [cliFlags={}] - explicit options
|
|
255
|
+
* @param {{ [key: string]: string | undefined }} [envVars={}] - typically `process.env`
|
|
256
|
+
* @returns {ResolvedConfig} the resolved, typed config object
|
|
257
|
+
*/
|
|
258
|
+
export function resolveConfig(startPath, cliFlags = {}, envVars = {}) {
|
|
259
|
+
return mergeConfig(loadConfig(startPath), cliFlags, envVars);
|
|
260
|
+
}
|
|
@@ -18,9 +18,27 @@ 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;
|
|
29
|
+
/**
|
|
30
|
+
* Attach hashes to already-added components by matching their PURL. This is a
|
|
31
|
+
* post-processing step so ecosystem-specific hash sources (e.g. Maven reading
|
|
32
|
+
* the local .m2 cache) can enrich the SBOM without threading their concern
|
|
33
|
+
* through the shared dependency-tree parser. Components without a matching
|
|
34
|
+
* entry, or whose hashes are already set, are left untouched.
|
|
35
|
+
* @param {Map<string, Array<{alg: string, content: string}>>} hashMap - PURL→hashes map
|
|
36
|
+
* @return {CycloneDxSbom} the updated SBOM
|
|
37
|
+
*/
|
|
38
|
+
attachHashes(hashMap: Map<string, Array<{
|
|
39
|
+
alg: string;
|
|
40
|
+
content: string;
|
|
41
|
+
}>>): CycloneDxSbom;
|
|
24
42
|
/** @param {{}} opts - various options, settings and configuration of application.
|
|
25
43
|
* @return String CycloneDx Sbom json object in a string format
|
|
26
44
|
*/
|
|
@@ -50,6 +68,7 @@ export default class CycloneDxSbom {
|
|
|
50
68
|
version: any;
|
|
51
69
|
scope: any;
|
|
52
70
|
licenses?: any;
|
|
71
|
+
hashes?: any;
|
|
53
72
|
};
|
|
54
73
|
/**
|
|
55
74
|
* This method gets an array of dependencies to be ignored, and remove all of them from CycloneDx Sbom
|