@trustify-da/trustify-da-javascript-client 0.3.0-ea.320ec49 → 0.3.0-ea.329b3cf

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 (73) hide show
  1. package/README.md +2 -2
  2. package/dist/package.json +25 -22
  3. package/dist/src/analysis.d.ts +14 -7
  4. package/dist/src/analysis.js +29 -13
  5. package/dist/src/cli.js +58 -1
  6. package/dist/src/cyclone_dx_sbom.d.ts +7 -1
  7. package/dist/src/cyclone_dx_sbom.js +29 -8
  8. package/dist/src/index.d.ts +74 -11
  9. package/dist/src/index.js +78 -7
  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/provider.d.ts +4 -3
  18. package/dist/src/provider.js +6 -2
  19. package/dist/src/providers/base_java.d.ts +14 -12
  20. package/dist/src/providers/base_java.js +18 -44
  21. package/dist/src/providers/base_javascript.d.ts +20 -9
  22. package/dist/src/providers/base_javascript.js +10 -3
  23. package/dist/src/providers/base_pyproject.d.ts +15 -6
  24. package/dist/src/providers/base_pyproject.js +12 -4
  25. package/dist/src/providers/containerfile_parser.d.ts +5 -0
  26. package/dist/src/providers/containerfile_parser.js +20 -0
  27. package/dist/src/providers/golang_gomodules.d.ts +11 -1
  28. package/dist/src/providers/golang_gomodules.js +65 -8
  29. package/dist/src/providers/java_gradle.d.ts +19 -0
  30. package/dist/src/providers/java_gradle.js +116 -2
  31. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  32. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  33. package/dist/src/providers/java_maven.d.ts +10 -2
  34. package/dist/src/providers/java_maven.js +165 -1
  35. package/dist/src/providers/javascript_bun.d.ts +10 -0
  36. package/dist/src/providers/javascript_bun.js +100 -0
  37. package/dist/src/providers/javascript_npm.d.ts +6 -0
  38. package/dist/src/providers/javascript_npm.js +21 -0
  39. package/dist/src/providers/javascript_pnpm.js +6 -2
  40. package/dist/src/providers/marker_evaluator.d.ts +14 -0
  41. package/dist/src/providers/marker_evaluator.js +191 -0
  42. package/dist/src/providers/oci_dockerfile.d.ts +51 -0
  43. package/dist/src/providers/oci_dockerfile.js +177 -0
  44. package/dist/src/providers/processors/yarn_berry_processor.d.ts +2 -2
  45. package/dist/src/providers/processors/yarn_berry_processor.js +6 -2
  46. package/dist/src/providers/processors/yarn_classic_processor.d.ts +2 -2
  47. package/dist/src/providers/python_controller.d.ts +5 -1
  48. package/dist/src/providers/python_controller.js +69 -8
  49. package/dist/src/providers/python_pip.d.ts +5 -0
  50. package/dist/src/providers/python_pip.js +5 -5
  51. package/dist/src/providers/python_pip_pyproject.js +3 -1
  52. package/dist/src/providers/python_poetry.d.ts +64 -8
  53. package/dist/src/providers/python_poetry.js +139 -22
  54. package/dist/src/providers/python_uv.d.ts +20 -1
  55. package/dist/src/providers/python_uv.js +126 -3
  56. package/dist/src/providers/rust_cargo.d.ts +6 -2
  57. package/dist/src/providers/rust_cargo.js +31 -4
  58. package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
  59. package/dist/src/remediation.d.ts +34 -0
  60. package/dist/src/remediation.js +321 -0
  61. package/dist/src/remediation_report.d.ts +40 -0
  62. package/dist/src/remediation_report.js +159 -0
  63. package/dist/src/sbom.d.ts +7 -1
  64. package/dist/src/sbom.js +4 -2
  65. package/dist/src/tools.d.ts +28 -2
  66. package/dist/src/tools.js +58 -0
  67. package/dist/src/updaters/maven_updater.d.ts +26 -0
  68. package/dist/src/updaters/maven_updater.js +322 -0
  69. package/dist/src/updaters/toml_updater.d.ts +33 -0
  70. package/dist/src/updaters/toml_updater.js +244 -0
  71. package/dist/src/workspace.d.ts +9 -0
  72. package/dist/src/workspace.js +1 -1
  73. package/package.json +26 -23
@@ -19,7 +19,54 @@ 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
- /** @typedef {{name: string, version: string, dependencies: DependencyEntry[]}} DependencyEntry */
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
+ }
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;
25
72
  pathToPipBin;
@@ -95,7 +142,7 @@ export default class Python_controller {
95
142
  }
96
143
  /**
97
144
  * Parse the requirements.txt file using tree-sitter and return structured requirement data.
98
- * @return {Promise<{name: string, version: string|null}[]>}
145
+ * @return {Promise<{name: string, version: string|null, hasMarker: boolean}[]>}
99
146
  */
100
147
  async #parseRequirements() {
101
148
  const content = fs.readFileSync(this.pathToRequirements).toString();
@@ -107,7 +154,8 @@ export default class Python_controller {
107
154
  const version = versionMatches.length > 0
108
155
  ? versionMatches[0].captures.find(c => c.name === 'version').node.text
109
156
  : null;
110
- return { name, version };
157
+ const hasMarker = reqNode.children.some(c => c.type === 'marker_spec');
158
+ return { name, version, hasMarker };
111
159
  }));
112
160
  }
113
161
  #decideIfWindowsOrLinuxPath(fileName) {
@@ -130,8 +178,11 @@ export default class Python_controller {
130
178
  startingTime = new Date();
131
179
  console.log("Starting time to get requirements.txt dependency tree = " + startingTime);
132
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
+ }
133
185
  if (!this.realEnvironment) {
134
- let installBestEfforts = getCustom("TRUSTIFY_DA_PYTHON_INSTALL_BEST_EFFORTS", "false", this.options);
135
186
  if (installBestEfforts === "false") {
136
187
  try {
137
188
  invokeCommand(this.pathToPipBin, ['install', '-r', this.pathToRequirements]);
@@ -224,7 +275,12 @@ export default class Python_controller {
224
275
  CachedEnvironmentDeps[packageName.replace("_", "-")] = pipDepTreeEntryForCache;
225
276
  });
226
277
  }
227
- parsedRequirements.forEach(({ name: depName, version: manifestVersion }) => {
278
+ const inspectOutput = getPipInspectOutput.call(this);
279
+ const hashMap = parsePipInspectHashes(inspectOutput);
280
+ parsedRequirements.forEach(({ name: depName, version: manifestVersion, hasMarker }) => {
281
+ if (hasMarker && CachedEnvironmentDeps[depName.toLowerCase()] === undefined) {
282
+ return;
283
+ }
228
284
  if (matchManifestVersions === "true" && manifestVersion != null) {
229
285
  let installedVersion;
230
286
  if (CachedEnvironmentDeps[depName.toLowerCase()] !== undefined) {
@@ -243,7 +299,7 @@ export default class Python_controller {
243
299
  }
244
300
  let path = [];
245
301
  path.push(depName.toLowerCase());
246
- bringAllDependencies(dependencies, depName, CachedEnvironmentDeps, includeTransitive, path, usePipDepTree);
302
+ bringAllDependencies(dependencies, depName, CachedEnvironmentDeps, includeTransitive, path, usePipDepTree, hashMap);
247
303
  });
248
304
  dependencies.sort((dep1, dep2) => {
249
305
  const DEP1 = dep1.name.toLowerCase();
@@ -323,8 +379,9 @@ function getDepsList(record) {
323
379
  * @param includeTransitive
324
380
  * @param usePipDepTree
325
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
326
383
  */
327
- function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDeps, includeTransitive, path, usePipDepTree) {
384
+ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDeps, includeTransitive, path, usePipDepTree, hashMap) {
328
385
  if (dependencyName?.trim() === "") {
329
386
  return;
330
387
  }
@@ -346,7 +403,11 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
346
403
  directDeps = record.dependencies;
347
404
  }
348
405
  let targetDeps = [];
406
+ let hashes = hashMap?.get(depName.toLowerCase());
349
407
  let entry = { "name": depName, "version": version, "dependencies": [] };
408
+ if (hashes) {
409
+ entry.hashes = hashes;
410
+ }
350
411
  dependencies.push(entry);
351
412
  directDeps.forEach((dep) => {
352
413
  let depArray = [];
@@ -356,7 +417,7 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
356
417
  depArray.push(dep.toLowerCase());
357
418
  if (includeTransitive) {
358
419
  // send to recurrsion the array of all deps in path + the current dependency name which is not on the path.
359
- bringAllDependencies(targetDeps, dep, cachedEnvironmentDeps, includeTransitive, path.concat(depArray), usePipDepTree);
420
+ bringAllDependencies(targetDeps, dep, cachedEnvironmentDeps, includeTransitive, path.concat(depArray), usePipDepTree, hashMap);
360
421
  }
361
422
  }
362
423
  // sort ra
@@ -4,12 +4,17 @@ 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 = {
10
11
  name: string;
11
12
  version: string;
12
13
  dependencies: DependencyEntry[];
14
+ hashes?: Array<{
15
+ alg: string;
16
+ content: string;
17
+ }>;
13
18
  };
14
19
  /**
15
20
  * @param {string} manifestName - the subject manifest name-type
@@ -5,13 +5,14 @@ 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 };
9
- /** @typedef {{name: string, version: string, dependencies: DependencyEntry[]}} DependencyEntry */
8
+ export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest, packageManagerName() { return 'pip'; } };
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'
12
12
  * @private
13
13
  */
14
14
  const ecosystem = 'pip';
15
+ const NO_SCOPE = undefined;
15
16
  /**
16
17
  * @param {string} manifestName - the subject manifest name-type
17
18
  * @returns {boolean} - return true if `requirements.txt` is the manifest name-type
@@ -56,7 +57,6 @@ async function provideComponent(manifest, opts = {}) {
56
57
  contentType: 'application/vnd.cyclonedx+json'
57
58
  };
58
59
  }
59
- /** @typedef {{name: string, , version: string, dependencies: DependencyEntry[]}} DependencyEntry */
60
60
  /**
61
61
  *
62
62
  * @param {PackageURL}source
@@ -66,7 +66,7 @@ async function provideComponent(manifest, opts = {}) {
66
66
  */
67
67
  function addAllDependencies(source, dep, sbom) {
68
68
  let targetPurl = toPurl(dep["name"], dep["version"]);
69
- sbom.addDependency(source, targetPurl);
69
+ sbom.addDependency(source, targetPurl, NO_SCOPE, dep["hashes"]);
70
70
  let directDeps = dep["dependencies"];
71
71
  if (directDeps !== undefined && directDeps.length > 0) {
72
72
  directDeps.forEach((dependency) => { addAllDependencies(toPurl(dep["name"], dep["version"]), dependency, sbom); });
@@ -202,7 +202,7 @@ async function getSbomForComponentAnalysis(manifest, opts = {}) {
202
202
  const license = readLicenseFromManifest(manifest);
203
203
  sbom.addRoot(rootPurl, license);
204
204
  dependencies.forEach(dep => {
205
- sbom.addDependency(rootPurl, toPurl(dep.name, dep.version));
205
+ sbom.addDependency(rootPurl, toPurl(dep.name, dep.version), NO_SCOPE, dep.hashes);
206
206
  });
207
207
  await handleIgnoredDependencies(manifest, sbom, opts);
208
208
  // In python there is no root component, then we must remove the dummy root we added, so the sbom json will be accepted by the DA backend
@@ -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,18 +1,35 @@
1
1
  export default class Python_poetry extends Base_pyproject {
2
+ _verifyPoetryAccessible(poetryBin: any): void;
2
3
  /**
3
- * Get poetry show --tree output.
4
4
  * @param {string} manifestDir
5
+ * @param {string} _workspaceDir - unused (poetry has no workspace support)
6
+ * @param {object} parsed - parsed pyproject.toml
5
7
  * @param {Object} opts
8
+ * @returns {Promise<{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}>}
9
+ */
10
+ _getDependencyData(manifestDir: string, _workspaceDir: string, parsed: object, opts: Object): Promise<{
11
+ directDeps: string[];
12
+ graph: Map<string, {
13
+ name: string;
14
+ version: string;
15
+ children: string[];
16
+ }>;
17
+ }>;
18
+ /**
19
+ * Get poetry show --tree output.
20
+ * @param {string} manifestDir
21
+ * @param {boolean} hasDevGroup
22
+ * @param {string} poetryBin
6
23
  * @returns {string}
7
24
  */
8
- _getPoetryShowTreeOutput(manifestDir: string, opts: any): string;
25
+ _getPoetryShowTreeOutput(manifestDir: string, hasDevGroup: boolean, poetryBin: string): string;
9
26
  /**
10
27
  * Get poetry show --all output (flat list with resolved versions).
11
28
  * @param {string} manifestDir
12
- * @param {Object} opts
29
+ * @param {string} poetryBin
13
30
  * @returns {string}
14
31
  */
15
- _getPoetryShowAllOutput(manifestDir: string, opts: any): string;
32
+ _getPoetryShowAllOutput(manifestDir: string, poetryBin: string): string;
16
33
  /**
17
34
  * Parse poetry show --all output into a version map.
18
35
  * Lines look like: "name (!) 1.2.3 Description text..."
@@ -21,21 +38,60 @@ export default class Python_poetry extends Base_pyproject {
21
38
  * @returns {Map<string, string>} canonical name -> version
22
39
  */
23
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;
24
70
  /**
25
71
  * Parse poetry show --tree output into a dependency graph structure.
26
- * Top-level lines (no indentation/tree chars) are direct deps: "name version description"
27
- * Indented lines are transitive deps with tree chars: "├── name >=constraint"
28
72
  *
29
73
  * @param {string} treeOutput
30
74
  * @param {Map<string, string>} versionMap - canonical name -> resolved version
31
- * @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}>}>}}
32
77
  */
33
- _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
+ }): {
34
86
  directDeps: string[];
35
87
  graph: Map<string, {
36
88
  name: string;
37
89
  version: string;
38
90
  children: string[];
91
+ hashes?: Array<{
92
+ alg: string;
93
+ content: string;
94
+ }>;
39
95
  }>;
40
96
  };
41
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,35 +52,47 @@ 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) {
42
- let treeOutput = this._getPoetryShowTreeOutput(manifestDir, opts);
43
- let showAllOutput = this._getPoetryShowAllOutput(manifestDir, 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
+ }
61
+ let hasDevGroup = !!(parsed.tool?.poetry?.group?.dev || parsed.tool?.poetry?.['dev-dependencies']);
62
+ let treeOutput = this._getPoetryShowTreeOutput(manifestDir, hasDevGroup, poetryBin);
63
+ let showAllOutput = this._getPoetryShowAllOutput(manifestDir, poetryBin);
44
64
  let versionMap = this._parsePoetryShowAll(showAllOutput);
45
- 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);
46
68
  }
47
69
  /**
48
70
  * Get poetry show --tree output.
49
71
  * @param {string} manifestDir
50
- * @param {Object} opts
72
+ * @param {boolean} hasDevGroup
73
+ * @param {string} poetryBin
51
74
  * @returns {string}
52
75
  */
53
- _getPoetryShowTreeOutput(manifestDir, opts) {
76
+ _getPoetryShowTreeOutput(manifestDir, hasDevGroup, poetryBin) {
54
77
  if (environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_TREE')) {
55
78
  return Buffer.from(process.env['TRUSTIFY_DA_POETRY_SHOW_TREE'], 'base64').toString('utf-8');
56
79
  }
57
- let poetryBin = getCustomPath('poetry', opts);
58
- return invokeCommand(poetryBin, ['show', '--tree', '--no-ansi'], { cwd: manifestDir }).toString();
80
+ let args = ['show', '--tree', '--no-ansi'];
81
+ if (hasDevGroup) {
82
+ args.push('--without', 'dev');
83
+ }
84
+ return invokeCommand(poetryBin, args, { cwd: manifestDir }).toString();
59
85
  }
60
86
  /**
61
87
  * Get poetry show --all output (flat list with resolved versions).
62
88
  * @param {string} manifestDir
63
- * @param {Object} opts
89
+ * @param {string} poetryBin
64
90
  * @returns {string}
65
91
  */
66
- _getPoetryShowAllOutput(manifestDir, opts) {
92
+ _getPoetryShowAllOutput(manifestDir, poetryBin) {
67
93
  if (environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_ALL')) {
68
94
  return Buffer.from(process.env['TRUSTIFY_DA_POETRY_SHOW_ALL'], 'base64').toString('utf-8');
69
95
  }
70
- let poetryBin = getCustomPath('poetry', opts);
71
96
  return invokeCommand(poetryBin, ['show', '--no-ansi', '--all'], { cwd: manifestDir }).toString();
72
97
  }
73
98
  /**
@@ -92,16 +117,83 @@ export default class Python_poetry extends Base_pyproject {
92
117
  }
93
118
  return versions;
94
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
+ }
95
188
  /**
96
189
  * Parse poetry show --tree output into a dependency graph structure.
97
- * Top-level lines (no indentation/tree chars) are direct deps: "name version description"
98
- * Indented lines are transitive deps with tree chars: "├── name >=constraint"
99
190
  *
100
191
  * @param {string} treeOutput
101
192
  * @param {Map<string, string>} versionMap - canonical name -> resolved version
102
- * @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}>}>}}
103
195
  */
104
- _parsePoetryTree(treeOutput, versionMap) {
196
+ _parsePoetryTree(treeOutput, versionMap, markerData) {
105
197
  let lines = treeOutput.split(/\r?\n/);
106
198
  let graph = new Map();
107
199
  let directDeps = [];
@@ -117,9 +209,20 @@ export default class Python_poetry extends Base_pyproject {
117
209
  let name = topMatch[1];
118
210
  let version = topMatch[2];
119
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
+ }
120
218
  directDeps.push(key);
121
219
  if (!graph.has(key)) {
122
- 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);
123
226
  }
124
227
  currentDirectDep = key;
125
228
  stack = [{ key, depth: -1 }];
@@ -143,20 +246,34 @@ export default class Python_poetry extends Base_pyproject {
143
246
  // determine depth by counting tree-drawing groups in the prefix
144
247
  let prefix = line.substring(0, nameStart);
145
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
+ }
146
263
  // resolve version from the version map
147
264
  let version = versionMap.get(depKey) || null;
148
265
  if (!version) {
149
266
  throw new Error(`poetry: package '${depName}' has no resolved version`);
150
267
  }
151
268
  if (!graph.has(depKey)) {
152
- graph.set(depKey, { name: depName, version, children: [] });
153
- }
154
- // pop stack back to find the parent at depth-1
155
- while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
156
- 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);
157
275
  }
158
- if (stack.length > 0) {
159
- let parentKey = stack[stack.length - 1].key;
276
+ if (parentKey) {
160
277
  let parentEntry = graph.get(parentKey);
161
278
  if (parentEntry && !parentEntry.children.includes(depKey)) {
162
279
  parentEntry.children.push(depKey);
@@ -1,11 +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 {
15
+ /**
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
19
+ */
20
+ _attachHashesFromLockFile(lockFilePath: string, graph: Map<string, import("./base_pyproject.js").GraphEntry>): Promise<void>;
2
21
  /**
3
22
  * Get the uv export output, either from env var or by running the command.
4
23
  * @param {string} manifestDir
5
24
  * @param {Object} opts
6
25
  * @returns {string}
7
26
  */
8
- _getUvExportOutput(manifestDir: string, opts: any): string;
27
+ _getUvExportOutput(manifestDir: string, opts: Object): string;
9
28
  /**
10
29
  * Parse uv export output into a dependency graph using tree-sitter-requirements
11
30
  * for package/version extraction and string parsing for "# via" comments.