@trustify-da/trustify-da-javascript-client 0.3.0-ea.bbe2094 → 0.3.0-ea.bdaadb2

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 (75) hide show
  1. package/README.md +177 -3
  2. package/dist/package.json +29 -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 +15 -26
  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 +4 -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 +68 -9
  24. package/dist/src/providers/base_javascript.js +112 -3
  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 +44 -3
  31. package/dist/src/providers/java_gradle.d.ts +48 -0
  32. package/dist/src/providers/java_gradle.js +194 -15
  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 +30 -5
  36. package/dist/src/providers/java_maven.js +168 -7
  37. package/dist/src/providers/javascript_bun.d.ts +12 -0
  38. package/dist/src/providers/javascript_bun.js +42 -1
  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/oci_dockerfile.d.ts +51 -0
  46. package/dist/src/providers/oci_dockerfile.js +177 -0
  47. package/dist/src/providers/processors/yarn_berry_processor.d.ts +6 -2
  48. package/dist/src/providers/processors/yarn_berry_processor.js +3 -2
  49. package/dist/src/providers/processors/yarn_classic_processor.d.ts +6 -2
  50. package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
  51. package/dist/src/providers/python_controller.js +61 -4
  52. package/dist/src/providers/python_pip.d.ts +1 -0
  53. package/dist/src/providers/python_pip.js +1 -1
  54. package/dist/src/providers/python_poetry.d.ts +31 -8
  55. package/dist/src/providers/python_poetry.js +62 -14
  56. package/dist/src/providers/python_uv.d.ts +5 -14
  57. package/dist/src/providers/python_uv.js +44 -2
  58. package/dist/src/providers/rust_cargo.d.ts +6 -2
  59. package/dist/src/providers/rust_cargo.js +101 -33
  60. package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
  61. package/dist/src/remediate.d.ts +21 -0
  62. package/dist/src/remediate.js +149 -0
  63. package/dist/src/remediation.d.ts +57 -0
  64. package/dist/src/remediation.js +436 -0
  65. package/dist/src/remediation_report.d.ts +40 -0
  66. package/dist/src/remediation_report.js +159 -0
  67. package/dist/src/sbom.d.ts +11 -0
  68. package/dist/src/sbom.js +10 -0
  69. package/dist/src/tools.d.ts +2 -2
  70. package/dist/src/tools.js +24 -1
  71. package/dist/src/updaters/maven_updater.d.ts +26 -0
  72. package/dist/src/updaters/maven_updater.js +322 -0
  73. package/dist/src/updaters/toml_updater.d.ts +33 -0
  74. package/dist/src/updaters/toml_updater.js +244 -0
  75. package/package.json +30 -23
@@ -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'
@@ -1,4 +1,5 @@
1
1
  export default class Python_poetry extends Base_pyproject {
2
+ _verifyPoetryAccessible(poetryBin: any): void;
2
3
  /**
3
4
  * @param {string} manifestDir
4
5
  * @param {string} _workspaceDir - unused (poetry has no workspace support)
@@ -6,7 +7,7 @@ export default class Python_poetry extends Base_pyproject {
6
7
  * @param {Object} opts
7
8
  * @returns {Promise<{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}>}
8
9
  */
9
- _getDependencyData(manifestDir: string, _workspaceDir: string, parsed: object, opts: any): Promise<{
10
+ _getDependencyData(manifestDir: string, _workspaceDir: string, parsed: object, opts: Object): Promise<{
10
11
  directDeps: string[];
11
12
  graph: Map<string, {
12
13
  name: string;
@@ -18,17 +19,17 @@ export default class Python_poetry extends Base_pyproject {
18
19
  * Get poetry show --tree output.
19
20
  * @param {string} manifestDir
20
21
  * @param {boolean} hasDevGroup
21
- * @param {Object} opts
22
+ * @param {string} poetryBin
22
23
  * @returns {string}
23
24
  */
24
- _getPoetryShowTreeOutput(manifestDir: string, hasDevGroup: boolean, opts: any): string;
25
+ _getPoetryShowTreeOutput(manifestDir: string, hasDevGroup: boolean, poetryBin: string): string;
25
26
  /**
26
27
  * Get poetry show --all output (flat list with resolved versions).
27
28
  * @param {string} manifestDir
28
- * @param {Object} opts
29
+ * @param {string} poetryBin
29
30
  * @returns {string}
30
31
  */
31
- _getPoetryShowAllOutput(manifestDir: string, opts: any): string;
32
+ _getPoetryShowAllOutput(manifestDir: string, poetryBin: string): string;
32
33
  /**
33
34
  * Parse poetry show --all output into a version map.
34
35
  * Lines look like: "name (!) 1.2.3 Description text..."
@@ -46,29 +47,51 @@ export default class Python_poetry extends Base_pyproject {
46
47
  * → transitiveMarkers['click']['colorama'] = "sys_platform == 'win32'"
47
48
  * @param {string|null} lockDir
48
49
  * @param {object} parsed - parsed pyproject.toml
49
- * @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}}
50
+ * @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>, hashMap: Map<string, Array<{alg: string, content: string}>>}}
50
51
  */
51
52
  _extractMarkerData(lockDir: string | null, parsed: object): {
52
53
  directMarkers: Map<string, string>;
53
54
  transitiveMarkers: Map<string, Map<string, string>>;
55
+ hashMap: Map<string, Array<{
56
+ alg: string;
57
+ content: string;
58
+ }>>;
54
59
  };
60
+ /**
61
+ * Extract a SHA-256 hex digest from poetry.lock files array.
62
+ * Prefers the sdist (.tar.gz) hash; falls back to the first wheel hash.
63
+ * @param {Array<{file: string, hash: string}>} [files]
64
+ * @returns {string|null}
65
+ */
66
+ _extractSha256FromFiles(files?: Array<{
67
+ file: string;
68
+ hash: string;
69
+ }>): string | null;
55
70
  /**
56
71
  * Parse poetry show --tree output into a dependency graph structure.
57
72
  *
58
73
  * @param {string} treeOutput
59
74
  * @param {Map<string, string>} versionMap - canonical name -> resolved version
60
- * @param {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}} markerData
61
- * @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}}
75
+ * @param {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>, hashMap: Map<string, Array<{alg: string, content: string}>>}} markerData
76
+ * @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[], hashes?: Array<{alg: string, content: string}>}>}}
62
77
  */
63
78
  _parsePoetryTree(treeOutput: string, versionMap: Map<string, string>, markerData: {
64
79
  directMarkers: Map<string, string>;
65
80
  transitiveMarkers: Map<string, Map<string, string>>;
81
+ hashMap: Map<string, Array<{
82
+ alg: string;
83
+ content: string;
84
+ }>>;
66
85
  }): {
67
86
  directDeps: string[];
68
87
  graph: Map<string, {
69
88
  name: string;
70
89
  version: string;
71
90
  children: string[];
91
+ hashes?: Array<{
92
+ alg: string;
93
+ content: string;
94
+ }>;
72
95
  }>;
73
96
  };
74
97
  }
@@ -32,6 +32,17 @@ export default class Python_poetry extends Base_pyproject {
32
32
  _cmdName() {
33
33
  return 'poetry';
34
34
  }
35
+ _verifyPoetryAccessible(poetryBin) {
36
+ try {
37
+ invokeCommand(poetryBin, ['--version']);
38
+ }
39
+ catch (error) {
40
+ if (error.code === 'ENOENT') {
41
+ throw new Error(`poetry is not accessible at "${poetryBin}"`, { cause: error });
42
+ }
43
+ throw new Error('failed to check for poetry binary', { cause: error });
44
+ }
45
+ }
35
46
  /**
36
47
  * @param {string} manifestDir
37
48
  * @param {string} _workspaceDir - unused (poetry has no workspace support)
@@ -41,9 +52,15 @@ export default class Python_poetry extends Base_pyproject {
41
52
  */
42
53
  // eslint-disable-next-line no-unused-vars
43
54
  async _getDependencyData(manifestDir, _workspaceDir, parsed, opts) {
55
+ let poetryBin = getCustomPath('poetry', opts);
56
+ let envBypass = environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_TREE')
57
+ && environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_ALL');
58
+ if (!envBypass) {
59
+ this._verifyPoetryAccessible(poetryBin);
60
+ }
44
61
  let hasDevGroup = !!(parsed.tool?.poetry?.group?.dev || parsed.tool?.poetry?.['dev-dependencies']);
45
- let treeOutput = this._getPoetryShowTreeOutput(manifestDir, hasDevGroup, opts);
46
- let showAllOutput = this._getPoetryShowAllOutput(manifestDir, opts);
62
+ let treeOutput = this._getPoetryShowTreeOutput(manifestDir, hasDevGroup, poetryBin);
63
+ let showAllOutput = this._getPoetryShowAllOutput(manifestDir, poetryBin);
47
64
  let versionMap = this._parsePoetryShowAll(showAllOutput);
48
65
  let lockDir = this._findLockFileDir(manifestDir, opts);
49
66
  let markerData = this._extractMarkerData(lockDir, parsed);
@@ -53,14 +70,13 @@ export default class Python_poetry extends Base_pyproject {
53
70
  * Get poetry show --tree output.
54
71
  * @param {string} manifestDir
55
72
  * @param {boolean} hasDevGroup
56
- * @param {Object} opts
73
+ * @param {string} poetryBin
57
74
  * @returns {string}
58
75
  */
59
- _getPoetryShowTreeOutput(manifestDir, hasDevGroup, opts) {
76
+ _getPoetryShowTreeOutput(manifestDir, hasDevGroup, poetryBin) {
60
77
  if (environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_TREE')) {
61
78
  return Buffer.from(process.env['TRUSTIFY_DA_POETRY_SHOW_TREE'], 'base64').toString('utf-8');
62
79
  }
63
- let poetryBin = getCustomPath('poetry', opts);
64
80
  let args = ['show', '--tree', '--no-ansi'];
65
81
  if (hasDevGroup) {
66
82
  args.push('--without', 'dev');
@@ -70,14 +86,13 @@ export default class Python_poetry extends Base_pyproject {
70
86
  /**
71
87
  * Get poetry show --all output (flat list with resolved versions).
72
88
  * @param {string} manifestDir
73
- * @param {Object} opts
89
+ * @param {string} poetryBin
74
90
  * @returns {string}
75
91
  */
76
- _getPoetryShowAllOutput(manifestDir, opts) {
92
+ _getPoetryShowAllOutput(manifestDir, poetryBin) {
77
93
  if (environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_ALL')) {
78
94
  return Buffer.from(process.env['TRUSTIFY_DA_POETRY_SHOW_ALL'], 'base64').toString('utf-8');
79
95
  }
80
- let poetryBin = getCustomPath('poetry', opts);
81
96
  return invokeCommand(poetryBin, ['show', '--no-ansi', '--all'], { cwd: manifestDir }).toString();
82
97
  }
83
98
  /**
@@ -111,11 +126,12 @@ export default class Python_poetry extends Base_pyproject {
111
126
  * → transitiveMarkers['click']['colorama'] = "sys_platform == 'win32'"
112
127
  * @param {string|null} lockDir
113
128
  * @param {object} parsed - parsed pyproject.toml
114
- * @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}}
129
+ * @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>, hashMap: Map<string, Array<{alg: string, content: string}>>}}
115
130
  */
116
131
  _extractMarkerData(lockDir, parsed) {
117
132
  let directMarkers = new Map();
118
133
  let transitiveMarkers = new Map();
134
+ let hashMap = new Map();
119
135
  // Extract markers from PEP 621 dependency strings: "name[extras]>=ver ; marker"
120
136
  let deps = parsed.project?.dependencies || [];
121
137
  for (let dep of deps) {
@@ -142,18 +158,40 @@ export default class Python_poetry extends Base_pyproject {
142
158
  transitiveMarkers.get(pkgKey).set(this._canonicalize(depName), markers);
143
159
  }
144
160
  }
161
+ let sha256Hex = this._extractSha256FromFiles(pkg.files);
162
+ if (sha256Hex) {
163
+ hashMap.set(pkgKey, [{ alg: "SHA-256", content: sha256Hex }]);
164
+ }
145
165
  }
146
166
  }
147
167
  }
148
- return { directMarkers, transitiveMarkers };
168
+ return { directMarkers, transitiveMarkers, hashMap };
169
+ }
170
+ /**
171
+ * Extract a SHA-256 hex digest from poetry.lock files array.
172
+ * Prefers the sdist (.tar.gz) hash; falls back to the first wheel hash.
173
+ * @param {Array<{file: string, hash: string}>} [files]
174
+ * @returns {string|null}
175
+ */
176
+ _extractSha256FromFiles(files) {
177
+ if (!Array.isArray(files) || files.length === 0) {
178
+ return null;
179
+ }
180
+ let sdist = files.find(f => f.file.endsWith('.tar.gz'));
181
+ let entry = sdist || files[0];
182
+ let hash = entry.hash;
183
+ if (hash && hash.startsWith('sha256:')) {
184
+ return hash.slice(7);
185
+ }
186
+ return null;
149
187
  }
150
188
  /**
151
189
  * Parse poetry show --tree output into a dependency graph structure.
152
190
  *
153
191
  * @param {string} treeOutput
154
192
  * @param {Map<string, string>} versionMap - canonical name -> resolved version
155
- * @param {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}} markerData
156
- * @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}}
193
+ * @param {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>, hashMap: Map<string, Array<{alg: string, content: string}>>}} markerData
194
+ * @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[], hashes?: Array<{alg: string, content: string}>}>}}
157
195
  */
158
196
  _parsePoetryTree(treeOutput, versionMap, markerData) {
159
197
  let lines = treeOutput.split(/\r?\n/);
@@ -179,7 +217,12 @@ export default class Python_poetry extends Base_pyproject {
179
217
  }
180
218
  directDeps.push(key);
181
219
  if (!graph.has(key)) {
182
- graph.set(key, { name, version, children: [] });
220
+ let entry = { name, version, children: [] };
221
+ let hashes = markerData.hashMap.get(key);
222
+ if (hashes) {
223
+ entry.hashes = hashes;
224
+ }
225
+ graph.set(key, entry);
183
226
  }
184
227
  currentDirectDep = key;
185
228
  stack = [{ key, depth: -1 }];
@@ -223,7 +266,12 @@ export default class Python_poetry extends Base_pyproject {
223
266
  throw new Error(`poetry: package '${depName}' has no resolved version`);
224
267
  }
225
268
  if (!graph.has(depKey)) {
226
- graph.set(depKey, { name: depName, version, children: [] });
269
+ let entry = { name: depName, version, children: [] };
270
+ let hashes = markerData.hashMap.get(depKey);
271
+ if (hashes) {
272
+ entry.hashes = hashes;
273
+ }
274
+ graph.set(depKey, entry);
227
275
  }
228
276
  if (parentKey) {
229
277
  let parentEntry = graph.get(parentKey);