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

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 (39) hide show
  1. package/dist/package.json +18 -19
  2. package/dist/src/analysis.d.ts +7 -7
  3. package/dist/src/cyclone_dx_sbom.js +11 -3
  4. package/dist/src/index.d.ts +9 -9
  5. package/dist/src/index.js +3 -1
  6. package/dist/src/license/index.d.ts +3 -3
  7. package/dist/src/license/index.js +4 -1
  8. package/dist/src/license/license_utils.js +4 -1
  9. package/dist/src/license/licenses_api.d.ts +2 -2
  10. package/dist/src/oci_image/images.d.ts +1 -1
  11. package/dist/src/oci_image/utils.d.ts +5 -5
  12. package/dist/src/provider.d.ts +4 -3
  13. package/dist/src/provider.js +1 -1
  14. package/dist/src/providers/base_java.d.ts +14 -3
  15. package/dist/src/providers/base_java.js +16 -6
  16. package/dist/src/providers/base_javascript.d.ts +14 -9
  17. package/dist/src/providers/base_javascript.js +7 -0
  18. package/dist/src/providers/base_pyproject.d.ts +10 -5
  19. package/dist/src/providers/base_pyproject.js +7 -0
  20. package/dist/src/providers/golang_gomodules.d.ts +2 -1
  21. package/dist/src/providers/golang_gomodules.js +1 -1
  22. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  23. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  24. package/dist/src/providers/java_maven.d.ts +2 -2
  25. package/dist/src/providers/java_maven.js +72 -0
  26. package/dist/src/providers/javascript_npm.d.ts +6 -1
  27. package/dist/src/providers/processors/yarn_berry_processor.d.ts +2 -2
  28. package/dist/src/providers/processors/yarn_classic_processor.d.ts +2 -2
  29. package/dist/src/providers/python_controller.js +57 -3
  30. package/dist/src/providers/python_pip.d.ts +1 -0
  31. package/dist/src/providers/python_pip.js +1 -1
  32. package/dist/src/providers/python_poetry.d.ts +31 -8
  33. package/dist/src/providers/python_poetry.js +62 -14
  34. package/dist/src/providers/python_uv.d.ts +5 -14
  35. package/dist/src/providers/python_uv.js +44 -2
  36. package/dist/src/providers/rust_cargo.d.ts +6 -2
  37. package/dist/src/providers/rust_cargo.js +31 -4
  38. package/dist/src/tools.d.ts +2 -2
  39. package/package.json +19 -20
@@ -13,9 +13,10 @@ declare namespace _default {
13
13
  export { provideComponent };
14
14
  export { provideStack };
15
15
  export { readLicenseFromManifest };
16
+ export function packageManagerName(): string;
16
17
  }
17
18
  export default _default;
18
- export type Provided = import("../provider").Provided;
19
+ export type Provided = any;
19
20
  export type Package = {
20
21
  name: string;
21
22
  version: string;
@@ -6,7 +6,7 @@ import Sbom from '../sbom.js';
6
6
  import { getCustom, getCustomPath, invokeCommand } from "../tools.js";
7
7
  import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js';
8
8
  import { getParser, getRequireQuery } from './gomod_parser.js';
9
- export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest };
9
+ export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest, packageManagerName() { return 'go'; } };
10
10
  /** @typedef {import('../provider').Provider} */
11
11
  /** @typedef {import('../provider').Provided} Provided */
12
12
  /** @typedef {{name: string, version: string}} Package */
@@ -3,5 +3,5 @@ export default class Java_gradle_groovy extends Java_gradle {
3
3
  _parseAliasForLibsNotation(alias: any): any;
4
4
  _extractDepToBeIgnored(dep: any): any;
5
5
  }
6
- export type Provided = import("../provider").Provided;
6
+ export type Provided = any;
7
7
  import Java_gradle from './java_gradle.js';
@@ -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/**',
@@ -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';
@@ -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;
@@ -225,6 +272,8 @@ export default class Python_controller {
225
272
  CachedEnvironmentDeps[packageName.replace("_", "-")] = pipDepTreeEntryForCache;
226
273
  });
227
274
  }
275
+ const inspectOutput = getPipInspectOutput.call(this);
276
+ const hashMap = parsePipInspectHashes(inspectOutput);
228
277
  parsedRequirements.forEach(({ name: depName, version: manifestVersion, hasMarker }) => {
229
278
  if (hasMarker && CachedEnvironmentDeps[depName.toLowerCase()] === undefined) {
230
279
  return;
@@ -247,7 +296,7 @@ export default class Python_controller {
247
296
  }
248
297
  let path = [];
249
298
  path.push(depName.toLowerCase());
250
- bringAllDependencies(dependencies, depName, CachedEnvironmentDeps, includeTransitive, path, usePipDepTree);
299
+ bringAllDependencies(dependencies, depName, CachedEnvironmentDeps, includeTransitive, path, usePipDepTree, hashMap);
251
300
  });
252
301
  dependencies.sort((dep1, dep2) => {
253
302
  const DEP1 = dep1.name.toLowerCase();
@@ -327,8 +376,9 @@ function getDepsList(record) {
327
376
  * @param includeTransitive
328
377
  * @param usePipDepTree
329
378
  * @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
379
+ * @param {Map<string, Array<{alg: string, content: string}>>} hashMap - map of lowercase package name to hashes
330
380
  */
331
- function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDeps, includeTransitive, path, usePipDepTree) {
381
+ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDeps, includeTransitive, path, usePipDepTree, hashMap) {
332
382
  if (dependencyName?.trim() === "") {
333
383
  return;
334
384
  }
@@ -350,7 +400,11 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
350
400
  directDeps = record.dependencies;
351
401
  }
352
402
  let targetDeps = [];
403
+ let hashes = hashMap?.get(depName.toLowerCase());
353
404
  let entry = { "name": depName, "version": version, "dependencies": [] };
405
+ if (hashes) {
406
+ entry.hashes = hashes;
407
+ }
354
408
  dependencies.push(entry);
355
409
  directDeps.forEach((dep) => {
356
410
  let depArray = [];
@@ -360,7 +414,7 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
360
414
  depArray.push(dep.toLowerCase());
361
415
  if (includeTransitive) {
362
416
  // 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);
417
+ bringAllDependencies(targetDeps, dep, cachedEnvironmentDeps, includeTransitive, path.concat(depArray), usePipDepTree, hashMap);
364
418
  }
365
419
  }
366
420
  // 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);
@@ -13,27 +13,18 @@ export function discoverUvWorkspaceMembers(workspaceRoot: string, opts?: {
13
13
  }): Promise<string[]>;
14
14
  export default class Python_uv extends Base_pyproject {
15
15
  /**
16
- * @param {string} manifestDir - directory containing the target pyproject.toml
17
- * @param {string} workspaceDir - workspace root (for resolving editable install paths)
18
- * @param {object} parsed - parsed pyproject.toml
19
- * @param {Object} opts
20
- * @returns {Promise<{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}>}
16
+ * Parse uv.lock and attach SHA-256 hashes to graph entries.
17
+ * @param {string} lockFilePath - path to uv.lock
18
+ * @param {Map<string, import('./base_pyproject.js').GraphEntry>} graph
21
19
  */
22
- _getDependencyData(manifestDir: string, workspaceDir: string, parsed: object, opts: any): Promise<{
23
- directDeps: string[];
24
- graph: Map<string, {
25
- name: string;
26
- version: string;
27
- children: string[];
28
- }>;
29
- }>;
20
+ _attachHashesFromLockFile(lockFilePath: string, graph: Map<string, import("./base_pyproject.js").GraphEntry>): Promise<void>;
30
21
  /**
31
22
  * Get the uv export output, either from env var or by running the command.
32
23
  * @param {string} manifestDir
33
24
  * @param {Object} opts
34
25
  * @returns {string}
35
26
  */
36
- _getUvExportOutput(manifestDir: string, opts: any): string;
27
+ _getUvExportOutput(manifestDir: string, opts: Object): string;
37
28
  /**
38
29
  * Parse uv export output into a dependency graph using tree-sitter-requirements
39
30
  * for package/version extraction and string parsing for "# via" comments.
@@ -21,12 +21,54 @@ export default class Python_uv extends Base_pyproject {
21
21
  * @param {string} workspaceDir - workspace root (for resolving editable install paths)
22
22
  * @param {object} parsed - parsed pyproject.toml
23
23
  * @param {Object} opts
24
- * @returns {Promise<{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}>}
24
+ * @returns {Promise<{directDeps: string[], graph: Map<string, import('./base_pyproject.js').GraphEntry>}>}
25
25
  */
26
26
  async _getDependencyData(manifestDir, workspaceDir, parsed, opts) {
27
27
  let projectName = this._getProjectName(parsed);
28
28
  let uvOutput = this._getUvExportOutput(manifestDir, opts);
29
- return this._parseUvExport(uvOutput, projectName, workspaceDir);
29
+ let { directDeps, graph } = await this._parseUvExport(uvOutput, projectName, workspaceDir);
30
+ await this._attachHashesFromLockFile(path.join(workspaceDir, 'uv.lock'), graph);
31
+ return { directDeps, graph };
32
+ }
33
+ /**
34
+ * Parse uv.lock and attach SHA-256 hashes to graph entries.
35
+ * @param {string} lockFilePath - path to uv.lock
36
+ * @param {Map<string, import('./base_pyproject.js').GraphEntry>} graph
37
+ */
38
+ async _attachHashesFromLockFile(lockFilePath, graph) {
39
+ let lockContent;
40
+ try {
41
+ lockContent = await fs.promises.readFile(lockFilePath, 'utf-8');
42
+ }
43
+ catch {
44
+ return;
45
+ }
46
+ let parsed;
47
+ try {
48
+ parsed = parseToml(lockContent);
49
+ }
50
+ catch {
51
+ return;
52
+ }
53
+ let packages = parsed.package;
54
+ if (!Array.isArray(packages)) {
55
+ return;
56
+ }
57
+ for (let pkg of packages) {
58
+ if (!pkg.name) {
59
+ continue;
60
+ }
61
+ let hashStr = pkg.sdist?.hash || pkg.wheels?.[0]?.hash;
62
+ if (!hashStr || !hashStr.startsWith('sha256:')) {
63
+ continue;
64
+ }
65
+ let key = this._canonicalize(pkg.name);
66
+ let entry = graph.get(key);
67
+ if (!entry || entry.hashes) {
68
+ continue;
69
+ }
70
+ entry.hashes = [{ alg: "SHA-256", content: hashStr.slice(7) }];
71
+ }
30
72
  }
31
73
  /**
32
74
  * Get the uv export output, either from env var or by running the command.