@trustify-da/trustify-da-javascript-client 0.3.0-ea.ec6824b → 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 +15 -10
- package/dist/src/analysis.d.ts +47 -19
- 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 +2 -2
- package/dist/src/license/index.js +9 -6
- package/dist/src/license/licenses_api.d.ts +2 -2
- package/dist/src/license/licenses_api.js +1 -1
- package/dist/src/oci_image/images.js +15 -1
- package/dist/src/package_version.d.ts +8 -0
- package/dist/src/package_version.js +31 -0
- package/dist/src/provider.js +3 -1
- package/dist/src/providers/base_java.d.ts +53 -0
- package/dist/src/providers/base_java.js +64 -17
- package/dist/src/providers/base_javascript.d.ts +54 -0
- package/dist/src/providers/base_javascript.js +106 -4
- 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.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_maven.d.ts +28 -8
- 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 +13 -0
- 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 +5 -1
- package/dist/src/providers/processors/yarn_berry_processor.js +3 -2
- package/dist/src/providers/processors/yarn_classic_processor.d.ts +5 -1
- package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
- package/dist/src/providers/python_controller.js +4 -1
- package/dist/src/providers/requirements_parser.js +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 +9 -11
- 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 +16 -11
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { generateImageSBOM, parseImageRef } from '../oci_image/utils.js';
|
|
3
|
+
import { getArgQuery, getFromQuery, getParser } from './containerfile_parser.js';
|
|
4
|
+
export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest, packageManagerName() { return 'oci'; } };
|
|
5
|
+
/** @typedef {import('../provider').Provider} */
|
|
6
|
+
/** @typedef {import('../provider').Provided} Provided */
|
|
7
|
+
/**
|
|
8
|
+
* @type {string} ecosystem identifier for OCI image packages
|
|
9
|
+
* @private
|
|
10
|
+
*/
|
|
11
|
+
const ecosystem = 'oci';
|
|
12
|
+
/**
|
|
13
|
+
* Check if the given manifest name is a Dockerfile or Containerfile.
|
|
14
|
+
* Supports dot-suffixed variants such as Dockerfile.dev or Containerfile.prod.
|
|
15
|
+
* @param {string} manifestName the manifest file name to check
|
|
16
|
+
* @returns {boolean} true if the manifest is a Dockerfile or Containerfile
|
|
17
|
+
*/
|
|
18
|
+
function isSupported(manifestName) {
|
|
19
|
+
return /^(Dockerfile|Containerfile)(\..+)?$/.test(manifestName);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Dockerfiles have no lock file, so validation always passes.
|
|
23
|
+
* @returns {boolean} always true
|
|
24
|
+
*/
|
|
25
|
+
function validateLockFile() { return true; }
|
|
26
|
+
/**
|
|
27
|
+
* Check whether a syntax node contains any expansion (variable substitution) children.
|
|
28
|
+
* @param {import('web-tree-sitter').SyntaxNode} node
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
* @private
|
|
31
|
+
*/
|
|
32
|
+
function containsExpansion(node) {
|
|
33
|
+
if (node.type === 'expansion') {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
37
|
+
if (containsExpansion(node.child(i))) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Strip surrounding single or double quotes from a string.
|
|
45
|
+
* @param {string} text
|
|
46
|
+
* @returns {string}
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
function stripQuotes(text) {
|
|
50
|
+
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
51
|
+
return text.slice(1, -1);
|
|
52
|
+
}
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Collect ARG key-value pairs from the Dockerfile AST.
|
|
57
|
+
* Only ARGs with a default value are collected (ARGs without defaults cannot be resolved statically).
|
|
58
|
+
* @param {import('web-tree-sitter').Tree} tree the parsed Dockerfile tree
|
|
59
|
+
* @param {import('web-tree-sitter').Query} argQuery the tree-sitter query for arg_instruction nodes
|
|
60
|
+
* @returns {Map<string, string>} map of ARG names to their default values
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
function collectArgs(tree, argQuery) {
|
|
64
|
+
const args = new Map();
|
|
65
|
+
for (const match of argQuery.matches(tree.rootNode)) {
|
|
66
|
+
const name = match.captures.find(c => c.name === 'name')?.node.text;
|
|
67
|
+
const defaultValue = match.captures.find(c => c.name === 'default')?.node.text;
|
|
68
|
+
if (name && defaultValue) {
|
|
69
|
+
args.set(name, stripQuotes(defaultValue));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return args;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve ARG substitutions in an image spec text using the collected ARG map.
|
|
76
|
+
* Replaces ${VAR} and $VAR patterns with their ARG default values.
|
|
77
|
+
* @param {string} text the image spec text potentially containing variable references
|
|
78
|
+
* @param {Map<string, string>} args map of ARG names to their default values
|
|
79
|
+
* @returns {string|null} the resolved text, or null if any variable could not be resolved
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
function resolveArgs(text, args) {
|
|
83
|
+
let resolved = text;
|
|
84
|
+
let hasUnresolved = false;
|
|
85
|
+
resolved = resolved.replace(/\$\{([^}]+)\}|\$([A-Za-z_]\w*)/g, (_match, braced, plain) => {
|
|
86
|
+
const key = braced || plain;
|
|
87
|
+
if (args.has(key)) {
|
|
88
|
+
return args.get(key);
|
|
89
|
+
}
|
|
90
|
+
hasUnresolved = true;
|
|
91
|
+
return _match;
|
|
92
|
+
});
|
|
93
|
+
return hasUnresolved ? null : resolved;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse all FROM instructions from a Dockerfile using tree-sitter to extract base image references.
|
|
97
|
+
* In multi-stage builds, every FROM line is returned so each image can be analyzed independently.
|
|
98
|
+
* ARG substitutions are resolved using declared default values. FROM lines with unresolvable
|
|
99
|
+
* variables (ARGs without defaults) are silently skipped.
|
|
100
|
+
* @param {string} manifestContent the content of the Dockerfile
|
|
101
|
+
* @returns {Promise<string[]>} array of image references from all resolvable FROM instructions
|
|
102
|
+
* @throws {Error} when no FROM instruction is found or when no FROM lines can be resolved
|
|
103
|
+
*/
|
|
104
|
+
export async function parseAllFromImages(manifestContent) {
|
|
105
|
+
const [parser, fromQuery, argQuery] = await Promise.all([getParser(), getFromQuery(), getArgQuery()]);
|
|
106
|
+
const tree = parser.parse(manifestContent);
|
|
107
|
+
const matches = fromQuery.matches(tree.rootNode);
|
|
108
|
+
if (matches.length === 0) {
|
|
109
|
+
throw new Error('No FROM line found in Dockerfile');
|
|
110
|
+
}
|
|
111
|
+
const args = collectArgs(tree, argQuery);
|
|
112
|
+
const images = [];
|
|
113
|
+
for (const match of matches) {
|
|
114
|
+
const imageSpec = match.captures.find(c => c.name === 'image').node;
|
|
115
|
+
if (containsExpansion(imageSpec)) {
|
|
116
|
+
const resolved = resolveArgs(imageSpec.text, args);
|
|
117
|
+
if (resolved != null) {
|
|
118
|
+
images.push(resolved);
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
images.push(imageSpec.text);
|
|
123
|
+
}
|
|
124
|
+
if (images.length === 0) {
|
|
125
|
+
throw new Error('Dockerfile uses ARG substitution in all FROM lines — cannot resolve variable references');
|
|
126
|
+
}
|
|
127
|
+
return images;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Generate image SBOMs for all FROM instructions in a Dockerfile manifest using syft.
|
|
131
|
+
* Returns a batch result with a purl-to-SBOM map suitable for the batch-analysis endpoint.
|
|
132
|
+
* @param {string} manifest path to the Dockerfile
|
|
133
|
+
* @param {{}} [opts={}] optional various options to pass along the application
|
|
134
|
+
* @returns {Promise<{ecosystem: string, content: string, contentType: string, batch: boolean}>}
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
async function getImageSBOM(manifest, opts = {}) {
|
|
138
|
+
const manifestContent = fs.readFileSync(manifest, 'utf-8');
|
|
139
|
+
const images = await parseAllFromImages(manifestContent);
|
|
140
|
+
const sbomByPurl = {};
|
|
141
|
+
for (const image of images) {
|
|
142
|
+
const imageRef = parseImageRef(image, opts);
|
|
143
|
+
const purl = imageRef.getPackageURL().toString();
|
|
144
|
+
if (purl in sbomByPurl)
|
|
145
|
+
continue;
|
|
146
|
+
sbomByPurl[purl] = generateImageSBOM(imageRef, opts);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
ecosystem,
|
|
150
|
+
content: JSON.stringify(sbomByPurl),
|
|
151
|
+
contentType: 'application/vnd.cyclonedx+json',
|
|
152
|
+
batch: true
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Provide content and content type for Dockerfile component analysis.
|
|
157
|
+
* @param {string} manifest path to the Dockerfile
|
|
158
|
+
* @param {{}} [opts={}] optional various options to pass along the application
|
|
159
|
+
* @returns {Promise<Provided>}
|
|
160
|
+
*/
|
|
161
|
+
async function provideComponent(manifest, opts = {}) {
|
|
162
|
+
return getImageSBOM(manifest, opts);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Provide content and content type for Dockerfile stack analysis.
|
|
166
|
+
* @param {string} manifest path to the Dockerfile
|
|
167
|
+
* @param {{}} [opts={}] optional various options to pass along the application
|
|
168
|
+
* @returns {Promise<Provided>}
|
|
169
|
+
*/
|
|
170
|
+
async function provideStack(manifest, opts = {}) {
|
|
171
|
+
return getImageSBOM(manifest, opts);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Dockerfiles contain no license information.
|
|
175
|
+
* @returns {null} always null
|
|
176
|
+
*/
|
|
177
|
+
function readLicenseFromManifest() { return null; }
|
|
@@ -34,8 +34,12 @@ export default class Yarn_berry_processor extends Yarn_processor {
|
|
|
34
34
|
* Adds dependencies to the SBOM
|
|
35
35
|
* @param {Sbom} sbom - The SBOM object to add dependencies to
|
|
36
36
|
* @param {Object} depTree - The dependency tree object
|
|
37
|
+
* @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
|
|
37
38
|
*/
|
|
38
|
-
addDependenciesToSbom(sbom: Sbom, depTree: Object
|
|
39
|
+
addDependenciesToSbom(sbom: Sbom, depTree: Object, hashesForPurl?: (arg0: PackageURL) => (Array<{
|
|
40
|
+
alg: string;
|
|
41
|
+
content: string;
|
|
42
|
+
}> | undefined)): void;
|
|
39
43
|
#private;
|
|
40
44
|
}
|
|
41
45
|
import Yarn_processor from "./yarn_processor.js";
|
|
@@ -78,8 +78,9 @@ export default class Yarn_berry_processor extends Yarn_processor {
|
|
|
78
78
|
* Adds dependencies to the SBOM
|
|
79
79
|
* @param {Sbom} sbom - The SBOM object to add dependencies to
|
|
80
80
|
* @param {Object} depTree - The dependency tree object
|
|
81
|
+
* @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
|
|
81
82
|
*/
|
|
82
|
-
addDependenciesToSbom(sbom, depTree) {
|
|
83
|
+
addDependenciesToSbom(sbom, depTree, hashesForPurl = () => undefined) {
|
|
83
84
|
if (!depTree) {
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
@@ -137,7 +138,7 @@ export default class Yarn_berry_processor extends Yarn_processor {
|
|
|
137
138
|
}
|
|
138
139
|
const to = this.#purlFromLocator(d.locator);
|
|
139
140
|
if (to) {
|
|
140
|
-
sbom.addDependency(from, to);
|
|
141
|
+
sbom.addDependency(from, to, undefined, hashesForPurl(to));
|
|
141
142
|
}
|
|
142
143
|
});
|
|
143
144
|
});
|
|
@@ -30,8 +30,12 @@ export default class Yarn_classic_processor extends Yarn_processor {
|
|
|
30
30
|
* Adds dependencies to the SBOM
|
|
31
31
|
* @param {Sbom} sbom - The SBOM object to add dependencies to
|
|
32
32
|
* @param {Object} depTree - The dependency tree object
|
|
33
|
+
* @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
|
|
33
34
|
*/
|
|
34
|
-
addDependenciesToSbom(sbom: Sbom, depTree: Object
|
|
35
|
+
addDependenciesToSbom(sbom: Sbom, depTree: Object, hashesForPurl?: (arg0: PackageURL) => (Array<{
|
|
36
|
+
alg: string;
|
|
37
|
+
content: string;
|
|
38
|
+
}> | undefined)): void;
|
|
35
39
|
#private;
|
|
36
40
|
}
|
|
37
41
|
import Yarn_processor from "./yarn_processor.js";
|
|
@@ -50,8 +50,9 @@ export default class Yarn_classic_processor extends Yarn_processor {
|
|
|
50
50
|
* Adds dependencies to the SBOM
|
|
51
51
|
* @param {Sbom} sbom - The SBOM object to add dependencies to
|
|
52
52
|
* @param {Object} depTree - The dependency tree object
|
|
53
|
+
* @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
|
|
53
54
|
*/
|
|
54
|
-
addDependenciesToSbom(sbom, depTree) {
|
|
55
|
+
addDependenciesToSbom(sbom, depTree, hashesForPurl = () => undefined) {
|
|
55
56
|
if (!depTree?.data?.trees) {
|
|
56
57
|
return;
|
|
57
58
|
}
|
|
@@ -60,12 +61,12 @@ export default class Yarn_classic_processor extends Yarn_processor {
|
|
|
60
61
|
depTree.data.trees.forEach(n => {
|
|
61
62
|
const dep = new NodeMetaData(n);
|
|
62
63
|
if (this._manifest.dependencies.includes(dep.name)) {
|
|
63
|
-
sbom.addDependency(rootPurl, dep.purl);
|
|
64
|
+
sbom.addDependency(rootPurl, dep.purl, undefined, hashesForPurl(dep.purl));
|
|
64
65
|
}
|
|
65
66
|
purls.set(dep.name, dep.purl);
|
|
66
67
|
});
|
|
67
68
|
depTree.data.trees.forEach(n => {
|
|
68
|
-
this.#addChildrenToSbom(sbom, n, purls);
|
|
69
|
+
this.#addChildrenToSbom(sbom, n, purls, hashesForPurl);
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
72
|
/**
|
|
@@ -73,9 +74,10 @@ export default class Yarn_classic_processor extends Yarn_processor {
|
|
|
73
74
|
* @param {Sbom} sbom - The SBOM object to add dependencies to
|
|
74
75
|
* @param {Object} node - The current dependency node
|
|
75
76
|
* @param {Map<string, PackageURL>} purls - Map of dependency names to their PackageURL objects
|
|
77
|
+
* @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} hashesForPurl - Resolves lock-file hashes for a purl
|
|
76
78
|
* @private
|
|
77
79
|
*/
|
|
78
|
-
#addChildrenToSbom(sbom, node, purls) {
|
|
80
|
+
#addChildrenToSbom(sbom, node, purls, hashesForPurl) {
|
|
79
81
|
const dep = new NodeMetaData(node);
|
|
80
82
|
const children = node.children ? node.children : [];
|
|
81
83
|
children.forEach(c => {
|
|
@@ -83,9 +85,9 @@ export default class Yarn_classic_processor extends Yarn_processor {
|
|
|
83
85
|
const from = dep.shadow ? purls.get(dep.name) : dep.purl;
|
|
84
86
|
const to = child.shadow ? purls.get(child.name) : child.purl;
|
|
85
87
|
if (from && to) {
|
|
86
|
-
sbom.addDependency(from, to);
|
|
88
|
+
sbom.addDependency(from, to, undefined, hashesForPurl(to));
|
|
87
89
|
}
|
|
88
|
-
this.#addChildrenToSbom(sbom, c, purls);
|
|
90
|
+
this.#addChildrenToSbom(sbom, c, purls, hashesForPurl);
|
|
89
91
|
});
|
|
90
92
|
}
|
|
91
93
|
}
|
|
@@ -178,8 +178,11 @@ export default class Python_controller {
|
|
|
178
178
|
startingTime = new Date();
|
|
179
179
|
console.log("Starting time to get requirements.txt dependency tree = " + startingTime);
|
|
180
180
|
}
|
|
181
|
+
let installBestEfforts = getCustom("TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS", "false", this.options);
|
|
182
|
+
if (installBestEfforts === "true" && this.realEnvironment) {
|
|
183
|
+
throw new Error("Conflicting settings, TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS=true requires TRUSTIFY_DA_PYTHON_VIRTUAL_ENV=true");
|
|
184
|
+
}
|
|
181
185
|
if (!this.realEnvironment) {
|
|
182
|
-
let installBestEfforts = getCustom("TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS", "false", this.options);
|
|
183
186
|
if (installBestEfforts === "false") {
|
|
184
187
|
try {
|
|
185
188
|
invokeCommand(this.pathToPipBin, ['install', '-r', this.pathToRequirements]);
|
|
@@ -16,7 +16,7 @@ export async function getRequirementQuery() {
|
|
|
16
16
|
}
|
|
17
17
|
export async function getIgnoreQuery() {
|
|
18
18
|
const language = await init();
|
|
19
|
-
return new Query(language, '((requirement (package) @name) @req . (comment) @comment (#match? @comment "^#[\\t ]*exhortignore"))');
|
|
19
|
+
return new Query(language, '((requirement (package) @name) @req . (comment) @comment (#match? @comment "^#[\\t ]*(exhortignore|trustify-da-ignore)(?![\\w-])"))');
|
|
20
20
|
}
|
|
21
21
|
export async function getPinnedVersionQuery() {
|
|
22
22
|
const language = await init();
|
|
@@ -116,30 +116,25 @@ function resolveWorkspaceLicense(parsed, metadata) {
|
|
|
116
116
|
return parsed.workspace?.package?.license || null;
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* the workspace root, so when a member crate's Cargo.toml is provided we walk
|
|
122
|
-
* up the directory tree looking for Cargo.lock (stopping when we find a
|
|
123
|
-
* Cargo.toml that contains a [workspace] section, or when we reach the
|
|
124
|
-
* filesystem root).
|
|
125
|
-
* When TRUSTIFY_DA_WORKSPACE_DIR is provided (via env var or opts),
|
|
126
|
-
* checks only that directory for Cargo.lock — no walk-up.
|
|
119
|
+
* Finds the directory containing Cargo.lock, honoring TRUSTIFY_DA_WORKSPACE_DIR.
|
|
120
|
+
* Walks up from manifestDir looking for Cargo.lock, stopping at workspace root or filesystem root.
|
|
127
121
|
* @param {string} manifestDir - the directory where the manifest lies
|
|
128
122
|
* @param {{TRUSTIFY_DA_WORKSPACE_DIR?: string}} [opts={}] - optional workspace root
|
|
129
|
-
* @returns {
|
|
123
|
+
* @returns {string|null} the directory path containing Cargo.lock, or null if not found
|
|
124
|
+
* @private
|
|
130
125
|
*/
|
|
131
|
-
function
|
|
126
|
+
function _findCargoLockDir(manifestDir, opts = {}) {
|
|
132
127
|
const workspaceDir = getCustom('TRUSTIFY_DA_WORKSPACE_DIR', null, opts);
|
|
133
128
|
if (workspaceDir) {
|
|
134
129
|
const dir = path.resolve(workspaceDir);
|
|
135
|
-
return fs.existsSync(path.join(dir, 'Cargo.lock'));
|
|
130
|
+
return fs.existsSync(path.join(dir, 'Cargo.lock')) ? dir : null;
|
|
136
131
|
}
|
|
137
132
|
let dir = path.resolve(manifestDir);
|
|
138
133
|
let parent = dir;
|
|
139
134
|
do {
|
|
140
135
|
dir = parent;
|
|
141
136
|
if (fs.existsSync(path.join(dir, 'Cargo.lock'))) {
|
|
142
|
-
return
|
|
137
|
+
return dir;
|
|
143
138
|
}
|
|
144
139
|
// If this directory has a Cargo.toml with [workspace], the lock file
|
|
145
140
|
// should have been here — stop searching.
|
|
@@ -148,7 +143,7 @@ function validateLockFile(manifestDir, opts = {}) {
|
|
|
148
143
|
try {
|
|
149
144
|
let content = fs.readFileSync(cargoToml, 'utf-8');
|
|
150
145
|
if (/\[workspace\]/.test(content)) {
|
|
151
|
-
return
|
|
146
|
+
return null;
|
|
152
147
|
}
|
|
153
148
|
}
|
|
154
149
|
catch (_) {
|
|
@@ -157,7 +152,23 @@ function validateLockFile(manifestDir, opts = {}) {
|
|
|
157
152
|
}
|
|
158
153
|
parent = path.dirname(dir);
|
|
159
154
|
} while (parent !== dir);
|
|
160
|
-
return
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Validates that Cargo.lock exists in the manifest directory or in a parent
|
|
159
|
+
* workspace root directory. In Cargo workspaces the lock file always lives at
|
|
160
|
+
* the workspace root, so when a member crate's Cargo.toml is provided we walk
|
|
161
|
+
* up the directory tree looking for Cargo.lock (stopping when we find a
|
|
162
|
+
* Cargo.toml that contains a [workspace] section, or when we reach the
|
|
163
|
+
* filesystem root).
|
|
164
|
+
* When TRUSTIFY_DA_WORKSPACE_DIR is provided (via env var or opts),
|
|
165
|
+
* checks only that directory for Cargo.lock — no walk-up.
|
|
166
|
+
* @param {string} manifestDir - the directory where the manifest lies
|
|
167
|
+
* @param {{TRUSTIFY_DA_WORKSPACE_DIR?: string}} [opts={}] - optional workspace root
|
|
168
|
+
* @returns {boolean} true if Cargo.lock is found
|
|
169
|
+
*/
|
|
170
|
+
function validateLockFile(manifestDir, opts = {}) {
|
|
171
|
+
return _findCargoLockDir(manifestDir, opts) !== null;
|
|
161
172
|
}
|
|
162
173
|
/**
|
|
163
174
|
* Provide content and content type for Cargo stack analysis.
|
|
@@ -201,12 +212,13 @@ function getSBOM(manifest, opts = {}, includeTransitive) {
|
|
|
201
212
|
let ignoredDeps = getIgnoredDeps(manifest, metadata);
|
|
202
213
|
let crateType = detectCrateType(metadata);
|
|
203
214
|
let license = readLicenseFromManifest(manifest, metadata);
|
|
215
|
+
let hashMap = parseCargoLockHashes(manifestDir, opts);
|
|
204
216
|
let sbom;
|
|
205
217
|
if (crateType === CrateType.WORKSPACE_VIRTUAL) {
|
|
206
|
-
sbom = handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransitive, opts, license);
|
|
218
|
+
sbom = handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransitive, opts, license, hashMap);
|
|
207
219
|
}
|
|
208
220
|
else {
|
|
209
|
-
sbom = handleSingleCrate(metadata, ignoredDeps, includeTransitive, opts, license);
|
|
221
|
+
sbom = handleSingleCrate(metadata, ignoredDeps, includeTransitive, opts, license, hashMap);
|
|
210
222
|
}
|
|
211
223
|
return sbom;
|
|
212
224
|
}
|
|
@@ -244,6 +256,34 @@ function executeCargoMetadata(cargoBin, manifestDir) {
|
|
|
244
256
|
throw new Error('failed to execute cargo metadata', { cause: error });
|
|
245
257
|
}
|
|
246
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Parses Cargo.lock to extract SHA-256 checksums for each package.
|
|
261
|
+
* @param {string} manifestDir - directory containing Cargo.toml (Cargo.lock is searched upward)
|
|
262
|
+
* @param {{TRUSTIFY_DA_WORKSPACE_DIR?: string}} [opts={}] - optional workspace root
|
|
263
|
+
* @returns {Map<string, Array<{alg: string, content: string}>>} map of "name@version" to CycloneDX hashes
|
|
264
|
+
* @private
|
|
265
|
+
*/
|
|
266
|
+
function parseCargoLockHashes(manifestDir, opts = {}) {
|
|
267
|
+
let hashMap = new Map();
|
|
268
|
+
let lockDir = _findCargoLockDir(manifestDir, opts);
|
|
269
|
+
if (!lockDir) {
|
|
270
|
+
return hashMap;
|
|
271
|
+
}
|
|
272
|
+
let lockPath = path.join(lockDir, 'Cargo.lock');
|
|
273
|
+
try {
|
|
274
|
+
let content = fs.readFileSync(lockPath, 'utf-8');
|
|
275
|
+
let parsed = parseToml(content);
|
|
276
|
+
for (let pkg of (parsed.package || [])) {
|
|
277
|
+
if (pkg.name && pkg.version && pkg.checksum) {
|
|
278
|
+
hashMap.set(`${pkg.name}@${pkg.version}`, [{ alg: 'SHA-256', content: pkg.checksum }]);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
console.warn(`Failed to parse Cargo.lock at ${lockPath} for hashes: ${error.message}. SBOM will be generated without hashes.`);
|
|
284
|
+
}
|
|
285
|
+
return hashMap;
|
|
286
|
+
}
|
|
247
287
|
/**
|
|
248
288
|
* Detects the type of Cargo project from metadata.
|
|
249
289
|
* @param {object} metadata - parsed cargo metadata
|
|
@@ -275,7 +315,7 @@ function detectCrateType(metadata) {
|
|
|
275
315
|
* @returns {string} SBOM json string
|
|
276
316
|
* @private
|
|
277
317
|
*/
|
|
278
|
-
function handleSingleCrate(metadata, ignoredDeps, includeTransitive, opts, license) {
|
|
318
|
+
function handleSingleCrate(metadata, ignoredDeps, includeTransitive, opts, license, hashMap) {
|
|
279
319
|
let rootPackageId = metadata.resolve.root;
|
|
280
320
|
let rootPackage = findPackageById(metadata, rootPackageId);
|
|
281
321
|
let rootPurl = toPurl(rootPackage.name, rootPackage.version);
|
|
@@ -286,10 +326,10 @@ function handleSingleCrate(metadata, ignoredDeps, includeTransitive, opts, licen
|
|
|
286
326
|
return sbom.getAsJsonString(opts);
|
|
287
327
|
}
|
|
288
328
|
if (includeTransitive) {
|
|
289
|
-
addTransitiveDeps(sbom, metadata, rootPackageId, ignoredDeps, new Set(), rootPurl);
|
|
329
|
+
addTransitiveDeps(sbom, metadata, rootPackageId, ignoredDeps, new Set(), hashMap, rootPurl);
|
|
290
330
|
}
|
|
291
331
|
else {
|
|
292
|
-
addDirectDeps(sbom, metadata, rootPackageId, rootPurl, ignoredDeps);
|
|
332
|
+
addDirectDeps(sbom, metadata, rootPackageId, rootPurl, ignoredDeps, hashMap);
|
|
293
333
|
}
|
|
294
334
|
return sbom.getAsJsonString(opts);
|
|
295
335
|
}
|
|
@@ -315,7 +355,7 @@ function handleSingleCrate(metadata, ignoredDeps, includeTransitive, opts, licen
|
|
|
315
355
|
* @returns {string} SBOM json string
|
|
316
356
|
* @private
|
|
317
357
|
*/
|
|
318
|
-
function handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransitive, opts, license) {
|
|
358
|
+
function handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransitive, opts, license, hashMap) {
|
|
319
359
|
let workspaceRoot = metadata.workspace_root;
|
|
320
360
|
let rootName = path.basename(workspaceRoot);
|
|
321
361
|
let workspaceVersion = getWorkspaceVersion(metadata);
|
|
@@ -323,7 +363,6 @@ function handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransiti
|
|
|
323
363
|
let sbom = new Sbom();
|
|
324
364
|
sbom.addRoot(rootPurl, license);
|
|
325
365
|
if (includeTransitive) {
|
|
326
|
-
// Stack analysis: walk all members and their full dependency trees
|
|
327
366
|
let workspaceMembers = metadata.workspace_members || [];
|
|
328
367
|
for (let memberId of workspaceMembers) {
|
|
329
368
|
let memberPackage = findPackageById(metadata, memberId);
|
|
@@ -334,11 +373,10 @@ function handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransiti
|
|
|
334
373
|
? toPathDepPurl(memberPackage.name, memberPackage.version)
|
|
335
374
|
: toPurl(memberPackage.name, memberPackage.version);
|
|
336
375
|
sbom.addDependency(rootPurl, memberPurl);
|
|
337
|
-
addTransitiveDeps(sbom, metadata, memberId, ignoredDeps, new Set(), memberPurl);
|
|
376
|
+
addTransitiveDeps(sbom, metadata, memberId, ignoredDeps, new Set(), hashMap, memberPurl);
|
|
338
377
|
}
|
|
339
378
|
}
|
|
340
379
|
else {
|
|
341
|
-
// Component analysis: only [workspace.dependencies] from root Cargo.toml
|
|
342
380
|
let workspaceDeps = getWorkspaceDepsFromManifest(manifest);
|
|
343
381
|
for (let depName of workspaceDeps) {
|
|
344
382
|
if (isDepIgnored(depName, ignoredDeps)) {
|
|
@@ -355,7 +393,8 @@ function handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransiti
|
|
|
355
393
|
let depPurl = pkg.source == null
|
|
356
394
|
? toPathDepPurl(pkg.name, pkg.version)
|
|
357
395
|
: toPurl(pkg.name, pkg.version);
|
|
358
|
-
|
|
396
|
+
let hashes = hashMap.get(`${pkg.name}@${pkg.version}`);
|
|
397
|
+
sbom.addDependency(rootPurl, depPurl, undefined, hashes);
|
|
359
398
|
}
|
|
360
399
|
}
|
|
361
400
|
return sbom.getAsJsonString(opts);
|
|
@@ -374,7 +413,7 @@ function handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransiti
|
|
|
374
413
|
* so callers can ensure it matches the purl already added to the SBOM
|
|
375
414
|
* @private
|
|
376
415
|
*/
|
|
377
|
-
function addTransitiveDeps(sbom, metadata, packageId, ignoredDeps, visited, startingPurl) {
|
|
416
|
+
function addTransitiveDeps(sbom, metadata, packageId, ignoredDeps, visited, hashMap, startingPurl) {
|
|
378
417
|
if (visited.has(packageId)) {
|
|
379
418
|
return;
|
|
380
419
|
}
|
|
@@ -402,8 +441,9 @@ function addTransitiveDeps(sbom, metadata, packageId, ignoredDeps, visited, star
|
|
|
402
441
|
let depPurl = depPackage.source == null
|
|
403
442
|
? toPathDepPurl(depPackage.name, depPackage.version)
|
|
404
443
|
: toPurl(depPackage.name, depPackage.version);
|
|
405
|
-
|
|
406
|
-
|
|
444
|
+
let hashes = hashMap.get(`${depPackage.name}@${depPackage.version}`);
|
|
445
|
+
sbom.addDependency(sourcePurl, depPurl, undefined, hashes);
|
|
446
|
+
addTransitiveDeps(sbom, metadata, depId, ignoredDeps, visited, hashMap);
|
|
407
447
|
}
|
|
408
448
|
}
|
|
409
449
|
/**
|
|
@@ -416,7 +456,7 @@ function addTransitiveDeps(sbom, metadata, packageId, ignoredDeps, visited, star
|
|
|
416
456
|
* @param {Set<string>} ignoredDeps - set of ignored dependency names
|
|
417
457
|
* @private
|
|
418
458
|
*/
|
|
419
|
-
function addDirectDeps(sbom, metadata, packageId, parentPurl, ignoredDeps) {
|
|
459
|
+
function addDirectDeps(sbom, metadata, packageId, parentPurl, ignoredDeps, hashMap) {
|
|
420
460
|
let resolveNode = findResolveNode(metadata, packageId);
|
|
421
461
|
if (!resolveNode) {
|
|
422
462
|
return;
|
|
@@ -433,7 +473,8 @@ function addDirectDeps(sbom, metadata, packageId, parentPurl, ignoredDeps) {
|
|
|
433
473
|
let depPurl = depPackage.source == null
|
|
434
474
|
? toPathDepPurl(depPackage.name, depPackage.version)
|
|
435
475
|
: toPurl(depPackage.name, depPackage.version);
|
|
436
|
-
|
|
476
|
+
let hashes = hashMap.get(`${depPackage.name}@${depPackage.version}`);
|
|
477
|
+
sbom.addDependency(parentPurl, depPurl, undefined, hashes);
|
|
437
478
|
}
|
|
438
479
|
}
|
|
439
480
|
/**
|
|
Binary file
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a target path to the supported manifest files it contains: the single file
|
|
3
|
+
* if `targetPath` is a supported manifest, or every supported manifest discovered
|
|
4
|
+
* recursively if it's a directory. This is the single source of truth for what
|
|
5
|
+
* `runRemediation` will scan, so callers (e.g. the CLI) can reuse it to tell
|
|
6
|
+
* "no manifests here" apart from "manifests, but nothing to fix".
|
|
7
|
+
* @param {string} targetPath - path to a manifest file or directory
|
|
8
|
+
* @returns {string[]} absolute paths to supported manifests (empty for an empty directory)
|
|
9
|
+
* @throws if the path does not exist, or is a file of an unsupported manifest type
|
|
10
|
+
*/
|
|
11
|
+
export function findManifests(targetPath: string): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Orchestrates the full remediation pipeline for a single manifest or directory:
|
|
14
|
+
* discover manifests → scan via DA backend → extract remediations → apply or preview.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} targetPath - path to a manifest file or directory
|
|
17
|
+
* @param {object} [options]
|
|
18
|
+
* @param {boolean} [options.dryRun=false] - preview changes without modifying files (applies by default)
|
|
19
|
+
* @param {string} [options.providers] - comma-separated provider list
|
|
20
|
+
* @param {string} [options.sources] - comma-separated source list
|
|
21
|
+
* @param {string} [options.backendUrl] - Trustify DA backend URL
|
|
22
|
+
* @param {boolean} [options.perDependencyChanges=false] - when true, each remediation is populated with
|
|
23
|
+
* a `changes` array describing the isolated, single-dependency edit (see {@link DependencyFix}). This lets
|
|
24
|
+
* callers create one commit/PR per dependency without attributing diff hunks themselves.
|
|
25
|
+
* @returns {Promise<{exitCode: number, output: string, remediations: Remediation[], manifests: string[], appliedFiles: string[]}>}
|
|
26
|
+
* exitCode is 2 for a dry-run that found remediations (nothing written), 0 otherwise. `remediations`
|
|
27
|
+
* is the structured, per-manifest list of applicable updates — each entry carries the originating
|
|
28
|
+
* manifest path(s) in `files` so callers can group and create per-dependency changes. `appliedFiles`
|
|
29
|
+
* lists only the manifests actually written to disk (empty on a dry-run), so callers can report a
|
|
30
|
+
* truthful "updated N files" count without conflating "had remediations" with "was written".
|
|
31
|
+
*/
|
|
32
|
+
export function runRemediation(targetPath: string, options?: {
|
|
33
|
+
dryRun?: boolean | undefined;
|
|
34
|
+
providers?: string | undefined;
|
|
35
|
+
sources?: string | undefined;
|
|
36
|
+
backendUrl?: string | undefined;
|
|
37
|
+
perDependencyChanges?: boolean | undefined;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
exitCode: number;
|
|
40
|
+
output: string;
|
|
41
|
+
remediations: Remediation[];
|
|
42
|
+
manifests: string[];
|
|
43
|
+
appliedFiles: string[];
|
|
44
|
+
}>;
|
|
45
|
+
/**
|
|
46
|
+
* A single version change requested from an updater: bump `groupId:artifactId` to `newVersion`.
|
|
47
|
+
* The input side of every updater; each updater's output side (its `applied` entries) is its own
|
|
48
|
+
* type — see {@link AppliedChange}.
|
|
49
|
+
*/
|
|
50
|
+
export type VersionChangeRequest = {
|
|
51
|
+
groupId: string;
|
|
52
|
+
artifactId: string;
|
|
53
|
+
newVersion: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* One entry from an updater's `applied` list, describing where and how a version was changed.
|
|
57
|
+
* Owned by the updater layer: the union of each updater's applied-entry shape. `changeKey`
|
|
58
|
+
* (per {@link ManifestType}) turns one of these into a stable edit-site identifier.
|
|
59
|
+
*/
|
|
60
|
+
export type AppliedChange = import("./updaters/maven_updater.js").MavenAppliedChange | import("./updaters/toml_updater.js").TomlAppliedChange;
|
|
61
|
+
/**
|
|
62
|
+
* The result of running an updater over a manifest's raw content.
|
|
63
|
+
*/
|
|
64
|
+
export type UpdaterResult = {
|
|
65
|
+
content: string;
|
|
66
|
+
applied: AppliedChange[];
|
|
67
|
+
skipped: Array<{
|
|
68
|
+
groupId: string;
|
|
69
|
+
artifactId: string;
|
|
70
|
+
newVersion: string;
|
|
71
|
+
reason: string;
|
|
72
|
+
}>;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* A supported manifest type and the operations that act on it.
|
|
76
|
+
* `changeKey` builds a stable edit-site key from a single `applied` entry, so callers can detect
|
|
77
|
+
* inseparable remediations (same key => same commit/PR).
|
|
78
|
+
*/
|
|
79
|
+
export type ManifestType = {
|
|
80
|
+
test: (basename: string) => boolean;
|
|
81
|
+
updater: (content: string, versionChanges: VersionChangeRequest[]) => UpdaterResult;
|
|
82
|
+
label: ("maven" | "toml");
|
|
83
|
+
changeKey: (manifestPath: string, applied: AppliedChange) => string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* An isolated, single-dependency edit to one manifest file.
|
|
87
|
+
* - `after` is the *original* manifest content with only this dependency's fix applied, so a caller
|
|
88
|
+
* can create an isolated commit by writing `after` to `path` on a branch cut from the base.
|
|
89
|
+
* - `changeKey` is a stable identifier for the underlying edit site. Two remediations that share a
|
|
90
|
+
* `changeKey` are inseparable (e.g. two Maven deps whose versions resolve to the same `${property}`,
|
|
91
|
+
* or two Gradle libraries sharing one `version.ref`) and MUST land in the same commit/PR — the
|
|
92
|
+
* caller should union their CVEs/advisories.
|
|
93
|
+
*/
|
|
94
|
+
export type DependencyFix = {
|
|
95
|
+
path: string;
|
|
96
|
+
after: string;
|
|
97
|
+
changeKey: string;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* A single applicable remediation, as produced by `extractRemediations` and enriched by
|
|
101
|
+
* `runRemediation` with the originating manifest path(s) and (optionally) per-dependency changes.
|
|
102
|
+
*/
|
|
103
|
+
export type Remediation = {
|
|
104
|
+
purl: string;
|
|
105
|
+
groupId: string;
|
|
106
|
+
artifactId: string;
|
|
107
|
+
currentVersion: string;
|
|
108
|
+
fixedInVersion: string;
|
|
109
|
+
fixedInPurl: string;
|
|
110
|
+
provider: string;
|
|
111
|
+
source: string;
|
|
112
|
+
advisories: Array<{
|
|
113
|
+
id: string;
|
|
114
|
+
url: string;
|
|
115
|
+
}>;
|
|
116
|
+
severity: string;
|
|
117
|
+
cves: string[];
|
|
118
|
+
files: string[];
|
|
119
|
+
changes?: DependencyFix[];
|
|
120
|
+
};
|