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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +250 -11
  2. package/dist/package.json +33 -25
  3. package/dist/src/analysis.d.ts +14 -7
  4. package/dist/src/analysis.js +32 -15
  5. package/dist/src/cli.js +156 -3
  6. package/dist/src/cyclone_dx_sbom.d.ts +27 -1
  7. package/dist/src/cyclone_dx_sbom.js +82 -9
  8. package/dist/src/index.d.ts +86 -11
  9. package/dist/src/index.js +102 -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/oci_image/utils.js +11 -2
  18. package/dist/src/package_version.d.ts +8 -0
  19. package/dist/src/package_version.js +31 -0
  20. package/dist/src/provider.d.ts +4 -3
  21. package/dist/src/provider.js +12 -2
  22. package/dist/src/providers/base_java.d.ts +67 -12
  23. package/dist/src/providers/base_java.js +82 -61
  24. package/dist/src/providers/base_javascript.d.ts +74 -9
  25. package/dist/src/providers/base_javascript.js +149 -9
  26. package/dist/src/providers/base_pyproject.d.ts +158 -0
  27. package/dist/src/providers/base_pyproject.js +322 -0
  28. package/dist/src/providers/containerfile_parser.d.ts +5 -0
  29. package/dist/src/providers/containerfile_parser.js +20 -0
  30. package/dist/src/providers/golang_gomodules.d.ts +23 -13
  31. package/dist/src/providers/golang_gomodules.js +207 -120
  32. package/dist/src/providers/gomod_parser.d.ts +4 -0
  33. package/dist/src/providers/gomod_parser.js +16 -0
  34. package/dist/src/providers/java_gradle.d.ts +67 -0
  35. package/dist/src/providers/java_gradle.js +310 -17
  36. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  37. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  38. package/dist/src/providers/java_maven.d.ts +38 -5
  39. package/dist/src/providers/java_maven.js +261 -8
  40. package/dist/src/providers/javascript_bun.d.ts +22 -0
  41. package/dist/src/providers/javascript_bun.js +141 -0
  42. package/dist/src/providers/javascript_npm.d.ts +19 -0
  43. package/dist/src/providers/javascript_npm.js +61 -1
  44. package/dist/src/providers/javascript_pnpm.d.ts +13 -0
  45. package/dist/src/providers/javascript_pnpm.js +53 -3
  46. package/dist/src/providers/javascript_yarn.d.ts +12 -0
  47. package/dist/src/providers/javascript_yarn.js +75 -2
  48. package/dist/src/providers/manifest.d.ts +2 -0
  49. package/dist/src/providers/manifest.js +22 -4
  50. package/dist/src/providers/marker_evaluator.d.ts +14 -0
  51. package/dist/src/providers/marker_evaluator.js +191 -0
  52. package/dist/src/providers/oci_dockerfile.d.ts +51 -0
  53. package/dist/src/providers/oci_dockerfile.js +177 -0
  54. package/dist/src/providers/processors/yarn_berry_processor.d.ts +6 -2
  55. package/dist/src/providers/processors/yarn_berry_processor.js +91 -7
  56. package/dist/src/providers/processors/yarn_classic_processor.d.ts +6 -2
  57. package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
  58. package/dist/src/providers/python_controller.d.ts +5 -1
  59. package/dist/src/providers/python_controller.js +69 -8
  60. package/dist/src/providers/python_pip.d.ts +5 -0
  61. package/dist/src/providers/python_pip.js +6 -6
  62. package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
  63. package/dist/src/providers/python_pip_pyproject.js +146 -0
  64. package/dist/src/providers/python_poetry.d.ts +98 -0
  65. package/dist/src/providers/python_poetry.js +286 -0
  66. package/dist/src/providers/python_uv.d.ts +46 -0
  67. package/dist/src/providers/python_uv.js +269 -0
  68. package/dist/src/providers/rust_cargo.d.ts +6 -2
  69. package/dist/src/providers/rust_cargo.js +101 -33
  70. package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
  71. package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
  72. package/dist/src/remediate.d.ts +21 -0
  73. package/dist/src/remediate.js +149 -0
  74. package/dist/src/remediation.d.ts +57 -0
  75. package/dist/src/remediation.js +436 -0
  76. package/dist/src/remediation_report.d.ts +40 -0
  77. package/dist/src/remediation_report.js +159 -0
  78. package/dist/src/sbom.d.ts +25 -1
  79. package/dist/src/sbom.js +23 -2
  80. package/dist/src/tools.d.ts +28 -2
  81. package/dist/src/tools.js +82 -1
  82. package/dist/src/updaters/maven_updater.d.ts +26 -0
  83. package/dist/src/updaters/maven_updater.js +322 -0
  84. package/dist/src/updaters/toml_updater.d.ts +33 -0
  85. package/dist/src/updaters/toml_updater.js +244 -0
  86. package/dist/src/workspace.d.ts +9 -0
  87. package/dist/src/workspace.js +1 -1
  88. package/package.json +34 -26
@@ -29,13 +29,17 @@ export default class Yarn_berry_processor extends Yarn_processor {
29
29
  * @param {Object} depTree - The dependency tree object
30
30
  * @returns {Map<string, PackageURL>} Map of dependency names to their PackageURL objects
31
31
  */
32
- getRootDependencies(depTree: any): Map<string, PackageURL>;
32
+ getRootDependencies(depTree: Object): Map<string, PackageURL>;
33
33
  /**
34
34
  * Adds dependencies to the SBOM
35
35
  * @param {Sbom} sbom - The SBOM object to add dependencies to
36
36
  * @param {Object} depTree - The dependency tree object
37
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
37
38
  */
38
- addDependenciesToSbom(sbom: Sbom, depTree: any): void;
39
+ addDependenciesToSbom(sbom: Sbom, depTree: Object, hashesForPurl?: (arg0: PackageURL) => (Array<{
40
+ alg: string;
41
+ content: string;
42
+ }> | undefined)): void;
39
43
  #private;
40
44
  }
41
45
  import Yarn_processor from "./yarn_processor.js";
@@ -15,7 +15,10 @@ export default class Yarn_berry_processor extends Yarn_processor {
15
15
  * @returns {string[]} Command arguments for listing dependencies
16
16
  */
17
17
  listCmdArgs(includeTransitive) {
18
- return ['info', includeTransitive ? '--recursive' : '--all', '--json'];
18
+ // --all is needed to include workspace members in the output
19
+ return includeTransitive
20
+ ? ['info', '--recursive', '--all', '--json']
21
+ : ['info', '--all', '--json'];
19
22
  }
20
23
  /**
21
24
  * Returns the command arguments for updating the lock file
@@ -48,13 +51,15 @@ export default class Yarn_berry_processor extends Yarn_processor {
48
51
  if (!depTree) {
49
52
  return new Map();
50
53
  }
51
- return new Map(depTree.filter(dep => !this.#isRoot(dep.value)).map(dep => {
54
+ return new Map(depTree.filter(dep => !this.#isRoot(dep.value))
55
+ .map(dep => {
52
56
  const depName = dep.value;
53
57
  const idx = depName.lastIndexOf('@');
54
58
  const name = depName.substring(0, idx);
55
59
  const version = dep.children.Version;
56
60
  return [name, toPurl(purlType, name, version)];
57
- }));
61
+ })
62
+ .filter(([name]) => this._manifest.dependencies.includes(name)));
58
63
  }
59
64
  /**
60
65
  * Checks if a dependency is the root package
@@ -66,32 +71,111 @@ export default class Yarn_berry_processor extends Yarn_processor {
66
71
  if (!name) {
67
72
  return false;
68
73
  }
69
- return name.endsWith("@workspace:.");
74
+ // Workspace members use paths like "member-a@workspace:packages/member-a", not just "@workspace:."
75
+ return name.startsWith(`${this._manifest.name}@workspace:`);
70
76
  }
71
77
  /**
72
78
  * Adds dependencies to the SBOM
73
79
  * @param {Sbom} sbom - The SBOM object to add dependencies to
74
80
  * @param {Object} depTree - The dependency tree object
81
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
75
82
  */
76
- addDependenciesToSbom(sbom, depTree) {
83
+ addDependenciesToSbom(sbom, depTree, hashesForPurl = () => undefined) {
77
84
  if (!depTree) {
78
85
  return;
79
86
  }
87
+ // Build index of nodes by their value for quick lookup
88
+ const nodeIndex = new Map();
89
+ depTree.forEach(n => nodeIndex.set(n.value, n));
90
+ // Determine the set of node values reachable from root via production deps
91
+ const prodDeps = new Set(this._manifest.dependencies);
92
+ const reachable = new Set();
93
+ const queue = [];
94
+ // Seed with root's production dependencies
95
+ const rootNode = depTree.find(n => this.#isRoot(n.value));
96
+ if (rootNode?.children?.Dependencies) {
97
+ for (const d of rootNode.children.Dependencies) {
98
+ const to = this.#purlFromLocator(d.locator);
99
+ if (to) {
100
+ const fullName = to.namespace ? `${to.namespace}/${to.name}` : to.name;
101
+ if (prodDeps.has(fullName)) {
102
+ queue.push(d.locator);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ // BFS to find all transitively reachable packages
108
+ while (queue.length > 0) {
109
+ const locator = queue.shift();
110
+ if (reachable.has(locator)) {
111
+ continue;
112
+ }
113
+ reachable.add(locator);
114
+ const node = nodeIndex.get(this.#nodeValueFromLocator(locator));
115
+ if (node?.children?.Dependencies) {
116
+ for (const d of node.children.Dependencies) {
117
+ if (!reachable.has(d.locator)) {
118
+ queue.push(d.locator);
119
+ }
120
+ }
121
+ }
122
+ }
123
+ // Only emit edges for root and reachable nodes
80
124
  depTree.forEach(n => {
81
125
  const depName = n.value;
82
- const from = this.#isRoot(depName) ? toPurlFromString(sbom.getRoot().purl) : this.#purlFromNode(depName, n);
126
+ const isRoot = this.#isRoot(depName);
127
+ if (!isRoot && !this.#isReachableNode(depName, reachable)) {
128
+ return;
129
+ }
130
+ const from = isRoot ? toPurlFromString(sbom.getRoot().purl) : this.#purlFromNode(depName, n);
83
131
  const deps = n.children?.Dependencies;
84
132
  if (!deps) {
85
133
  return;
86
134
  }
87
135
  deps.forEach(d => {
136
+ if (!reachable.has(d.locator)) {
137
+ return;
138
+ }
88
139
  const to = this.#purlFromLocator(d.locator);
89
140
  if (to) {
90
- sbom.addDependency(from, to);
141
+ sbom.addDependency(from, to, undefined, hashesForPurl(to));
91
142
  }
92
143
  });
93
144
  });
94
145
  }
146
+ /**
147
+ * Converts a locator to the node value format used in yarn info output
148
+ * @param {string} locator - e.g. "express@npm:4.17.1"
149
+ * @returns {string} The node value, same as locator for non-virtual
150
+ * @private
151
+ */
152
+ #nodeValueFromLocator(locator) {
153
+ // Virtual locators: "@scope/name@virtual:hash#npm:version" → "@scope/name@npm:version"
154
+ const virtualMatch = Yarn_berry_processor.VIRTUAL_LOCATOR_PATTERN.exec(locator);
155
+ if (virtualMatch) {
156
+ return `${virtualMatch[1]}@npm:${virtualMatch[2]}`;
157
+ }
158
+ return locator;
159
+ }
160
+ /**
161
+ * Checks if a node is in the reachable set by matching its value against reachable locators
162
+ * @param {string} depName - The node value (e.g. "express@npm:4.17.1")
163
+ * @param {Set<string>} reachable - Set of reachable locators
164
+ * @returns {boolean}
165
+ * @private
166
+ */
167
+ #isReachableNode(depName, reachable) {
168
+ if (reachable.has(depName)) {
169
+ return true;
170
+ }
171
+ // Check if any reachable locator resolves to this node value
172
+ for (const locator of reachable) {
173
+ if (this.#nodeValueFromLocator(locator) === depName) {
174
+ return true;
175
+ }
176
+ }
177
+ return false;
178
+ }
95
179
  /**
96
180
  * Creates a PackageURL from a dependency locator
97
181
  * @param {string} locator - The dependency locator
@@ -25,13 +25,17 @@ export default class Yarn_classic_processor extends Yarn_processor {
25
25
  * @param {Object} depTree - The dependency tree object
26
26
  * @returns {Map<string, PackageURL>} Map of dependency names to their PackageURL objects
27
27
  */
28
- getRootDependencies(depTree: any): Map<string, PackageURL>;
28
+ getRootDependencies(depTree: Object): Map<string, PackageURL>;
29
29
  /**
30
30
  * Adds dependencies to the SBOM
31
31
  * @param {Sbom} sbom - The SBOM object to add dependencies to
32
32
  * @param {Object} depTree - The dependency tree object
33
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
33
34
  */
34
- addDependenciesToSbom(sbom: Sbom, depTree: any): void;
35
+ addDependenciesToSbom(sbom: Sbom, depTree: Object, hashesForPurl?: (arg0: PackageURL) => (Array<{
36
+ alg: string;
37
+ content: string;
38
+ }> | undefined)): void;
35
39
  #private;
36
40
  }
37
41
  import Yarn_processor from "./yarn_processor.js";
@@ -50,8 +50,9 @@ export default class Yarn_classic_processor extends Yarn_processor {
50
50
  * Adds dependencies to the SBOM
51
51
  * @param {Sbom} sbom - The SBOM object to add dependencies to
52
52
  * @param {Object} depTree - The dependency tree object
53
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
53
54
  */
54
- addDependenciesToSbom(sbom, depTree) {
55
+ addDependenciesToSbom(sbom, depTree, hashesForPurl = () => undefined) {
55
56
  if (!depTree?.data?.trees) {
56
57
  return;
57
58
  }
@@ -60,12 +61,12 @@ export default class Yarn_classic_processor extends Yarn_processor {
60
61
  depTree.data.trees.forEach(n => {
61
62
  const dep = new NodeMetaData(n);
62
63
  if (this._manifest.dependencies.includes(dep.name)) {
63
- sbom.addDependency(rootPurl, dep.purl);
64
+ sbom.addDependency(rootPurl, dep.purl, undefined, hashesForPurl(dep.purl));
64
65
  }
65
66
  purls.set(dep.name, dep.purl);
66
67
  });
67
68
  depTree.data.trees.forEach(n => {
68
- this.#addChildrenToSbom(sbom, n, purls);
69
+ this.#addChildrenToSbom(sbom, n, purls, hashesForPurl);
69
70
  });
70
71
  }
71
72
  /**
@@ -73,9 +74,10 @@ export default class Yarn_classic_processor extends Yarn_processor {
73
74
  * @param {Sbom} sbom - The SBOM object to add dependencies to
74
75
  * @param {Object} node - The current dependency node
75
76
  * @param {Map<string, PackageURL>} purls - Map of dependency names to their PackageURL objects
77
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} hashesForPurl - Resolves lock-file hashes for a purl
76
78
  * @private
77
79
  */
78
- #addChildrenToSbom(sbom, node, purls) {
80
+ #addChildrenToSbom(sbom, node, purls, hashesForPurl) {
79
81
  const dep = new NodeMetaData(node);
80
82
  const children = node.children ? node.children : [];
81
83
  children.forEach(c => {
@@ -83,9 +85,9 @@ export default class Yarn_classic_processor extends Yarn_processor {
83
85
  const from = dep.shadow ? purls.get(dep.name) : dep.purl;
84
86
  const to = child.shadow ? purls.get(child.name) : child.purl;
85
87
  if (from && to) {
86
- sbom.addDependency(from, to);
88
+ sbom.addDependency(from, to, undefined, hashesForPurl(to));
87
89
  }
88
- this.#addChildrenToSbom(sbom, c, purls);
90
+ this.#addChildrenToSbom(sbom, c, purls, hashesForPurl);
89
91
  });
90
92
  }
91
93
  }
@@ -1,4 +1,4 @@
1
- /** @typedef {{name: string, version: string, dependencies: DependencyEntry[]}} DependencyEntry */
1
+ /** @typedef {{name: string, version: string, dependencies: DependencyEntry[], hashes?: Array<{alg: string, content: string}>}} DependencyEntry */
2
2
  export default class Python_controller {
3
3
  /**
4
4
  * Constructor to create new python controller instance to interact with pip package manager
@@ -31,4 +31,8 @@ export type DependencyEntry = {
31
31
  name: string;
32
32
  version: string;
33
33
  dependencies: DependencyEntry[];
34
+ hashes?: Array<{
35
+ alg: string;
36
+ content: string;
37
+ }>;
34
38
  };
@@ -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); });
@@ -75,7 +75,7 @@ function addAllDependencies(source, dep, sbom) {
75
75
  /**
76
76
  *
77
77
  * @param {string} manifest - path to requirements.txt
78
- * @return {PackageURL []}
78
+ * @return {Promise<PackageURL[]>}
79
79
  */
80
80
  async function getIgnoredDependencies(manifest) {
81
81
  const [parser, ignoreQuery, pinnedVersionQuery] = await Promise.all([
@@ -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
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Python provider for pyproject.toml files using PEP 621 format without a lock file.
3
+ * Uses `pip install --dry-run --ignore-installed --report` to resolve the full dependency tree.
4
+ * Acts as the fallback provider when no lock file (uv.lock/poetry.lock) is found.
5
+ */
6
+ export default class Python_pip_pyproject extends Base_pyproject {
7
+ /**
8
+ * Always returns true — pip provider is the fallback when no lock file is found.
9
+ * @param {string} manifestDir
10
+ * @param {{}} [opts={}]
11
+ * @returns {boolean}
12
+ */
13
+ validateLockFile(manifestDir: string, opts?: {}): boolean;
14
+ /**
15
+ * Get pip report output from env var override or by running pip.
16
+ * @param {string} manifestDir - directory containing pyproject.toml
17
+ * @param {{}} [opts={}]
18
+ * @returns {string} pip report JSON string
19
+ */
20
+ _getPipReportOutput(manifestDir: string, opts?: {}): string;
21
+ /**
22
+ * Parse pip report JSON and build dependency graph.
23
+ * @param {string} reportJson - pip report JSON string
24
+ * @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}}
25
+ */
26
+ _parsePipReport(reportJson: string): {
27
+ directDeps: string[];
28
+ graph: Map<string, {
29
+ name: string;
30
+ version: string;
31
+ children: string[];
32
+ }>;
33
+ };
34
+ /**
35
+ * Check if a requires_dist entry is an extras-only dependency.
36
+ * @param {string} req - e.g. "PySocks!=1.5.7,>=1.5.6; extra == \"socks\""
37
+ * @returns {boolean}
38
+ */
39
+ _hasExtraMarker(req: string): boolean;
40
+ /**
41
+ * Extract package name from a requires_dist entry.
42
+ * @param {string} req - e.g. "charset_normalizer<4,>=2"
43
+ * @returns {string|null}
44
+ */
45
+ _extractDepName(req: string): string | null;
46
+ /**
47
+ * Resolve dependencies using pip install --dry-run --report.
48
+ * @param {string} manifestDir
49
+ * @param {string} _workspaceDir - unused (pip resolves from manifest directory)
50
+ * @param {object} parsed - parsed pyproject.toml
51
+ * @param {{}} [opts={}]
52
+ * @returns {Promise<{directDeps: string[], graph: Map}>}
53
+ */
54
+ _getDependencyData(manifestDir: string, _workspaceDir: string, parsed: object, opts?: {}): Promise<{
55
+ directDeps: string[];
56
+ graph: Map<any, any>;
57
+ }>;
58
+ _findEggInfoDirs(dir: any): string[];
59
+ _cleanupEggInfo(dir: any, existing: any): void;
60
+ }
61
+ import Base_pyproject from './base_pyproject.js';
@@ -0,0 +1,146 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { environmentVariableIsPopulated, getCustomPath, invokeCommand } from '../tools.js';
4
+ import Base_pyproject from './base_pyproject.js';
5
+ /**
6
+ * Python provider for pyproject.toml files using PEP 621 format without a lock file.
7
+ * Uses `pip install --dry-run --ignore-installed --report` to resolve the full dependency tree.
8
+ * Acts as the fallback provider when no lock file (uv.lock/poetry.lock) is found.
9
+ */
10
+ export default class Python_pip_pyproject extends Base_pyproject {
11
+ /** @returns {string} */
12
+ _lockFileName() {
13
+ return '.pip-lock-nonexistent';
14
+ }
15
+ /** @returns {string} */
16
+ _cmdName() {
17
+ return 'pip';
18
+ }
19
+ /**
20
+ * Always returns true — pip provider is the fallback when no lock file is found.
21
+ * @param {string} manifestDir
22
+ * @param {{}} [opts={}]
23
+ * @returns {boolean}
24
+ */
25
+ // eslint-disable-next-line no-unused-vars
26
+ validateLockFile(manifestDir, opts = {}) {
27
+ return true;
28
+ }
29
+ /**
30
+ * Get pip report output from env var override or by running pip.
31
+ * @param {string} manifestDir - directory containing pyproject.toml
32
+ * @param {{}} [opts={}]
33
+ * @returns {string} pip report JSON string
34
+ */
35
+ _getPipReportOutput(manifestDir, opts) {
36
+ if (environmentVariableIsPopulated('TRUSTIFY_DA_PIP_REPORT')) {
37
+ return Buffer.from(process.env['TRUSTIFY_DA_PIP_REPORT'], 'base64').toString('ascii');
38
+ }
39
+ let pipBin = getCustomPath('pip3', opts);
40
+ try {
41
+ invokeCommand(pipBin, ['--version']);
42
+ }
43
+ catch {
44
+ pipBin = getCustomPath('pip', opts);
45
+ }
46
+ let eggInfoDirs = this._findEggInfoDirs(manifestDir);
47
+ let result = invokeCommand(pipBin, [
48
+ 'install', '--dry-run', '--ignore-installed', '--quiet', '--report', '-', '.'
49
+ ], { cwd: manifestDir }).toString();
50
+ this._cleanupEggInfo(manifestDir, eggInfoDirs);
51
+ return result;
52
+ }
53
+ /**
54
+ * Parse pip report JSON and build dependency graph.
55
+ * @param {string} reportJson - pip report JSON string
56
+ * @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}}
57
+ */
58
+ _parsePipReport(reportJson) {
59
+ let report = JSON.parse(reportJson);
60
+ let packages = report.install || [];
61
+ let rootEntry = packages.find(p => p.download_info?.dir_info !== undefined);
62
+ let rootRequires = rootEntry?.metadata?.requires_dist || [];
63
+ let directDepNames = new Set();
64
+ for (let req of rootRequires) {
65
+ if (this._hasExtraMarker(req)) {
66
+ continue;
67
+ }
68
+ let name = this._extractDepName(req);
69
+ if (name) {
70
+ directDepNames.add(this._canonicalize(name));
71
+ }
72
+ }
73
+ let graph = new Map();
74
+ let nonRootPackages = packages.filter(p => p !== rootEntry);
75
+ for (let pkg of nonRootPackages) {
76
+ let name = pkg.metadata.name;
77
+ let version = pkg.metadata.version;
78
+ let key = this._canonicalize(name);
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 });
82
+ }
83
+ for (let pkg of nonRootPackages) {
84
+ let key = this._canonicalize(pkg.metadata.name);
85
+ let entry = graph.get(key);
86
+ let requires = pkg.metadata.requires_dist || [];
87
+ for (let req of requires) {
88
+ let depName = this._extractDepName(req);
89
+ if (!depName) {
90
+ continue;
91
+ }
92
+ let depKey = this._canonicalize(depName);
93
+ if (graph.has(depKey)) {
94
+ entry.children.push(depKey);
95
+ }
96
+ }
97
+ }
98
+ let directDeps = [...directDepNames].filter(key => graph.has(key));
99
+ return { directDeps, graph };
100
+ }
101
+ /**
102
+ * Check if a requires_dist entry is an extras-only dependency.
103
+ * @param {string} req - e.g. "PySocks!=1.5.7,>=1.5.6; extra == \"socks\""
104
+ * @returns {boolean}
105
+ */
106
+ _hasExtraMarker(req) {
107
+ return /;\s*.*extra\s*==/.test(req);
108
+ }
109
+ /**
110
+ * Extract package name from a requires_dist entry.
111
+ * @param {string} req - e.g. "charset_normalizer<4,>=2"
112
+ * @returns {string|null}
113
+ */
114
+ _extractDepName(req) {
115
+ let match = req.match(/^([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)/);
116
+ return match ? match[1] : null;
117
+ }
118
+ /**
119
+ * Resolve dependencies using pip install --dry-run --report.
120
+ * @param {string} manifestDir
121
+ * @param {string} _workspaceDir - unused (pip resolves from manifest directory)
122
+ * @param {object} parsed - parsed pyproject.toml
123
+ * @param {{}} [opts={}]
124
+ * @returns {Promise<{directDeps: string[], graph: Map}>}
125
+ */
126
+ // eslint-disable-next-line no-unused-vars
127
+ async _getDependencyData(manifestDir, _workspaceDir, parsed, opts) {
128
+ let reportOutput = this._getPipReportOutput(manifestDir, opts);
129
+ return this._parsePipReport(reportOutput);
130
+ }
131
+ _findEggInfoDirs(dir) {
132
+ try {
133
+ return fs.readdirSync(dir).filter(f => f.endsWith('.egg-info'));
134
+ }
135
+ catch {
136
+ return [];
137
+ }
138
+ }
139
+ _cleanupEggInfo(dir, existing) {
140
+ for (let entry of this._findEggInfoDirs(dir)) {
141
+ if (!existing.includes(entry)) {
142
+ fs.rmSync(path.join(dir, entry), { recursive: true, force: true });
143
+ }
144
+ }
145
+ }
146
+ }