@trustify-da/trustify-da-javascript-client 0.3.0-ea.a8c3942 → 0.3.0-ea.aaf5191

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 (77) hide show
  1. package/README.md +177 -3
  2. package/dist/package.json +30 -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.d.ts +13 -0
  7. package/dist/src/cyclone_dx_sbom.js +50 -4
  8. package/dist/src/index.d.ts +14 -10
  9. package/dist/src/index.js +17 -27
  10. package/dist/src/license/index.d.ts +3 -3
  11. package/dist/src/license/index.js +4 -1
  12. package/dist/src/license/license_utils.js +4 -1
  13. package/dist/src/license/licenses_api.d.ts +2 -2
  14. package/dist/src/oci_image/images.d.ts +1 -1
  15. package/dist/src/oci_image/images.js +15 -1
  16. package/dist/src/oci_image/utils.d.ts +5 -5
  17. package/dist/src/package_version.d.ts +8 -0
  18. package/dist/src/package_version.js +31 -0
  19. package/dist/src/provider.d.ts +4 -3
  20. package/dist/src/provider.js +6 -2
  21. package/dist/src/providers/base_java.d.ts +67 -3
  22. package/dist/src/providers/base_java.js +80 -23
  23. package/dist/src/providers/base_javascript.d.ts +74 -9
  24. package/dist/src/providers/base_javascript.js +115 -6
  25. package/dist/src/providers/base_pyproject.d.ts +10 -5
  26. package/dist/src/providers/base_pyproject.js +7 -0
  27. package/dist/src/providers/containerfile_parser.d.ts +5 -0
  28. package/dist/src/providers/containerfile_parser.js +20 -0
  29. package/dist/src/providers/golang_gomodules.d.ts +2 -1
  30. package/dist/src/providers/golang_gomodules.js +50 -8
  31. package/dist/src/providers/java_gradle.d.ts +48 -0
  32. package/dist/src/providers/java_gradle.js +196 -18
  33. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  34. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  35. package/dist/src/providers/java_maven.d.ts +29 -9
  36. package/dist/src/providers/java_maven.js +176 -10
  37. package/dist/src/providers/javascript_bun.d.ts +22 -0
  38. package/dist/src/providers/javascript_bun.js +141 -0
  39. package/dist/src/providers/javascript_npm.d.ts +19 -1
  40. package/dist/src/providers/javascript_npm.js +40 -1
  41. package/dist/src/providers/javascript_pnpm.d.ts +13 -0
  42. package/dist/src/providers/javascript_pnpm.js +47 -1
  43. package/dist/src/providers/javascript_yarn.d.ts +12 -0
  44. package/dist/src/providers/javascript_yarn.js +75 -2
  45. package/dist/src/providers/manifest.js +6 -3
  46. package/dist/src/providers/oci_dockerfile.d.ts +51 -0
  47. package/dist/src/providers/oci_dockerfile.js +177 -0
  48. package/dist/src/providers/processors/yarn_berry_processor.d.ts +6 -2
  49. package/dist/src/providers/processors/yarn_berry_processor.js +3 -2
  50. package/dist/src/providers/processors/yarn_classic_processor.d.ts +6 -2
  51. package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
  52. package/dist/src/providers/python_controller.js +61 -4
  53. package/dist/src/providers/python_pip.d.ts +1 -0
  54. package/dist/src/providers/python_pip.js +1 -1
  55. package/dist/src/providers/python_poetry.d.ts +31 -8
  56. package/dist/src/providers/python_poetry.js +62 -14
  57. package/dist/src/providers/python_uv.d.ts +5 -14
  58. package/dist/src/providers/python_uv.js +44 -2
  59. package/dist/src/providers/requirements_parser.js +1 -1
  60. package/dist/src/providers/rust_cargo.d.ts +6 -2
  61. package/dist/src/providers/rust_cargo.js +101 -33
  62. package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
  63. package/dist/src/remediate.d.ts +21 -0
  64. package/dist/src/remediate.js +149 -0
  65. package/dist/src/remediation.d.ts +57 -0
  66. package/dist/src/remediation.js +436 -0
  67. package/dist/src/remediation_report.d.ts +40 -0
  68. package/dist/src/remediation_report.js +159 -0
  69. package/dist/src/sbom.d.ts +11 -0
  70. package/dist/src/sbom.js +10 -0
  71. package/dist/src/tools.d.ts +2 -2
  72. package/dist/src/tools.js +24 -1
  73. package/dist/src/updaters/maven_updater.d.ts +26 -0
  74. package/dist/src/updaters/maven_updater.js +322 -0
  75. package/dist/src/updaters/toml_updater.d.ts +33 -0
  76. package/dist/src/updaters/toml_updater.js +244 -0
  77. package/package.json +31 -23
@@ -1,12 +1,85 @@
1
- import Base_javascript from './base_javascript.js';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseSyml } from '@yarnpkg/parsers';
4
+ import Base_javascript, { sriToHash } from './base_javascript.js';
2
5
  import Yarn_berry_processor from './processors/yarn_berry_processor.js';
3
6
  import Yarn_classic_processor from './processors/yarn_classic_processor.js';
7
+ /**
8
+ * Converts a Yarn Berry `checksum` value to a CycloneDX hash object. Berry
9
+ * checksums are formatted as `<cacheKey>/<hex-sha512>`; the digest is the
10
+ * hex-encoded SHA-512 following the final slash.
11
+ * @param {string} checksum - The checksum field value
12
+ * @returns {{alg: string, content: string}|null} CycloneDX hash, or null
13
+ */
14
+ function berryChecksumToHash(checksum) {
15
+ if (typeof checksum !== 'string') {
16
+ return null;
17
+ }
18
+ const slash = checksum.lastIndexOf('/');
19
+ const hex = (slash >= 0 ? checksum.slice(slash + 1) : checksum).trim();
20
+ if (!/^[0-9a-f]+$/i.test(hex)) {
21
+ return null;
22
+ }
23
+ return { alg: 'SHA-512', content: hex.toLowerCase() };
24
+ }
4
25
  export default class Javascript_yarn extends Base_javascript {
5
26
  static VERSION_PATTERN = /^([0-9]+)\./;
6
27
  #processor;
7
28
  _lockFileName() {
8
29
  return "yarn.lock";
9
30
  }
31
+ /**
32
+ * Parses `yarn.lock` into a hash map keyed by `name@version`. Supports both
33
+ * Yarn Classic (v1) entries with an `integrity` SRI field and Yarn Berry
34
+ * (v2+) entries with a `checksum` field.
35
+ * @param {string} lockDir - Directory containing yarn.lock
36
+ * @returns {Map<string, Array<{alg: string, content: string}>>} Hash map
37
+ * @protected
38
+ */
39
+ _parseLockFileHashes(lockDir) {
40
+ const map = new Map();
41
+ const lockPath = path.join(lockDir, this._lockFileName());
42
+ if (!fs.existsSync(lockPath)) {
43
+ return map;
44
+ }
45
+ let content;
46
+ try {
47
+ content = fs.readFileSync(lockPath, 'utf-8');
48
+ }
49
+ catch (_) {
50
+ return map;
51
+ }
52
+ const parsed = parseSyml(content);
53
+ for (const [spec, entry] of Object.entries(parsed)) {
54
+ if (spec === '__metadata') {
55
+ continue;
56
+ }
57
+ const version = entry.version;
58
+ if (!version) {
59
+ continue;
60
+ }
61
+ let hash = null;
62
+ if (entry.integrity) {
63
+ hash = sriToHash(entry.integrity);
64
+ }
65
+ else if (entry.checksum) {
66
+ hash = berryChecksumToHash(entry.checksum);
67
+ }
68
+ if (!hash) {
69
+ continue;
70
+ }
71
+ // Handle comma-separated specifiers (e.g., "pkg@^1.0.0, pkg@^2.0.0")
72
+ for (const rawSpec of spec.split(',')) {
73
+ const trimmed = rawSpec.trim();
74
+ const atIndex = trimmed.lastIndexOf('@');
75
+ if (atIndex > 0) {
76
+ const name = trimmed.slice(0, atIndex);
77
+ map.set(`${name}@${version}`, [hash]);
78
+ }
79
+ }
80
+ }
81
+ return map;
82
+ }
10
83
  _cmdName() {
11
84
  return "yarn";
12
85
  }
@@ -34,6 +107,6 @@ export default class Javascript_yarn extends Base_javascript {
34
107
  return this.#processor.parseDepTreeOutput(output);
35
108
  }
36
109
  _addDependenciesToSbom(sbom, depTree) {
37
- this.#processor.addDependenciesToSbom(sbom, depTree);
110
+ this.#processor.addDependenciesToSbom(sbom, depTree, purl => this._hashesForPurl(purl));
38
111
  }
39
112
  }
@@ -55,11 +55,14 @@ export default class Manifest {
55
55
  }
56
56
  loadIgnored(content) {
57
57
  let deps = [];
58
- if (!content.exhortignore) {
58
+ // Prefer the new 'trustify-da-ignore' key, falling back to the legacy
59
+ // 'exhortignore' key for backwards compatibility.
60
+ let ignored = content['trustify-da-ignore'] || content.exhortignore;
61
+ if (!ignored) {
59
62
  return deps;
60
63
  }
61
- for (let i = 0; i < content.exhortignore.length; i++) {
62
- deps.push(toPurl("npm", content.exhortignore[i]));
64
+ for (let i = 0; i < ignored.length; i++) {
65
+ deps.push(toPurl("npm", ignored[i]));
63
66
  }
64
67
  return deps;
65
68
  }
@@ -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,17 @@ 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
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
37
38
  */
38
- addDependenciesToSbom(sbom: Sbom, depTree: any): void;
39
+ addDependenciesToSbom(sbom: Sbom, depTree: Object, hashesForPurl?: (arg0: PackageURL) => (Array<{
40
+ alg: string;
41
+ content: string;
42
+ }> | undefined)): void;
39
43
  #private;
40
44
  }
41
45
  import Yarn_processor from "./yarn_processor.js";
@@ -78,8 +78,9 @@ export default class Yarn_berry_processor extends Yarn_processor {
78
78
  * Adds dependencies to the SBOM
79
79
  * @param {Sbom} sbom - The SBOM object to add dependencies to
80
80
  * @param {Object} depTree - The dependency tree object
81
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
81
82
  */
82
- addDependenciesToSbom(sbom, depTree) {
83
+ addDependenciesToSbom(sbom, depTree, hashesForPurl = () => undefined) {
83
84
  if (!depTree) {
84
85
  return;
85
86
  }
@@ -137,7 +138,7 @@ export default class Yarn_berry_processor extends Yarn_processor {
137
138
  }
138
139
  const to = this.#purlFromLocator(d.locator);
139
140
  if (to) {
140
- sbom.addDependency(from, to);
141
+ sbom.addDependency(from, to, undefined, hashesForPurl(to));
141
142
  }
142
143
  });
143
144
  });
@@ -25,13 +25,17 @@ 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
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
33
34
  */
34
- addDependenciesToSbom(sbom: Sbom, depTree: any): void;
35
+ addDependenciesToSbom(sbom: Sbom, depTree: Object, hashesForPurl?: (arg0: PackageURL) => (Array<{
36
+ alg: string;
37
+ content: string;
38
+ }> | undefined)): void;
35
39
  #private;
36
40
  }
37
41
  import Yarn_processor from "./yarn_processor.js";
@@ -50,8 +50,9 @@ export default class Yarn_classic_processor extends Yarn_processor {
50
50
  * Adds dependencies to the SBOM
51
51
  * @param {Sbom} sbom - The SBOM object to add dependencies to
52
52
  * @param {Object} depTree - The dependency tree object
53
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
53
54
  */
54
- addDependenciesToSbom(sbom, depTree) {
55
+ addDependenciesToSbom(sbom, depTree, hashesForPurl = () => undefined) {
55
56
  if (!depTree?.data?.trees) {
56
57
  return;
57
58
  }
@@ -60,12 +61,12 @@ export default class Yarn_classic_processor extends Yarn_processor {
60
61
  depTree.data.trees.forEach(n => {
61
62
  const dep = new NodeMetaData(n);
62
63
  if (this._manifest.dependencies.includes(dep.name)) {
63
- sbom.addDependency(rootPurl, dep.purl);
64
+ sbom.addDependency(rootPurl, dep.purl, undefined, hashesForPurl(dep.purl));
64
65
  }
65
66
  purls.set(dep.name, dep.purl);
66
67
  });
67
68
  depTree.data.trees.forEach(n => {
68
- this.#addChildrenToSbom(sbom, n, purls);
69
+ this.#addChildrenToSbom(sbom, n, purls, hashesForPurl);
69
70
  });
70
71
  }
71
72
  /**
@@ -73,9 +74,10 @@ export default class Yarn_classic_processor extends Yarn_processor {
73
74
  * @param {Sbom} sbom - The SBOM object to add dependencies to
74
75
  * @param {Object} node - The current dependency node
75
76
  * @param {Map<string, PackageURL>} purls - Map of dependency names to their PackageURL objects
77
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} hashesForPurl - Resolves lock-file hashes for a purl
76
78
  * @private
77
79
  */
78
- #addChildrenToSbom(sbom, node, purls) {
80
+ #addChildrenToSbom(sbom, node, purls, hashesForPurl) {
79
81
  const dep = new NodeMetaData(node);
80
82
  const children = node.children ? node.children : [];
81
83
  children.forEach(c => {
@@ -83,9 +85,9 @@ export default class Yarn_classic_processor extends Yarn_processor {
83
85
  const from = dep.shadow ? purls.get(dep.name) : dep.purl;
84
86
  const to = child.shadow ? purls.get(child.name) : child.purl;
85
87
  if (from && to) {
86
- sbom.addDependency(from, to);
88
+ sbom.addDependency(from, to, undefined, hashesForPurl(to));
87
89
  }
88
- this.#addChildrenToSbom(sbom, c, purls);
90
+ this.#addChildrenToSbom(sbom, c, purls, hashesForPurl);
89
91
  });
90
92
  }
91
93
  }
@@ -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'