@trustify-da/trustify-da-javascript-client 0.3.0-ea.f501753 → 0.3.0-ea.fa16f06

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.
Files changed (64) hide show
  1. package/README.md +2 -2
  2. package/dist/package.json +25 -22
  3. package/dist/src/analysis.d.ts +14 -7
  4. package/dist/src/analysis.js +29 -13
  5. package/dist/src/cli.js +106 -2
  6. package/dist/src/cyclone_dx_sbom.js +11 -3
  7. package/dist/src/index.d.ts +17 -12
  8. package/dist/src/index.js +21 -7
  9. package/dist/src/license/index.d.ts +3 -3
  10. package/dist/src/license/index.js +4 -1
  11. package/dist/src/license/license_utils.js +4 -1
  12. package/dist/src/license/licenses_api.d.ts +2 -2
  13. package/dist/src/oci_image/images.d.ts +1 -1
  14. package/dist/src/oci_image/images.js +15 -1
  15. package/dist/src/oci_image/utils.d.ts +5 -5
  16. package/dist/src/provider.d.ts +4 -3
  17. package/dist/src/provider.js +6 -2
  18. package/dist/src/providers/base_java.d.ts +14 -3
  19. package/dist/src/providers/base_java.js +16 -6
  20. package/dist/src/providers/base_javascript.d.ts +20 -9
  21. package/dist/src/providers/base_javascript.js +10 -3
  22. package/dist/src/providers/base_pyproject.d.ts +10 -5
  23. package/dist/src/providers/base_pyproject.js +7 -0
  24. package/dist/src/providers/containerfile_parser.d.ts +5 -0
  25. package/dist/src/providers/containerfile_parser.js +20 -0
  26. package/dist/src/providers/golang_gomodules.d.ts +2 -1
  27. package/dist/src/providers/golang_gomodules.js +1 -1
  28. package/dist/src/providers/java_gradle.js +2 -2
  29. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  30. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  31. package/dist/src/providers/java_maven.d.ts +2 -2
  32. package/dist/src/providers/java_maven.js +72 -0
  33. package/dist/src/providers/javascript_bun.d.ts +10 -0
  34. package/dist/src/providers/javascript_bun.js +100 -0
  35. package/dist/src/providers/javascript_npm.d.ts +6 -1
  36. package/dist/src/providers/oci_dockerfile.d.ts +51 -0
  37. package/dist/src/providers/oci_dockerfile.js +177 -0
  38. package/dist/src/providers/processors/yarn_berry_processor.d.ts +2 -2
  39. package/dist/src/providers/processors/yarn_classic_processor.d.ts +2 -2
  40. package/dist/src/providers/python_controller.js +61 -4
  41. package/dist/src/providers/python_pip.d.ts +1 -0
  42. package/dist/src/providers/python_pip.js +1 -1
  43. package/dist/src/providers/python_pip_pyproject.js +3 -1
  44. package/dist/src/providers/python_poetry.d.ts +31 -8
  45. package/dist/src/providers/python_poetry.js +62 -14
  46. package/dist/src/providers/python_uv.d.ts +18 -14
  47. package/dist/src/providers/python_uv.js +111 -2
  48. package/dist/src/providers/rust_cargo.d.ts +6 -2
  49. package/dist/src/providers/rust_cargo.js +105 -19
  50. package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
  51. package/dist/src/remediate.d.ts +21 -0
  52. package/dist/src/remediate.js +149 -0
  53. package/dist/src/remediation.d.ts +57 -0
  54. package/dist/src/remediation.js +436 -0
  55. package/dist/src/remediation_report.d.ts +40 -0
  56. package/dist/src/remediation_report.js +159 -0
  57. package/dist/src/tools.d.ts +2 -2
  58. package/dist/src/updaters/maven_updater.d.ts +26 -0
  59. package/dist/src/updaters/maven_updater.js +322 -0
  60. package/dist/src/updaters/toml_updater.d.ts +33 -0
  61. package/dist/src/updaters/toml_updater.js +244 -0
  62. package/dist/src/workspace.d.ts +9 -0
  63. package/dist/src/workspace.js +1 -1
  64. package/package.json +26 -23
@@ -7,5 +7,5 @@ export default class Java_gradle_kotlin extends Java_gradle {
7
7
  _parseAliasForLibsNotation(alias: any): any;
8
8
  _extractDepToBeIgnored(dep: any): string | null;
9
9
  }
10
- export type Provided = import("../provider").Provided;
10
+ export type Provided = any;
11
11
  import Java_gradle from './java_gradle.js';
@@ -51,8 +51,8 @@ export default class Java_maven extends Base_java {
51
51
  createSbomFileFromTextFormat(textGraphList: string, ignoredDeps: [string], opts: any, manifestPath: string): string;
52
52
  #private;
53
53
  }
54
- export type Java_maven = import("../provider").Provider;
55
- export type Provided = import("../provider").Provided;
54
+ export type Java_maven = any;
55
+ export type Provided = any;
56
56
  export type Package = {
57
57
  name: string;
58
58
  version: string;
@@ -181,6 +181,7 @@ export default class Java_maven extends Base_java {
181
181
  /** @type [Dependency] */
182
182
  let dependencies = this.#getDependencies(tmpEffectivePom)
183
183
  .filter(d => !this.#dependencyIn(d, ignored));
184
+ dependencies = this.#resolveVersionRanges(dependencies, manifestPath, opts);
184
185
  let sbom = new Sbom();
185
186
  let rootDependency = this.#getRootFromPom(tmpEffectivePom, manifestPath);
186
187
  let purlRoot = this.toPurl(rootDependency.groupId, rootDependency.artifactId, rootDependency.version);
@@ -289,6 +290,77 @@ export default class Java_maven extends Base_java {
289
290
  #dependencyIn(dep, deps) {
290
291
  return deps.filter(d => dep.artifactId === d.artifactId && dep.groupId === d.groupId && dep.scope === d.scope).length > 0;
291
292
  }
293
+ /**
294
+ * Returns true if the given version string is a Maven version range
295
+ * (starts with '[' or '(').
296
+ * @param {string} version
297
+ * @returns {boolean}
298
+ * @private
299
+ */
300
+ #isVersionRange(version) {
301
+ return typeof version === 'string' && (version.startsWith('[') || version.startsWith('('));
302
+ }
303
+ /**
304
+ * Resolves Maven version ranges in the given dependency list by running
305
+ * maven-dependency-plugin:tree and reading the concrete versions it selects.
306
+ * If no dependency uses a version range, returns the list unchanged.
307
+ * @param {Dependency[]} dependencies
308
+ * @param {string} manifestPath
309
+ * @param {object} opts
310
+ * @returns {Dependency[]}
311
+ * @private
312
+ */
313
+ #resolveVersionRanges(dependencies, manifestPath, opts = {}) {
314
+ // short-circuit if no dependency has a version range
315
+ if (!dependencies.some(dep => this.#isVersionRange(dep.version))) {
316
+ return dependencies;
317
+ }
318
+ const mvn = this.selectToolBinary(manifestPath, opts);
319
+ const mvnArgs = JSON.parse(getCustom('TRUSTIFY_DA_MVN_ARGS', '[]', opts));
320
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trustify_da_range_'));
321
+ const tmpDepTree = path.join(tmpDir, 'mvn_deptree_ranges.txt');
322
+ try {
323
+ this._invokeCommand(mvn, [
324
+ '-q',
325
+ 'org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree',
326
+ '-Dscope=compile',
327
+ '-DoutputType=text',
328
+ `-DoutputFile=${tmpDepTree}`,
329
+ ...mvnArgs
330
+ ], { cwd: path.dirname(manifestPath) });
331
+ const content = fs.readFileSync(tmpDepTree);
332
+ const lines = content.toString().split(EOL).filter(l => l.trim() !== '');
333
+ // Build a map of groupId:artifactId -> resolved version from depth-1 entries
334
+ /** @type {Map<string, string>} */
335
+ const resolvedVersions = new Map();
336
+ for (const line of lines) {
337
+ if (this._getDepth(line) === 1) {
338
+ const purl = this.parseDep(line);
339
+ resolvedVersions.set(`${purl.namespace}:${purl.name}`, purl.version);
340
+ }
341
+ }
342
+ // Replace version ranges with resolved concrete versions
343
+ return dependencies.map(dep => {
344
+ if (this.#isVersionRange(dep.version)) {
345
+ const key = `${dep.groupId}:${dep.artifactId}`;
346
+ const resolved = resolvedVersions.get(key);
347
+ if (resolved) {
348
+ return { ...dep, version: resolved };
349
+ }
350
+ }
351
+ return dep;
352
+ });
353
+ }
354
+ catch (error) {
355
+ if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
356
+ console.error("Failed to resolve Maven version ranges: " + error.message);
357
+ }
358
+ return dependencies;
359
+ }
360
+ finally {
361
+ fs.rmSync(tmpDir, { recursive: true, force: true });
362
+ }
363
+ }
292
364
  }
293
365
  const DEFAULT_MAVEN_DISCOVERY_IGNORE = [
294
366
  '**/target/**',
@@ -0,0 +1,10 @@
1
+ export default class Javascript_bun extends Base_javascript {
2
+ _listCmdArgs(): void;
3
+ _buildDependencyTree(includeTransitive: any, opts?: {}): {
4
+ name: any;
5
+ version: any;
6
+ dependencies: {};
7
+ };
8
+ #private;
9
+ }
10
+ import Base_javascript from './base_javascript.js';
@@ -0,0 +1,100 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parse } from 'jsonc-parser';
4
+ import Base_javascript from './base_javascript.js';
5
+ export default class Javascript_bun extends Base_javascript {
6
+ _lockFileName() {
7
+ return "bun.lock";
8
+ }
9
+ _cmdName() {
10
+ return "bun";
11
+ }
12
+ _listCmdArgs() {
13
+ throw new Error("not supported by Bun");
14
+ }
15
+ _updateLockFileCmdArgs() {
16
+ return ['install', '--lockfile-only'];
17
+ }
18
+ _buildDependencyTree(includeTransitive, opts = {}) {
19
+ this._version();
20
+ const manifestDir = path.dirname(this._getManifest().manifestPath);
21
+ const lockDir = this._findLockFileDir(manifestDir, opts) || manifestDir;
22
+ this._createLockFile(lockDir);
23
+ const lockContent = fs.readFileSync(path.join(lockDir, 'bun.lock'), 'utf-8');
24
+ const lockData = parse(lockContent);
25
+ const packages = lockData.packages || {};
26
+ const memberName = this._getManifest().name;
27
+ const workspaceEntry = this.#findWorkspaceEntry(lockData, lockDir, manifestDir, memberName);
28
+ const directDeps = workspaceEntry?.dependencies || {};
29
+ const tree = { name: memberName, version: this._getManifest().version, dependencies: {} };
30
+ const visited = new Set();
31
+ for (const depName of Object.keys(directDeps)) {
32
+ const resolved = this.#resolvePackage(depName, '', packages);
33
+ if (resolved) {
34
+ tree.dependencies[depName] = this.#buildNode(depName, resolved, packages, includeTransitive, visited);
35
+ }
36
+ }
37
+ return tree;
38
+ }
39
+ #findWorkspaceEntry(lockData, lockDir, manifestDir, memberName) {
40
+ const workspaces = lockData.workspaces || {};
41
+ const relPath = path.relative(lockDir, path.resolve(manifestDir));
42
+ if (!relPath || relPath === '.') {
43
+ return workspaces[''] || {};
44
+ }
45
+ const normalised = relPath.split(path.sep).join('/');
46
+ if (workspaces[normalised]) {
47
+ return workspaces[normalised];
48
+ }
49
+ for (const [wsPath, entry] of Object.entries(workspaces)) {
50
+ if (entry.name === memberName && wsPath !== '') {
51
+ return entry;
52
+ }
53
+ }
54
+ return workspaces[''] || {};
55
+ }
56
+ #resolvePackage(depName, parentKey, packages) {
57
+ if (parentKey) {
58
+ const scopedKey = `${parentKey}/${depName}`;
59
+ if (packages[scopedKey]) {
60
+ return packages[scopedKey];
61
+ }
62
+ }
63
+ return packages[depName] || null;
64
+ }
65
+ #buildNode(depName, resolved, packages, includeTransitive, visited) {
66
+ const resolvedId = Array.isArray(resolved) ? resolved[0] : resolved;
67
+ const version = this.#extractVersion(resolvedId);
68
+ const node = { version };
69
+ if (!includeTransitive) {
70
+ return node;
71
+ }
72
+ const metadata = Array.isArray(resolved) ? (resolved[2] || {}) : {};
73
+ const subDeps = metadata.dependencies || {};
74
+ if (Object.keys(subDeps).length > 0) {
75
+ const visitKey = `${depName}@${version}`;
76
+ if (visited.has(visitKey)) {
77
+ return node;
78
+ }
79
+ visited.add(visitKey);
80
+ node.dependencies = {};
81
+ for (const subName of Object.keys(subDeps)) {
82
+ const subResolved = this.#resolvePackage(subName, depName, packages);
83
+ if (subResolved) {
84
+ node.dependencies[subName] = this.#buildNode(subName, subResolved, packages, true, visited);
85
+ }
86
+ }
87
+ }
88
+ return node;
89
+ }
90
+ #extractVersion(resolvedId) {
91
+ if (typeof resolvedId !== 'string') {
92
+ return '0.0.0';
93
+ }
94
+ const atIdx = resolvedId.lastIndexOf('@');
95
+ if (atIdx > 0) {
96
+ return resolvedId.substring(atIdx + 1);
97
+ }
98
+ return '0.0.0';
99
+ }
100
+ }
@@ -1,5 +1,10 @@
1
1
  export default class Javascript_npm extends Base_javascript {
2
2
  _listCmdArgs(includeTransitive: any): string[];
3
- _buildDependencyTree(includeTransitive: any, opts?: {}): any;
3
+ _buildDependencyTree(includeTransitive: any, opts?: {}): Object | {
4
+ name: any;
5
+ version: any;
6
+ dependencies: any;
7
+ optionalDependencies: any;
8
+ };
4
9
  }
5
10
  import Base_javascript from './base_javascript.js';
@@ -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; }
@@ -29,13 +29,13 @@ export default class Yarn_berry_processor extends Yarn_processor {
29
29
  * @param {Object} depTree - The dependency tree object
30
30
  * @returns {Map<string, PackageURL>} Map of dependency names to their PackageURL objects
31
31
  */
32
- getRootDependencies(depTree: any): Map<string, PackageURL>;
32
+ getRootDependencies(depTree: Object): Map<string, PackageURL>;
33
33
  /**
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
37
  */
38
- addDependenciesToSbom(sbom: Sbom, depTree: any): void;
38
+ addDependenciesToSbom(sbom: Sbom, depTree: Object): void;
39
39
  #private;
40
40
  }
41
41
  import Yarn_processor from "./yarn_processor.js";
@@ -25,13 +25,13 @@ export default class Yarn_classic_processor extends Yarn_processor {
25
25
  * @param {Object} depTree - The dependency tree object
26
26
  * @returns {Map<string, PackageURL>} Map of dependency names to their PackageURL objects
27
27
  */
28
- getRootDependencies(depTree: any): Map<string, PackageURL>;
28
+ getRootDependencies(depTree: Object): Map<string, PackageURL>;
29
29
  /**
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
33
  */
34
- addDependenciesToSbom(sbom: Sbom, depTree: any): void;
34
+ addDependenciesToSbom(sbom: Sbom, depTree: Object): void;
35
35
  #private;
36
36
  }
37
37
  import Yarn_processor from "./yarn_processor.js";
@@ -19,6 +19,53 @@ function getPipShowOutput(depNames) {
19
19
  throw new Error('fail invoking \'pip show\' to fetch metadata for all installed packages in environment', { cause: error });
20
20
  }
21
21
  }
22
+ /**
23
+ * Get pip inspect output from env var override or by invoking pip inspect.
24
+ * Returns null if pip inspect is unavailable (pip < 22.2) or if freeze/show
25
+ * env vars are set without an inspect override (to avoid inconsistent data).
26
+ * @return {string|null} pip inspect JSON output, or null on failure
27
+ */
28
+ function getPipInspectOutput() {
29
+ if (environmentVariableIsPopulated("TRUSTIFY_DA_PIP_INSPECT")) {
30
+ return new Buffer.from(process.env["TRUSTIFY_DA_PIP_INSPECT"], 'base64').toString('ascii');
31
+ }
32
+ if (environmentVariableIsPopulated("TRUSTIFY_DA_PIP_FREEZE") || environmentVariableIsPopulated("TRUSTIFY_DA_PIP_SHOW")) {
33
+ return null;
34
+ }
35
+ try {
36
+ return invokeCommand(this.pathToPipBin, ['inspect']).toString();
37
+ }
38
+ catch (error) {
39
+ console.warn('pip inspect is not available (requires pip 22.2+), SBOM will be generated without hashes');
40
+ return null;
41
+ }
42
+ }
43
+ /**
44
+ * Parse pip inspect JSON output and build a hash lookup map.
45
+ * @param {string|null} inspectOutput - raw pip inspect JSON string
46
+ * @return {Map<string, Array<{alg: string, content: string}>>} map of lowercase package name to hashes
47
+ */
48
+ function parsePipInspectHashes(inspectOutput) {
49
+ if (!inspectOutput) {
50
+ return new Map();
51
+ }
52
+ try {
53
+ let inspectData = JSON.parse(inspectOutput);
54
+ let hashMap = new Map();
55
+ for (let pkg of (inspectData.installed || [])) {
56
+ let name = pkg.metadata?.name;
57
+ let sha256 = pkg.download_info?.archive_info?.hashes?.sha256;
58
+ if (name && sha256) {
59
+ hashMap.set(name.toLowerCase(), [{ alg: "SHA-256", content: sha256 }]);
60
+ }
61
+ }
62
+ return hashMap;
63
+ }
64
+ catch (error) {
65
+ console.warn('Failed to parse pip inspect output, SBOM will be generated without hashes');
66
+ return new Map();
67
+ }
68
+ }
22
69
  /** @typedef {{name: string, version: string, dependencies: DependencyEntry[], hashes?: Array<{alg: string, content: string}>}} DependencyEntry */
23
70
  export default class Python_controller {
24
71
  pythonEnvDir;
@@ -131,8 +178,11 @@ export default class Python_controller {
131
178
  startingTime = new Date();
132
179
  console.log("Starting time to get requirements.txt dependency tree = " + startingTime);
133
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
+ }
134
185
  if (!this.realEnvironment) {
135
- let installBestEfforts = getCustom("TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS", "false", this.options);
136
186
  if (installBestEfforts === "false") {
137
187
  try {
138
188
  invokeCommand(this.pathToPipBin, ['install', '-r', this.pathToRequirements]);
@@ -225,6 +275,8 @@ export default class Python_controller {
225
275
  CachedEnvironmentDeps[packageName.replace("_", "-")] = pipDepTreeEntryForCache;
226
276
  });
227
277
  }
278
+ const inspectOutput = getPipInspectOutput.call(this);
279
+ const hashMap = parsePipInspectHashes(inspectOutput);
228
280
  parsedRequirements.forEach(({ name: depName, version: manifestVersion, hasMarker }) => {
229
281
  if (hasMarker && CachedEnvironmentDeps[depName.toLowerCase()] === undefined) {
230
282
  return;
@@ -247,7 +299,7 @@ export default class Python_controller {
247
299
  }
248
300
  let path = [];
249
301
  path.push(depName.toLowerCase());
250
- bringAllDependencies(dependencies, depName, CachedEnvironmentDeps, includeTransitive, path, usePipDepTree);
302
+ bringAllDependencies(dependencies, depName, CachedEnvironmentDeps, includeTransitive, path, usePipDepTree, hashMap);
251
303
  });
252
304
  dependencies.sort((dep1, dep2) => {
253
305
  const DEP1 = dep1.name.toLowerCase();
@@ -327,8 +379,9 @@ function getDepsList(record) {
327
379
  * @param includeTransitive
328
380
  * @param usePipDepTree
329
381
  * @param {[string]}path array representing the path of the current branch in dependency tree, starting with a root dependency - that is - a given dependency in requirements.txt
382
+ * @param {Map<string, Array<{alg: string, content: string}>>} hashMap - map of lowercase package name to hashes
330
383
  */
331
- function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDeps, includeTransitive, path, usePipDepTree) {
384
+ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDeps, includeTransitive, path, usePipDepTree, hashMap) {
332
385
  if (dependencyName?.trim() === "") {
333
386
  return;
334
387
  }
@@ -350,7 +403,11 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
350
403
  directDeps = record.dependencies;
351
404
  }
352
405
  let targetDeps = [];
406
+ let hashes = hashMap?.get(depName.toLowerCase());
353
407
  let entry = { "name": depName, "version": version, "dependencies": [] };
408
+ if (hashes) {
409
+ entry.hashes = hashes;
410
+ }
354
411
  dependencies.push(entry);
355
412
  directDeps.forEach((dep) => {
356
413
  let depArray = [];
@@ -360,7 +417,7 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
360
417
  depArray.push(dep.toLowerCase());
361
418
  if (includeTransitive) {
362
419
  // send to recurrsion the array of all deps in path + the current dependency name which is not on the path.
363
- bringAllDependencies(targetDeps, dep, cachedEnvironmentDeps, includeTransitive, path.concat(depArray), usePipDepTree);
420
+ bringAllDependencies(targetDeps, dep, cachedEnvironmentDeps, includeTransitive, path.concat(depArray), usePipDepTree, hashMap);
364
421
  }
365
422
  }
366
423
  // sort ra
@@ -4,6 +4,7 @@ declare namespace _default {
4
4
  export { provideComponent };
5
5
  export { provideStack };
6
6
  export { readLicenseFromManifest };
7
+ export function packageManagerName(): string;
7
8
  }
8
9
  export default _default;
9
10
  export type DependencyEntry = {
@@ -5,7 +5,7 @@ import Sbom from '../sbom.js';
5
5
  import { environmentVariableIsPopulated, getCustom, getCustomPath, invokeCommand } from "../tools.js";
6
6
  import Python_controller from './python_controller.js';
7
7
  import { getParser, getIgnoreQuery, getPinnedVersionQuery } from './requirements_parser.js';
8
- export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest };
8
+ export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest, packageManagerName() { return 'pip'; } };
9
9
  /** @typedef {{name: string, version: string, dependencies: DependencyEntry[], hashes?: Array<{alg: string, content: string}>}} DependencyEntry */
10
10
  /**
11
11
  * @type {string} ecosystem for python-pip is 'pip'
@@ -76,7 +76,9 @@ export default class Python_pip_pyproject extends Base_pyproject {
76
76
  let name = pkg.metadata.name;
77
77
  let version = pkg.metadata.version;
78
78
  let key = this._canonicalize(name);
79
- graph.set(key, { name, version, children: [] });
79
+ let sha256 = pkg.download_info?.archive_info?.hashes?.sha256;
80
+ let hashes = sha256 ? [{ alg: "SHA-256", content: sha256 }] : undefined;
81
+ graph.set(key, { name, version, children: [], hashes });
80
82
  }
81
83
  for (let pkg of nonRootPackages) {
82
84
  let key = this._canonicalize(pkg.metadata.name);