@trustify-da/trustify-da-javascript-client 0.3.0-ea.b68592e → 0.3.0-ea.b6d9c3f

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
@@ -688,14 +688,14 @@ This increasing the chances and the probability that the automatic installation
688
688
  ###### Usage
689
689
  A New setting is introduced - `TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS` (as both env variable/key in `options` object)
690
690
  1. `TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS`="false" - install requirements.txt while respecting declared versions for all packages.
691
- 2. `TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS`="true" - install all packages from requirements.txt, not respecting the declared version, but trying to install a version tailored for the used python version. When using this setting, you must set setting `MATCH_MANIFEST_VERSIONS` to 'false'.
691
+ 2. `TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS`="true" - install all packages from requirements.txt, not respecting the declared version, but trying to install a version tailored for the used python version. When using this setting, you must set `TRUSTIFY_DA_PYTHON_VIRTUAL_ENV` to 'true' and `MATCH_MANIFEST_VERSIONS` to 'false'.
692
692
 
693
693
  ##### Using `pipdeptree`
694
694
  By default, The API algorithm will use native commands of PIP installer as data source to build the dependency tree.
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",
71
- "@trustify-da/trustify-da-api-model": "^2.0.8",
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",
@@ -113,9 +114,13 @@
113
114
  "reporter": [
114
115
  "html",
115
116
  "json",
117
+ "lcov",
116
118
  "text"
117
119
  ]
118
120
  },
121
+ "overrides": {
122
+ "tree-sitter": "0.22.4"
123
+ },
119
124
  "resolutions": {
120
125
  "@hapi/joi": "17.1.1"
121
126
  }
@@ -6,6 +6,7 @@ declare namespace _default {
6
6
  export { requestStackBatch };
7
7
  export { requestImages };
8
8
  export { validateToken };
9
+ export { appendAnalysisQueryParams };
9
10
  }
10
11
  export default _default;
11
12
  /**
@@ -57,3 +58,9 @@ declare function requestImages(imageRefs: Array<string>, url: string, html?: boo
57
58
  * @return {Promise<number>} return the HTTP status Code of the response from the validate token request.
58
59
  */
59
60
  declare function validateToken(url: any, opts?: any): Promise<number>;
61
+ /**
62
+ * Append common analysis query parameters (recommend, providers, sources) to a URL.
63
+ * @param {URL} url - the URL object to append query parameters to
64
+ * @param {import("index.js").Options} [opts={}] - options containing parameter overrides
65
+ */
66
+ declare function appendAnalysisQueryParams(url: URL, opts?: any): void;
@@ -6,7 +6,7 @@ import { generateImageSBOM, parseImageRef } from "./oci_image/utils.js";
6
6
  import { addProxyAgent, getCustom, getTokenHeaders, TRUSTIFY_DA_OPERATION_TYPE_HEADER, TRUSTIFY_DA_PACKAGE_MANAGER_HEADER } from "./tools.js";
7
7
  /** Media type for CycloneDX JSON batch payloads (batch-analysis API). */
8
8
  export const CYCLONEDX_JSON_MEDIA_TYPE = 'application/vnd.cyclonedx+json';
9
- export default { requestComponent, requestStack, requestStackBatch, requestImages, validateToken };
9
+ export default { requestComponent, requestStack, requestStackBatch, requestImages, validateToken, appendAnalysisQueryParams };
10
10
  /**
11
11
  * Send a stack analysis request and get the report as 'text/html' or 'application/json'.
12
12
  * @param {import('./provider').Provider} provider - the provided data for constructing the request
@@ -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,9 +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 (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
42
- finalUrl.searchParams.append('recommend', 'false');
43
- }
44
+ appendAnalysisQueryParams(finalUrl, opts);
44
45
  let resp = await fetch(finalUrl, fetchOptions);
45
46
  let result;
46
47
  if (resp.status === 200) {
@@ -79,6 +80,9 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
79
80
  async function requestComponent(provider, manifest, url, opts = {}) {
80
81
  opts["source-manifest"] = Buffer.from(fs.readFileSync(manifest).toString()).toString('base64');
81
82
  let provided = await provider.provideComponent(manifest, opts); // throws error if content providing failed
83
+ if (provided.batch) {
84
+ return requestStackBatch(JSON.parse(provided.content), url, false, opts);
85
+ }
82
86
  opts["source-manifest"] = "";
83
87
  opts[TRUSTIFY_DA_OPERATION_TYPE_HEADER.toUpperCase().replaceAll("-", "_")] = "component-analysis";
84
88
  if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
@@ -95,9 +99,7 @@ async function requestComponent(provider, manifest, url, opts = {}) {
95
99
  body: provided.content
96
100
  }, opts);
97
101
  const finalUrl = new URL(`${url}/api/v5/analysis`);
98
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
99
- finalUrl.searchParams.append('recommend', 'false');
100
- }
102
+ appendAnalysisQueryParams(finalUrl, opts);
101
103
  let resp = await fetch(finalUrl, fetchOptions);
102
104
  let result;
103
105
  if (resp.status === 200) {
@@ -136,9 +138,7 @@ async function requestComponent(provider, manifest, url, opts = {}) {
136
138
  */
137
139
  async function requestStackBatch(sbomByPurl, url, html = false, opts = {}) {
138
140
  const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
139
- if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
140
- finalUrl.searchParams.append('recommend', 'false');
141
- }
141
+ appendAnalysisQueryParams(finalUrl, opts);
142
142
  const fetchOptions = addProxyAgent({
143
143
  method: 'POST',
144
144
  headers: {
@@ -186,9 +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') {
190
- finalUrl.searchParams.append('recommend', 'false');
191
- }
189
+ appendAnalysisQueryParams(finalUrl, opts);
192
190
  const fetchOptions = addProxyAgent({
193
191
  method: 'POST',
194
192
  headers: {
@@ -222,6 +220,24 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
222
220
  throw new Error(`Got error response from Trustify DA backend - http return code : ${resp.status}, ex-request-id: ${resp.headers.get("ex-request-id")} error message => ${await resp.text()}`);
223
221
  }
224
222
  }
223
+ /**
224
+ * Append common analysis query parameters (recommend, providers, sources) to a URL.
225
+ * @param {URL} url - the URL object to append query parameters to
226
+ * @param {import("index.js").Options} [opts={}] - options containing parameter overrides
227
+ */
228
+ function appendAnalysisQueryParams(url, opts = {}) {
229
+ if (getCustom('TRUSTIFY_DA_RECOMMEND', 'true', opts) === 'false') {
230
+ url.searchParams.append('recommend', 'false');
231
+ }
232
+ const providers = getCustom('TRUSTIFY_DA_PROVIDERS', null, opts);
233
+ if (providers) {
234
+ url.searchParams.append('providers', providers);
235
+ }
236
+ const sources = getCustom('TRUSTIFY_DA_SOURCES', null, opts);
237
+ if (sources) {
238
+ url.searchParams.append('sources', sources);
239
+ }
240
+ }
225
241
  /**
226
242
  *
227
243
  * @param url the backend url to send the request to
package/dist/src/cli.js CHANGED
@@ -19,11 +19,25 @@ const component = {
19
19
  desc: 'Workspace root directory (for monorepos; lock file is expected here)',
20
20
  type: 'string',
21
21
  normalize: true,
22
+ },
23
+ providers: {
24
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
25
+ type: 'string',
26
+ },
27
+ sources: {
28
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
29
+ type: 'string',
22
30
  }
23
31
  }),
24
32
  handler: async (args) => {
25
33
  let manifestName = args['/path/to/manifest'];
26
34
  const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
35
+ if (args.providers) {
36
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
37
+ }
38
+ if (args.sources) {
39
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
40
+ }
27
41
  let res = await client.componentAnalysis(manifestName, opts);
28
42
  console.log(JSON.stringify(res, null, 2));
29
43
  }
@@ -72,6 +86,14 @@ const image = {
72
86
  desc: 'For JSON report, get only the \'summary\'',
73
87
  type: 'boolean',
74
88
  conflicts: 'html'
89
+ },
90
+ providers: {
91
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
92
+ type: 'string',
93
+ },
94
+ sources: {
95
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
96
+ type: 'string',
75
97
  }
76
98
  }),
77
99
  handler: async (args) => {
@@ -81,7 +103,14 @@ const image = {
81
103
  }
82
104
  let html = args['html'];
83
105
  let summary = args['summary'];
84
- let res = await client.imageAnalysis(imageRefs, html);
106
+ const opts = {};
107
+ if (args.providers) {
108
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
109
+ }
110
+ if (args.sources) {
111
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
112
+ }
113
+ let res = await client.imageAnalysis(imageRefs, html, opts);
85
114
  if (summary && !html) {
86
115
  let summaries = {};
87
116
  for (let [imageRef, report] of Object.entries(res)) {
@@ -132,6 +161,14 @@ const stack = {
132
161
  desc: 'Workspace root directory (for monorepos; lock file is expected here)',
133
162
  type: 'string',
134
163
  normalize: true,
164
+ },
165
+ providers: {
166
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
167
+ type: 'string',
168
+ },
169
+ sources: {
170
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
171
+ type: 'string',
135
172
  }
136
173
  }),
137
174
  handler: async (args) => {
@@ -139,6 +176,12 @@ const stack = {
139
176
  let html = args['html'];
140
177
  let summary = args['summary'];
141
178
  const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
179
+ if (args.providers) {
180
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
181
+ }
182
+ if (args.sources) {
183
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
184
+ }
142
185
  let theProvidersSummary = new Map();
143
186
  let theProvidersObject = {};
144
187
  let res = await client.stackAnalysis(manifest, html, opts);
@@ -201,6 +244,14 @@ const stackBatch = {
201
244
  desc: 'Stop on first invalid package.json or SBOM error (env: TRUSTIFY_DA_CONTINUE_ON_ERROR=false)',
202
245
  type: 'boolean',
203
246
  default: false,
247
+ },
248
+ providers: {
249
+ desc: 'Comma-separated list of vulnerability providers (env: TRUSTIFY_DA_PROVIDERS)',
250
+ type: 'string',
251
+ },
252
+ sources: {
253
+ desc: 'Comma-separated list of vulnerability sources (env: TRUSTIFY_DA_SOURCES)',
254
+ type: 'string',
204
255
  }
205
256
  }),
206
257
  handler: async (args) => {
@@ -221,6 +272,12 @@ const stackBatch = {
221
272
  if (args.failFast) {
222
273
  opts.continueOnError = false;
223
274
  }
275
+ if (args.providers) {
276
+ opts.TRUSTIFY_DA_PROVIDERS = args.providers;
277
+ }
278
+ if (args.sources) {
279
+ opts.TRUSTIFY_DA_SOURCES = args.sources;
280
+ }
224
281
  let res = await client.stackAnalysisBatch(workspaceRoot, html, opts);
225
282
  const batchAnalysis = res && typeof res === 'object' && res != null && 'analysis' in res ? res.analysis : res;
226
283
  if (summary && !html && typeof batchAnalysis === 'object') {
@@ -24,6 +24,7 @@ export function selectTrustifyDABackend(opts?: {
24
24
  export function generateSbom(manifestPath: string, opts?: Options): Promise<object>;
25
25
  export { parseImageRef } from "./oci_image/utils.js";
26
26
  export { ImageRef } from "./oci_image/images.js";
27
+ export { extractRemediations } from "./remediation.js";
27
28
  declare namespace _default {
28
29
  export { componentAnalysis };
29
30
  export { stackAnalysis };
@@ -53,7 +54,9 @@ export type Options = {
53
54
  TRUSTIFY_DA_PYTHON_PATH?: string | undefined;
54
55
  TRUSTIFY_DA_PYTHON_VIRTUAL_ENV?: string | undefined;
55
56
  TRUSTIFY_DA_PYTHON3_PATH?: string | undefined;
56
- TRUSTIFY_DA_RECOMMENDATIONS_ENABLED?: string | undefined;
57
+ TRUSTIFY_DA_PROVIDERS?: string | undefined;
58
+ TRUSTIFY_DA_RECOMMEND?: string | undefined;
59
+ TRUSTIFY_DA_SOURCES?: string | undefined;
57
60
  TRUSTIFY_DA_SKOPEO_CONFIG_PATH?: string | undefined;
58
61
  TRUSTIFY_DA_SKOPEO_PATH?: string | undefined;
59
62
  TRUSTIFY_DA_SYFT_CONFIG_PATH?: string | undefined;
package/dist/src/index.js CHANGED
@@ -16,6 +16,7 @@ import * as url from 'url';
16
16
  export { parseImageRef } from "./oci_image/utils.js";
17
17
  export { ImageRef } from "./oci_image/images.js";
18
18
  export { getProjectLicense, findLicenseFilePath, identifyLicense, getLicenseDetails, licensesFromReport, normalizeLicensesResponse, runLicenseCheck, getCompatibility } from "./license/index.js";
19
+ export { extractRemediations } from "./remediation.js";
19
20
  export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnalysis, validateToken, generateSbom };
20
21
  export { discoverMavenModules, discoverGradleSubprojects, discoverGoWorkspaceModules, discoverUvWorkspaceMembers, discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson, resolveWorkspaceDiscoveryIgnore, filterManifestPathsByDiscoveryIgnore, resolveContinueOnError, resolveBatchMetadata, };
21
22
  /**
@@ -39,7 +40,9 @@ export { discoverMavenModules, discoverGradleSubprojects, discoverGoWorkspaceMod
39
40
  * TRUSTIFY_DA_PYTHON_PATH?: string | undefined,
40
41
  * TRUSTIFY_DA_PYTHON_VIRTUAL_ENV?: string | undefined,
41
42
  * TRUSTIFY_DA_PYTHON3_PATH?: string | undefined,
42
- * TRUSTIFY_DA_RECOMMENDATIONS_ENABLED?: string | undefined,
43
+ * TRUSTIFY_DA_PROVIDERS?: string | undefined,
44
+ * TRUSTIFY_DA_RECOMMEND?: string | undefined,
45
+ * TRUSTIFY_DA_SOURCES?: string | undefined,
43
46
  * TRUSTIFY_DA_SKOPEO_CONFIG_PATH?: string | undefined,
44
47
  * TRUSTIFY_DA_SKOPEO_PATH?: string | undefined,
45
48
  * 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,5 @@
1
+ export function getParser(): Promise<Parser>;
2
+ export function getFromQuery(): Promise<Query>;
3
+ export function getArgQuery(): Promise<Query>;
4
+ import { Parser } from 'web-tree-sitter';
5
+ import { Query } from 'web-tree-sitter';
@@ -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; }
@@ -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]);