@trustify-da/trustify-da-javascript-client 0.3.0-ea.bdb4511 → 0.3.0-ea.be56bee

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 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 `TRUSTIFY_DA_RECOMMENDATIONS_ENABLED` to 'false' via environment variables or options.
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",
72
+ "@eslint/js": "^10.0.0",
71
73
  "@trustify-da/trustify-da-api-model": "^2.0.9",
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
  }
@@ -38,7 +38,7 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
38
38
  body: provided.content
39
39
  }, opts);
40
40
  const finalUrl = new URL(`${url}/api/v5/analysis`);
41
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
41
+ if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
42
42
  finalUrl.searchParams.append('recommend', 'false');
43
43
  }
44
44
  let resp = await fetch(finalUrl, fetchOptions);
@@ -95,7 +95,7 @@ async function requestComponent(provider, manifest, url, opts = {}) {
95
95
  body: provided.content
96
96
  }, opts);
97
97
  const finalUrl = new URL(`${url}/api/v5/analysis`);
98
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
98
+ if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
99
99
  finalUrl.searchParams.append('recommend', 'false');
100
100
  }
101
101
  let resp = await fetch(finalUrl, fetchOptions);
@@ -136,7 +136,7 @@ async function requestComponent(provider, manifest, url, opts = {}) {
136
136
  */
137
137
  async function requestStackBatch(sbomByPurl, url, html = false, opts = {}) {
138
138
  const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
139
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
139
+ if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
140
140
  finalUrl.searchParams.append('recommend', 'false');
141
141
  }
142
142
  const fetchOptions = addProxyAgent({
@@ -186,7 +186,7 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
186
186
  imageSboms[parsedImageRef.getPackageURL().toString()] = generateImageSBOM(parsedImageRef, opts);
187
187
  }
188
188
  const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
189
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
189
+ if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
190
190
  finalUrl.searchParams.append('recommend', 'false');
191
191
  }
192
192
  const fetchOptions = addProxyAgent({
@@ -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
- TRUSTIFY_DA_RECOMMENDATIONS_ENABLED?: string | undefined;
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
- * TRUSTIFY_DA_RECOMMENDATIONS_ENABLED?: string | undefined,
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
- const repositoryUrl = this.image.getNameWithoutTag();
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
  }
@@ -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,4 @@
1
+ export function getParser(): Promise<Parser>;
2
+ export function getFromQuery(): Promise<Query>;
3
+ import { Parser } from 'web-tree-sitter';
4
+ import { Query } from 'web-tree-sitter';
@@ -0,0 +1,16 @@
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
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Parse the last FROM instruction from a Dockerfile using tree-sitter to extract the base image reference.
3
+ * In multi-stage builds, the last FROM represents the final stage.
4
+ * @param {string} manifestContent the content of the Dockerfile
5
+ * @returns {Promise<string>} the image reference from the last FROM instruction
6
+ * @throws {Error} when no FROM instruction is found or when ARG substitution is used
7
+ */
8
+ export function parseFromImage(manifestContent: string): Promise<string>;
9
+ declare namespace _default {
10
+ export { isSupported };
11
+ export { validateLockFile };
12
+ export { provideComponent };
13
+ export { provideStack };
14
+ export { readLicenseFromManifest };
15
+ export function packageManagerName(): string;
16
+ }
17
+ export default _default;
18
+ export type Provided = any;
19
+ /**
20
+ * Check if the given manifest name is a Dockerfile or Containerfile.
21
+ * Supports dot-suffixed variants such as Dockerfile.dev or Containerfile.prod.
22
+ * @param {string} manifestName the manifest file name to check
23
+ * @returns {boolean} true if the manifest is a Dockerfile or Containerfile
24
+ */
25
+ declare function isSupported(manifestName: string): boolean;
26
+ /**
27
+ * Dockerfiles have no lock file, so validation always passes.
28
+ * @returns {boolean} always true
29
+ */
30
+ declare function validateLockFile(): boolean;
31
+ /**
32
+ * Provide content and content type for Dockerfile component analysis.
33
+ * @param {string} manifest path to the Dockerfile
34
+ * @param {{}} [opts={}] optional various options to pass along the application
35
+ * @returns {Promise<Provided>}
36
+ */
37
+ declare function provideComponent(manifest: string, opts?: {}): Promise<Provided>;
38
+ /**
39
+ * Provide content and content type for Dockerfile stack analysis.
40
+ * @param {string} manifest path to the Dockerfile
41
+ * @param {{}} [opts={}] optional various options to pass along the application
42
+ * @returns {Promise<Provided>}
43
+ */
44
+ declare function provideStack(manifest: string, opts?: {}): Promise<Provided>;
45
+ /**
46
+ * Dockerfiles contain no license information.
47
+ * @returns {null} always null
48
+ */
49
+ declare function readLicenseFromManifest(): null;
@@ -0,0 +1,104 @@
1
+ import fs from 'node:fs';
2
+ import { generateImageSBOM, parseImageRef } from '../oci_image/utils.js';
3
+ import { 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
+ * Parse the last FROM instruction from a Dockerfile using tree-sitter to extract the base image reference.
45
+ * In multi-stage builds, the last FROM represents the final stage.
46
+ * @param {string} manifestContent the content of the Dockerfile
47
+ * @returns {Promise<string>} the image reference from the last FROM instruction
48
+ * @throws {Error} when no FROM instruction is found or when ARG substitution is used
49
+ */
50
+ export async function parseFromImage(manifestContent) {
51
+ const [parser, fromQuery] = await Promise.all([getParser(), getFromQuery()]);
52
+ const tree = parser.parse(manifestContent);
53
+ const matches = fromQuery.matches(tree.rootNode);
54
+ if (matches.length === 0) {
55
+ throw new Error('No FROM line found in Dockerfile');
56
+ }
57
+ const lastMatch = matches[matches.length - 1];
58
+ const imageSpec = lastMatch.captures.find(c => c.name === 'image').node;
59
+ if (containsExpansion(imageSpec)) {
60
+ throw new Error('Dockerfile uses ARG substitution in FROM line — cannot resolve variable references');
61
+ }
62
+ return imageSpec.text;
63
+ }
64
+ /**
65
+ * Generate an image SBOM from a Dockerfile manifest using syft.
66
+ * @param {string} manifest path to the Dockerfile
67
+ * @param {{}} [opts={}] optional various options to pass along the application
68
+ * @returns {Promise<{ecosystem: string, content: string, contentType: string}>}
69
+ * @private
70
+ */
71
+ async function getImageSBOM(manifest, opts = {}) {
72
+ const manifestContent = fs.readFileSync(manifest, 'utf-8');
73
+ const image = await parseFromImage(manifestContent);
74
+ const imageRef = parseImageRef(image, opts);
75
+ const sbom = generateImageSBOM(imageRef, opts);
76
+ return {
77
+ ecosystem,
78
+ content: JSON.stringify(sbom),
79
+ contentType: 'application/vnd.cyclonedx+json'
80
+ };
81
+ }
82
+ /**
83
+ * Provide content and content type for Dockerfile component analysis.
84
+ * @param {string} manifest path to the Dockerfile
85
+ * @param {{}} [opts={}] optional various options to pass along the application
86
+ * @returns {Promise<Provided>}
87
+ */
88
+ async function provideComponent(manifest, opts = {}) {
89
+ return getImageSBOM(manifest, opts);
90
+ }
91
+ /**
92
+ * Provide content and content type for Dockerfile stack analysis.
93
+ * @param {string} manifest path to the Dockerfile
94
+ * @param {{}} [opts={}] optional various options to pass along the application
95
+ * @returns {Promise<Provided>}
96
+ */
97
+ async function provideStack(manifest, opts = {}) {
98
+ return getImageSBOM(manifest, opts);
99
+ }
100
+ /**
101
+ * Dockerfiles contain no license information.
102
+ * @returns {null} always null
103
+ */
104
+ function readLicenseFromManifest() { return null; }
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.bdb4511",
3
+ "version": "0.3.0-ea.be56bee",
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",
72
+ "@eslint/js": "^10.0.0",
71
73
  "@trustify-da/trustify-da-api-model": "^2.0.9",
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
  }