@trustify-da/trustify-da-javascript-client 0.3.0-ea.ab5949f → 0.3.0-ea.b0e2843
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 +1 -1
- package/dist/package.json +9 -5
- package/dist/src/analysis.js +10 -4
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/oci_image/images.js +15 -1
- package/dist/src/provider.js +3 -1
- package/dist/src/providers/containerfile_parser.d.ts +5 -0
- package/dist/src/providers/containerfile_parser.js +20 -0
- package/dist/src/providers/oci_dockerfile.d.ts +51 -0
- package/dist/src/providers/oci_dockerfile.js +177 -0
- package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -695,7 +695,7 @@ By default, The API algorithm will use native commands of PIP installer as data
|
|
|
695
695
|
It's also possible to use the lightweight Python PIP utility [pipdeptree](https://pypi.org/project/pipdeptree/) as data source instead. In order to activate this, you need to set the environment variable/option `TRUSTIFY_DA_PIP_USE_DEP_TREE` to 'true'.
|
|
696
696
|
|
|
697
697
|
#### Toggle Red Hat Trusted Content recommendations
|
|
698
|
-
Both the HTML-based report and JSON response will by default contain recommendations for migrating to Red Hat-based Trusted Content repositories. This feature can be disabled by setting `
|
|
698
|
+
Both the HTML-based report and JSON response will by default contain recommendations for migrating to Red Hat-based Trusted Content repositories. This feature can be disabled by setting `TRUSTIFY_DA_RECOMMEND` to 'false' via environment variables or options.
|
|
699
699
|
|
|
700
700
|
#### Additional CLI arguments
|
|
701
701
|
For some ecosystems we support passing additional CLI arguments to the underlying tools. The following table outlines the supported ecosystems and the environment variable/option that configures this. Note that the arguments are expected to be in the format of a JSON array.
|
package/dist/package.json
CHANGED
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"test": "c8 npm run tests",
|
|
41
41
|
"tests": "mocha --config .mocharc.json",
|
|
42
42
|
"tests:rep": "mocha --reporter-option maxDiffSize=0 --reporter json > unit-tests-result.json",
|
|
43
|
-
"pretest": "cp node_modules/tree-sitter-requirements/tree-sitter-requirements.wasm src/providers/tree-sitter-requirements.wasm && cp node_modules/tree-sitter-gomod/tree-sitter-gomod.wasm src/providers/tree-sitter-gomod.wasm",
|
|
43
|
+
"pretest": "cp node_modules/tree-sitter-requirements/tree-sitter-requirements.wasm src/providers/tree-sitter-requirements.wasm && cp node_modules/tree-sitter-gomod/tree-sitter-gomod.wasm src/providers/tree-sitter-gomod.wasm && cp node_modules/tree-sitter-containerfile/tree-sitter-containerfile.wasm src/providers/tree-sitter-containerfile.wasm",
|
|
44
44
|
"precompile": "rm -rf dist",
|
|
45
45
|
"compile": "tsc -p tsconfig.json",
|
|
46
46
|
"compile:dev": "tsc -p tsconfig.dev.json",
|
|
47
|
-
"postcompile": "cp node_modules/tree-sitter-requirements/tree-sitter-requirements.wasm dist/src/providers/tree-sitter-requirements.wasm && cp node_modules/tree-sitter-gomod/tree-sitter-gomod.wasm dist/src/providers/tree-sitter-gomod.wasm"
|
|
47
|
+
"postcompile": "cp node_modules/tree-sitter-requirements/tree-sitter-requirements.wasm dist/src/providers/tree-sitter-requirements.wasm && cp node_modules/tree-sitter-gomod/tree-sitter-gomod.wasm dist/src/providers/tree-sitter-gomod.wasm && cp node_modules/tree-sitter-containerfile/tree-sitter-containerfile.wasm dist/src/providers/tree-sitter-containerfile.wasm"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@babel/core": "^7.23.2",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"p-limit": "^7.3.0",
|
|
62
62
|
"packageurl-js": "~1.0.2",
|
|
63
63
|
"smol-toml": "^1.6.0",
|
|
64
|
+
"tree-sitter-containerfile": "^0.8.1",
|
|
64
65
|
"tree-sitter-gomod": "github:strum355/tree-sitter-go-mod#56326f2ad478892ace58ff247a97d492a3cbcdda",
|
|
65
66
|
"tree-sitter-requirements": "github:Strum355/tree-sitter-requirements#d0261ee76b84253997fe70d7d397e78c006c3801",
|
|
66
67
|
"web-tree-sitter": "^0.26.7",
|
|
@@ -68,18 +69,18 @@
|
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
70
71
|
"@babel/core": "^7.23.2",
|
|
71
|
-
"@
|
|
72
|
+
"@eslint/js": "^10.0.0",
|
|
73
|
+
"@trustify-da/trustify-da-api-model": "^2.0.12",
|
|
72
74
|
"@types/node": "^25.9.1",
|
|
73
75
|
"@types/which": "^3.0.4",
|
|
74
76
|
"babel-plugin-rewire": "^1.2.0",
|
|
75
77
|
"c8": "^11.0.0",
|
|
76
78
|
"chai": "^6.2.2",
|
|
77
|
-
"@eslint/js": "^10.0.0",
|
|
78
79
|
"eslint": "^10.4.1",
|
|
79
80
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
80
81
|
"eslint-plugin-import-x": "^4.16.2",
|
|
81
|
-
"globals": "^17.6.0",
|
|
82
82
|
"esmock": "^2.6.2",
|
|
83
|
+
"globals": "^17.6.0",
|
|
83
84
|
"mocha": "^11.7.6",
|
|
84
85
|
"msw": "^2.12.7",
|
|
85
86
|
"sinon": "^22.0.0",
|
|
@@ -117,6 +118,9 @@
|
|
|
117
118
|
"text"
|
|
118
119
|
]
|
|
119
120
|
},
|
|
121
|
+
"overrides": {
|
|
122
|
+
"tree-sitter": "0.22.4"
|
|
123
|
+
},
|
|
120
124
|
"resolutions": {
|
|
121
125
|
"@hapi/joi": "17.1.1"
|
|
122
126
|
}
|
package/dist/src/analysis.js
CHANGED
|
@@ -20,6 +20,9 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
|
|
|
20
20
|
opts["source-manifest"] = Buffer.from(fs.readFileSync(manifest).toString()).toString('base64');
|
|
21
21
|
opts["manifest-type"] = path.parse(manifest).base;
|
|
22
22
|
let provided = await provider.provideStack(manifest, opts); // throws error if content providing failed
|
|
23
|
+
if (provided.batch) {
|
|
24
|
+
return requestStackBatch(JSON.parse(provided.content), url, html, opts);
|
|
25
|
+
}
|
|
23
26
|
opts["source-manifest"] = "";
|
|
24
27
|
opts[TRUSTIFY_DA_OPERATION_TYPE_HEADER.toUpperCase().replaceAll("-", "_")] = "stack-analysis";
|
|
25
28
|
let startTime = new Date();
|
|
@@ -38,7 +41,7 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
|
|
|
38
41
|
body: provided.content
|
|
39
42
|
}, opts);
|
|
40
43
|
const finalUrl = new URL(`${url}/api/v5/analysis`);
|
|
41
|
-
if (
|
|
44
|
+
if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
|
|
42
45
|
finalUrl.searchParams.append('recommend', 'false');
|
|
43
46
|
}
|
|
44
47
|
let resp = await fetch(finalUrl, fetchOptions);
|
|
@@ -79,6 +82,9 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
|
|
|
79
82
|
async function requestComponent(provider, manifest, url, opts = {}) {
|
|
80
83
|
opts["source-manifest"] = Buffer.from(fs.readFileSync(manifest).toString()).toString('base64');
|
|
81
84
|
let provided = await provider.provideComponent(manifest, opts); // throws error if content providing failed
|
|
85
|
+
if (provided.batch) {
|
|
86
|
+
return requestStackBatch(JSON.parse(provided.content), url, false, opts);
|
|
87
|
+
}
|
|
82
88
|
opts["source-manifest"] = "";
|
|
83
89
|
opts[TRUSTIFY_DA_OPERATION_TYPE_HEADER.toUpperCase().replaceAll("-", "_")] = "component-analysis";
|
|
84
90
|
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
|
|
@@ -95,7 +101,7 @@ async function requestComponent(provider, manifest, url, opts = {}) {
|
|
|
95
101
|
body: provided.content
|
|
96
102
|
}, opts);
|
|
97
103
|
const finalUrl = new URL(`${url}/api/v5/analysis`);
|
|
98
|
-
if (
|
|
104
|
+
if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
|
|
99
105
|
finalUrl.searchParams.append('recommend', 'false');
|
|
100
106
|
}
|
|
101
107
|
let resp = await fetch(finalUrl, fetchOptions);
|
|
@@ -136,7 +142,7 @@ async function requestComponent(provider, manifest, url, opts = {}) {
|
|
|
136
142
|
*/
|
|
137
143
|
async function requestStackBatch(sbomByPurl, url, html = false, opts = {}) {
|
|
138
144
|
const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
|
|
139
|
-
if (
|
|
145
|
+
if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
|
|
140
146
|
finalUrl.searchParams.append('recommend', 'false');
|
|
141
147
|
}
|
|
142
148
|
const fetchOptions = addProxyAgent({
|
|
@@ -186,7 +192,7 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
|
|
|
186
192
|
imageSboms[parsedImageRef.getPackageURL().toString()] = generateImageSBOM(parsedImageRef, opts);
|
|
187
193
|
}
|
|
188
194
|
const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
|
|
189
|
-
if (
|
|
195
|
+
if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
|
|
190
196
|
finalUrl.searchParams.append('recommend', 'false');
|
|
191
197
|
}
|
|
192
198
|
const fetchOptions = addProxyAgent({
|
package/dist/src/index.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export type Options = {
|
|
|
53
53
|
TRUSTIFY_DA_PYTHON_PATH?: string | undefined;
|
|
54
54
|
TRUSTIFY_DA_PYTHON_VIRTUAL_ENV?: string | undefined;
|
|
55
55
|
TRUSTIFY_DA_PYTHON3_PATH?: string | undefined;
|
|
56
|
-
|
|
56
|
+
TRUSTIFY_DA_RECOMMEND?: string | undefined;
|
|
57
57
|
TRUSTIFY_DA_SKOPEO_CONFIG_PATH?: string | undefined;
|
|
58
58
|
TRUSTIFY_DA_SKOPEO_PATH?: string | undefined;
|
|
59
59
|
TRUSTIFY_DA_SYFT_CONFIG_PATH?: string | undefined;
|
package/dist/src/index.js
CHANGED
|
@@ -39,7 +39,7 @@ export { discoverMavenModules, discoverGradleSubprojects, discoverGoWorkspaceMod
|
|
|
39
39
|
* TRUSTIFY_DA_PYTHON_PATH?: string | undefined,
|
|
40
40
|
* TRUSTIFY_DA_PYTHON_VIRTUAL_ENV?: string | undefined,
|
|
41
41
|
* TRUSTIFY_DA_PYTHON3_PATH?: string | undefined,
|
|
42
|
-
*
|
|
42
|
+
* TRUSTIFY_DA_RECOMMEND?: string | undefined,
|
|
43
43
|
* TRUSTIFY_DA_SKOPEO_CONFIG_PATH?: string | undefined,
|
|
44
44
|
* TRUSTIFY_DA_SKOPEO_PATH?: string | undefined,
|
|
45
45
|
* TRUSTIFY_DA_SYFT_CONFIG_PATH?: string | undefined,
|
|
@@ -242,8 +242,22 @@ export class ImageRef {
|
|
|
242
242
|
getPackageURL() {
|
|
243
243
|
/** @type {Object.<string, string>} */
|
|
244
244
|
const qualifiers = {};
|
|
245
|
-
|
|
245
|
+
let repositoryUrl = this.image.getNameWithoutTag();
|
|
246
246
|
const simpleName = this.image.getSimpleName();
|
|
247
|
+
// Normalize Docker Hub image references so all forms produce the same PURL:
|
|
248
|
+
// node → docker.io/node
|
|
249
|
+
// docker.io/library/node → docker.io/node
|
|
250
|
+
if (repositoryUrl != null) {
|
|
251
|
+
const lower = repositoryUrl.toLowerCase();
|
|
252
|
+
if (lower === simpleName.toLowerCase()) {
|
|
253
|
+
// Bare name (e.g. "node") — default to docker.io
|
|
254
|
+
repositoryUrl = `docker.io/${simpleName}`;
|
|
255
|
+
}
|
|
256
|
+
else if (lower.startsWith('docker.io/library/')) {
|
|
257
|
+
// Official image with library/ prefix — strip it
|
|
258
|
+
repositoryUrl = `docker.io/${lower.substring('docker.io/library/'.length)}`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
247
261
|
if (repositoryUrl != null && repositoryUrl.toLowerCase() !== simpleName.toLowerCase()) {
|
|
248
262
|
qualifiers[ImageRef.REPOSITORY_QUALIFIER] = repositoryUrl.toLowerCase();
|
|
249
263
|
}
|
package/dist/src/provider.js
CHANGED
|
@@ -7,6 +7,7 @@ import Javascript_bun from './providers/javascript_bun.js';
|
|
|
7
7
|
import Javascript_npm from './providers/javascript_npm.js';
|
|
8
8
|
import Javascript_pnpm from './providers/javascript_pnpm.js';
|
|
9
9
|
import Javascript_yarn from './providers/javascript_yarn.js';
|
|
10
|
+
import dockerfileProvider from './providers/oci_dockerfile.js';
|
|
10
11
|
import pythonPipProvider from './providers/python_pip.js';
|
|
11
12
|
import Python_pip_pyproject from './providers/python_pip_pyproject.js';
|
|
12
13
|
import Python_poetry from './providers/python_poetry.js';
|
|
@@ -31,7 +32,8 @@ export const availableProviders = [
|
|
|
31
32
|
new Python_poetry(),
|
|
32
33
|
new Python_uv(),
|
|
33
34
|
new Python_pip_pyproject(),
|
|
34
|
-
rustCargoProvider
|
|
35
|
+
rustCargoProvider,
|
|
36
|
+
dockerfileProvider
|
|
35
37
|
];
|
|
36
38
|
/**
|
|
37
39
|
* Match a provider by manifest type only (no lock file check). Used for license reading.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { Language, Parser, Query } from 'web-tree-sitter';
|
|
3
|
+
const wasmUrl = new URL('./tree-sitter-containerfile.wasm', import.meta.url);
|
|
4
|
+
async function init() {
|
|
5
|
+
await Parser.init();
|
|
6
|
+
const wasmBytes = new Uint8Array(await readFile(wasmUrl));
|
|
7
|
+
return await Language.load(wasmBytes);
|
|
8
|
+
}
|
|
9
|
+
export async function getParser() {
|
|
10
|
+
const language = await init();
|
|
11
|
+
return new Parser().setLanguage(language);
|
|
12
|
+
}
|
|
13
|
+
export async function getFromQuery() {
|
|
14
|
+
const language = await init();
|
|
15
|
+
return new Query(language, '(from_instruction (image_spec) @image)');
|
|
16
|
+
}
|
|
17
|
+
export async function getArgQuery() {
|
|
18
|
+
const language = await init();
|
|
19
|
+
return new Query(language, '(arg_instruction (arg_pair name: (_) @name default: (_) @default))');
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse all FROM instructions from a Dockerfile using tree-sitter to extract base image references.
|
|
3
|
+
* In multi-stage builds, every FROM line is returned so each image can be analyzed independently.
|
|
4
|
+
* ARG substitutions are resolved using declared default values. FROM lines with unresolvable
|
|
5
|
+
* variables (ARGs without defaults) are silently skipped.
|
|
6
|
+
* @param {string} manifestContent the content of the Dockerfile
|
|
7
|
+
* @returns {Promise<string[]>} array of image references from all resolvable FROM instructions
|
|
8
|
+
* @throws {Error} when no FROM instruction is found or when no FROM lines can be resolved
|
|
9
|
+
*/
|
|
10
|
+
export function parseAllFromImages(manifestContent: string): Promise<string[]>;
|
|
11
|
+
declare namespace _default {
|
|
12
|
+
export { isSupported };
|
|
13
|
+
export { validateLockFile };
|
|
14
|
+
export { provideComponent };
|
|
15
|
+
export { provideStack };
|
|
16
|
+
export { readLicenseFromManifest };
|
|
17
|
+
export function packageManagerName(): string;
|
|
18
|
+
}
|
|
19
|
+
export default _default;
|
|
20
|
+
export type Provided = any;
|
|
21
|
+
/**
|
|
22
|
+
* Check if the given manifest name is a Dockerfile or Containerfile.
|
|
23
|
+
* Supports dot-suffixed variants such as Dockerfile.dev or Containerfile.prod.
|
|
24
|
+
* @param {string} manifestName the manifest file name to check
|
|
25
|
+
* @returns {boolean} true if the manifest is a Dockerfile or Containerfile
|
|
26
|
+
*/
|
|
27
|
+
declare function isSupported(manifestName: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Dockerfiles have no lock file, so validation always passes.
|
|
30
|
+
* @returns {boolean} always true
|
|
31
|
+
*/
|
|
32
|
+
declare function validateLockFile(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Provide content and content type for Dockerfile component analysis.
|
|
35
|
+
* @param {string} manifest path to the Dockerfile
|
|
36
|
+
* @param {{}} [opts={}] optional various options to pass along the application
|
|
37
|
+
* @returns {Promise<Provided>}
|
|
38
|
+
*/
|
|
39
|
+
declare function provideComponent(manifest: string, opts?: {}): Promise<Provided>;
|
|
40
|
+
/**
|
|
41
|
+
* Provide content and content type for Dockerfile stack analysis.
|
|
42
|
+
* @param {string} manifest path to the Dockerfile
|
|
43
|
+
* @param {{}} [opts={}] optional various options to pass along the application
|
|
44
|
+
* @returns {Promise<Provided>}
|
|
45
|
+
*/
|
|
46
|
+
declare function provideStack(manifest: string, opts?: {}): Promise<Provided>;
|
|
47
|
+
/**
|
|
48
|
+
* Dockerfiles contain no license information.
|
|
49
|
+
* @returns {null} always null
|
|
50
|
+
*/
|
|
51
|
+
declare function readLicenseFromManifest(): null;
|
|
@@ -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; }
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trustify-da/trustify-da-javascript-client",
|
|
3
|
-
"version": "0.3.0-ea.
|
|
3
|
+
"version": "0.3.0-ea.b0e2843",
|
|
4
4
|
"description": "Code-Ready Dependency Analytics JavaScript API.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/guacsec/trustify-da-javascript-client#README.md",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"test": "c8 npm run tests",
|
|
41
41
|
"tests": "mocha --config .mocharc.json",
|
|
42
42
|
"tests:rep": "mocha --reporter-option maxDiffSize=0 --reporter json > unit-tests-result.json",
|
|
43
|
-
"pretest": "cp node_modules/tree-sitter-requirements/tree-sitter-requirements.wasm src/providers/tree-sitter-requirements.wasm && cp node_modules/tree-sitter-gomod/tree-sitter-gomod.wasm src/providers/tree-sitter-gomod.wasm",
|
|
43
|
+
"pretest": "cp node_modules/tree-sitter-requirements/tree-sitter-requirements.wasm src/providers/tree-sitter-requirements.wasm && cp node_modules/tree-sitter-gomod/tree-sitter-gomod.wasm src/providers/tree-sitter-gomod.wasm && cp node_modules/tree-sitter-containerfile/tree-sitter-containerfile.wasm src/providers/tree-sitter-containerfile.wasm",
|
|
44
44
|
"precompile": "rm -rf dist",
|
|
45
45
|
"compile": "tsc -p tsconfig.json",
|
|
46
46
|
"compile:dev": "tsc -p tsconfig.dev.json",
|
|
47
|
-
"postcompile": "cp node_modules/tree-sitter-requirements/tree-sitter-requirements.wasm dist/src/providers/tree-sitter-requirements.wasm && cp node_modules/tree-sitter-gomod/tree-sitter-gomod.wasm dist/src/providers/tree-sitter-gomod.wasm"
|
|
47
|
+
"postcompile": "cp node_modules/tree-sitter-requirements/tree-sitter-requirements.wasm dist/src/providers/tree-sitter-requirements.wasm && cp node_modules/tree-sitter-gomod/tree-sitter-gomod.wasm dist/src/providers/tree-sitter-gomod.wasm && cp node_modules/tree-sitter-containerfile/tree-sitter-containerfile.wasm dist/src/providers/tree-sitter-containerfile.wasm"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@babel/core": "^7.23.2",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"p-limit": "^7.3.0",
|
|
62
62
|
"packageurl-js": "~1.0.2",
|
|
63
63
|
"smol-toml": "^1.6.0",
|
|
64
|
+
"tree-sitter-containerfile": "^0.8.1",
|
|
64
65
|
"tree-sitter-gomod": "github:strum355/tree-sitter-go-mod#56326f2ad478892ace58ff247a97d492a3cbcdda",
|
|
65
66
|
"tree-sitter-requirements": "github:Strum355/tree-sitter-requirements#d0261ee76b84253997fe70d7d397e78c006c3801",
|
|
66
67
|
"web-tree-sitter": "^0.26.7",
|
|
@@ -68,18 +69,18 @@
|
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
70
71
|
"@babel/core": "^7.23.2",
|
|
71
|
-
"@
|
|
72
|
+
"@eslint/js": "^10.0.0",
|
|
73
|
+
"@trustify-da/trustify-da-api-model": "^2.0.12",
|
|
72
74
|
"@types/node": "^25.9.1",
|
|
73
75
|
"@types/which": "^3.0.4",
|
|
74
76
|
"babel-plugin-rewire": "^1.2.0",
|
|
75
77
|
"c8": "^11.0.0",
|
|
76
78
|
"chai": "^6.2.2",
|
|
77
|
-
"@eslint/js": "^10.0.0",
|
|
78
79
|
"eslint": "^10.4.1",
|
|
79
80
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
80
81
|
"eslint-plugin-import-x": "^4.16.2",
|
|
81
|
-
"globals": "^17.6.0",
|
|
82
82
|
"esmock": "^2.6.2",
|
|
83
|
+
"globals": "^17.6.0",
|
|
83
84
|
"mocha": "^11.7.6",
|
|
84
85
|
"msw": "^2.12.7",
|
|
85
86
|
"sinon": "^22.0.0",
|
|
@@ -117,6 +118,9 @@
|
|
|
117
118
|
"text"
|
|
118
119
|
]
|
|
119
120
|
},
|
|
121
|
+
"overrides": {
|
|
122
|
+
"tree-sitter": "0.22.4"
|
|
123
|
+
},
|
|
120
124
|
"resolutions": {
|
|
121
125
|
"@hapi/joi": "17.1.1"
|
|
122
126
|
}
|