@trustify-da/trustify-da-javascript-client 0.3.0-ea.1d590b2 → 0.3.0-ea.243eaef

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 (82) hide show
  1. package/README.md +177 -3
  2. package/dist/package.json +30 -23
  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 +18 -12
  9. package/dist/src/index.js +34 -30
  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 +11 -1
  30. package/dist/src/providers/golang_gomodules.js +114 -15
  31. package/dist/src/providers/java_gradle.d.ts +48 -0
  32. package/dist/src/providers/java_gradle.js +198 -20
  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/marker_evaluator.d.ts +14 -0
  47. package/dist/src/providers/marker_evaluator.js +191 -0
  48. package/dist/src/providers/oci_dockerfile.d.ts +51 -0
  49. package/dist/src/providers/oci_dockerfile.js +177 -0
  50. package/dist/src/providers/processors/yarn_berry_processor.d.ts +6 -2
  51. package/dist/src/providers/processors/yarn_berry_processor.js +3 -2
  52. package/dist/src/providers/processors/yarn_classic_processor.d.ts +6 -2
  53. package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
  54. package/dist/src/providers/python_controller.js +61 -4
  55. package/dist/src/providers/python_pip.d.ts +1 -0
  56. package/dist/src/providers/python_pip.js +1 -1
  57. package/dist/src/providers/python_pip_pyproject.js +3 -1
  58. package/dist/src/providers/python_poetry.d.ts +49 -9
  59. package/dist/src/providers/python_poetry.js +132 -21
  60. package/dist/src/providers/python_uv.d.ts +18 -14
  61. package/dist/src/providers/python_uv.js +122 -2
  62. package/dist/src/providers/requirements_parser.js +1 -1
  63. package/dist/src/providers/rust_cargo.d.ts +6 -2
  64. package/dist/src/providers/rust_cargo.js +101 -33
  65. package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
  66. package/dist/src/remediate.d.ts +21 -0
  67. package/dist/src/remediate.js +149 -0
  68. package/dist/src/remediation.d.ts +57 -0
  69. package/dist/src/remediation.js +436 -0
  70. package/dist/src/remediation_report.d.ts +40 -0
  71. package/dist/src/remediation_report.js +159 -0
  72. package/dist/src/sbom.d.ts +11 -0
  73. package/dist/src/sbom.js +10 -0
  74. package/dist/src/tools.d.ts +2 -2
  75. package/dist/src/tools.js +24 -1
  76. package/dist/src/updaters/maven_updater.d.ts +26 -0
  77. package/dist/src/updaters/maven_updater.js +322 -0
  78. package/dist/src/updaters/toml_updater.d.ts +33 -0
  79. package/dist/src/updaters/toml_updater.js +244 -0
  80. package/dist/src/workspace.d.ts +9 -0
  81. package/dist/src/workspace.js +1 -1
  82. package/package.json +31 -24
@@ -19,6 +19,53 @@ function getPipShowOutput(depNames) {
19
19
  throw new Error('fail invoking \'pip show\' to fetch metadata for all installed packages in environment', { cause: error });
20
20
  }
21
21
  }
22
+ /**
23
+ * Get pip inspect output from env var override or by invoking pip inspect.
24
+ * Returns null if pip inspect is unavailable (pip < 22.2) or if freeze/show
25
+ * env vars are set without an inspect override (to avoid inconsistent data).
26
+ * @return {string|null} pip inspect JSON output, or null on failure
27
+ */
28
+ function getPipInspectOutput() {
29
+ if (environmentVariableIsPopulated("TRUSTIFY_DA_PIP_INSPECT")) {
30
+ return new Buffer.from(process.env["TRUSTIFY_DA_PIP_INSPECT"], 'base64').toString('ascii');
31
+ }
32
+ if (environmentVariableIsPopulated("TRUSTIFY_DA_PIP_FREEZE") || environmentVariableIsPopulated("TRUSTIFY_DA_PIP_SHOW")) {
33
+ return null;
34
+ }
35
+ try {
36
+ return invokeCommand(this.pathToPipBin, ['inspect']).toString();
37
+ }
38
+ catch (error) {
39
+ console.warn('pip inspect is not available (requires pip 22.2+), SBOM will be generated without hashes');
40
+ return null;
41
+ }
42
+ }
43
+ /**
44
+ * Parse pip inspect JSON output and build a hash lookup map.
45
+ * @param {string|null} inspectOutput - raw pip inspect JSON string
46
+ * @return {Map<string, Array<{alg: string, content: string}>>} map of lowercase package name to hashes
47
+ */
48
+ function parsePipInspectHashes(inspectOutput) {
49
+ if (!inspectOutput) {
50
+ return new Map();
51
+ }
52
+ try {
53
+ let inspectData = JSON.parse(inspectOutput);
54
+ let hashMap = new Map();
55
+ for (let pkg of (inspectData.installed || [])) {
56
+ let name = pkg.metadata?.name;
57
+ let sha256 = pkg.download_info?.archive_info?.hashes?.sha256;
58
+ if (name && sha256) {
59
+ hashMap.set(name.toLowerCase(), [{ alg: "SHA-256", content: sha256 }]);
60
+ }
61
+ }
62
+ return hashMap;
63
+ }
64
+ catch (error) {
65
+ console.warn('Failed to parse pip inspect output, SBOM will be generated without hashes');
66
+ return new Map();
67
+ }
68
+ }
22
69
  /** @typedef {{name: string, version: string, dependencies: DependencyEntry[], hashes?: Array<{alg: string, content: string}>}} DependencyEntry */
23
70
  export default class Python_controller {
24
71
  pythonEnvDir;
@@ -131,8 +178,11 @@ export default class Python_controller {
131
178
  startingTime = new Date();
132
179
  console.log("Starting time to get requirements.txt dependency tree = " + startingTime);
133
180
  }
181
+ let installBestEfforts = getCustom("TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS", "false", this.options);
182
+ if (installBestEfforts === "true" && this.realEnvironment) {
183
+ throw new Error("Conflicting settings, TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS=true requires TRUSTIFY_DA_PYTHON_VIRTUAL_ENV=true");
184
+ }
134
185
  if (!this.realEnvironment) {
135
- let installBestEfforts = getCustom("TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS", "false", this.options);
136
186
  if (installBestEfforts === "false") {
137
187
  try {
138
188
  invokeCommand(this.pathToPipBin, ['install', '-r', this.pathToRequirements]);
@@ -225,6 +275,8 @@ export default class Python_controller {
225
275
  CachedEnvironmentDeps[packageName.replace("_", "-")] = pipDepTreeEntryForCache;
226
276
  });
227
277
  }
278
+ const inspectOutput = getPipInspectOutput.call(this);
279
+ const hashMap = parsePipInspectHashes(inspectOutput);
228
280
  parsedRequirements.forEach(({ name: depName, version: manifestVersion, hasMarker }) => {
229
281
  if (hasMarker && CachedEnvironmentDeps[depName.toLowerCase()] === undefined) {
230
282
  return;
@@ -247,7 +299,7 @@ export default class Python_controller {
247
299
  }
248
300
  let path = [];
249
301
  path.push(depName.toLowerCase());
250
- bringAllDependencies(dependencies, depName, CachedEnvironmentDeps, includeTransitive, path, usePipDepTree);
302
+ bringAllDependencies(dependencies, depName, CachedEnvironmentDeps, includeTransitive, path, usePipDepTree, hashMap);
251
303
  });
252
304
  dependencies.sort((dep1, dep2) => {
253
305
  const DEP1 = dep1.name.toLowerCase();
@@ -327,8 +379,9 @@ function getDepsList(record) {
327
379
  * @param includeTransitive
328
380
  * @param usePipDepTree
329
381
  * @param {[string]}path array representing the path of the current branch in dependency tree, starting with a root dependency - that is - a given dependency in requirements.txt
382
+ * @param {Map<string, Array<{alg: string, content: string}>>} hashMap - map of lowercase package name to hashes
330
383
  */
331
- function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDeps, includeTransitive, path, usePipDepTree) {
384
+ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDeps, includeTransitive, path, usePipDepTree, hashMap) {
332
385
  if (dependencyName?.trim() === "") {
333
386
  return;
334
387
  }
@@ -350,7 +403,11 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
350
403
  directDeps = record.dependencies;
351
404
  }
352
405
  let targetDeps = [];
406
+ let hashes = hashMap?.get(depName.toLowerCase());
353
407
  let entry = { "name": depName, "version": version, "dependencies": [] };
408
+ if (hashes) {
409
+ entry.hashes = hashes;
410
+ }
354
411
  dependencies.push(entry);
355
412
  directDeps.forEach((dep) => {
356
413
  let depArray = [];
@@ -360,7 +417,7 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
360
417
  depArray.push(dep.toLowerCase());
361
418
  if (includeTransitive) {
362
419
  // send to recurrsion the array of all deps in path + the current dependency name which is not on the path.
363
- bringAllDependencies(targetDeps, dep, cachedEnvironmentDeps, includeTransitive, path.concat(depArray), usePipDepTree);
420
+ bringAllDependencies(targetDeps, dep, cachedEnvironmentDeps, includeTransitive, path.concat(depArray), usePipDepTree, hashMap);
364
421
  }
365
422
  }
366
423
  // sort ra
@@ -4,6 +4,7 @@ declare namespace _default {
4
4
  export { provideComponent };
5
5
  export { provideStack };
6
6
  export { readLicenseFromManifest };
7
+ export function packageManagerName(): string;
7
8
  }
8
9
  export default _default;
9
10
  export type DependencyEntry = {
@@ -5,7 +5,7 @@ import Sbom from '../sbom.js';
5
5
  import { environmentVariableIsPopulated, getCustom, getCustomPath, invokeCommand } from "../tools.js";
6
6
  import Python_controller from './python_controller.js';
7
7
  import { getParser, getIgnoreQuery, getPinnedVersionQuery } from './requirements_parser.js';
8
- export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest };
8
+ export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest, packageManagerName() { return 'pip'; } };
9
9
  /** @typedef {{name: string, version: string, dependencies: DependencyEntry[], hashes?: Array<{alg: string, content: string}>}} DependencyEntry */
10
10
  /**
11
11
  * @type {string} ecosystem for python-pip is 'pip'
@@ -76,7 +76,9 @@ export default class Python_pip_pyproject extends Base_pyproject {
76
76
  let name = pkg.metadata.name;
77
77
  let version = pkg.metadata.version;
78
78
  let key = this._canonicalize(name);
79
- graph.set(key, { name, version, children: [] });
79
+ let sha256 = pkg.download_info?.archive_info?.hashes?.sha256;
80
+ let hashes = sha256 ? [{ alg: "SHA-256", content: sha256 }] : undefined;
81
+ graph.set(key, { name, version, children: [], hashes });
80
82
  }
81
83
  for (let pkg of nonRootPackages) {
82
84
  let key = this._canonicalize(pkg.metadata.name);
@@ -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..."
@@ -37,21 +38,60 @@ export default class Python_poetry extends Base_pyproject {
37
38
  * @returns {Map<string, string>} canonical name -> version
38
39
  */
39
40
  _parsePoetryShowAll(output: string): Map<string, string>;
41
+ /**
42
+ * Collects PEP 508 marker expressions for direct and transitive deps.
43
+ * Direct markers come from pyproject.toml dependency strings, e.g.:
44
+ * "pywin32>=311 ; sys_platform == 'win32'" → directMarkers['pywin32'] = "sys_platform == 'win32'"
45
+ * Transitive markers come from poetry.lock [package.dependencies] entries, e.g.:
46
+ * colorama = {version = "*", markers = "sys_platform == 'win32'"}
47
+ * → transitiveMarkers['click']['colorama'] = "sys_platform == 'win32'"
48
+ * @param {string|null} lockDir
49
+ * @param {object} parsed - parsed pyproject.toml
50
+ * @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>, hashMap: Map<string, Array<{alg: string, content: string}>>}}
51
+ */
52
+ _extractMarkerData(lockDir: string | null, parsed: object): {
53
+ directMarkers: Map<string, string>;
54
+ transitiveMarkers: Map<string, Map<string, string>>;
55
+ hashMap: Map<string, Array<{
56
+ alg: string;
57
+ content: string;
58
+ }>>;
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;
40
70
  /**
41
71
  * Parse poetry show --tree output into a dependency graph structure.
42
- * Top-level lines (no indentation/tree chars) are direct deps: "name version description"
43
- * Indented lines are transitive deps with tree chars: "├── name >=constraint"
44
72
  *
45
73
  * @param {string} treeOutput
46
74
  * @param {Map<string, string>} versionMap - canonical name -> resolved version
47
- * @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}>}>}}
48
77
  */
49
- _parsePoetryTree(treeOutput: string, versionMap: Map<string, string>): {
78
+ _parsePoetryTree(treeOutput: string, versionMap: Map<string, string>, markerData: {
79
+ directMarkers: Map<string, string>;
80
+ transitiveMarkers: Map<string, Map<string, string>>;
81
+ hashMap: Map<string, Array<{
82
+ alg: string;
83
+ content: string;
84
+ }>>;
85
+ }): {
50
86
  directDeps: string[];
51
87
  graph: Map<string, {
52
88
  name: string;
53
89
  version: string;
54
90
  children: string[];
91
+ hashes?: Array<{
92
+ alg: string;
93
+ content: string;
94
+ }>;
55
95
  }>;
56
96
  };
57
97
  }
@@ -1,7 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { parse as parseToml } from 'smol-toml';
3
4
  import { environmentVariableIsPopulated, getCustom, getCustomPath, invokeCommand } from '../tools.js';
4
5
  import Base_pyproject from './base_pyproject.js';
6
+ import { evaluateMarker } from './marker_evaluator.js';
5
7
  export default class Python_poetry extends Base_pyproject {
6
8
  /**
7
9
  * Poetry has no native workspace/monorepo support (python-poetry/poetry#2270).
@@ -30,6 +32,17 @@ export default class Python_poetry extends Base_pyproject {
30
32
  _cmdName() {
31
33
  return 'poetry';
32
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
+ }
33
46
  /**
34
47
  * @param {string} manifestDir
35
48
  * @param {string} _workspaceDir - unused (poetry has no workspace support)
@@ -39,24 +52,31 @@ export default class Python_poetry extends Base_pyproject {
39
52
  */
40
53
  // eslint-disable-next-line no-unused-vars
41
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
+ }
42
61
  let hasDevGroup = !!(parsed.tool?.poetry?.group?.dev || parsed.tool?.poetry?.['dev-dependencies']);
43
- let treeOutput = this._getPoetryShowTreeOutput(manifestDir, hasDevGroup, opts);
44
- let showAllOutput = this._getPoetryShowAllOutput(manifestDir, opts);
62
+ let treeOutput = this._getPoetryShowTreeOutput(manifestDir, hasDevGroup, poetryBin);
63
+ let showAllOutput = this._getPoetryShowAllOutput(manifestDir, poetryBin);
45
64
  let versionMap = this._parsePoetryShowAll(showAllOutput);
46
- return this._parsePoetryTree(treeOutput, versionMap);
65
+ let lockDir = this._findLockFileDir(manifestDir, opts);
66
+ let markerData = this._extractMarkerData(lockDir, parsed);
67
+ return this._parsePoetryTree(treeOutput, versionMap, markerData);
47
68
  }
48
69
  /**
49
70
  * Get poetry show --tree output.
50
71
  * @param {string} manifestDir
51
72
  * @param {boolean} hasDevGroup
52
- * @param {Object} opts
73
+ * @param {string} poetryBin
53
74
  * @returns {string}
54
75
  */
55
- _getPoetryShowTreeOutput(manifestDir, hasDevGroup, opts) {
76
+ _getPoetryShowTreeOutput(manifestDir, hasDevGroup, poetryBin) {
56
77
  if (environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_TREE')) {
57
78
  return Buffer.from(process.env['TRUSTIFY_DA_POETRY_SHOW_TREE'], 'base64').toString('utf-8');
58
79
  }
59
- let poetryBin = getCustomPath('poetry', opts);
60
80
  let args = ['show', '--tree', '--no-ansi'];
61
81
  if (hasDevGroup) {
62
82
  args.push('--without', 'dev');
@@ -66,14 +86,13 @@ export default class Python_poetry extends Base_pyproject {
66
86
  /**
67
87
  * Get poetry show --all output (flat list with resolved versions).
68
88
  * @param {string} manifestDir
69
- * @param {Object} opts
89
+ * @param {string} poetryBin
70
90
  * @returns {string}
71
91
  */
72
- _getPoetryShowAllOutput(manifestDir, opts) {
92
+ _getPoetryShowAllOutput(manifestDir, poetryBin) {
73
93
  if (environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_ALL')) {
74
94
  return Buffer.from(process.env['TRUSTIFY_DA_POETRY_SHOW_ALL'], 'base64').toString('utf-8');
75
95
  }
76
- let poetryBin = getCustomPath('poetry', opts);
77
96
  return invokeCommand(poetryBin, ['show', '--no-ansi', '--all'], { cwd: manifestDir }).toString();
78
97
  }
79
98
  /**
@@ -98,16 +117,83 @@ export default class Python_poetry extends Base_pyproject {
98
117
  }
99
118
  return versions;
100
119
  }
120
+ /**
121
+ * Collects PEP 508 marker expressions for direct and transitive deps.
122
+ * Direct markers come from pyproject.toml dependency strings, e.g.:
123
+ * "pywin32>=311 ; sys_platform == 'win32'" → directMarkers['pywin32'] = "sys_platform == 'win32'"
124
+ * Transitive markers come from poetry.lock [package.dependencies] entries, e.g.:
125
+ * colorama = {version = "*", markers = "sys_platform == 'win32'"}
126
+ * → transitiveMarkers['click']['colorama'] = "sys_platform == 'win32'"
127
+ * @param {string|null} lockDir
128
+ * @param {object} parsed - parsed pyproject.toml
129
+ * @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>, hashMap: Map<string, Array<{alg: string, content: string}>>}}
130
+ */
131
+ _extractMarkerData(lockDir, parsed) {
132
+ let directMarkers = new Map();
133
+ let transitiveMarkers = new Map();
134
+ let hashMap = new Map();
135
+ // Extract markers from PEP 621 dependency strings: "name[extras]>=ver ; marker"
136
+ let deps = parsed.project?.dependencies || [];
137
+ for (let dep of deps) {
138
+ let m = dep.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)\s*[^;]*;\s*(.+)$/);
139
+ if (m) {
140
+ directMarkers.set(this._canonicalize(m[1]), m[2].trim());
141
+ }
142
+ }
143
+ if (lockDir) {
144
+ let lockPath = path.join(lockDir, this._lockFileName());
145
+ if (fs.existsSync(lockPath)) {
146
+ let lockContent = fs.readFileSync(lockPath, 'utf-8');
147
+ let lock = parseToml(lockContent);
148
+ let packages = lock.package || [];
149
+ for (let pkg of packages) {
150
+ let pkgKey = this._canonicalize(pkg.name);
151
+ let pkgDeps = pkg.dependencies || {};
152
+ for (let [depName, depSpec] of Object.entries(pkgDeps)) {
153
+ let markers = typeof depSpec === 'object' && depSpec != null ? depSpec.markers : null;
154
+ if (markers) {
155
+ if (!transitiveMarkers.has(pkgKey)) {
156
+ transitiveMarkers.set(pkgKey, new Map());
157
+ }
158
+ transitiveMarkers.get(pkgKey).set(this._canonicalize(depName), markers);
159
+ }
160
+ }
161
+ let sha256Hex = this._extractSha256FromFiles(pkg.files);
162
+ if (sha256Hex) {
163
+ hashMap.set(pkgKey, [{ alg: "SHA-256", content: sha256Hex }]);
164
+ }
165
+ }
166
+ }
167
+ }
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;
187
+ }
101
188
  /**
102
189
  * Parse poetry show --tree output into a dependency graph structure.
103
- * Top-level lines (no indentation/tree chars) are direct deps: "name version description"
104
- * Indented lines are transitive deps with tree chars: "├── name >=constraint"
105
190
  *
106
191
  * @param {string} treeOutput
107
192
  * @param {Map<string, string>} versionMap - canonical name -> resolved version
108
- * @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}>}>}}
109
195
  */
110
- _parsePoetryTree(treeOutput, versionMap) {
196
+ _parsePoetryTree(treeOutput, versionMap, markerData) {
111
197
  let lines = treeOutput.split(/\r?\n/);
112
198
  let graph = new Map();
113
199
  let directDeps = [];
@@ -123,9 +209,20 @@ export default class Python_poetry extends Base_pyproject {
123
209
  let name = topMatch[1];
124
210
  let version = topMatch[2];
125
211
  let key = this._canonicalize(name);
212
+ let marker = markerData.directMarkers.get(key);
213
+ if (marker && !evaluateMarker(marker)) {
214
+ currentDirectDep = null;
215
+ stack = [];
216
+ continue;
217
+ }
126
218
  directDeps.push(key);
127
219
  if (!graph.has(key)) {
128
- 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);
129
226
  }
130
227
  currentDirectDep = key;
131
228
  stack = [{ key, depth: -1 }];
@@ -149,20 +246,34 @@ export default class Python_poetry extends Base_pyproject {
149
246
  // determine depth by counting tree-drawing groups in the prefix
150
247
  let prefix = line.substring(0, nameStart);
151
248
  let depth = (prefix.match(/(?:[├└│ ][\s─]{2} ?)/g) || []).length;
249
+ // pop stack back to find the parent at depth-1
250
+ while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
251
+ stack.pop();
252
+ }
253
+ let parentKey = stack.length > 0 ? stack[stack.length - 1].key : null;
254
+ if (parentKey) {
255
+ let parentMarkers = markerData.transitiveMarkers.get(parentKey);
256
+ if (parentMarkers) {
257
+ let marker = parentMarkers.get(depKey);
258
+ if (marker && !evaluateMarker(marker)) {
259
+ continue;
260
+ }
261
+ }
262
+ }
152
263
  // resolve version from the version map
153
264
  let version = versionMap.get(depKey) || null;
154
265
  if (!version) {
155
266
  throw new Error(`poetry: package '${depName}' has no resolved version`);
156
267
  }
157
268
  if (!graph.has(depKey)) {
158
- graph.set(depKey, { name: depName, version, children: [] });
159
- }
160
- // pop stack back to find the parent at depth-1
161
- while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
162
- stack.pop();
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);
163
275
  }
164
- if (stack.length > 0) {
165
- let parentKey = stack[stack.length - 1].key;
276
+ if (parentKey) {
166
277
  let parentEntry = graph.get(parentKey);
167
278
  if (parentEntry && !parentEntry.children.includes(depKey)) {
168
279
  parentEntry.children.push(depKey);
@@ -1,26 +1,30 @@
1
+ /**
2
+ * Discover all pyproject.toml manifest paths in a uv workspace.
3
+ * Parses `[tool.uv.workspace]` from root pyproject.toml and glob-expands member patterns.
4
+ *
5
+ * @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain pyproject.toml and uv.lock)
6
+ * @param {{ workspaceDiscoveryIgnore?: string[], TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE?: string, [key: string]: unknown }} [opts={}]
7
+ * @returns {Promise<string[]>} Paths to pyproject.toml files (absolute)
8
+ */
9
+ export function discoverUvWorkspaceMembers(workspaceRoot: string, opts?: {
10
+ workspaceDiscoveryIgnore?: string[];
11
+ TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE?: string;
12
+ [key: string]: unknown;
13
+ }): Promise<string[]>;
1
14
  export default class Python_uv extends Base_pyproject {
2
15
  /**
3
- * @param {string} manifestDir - directory containing the target pyproject.toml
4
- * @param {string} workspaceDir - workspace root (for resolving editable install paths)
5
- * @param {object} parsed - parsed pyproject.toml
6
- * @param {Object} opts
7
- * @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
8
19
  */
9
- _getDependencyData(manifestDir: string, workspaceDir: string, parsed: object, opts: any): Promise<{
10
- directDeps: string[];
11
- graph: Map<string, {
12
- name: string;
13
- version: string;
14
- children: string[];
15
- }>;
16
- }>;
20
+ _attachHashesFromLockFile(lockFilePath: string, graph: Map<string, import("./base_pyproject.js").GraphEntry>): Promise<void>;
17
21
  /**
18
22
  * Get the uv export output, either from env var or by running the command.
19
23
  * @param {string} manifestDir
20
24
  * @param {Object} opts
21
25
  * @returns {string}
22
26
  */
23
- _getUvExportOutput(manifestDir: string, opts: any): string;
27
+ _getUvExportOutput(manifestDir: string, opts: Object): string;
24
28
  /**
25
29
  * Parse uv export output into a dependency graph using tree-sitter-requirements
26
30
  * for package/version extraction and string parsing for "# via" comments.