@trustify-da/trustify-da-javascript-client 0.3.0-ea.e645720 → 0.3.0-ea.f1e4e15

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.
package/dist/package.json CHANGED
@@ -55,6 +55,7 @@
55
55
  "help": "^3.0.2",
56
56
  "https-proxy-agent": "^7.0.6",
57
57
  "js-yaml": "^4.1.1",
58
+ "jsonc-parser": "^3.3.1",
58
59
  "micromatch": "^4.0.8",
59
60
  "node-fetch": "^3.3.2",
60
61
  "p-limit": "^4.0.0",
@@ -41,12 +41,20 @@ function getComponent(component, type, scope, licenses, hashes) {
41
41
  // Add licenses if provided (CycloneDX format). Callers must provide valid SPDX identifiers.
42
42
  if (licenses) {
43
43
  const licenseArray = Array.isArray(licenses) ? licenses : [licenses];
44
- componentObject.licenses = licenseArray.map(lic => {
44
+ const validLicenses = licenseArray
45
+ .map(lic => {
45
46
  if (typeof lic === 'string') {
46
47
  return { license: { id: lic } };
47
48
  }
48
- return lic;
49
- });
49
+ if (typeof lic === 'object' && lic !== null && ('license' in lic || 'expression' in lic)) {
50
+ return lic;
51
+ }
52
+ return null;
53
+ })
54
+ .filter(Boolean);
55
+ if (validLicenses.length > 0) {
56
+ componentObject.licenses = validLicenses;
57
+ }
50
58
  }
51
59
  // Add hashes if provided (CycloneDX 1.4 format).
52
60
  if (hashes && hashes.length > 0) {
package/dist/src/index.js CHANGED
@@ -175,7 +175,9 @@ async function componentAnalysis(manifest, opts = {}) {
175
175
  fs.accessSync(manifest, fs.constants.R_OK);
176
176
  opts["manifest-type"] = path.basename(manifest);
177
177
  let provider = match(manifest, availableProviders, opts); // throws error if no matching provider
178
- return await analysis.requestComponent(provider, manifest, theUrl, opts); // throws error request sending failed
178
+ const result = await analysis.requestComponent(provider, manifest, theUrl, opts); // throws error request sending failed
179
+ result.packageManager = provider.packageManagerName();
180
+ return result;
179
181
  }
180
182
  /**
181
183
  * @overload
@@ -320,7 +322,8 @@ async function detectWorkspaceManifests(root, opts) {
320
322
  return { ecosystem: 'pyproject', manifestPaths };
321
323
  }
322
324
  }
323
- const hasJsLock = fs.existsSync(path.join(root, 'pnpm-lock.yaml'))
325
+ const hasJsLock = fs.existsSync(path.join(root, 'bun.lock'))
326
+ || fs.existsSync(path.join(root, 'pnpm-lock.yaml'))
324
327
  || fs.existsSync(path.join(root, 'yarn.lock'))
325
328
  || fs.existsSync(path.join(root, 'package-lock.json'));
326
329
  if (fs.existsSync(packageJson) && hasJsLock) {
@@ -21,7 +21,7 @@ export function match(manifest: string, providers: [Provider], opts?: {
21
21
  TRUSTIFY_DA_WORKSPACE_DIR?: string;
22
22
  }): Provider;
23
23
  /** @typedef {{ecosystem: string, contentType: string, content: string}} Provided */
24
- /** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string, Object): void, provideComponent: function(string, {}): Provided | Promise<Provided>, provideStack: function(string, {}): Provided | Promise<Provided>, readLicenseFromManifest: function(string): string | null}} Provider */
24
+ /** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string, Object): void, provideComponent: function(string, {}): Provided | Promise<Provided>, provideStack: function(string, {}): Provided | Promise<Provided>, readLicenseFromManifest: function(string, Object=): string | null, packageManagerName: function(): string}} Provider */
25
25
  /**
26
26
  * MUST include all providers here.
27
27
  * @type {[Provider]}
@@ -37,5 +37,6 @@ export type Provider = {
37
37
  validateLockFile: (arg0: string, arg1: any) => void;
38
38
  provideComponent: (arg0: string, arg1: {}) => Provided | Promise<Provided>;
39
39
  provideStack: (arg0: string, arg1: {}) => Provided | Promise<Provided>;
40
- readLicenseFromManifest: (arg0: string) => string | null;
40
+ readLicenseFromManifest: (arg0: string, arg1: any | undefined) => string | null;
41
+ packageManagerName: () => string;
41
42
  };
@@ -3,6 +3,7 @@ import golangGomodulesProvider from './providers/golang_gomodules.js';
3
3
  import Java_gradle_groovy from "./providers/java_gradle_groovy.js";
4
4
  import Java_gradle_kotlin from "./providers/java_gradle_kotlin.js";
5
5
  import Java_maven from "./providers/java_maven.js";
6
+ import Javascript_bun from './providers/javascript_bun.js';
6
7
  import Javascript_npm from './providers/javascript_npm.js';
7
8
  import Javascript_pnpm from './providers/javascript_pnpm.js';
8
9
  import Javascript_yarn from './providers/javascript_yarn.js';
@@ -12,7 +13,7 @@ import Python_poetry from './providers/python_poetry.js';
12
13
  import Python_uv from './providers/python_uv.js';
13
14
  import rustCargoProvider from './providers/rust_cargo.js';
14
15
  /** @typedef {{ecosystem: string, contentType: string, content: string}} Provided */
15
- /** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string, Object): void, provideComponent: function(string, {}): Provided | Promise<Provided>, provideStack: function(string, {}): Provided | Promise<Provided>, readLicenseFromManifest: function(string): string | null}} Provider */
16
+ /** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string, Object): void, provideComponent: function(string, {}): Provided | Promise<Provided>, provideStack: function(string, {}): Provided | Promise<Provided>, readLicenseFromManifest: function(string, Object=): string | null, packageManagerName: function(): string}} Provider */
16
17
  /**
17
18
  * MUST include all providers here.
18
19
  * @type {[Provider]}
@@ -21,6 +22,7 @@ export const availableProviders = [
21
22
  new Java_maven(),
22
23
  new Java_gradle_groovy(),
23
24
  new Java_gradle_kotlin(),
25
+ new Javascript_bun(),
24
26
  new Javascript_pnpm(),
25
27
  new Javascript_yarn(),
26
28
  new Javascript_npm(),
@@ -20,6 +20,11 @@ export default class Base_Java {
20
20
  CONFLICT_REGEX: RegExp;
21
21
  globalBinary: string;
22
22
  localWrapper: string;
23
+ /**
24
+ * Returns the package manager name (e.g. mvn, gradle)
25
+ * @returns {string}
26
+ */
27
+ packageManagerName(): string;
23
28
  /**
24
29
  * Recursively populates the SBOM instance with the parsed graph
25
30
  * @param {string} src - Source dependency to start the calculations from
@@ -28,6 +33,13 @@ export default class Base_Java {
28
33
  * @param {Sbom} sbom - The SBOM where the dependencies are being added
29
34
  */
30
35
  parseDependencyTree(src: string, srcDepth: number, lines: any[], sbom: Sbom): void;
36
+ /**
37
+ * Calculates how deep in the graph is the given line
38
+ * @param {string} line - line to calculate the depth from
39
+ * @returns {number} The calculated depth
40
+ * @protected
41
+ */
42
+ protected _getDepth(line: string): number;
31
43
  /**
32
44
  * Create a PackageURL from any line in a Text Graph dependency tree for a manifest path.
33
45
  * @param {string} line - line to parse from a dependencies.txt file
@@ -57,7 +69,6 @@ export default class Base_Java {
57
69
  * @returns string
58
70
  */
59
71
  selectToolBinary(manifestPath: string, opts: {}): string | null;
60
- #private;
61
72
  }
62
73
  export type Provided = import("../provider").Provided;
63
74
  export type Package = {
@@ -25,6 +25,13 @@ export default class Base_Java {
25
25
  this.globalBinary = globalBinary;
26
26
  this.localWrapper = localWrapper;
27
27
  }
28
+ /**
29
+ * Returns the package manager name (e.g. mvn, gradle)
30
+ * @returns {string}
31
+ */
32
+ packageManagerName() {
33
+ return this.globalBinary;
34
+ }
28
35
  /**
29
36
  * Recursively populates the SBOM instance with the parsed graph
30
37
  * @param {string} src - Source dependency to start the calculations from
@@ -41,7 +48,7 @@ export default class Base_Java {
41
48
  }
42
49
  let index = 0;
43
50
  let target = lines[index];
44
- let targetDepth = this.#getDepth(target);
51
+ let targetDepth = this._getDepth(target);
45
52
  while (targetDepth > srcDepth && index < lines.length) {
46
53
  if (targetDepth === srcDepth + 1) {
47
54
  let from = this.parseDep(src);
@@ -54,22 +61,25 @@ export default class Base_Java {
54
61
  }
55
62
  }
56
63
  else {
57
- this.parseDependencyTree(lines[index - 1], this.#getDepth(lines[index - 1]), lines.slice(index), sbom);
64
+ this.parseDependencyTree(lines[index - 1], this._getDepth(lines[index - 1]), lines.slice(index), sbom);
58
65
  }
59
66
  target = lines[++index];
60
- targetDepth = this.#getDepth(target);
67
+ targetDepth = this._getDepth(target);
61
68
  }
62
69
  }
63
70
  /**
64
71
  * Calculates how deep in the graph is the given line
65
72
  * @param {string} line - line to calculate the depth from
66
73
  * @returns {number} The calculated depth
67
- * @private
74
+ * @protected
68
75
  */
69
- #getDepth(line) {
70
- if (line === undefined) {
76
+ _getDepth(line) {
77
+ if (!line || line.trim() === '') {
71
78
  return -1;
72
79
  }
80
+ if (line.match(/^\w/)) {
81
+ return 0;
82
+ }
73
83
  return ((line.indexOf('-') - 1) / 3) + 1;
74
84
  }
75
85
  /**
@@ -47,6 +47,11 @@ export default class Base_javascript {
47
47
  */
48
48
  protected _cmdName(): string;
49
49
  /**
50
+ * Returns the package manager name (e.g. npm, yarn, pnpm, bun)
51
+ * @returns {string} The package manager name
52
+ */
53
+ packageManagerName(): string;
54
+ /**
50
55
  * Returns the command arguments for listing dependencies
51
56
  * @returns {Array<string>} The command arguments
52
57
  * @abstract
@@ -136,6 +141,12 @@ export default class Base_javascript {
136
141
  */
137
142
  protected _version(): string;
138
143
  /**
144
+ * Creates or updates the lock file for the package manager
145
+ * @param {string} manifestDir - Directory containing the manifest file
146
+ * @protected
147
+ */
148
+ protected _createLockFile(manifestDir: string): void;
149
+ /**
139
150
  * Parses the dependency tree output
140
151
  * @param {string} output - The output to parse
141
152
  * @returns {string} The parsed output
@@ -71,6 +71,13 @@ export default class Base_javascript {
71
71
  throw new TypeError("_cmdName must be implemented");
72
72
  }
73
73
  /**
74
+ * Returns the package manager name (e.g. npm, yarn, pnpm, bun)
75
+ * @returns {string} The package manager name
76
+ */
77
+ packageManagerName() {
78
+ return this._cmdName();
79
+ }
80
+ /**
74
81
  * Returns the command arguments for listing dependencies
75
82
  * @returns {Array<string>} The command arguments
76
83
  * @abstract
@@ -217,7 +224,7 @@ export default class Base_javascript {
217
224
  this._version();
218
225
  const manifestDir = path.dirname(this.#manifest.manifestPath);
219
226
  const cmdDir = this._findLockFileDir(manifestDir, opts) || manifestDir;
220
- this.#createLockFile(cmdDir);
227
+ this._createLockFile(cmdDir);
221
228
  let output = this.#executeListCmd(includeTransitive, cmdDir);
222
229
  output = this._parseDepTreeOutput(output);
223
230
  return JSON.parse(output);
@@ -363,9 +370,9 @@ export default class Base_javascript {
363
370
  /**
364
371
  * Creates or updates the lock file for the package manager
365
372
  * @param {string} manifestDir - Directory containing the manifest file
366
- * @private
373
+ * @protected
367
374
  */
368
- #createLockFile(manifestDir) {
375
+ _createLockFile(manifestDir) {
369
376
  const originalDir = process.cwd();
370
377
  const isWindows = os.platform() === 'win32';
371
378
  if (isWindows) {
@@ -61,6 +61,11 @@ export default class Base_pyproject {
61
61
  * @protected
62
62
  */
63
63
  protected _cmdName(): string;
64
+ /**
65
+ * Returns the package manager name (e.g. pip, poetry, uv)
66
+ * @returns {string}
67
+ */
68
+ packageManagerName(): string;
64
69
  /**
65
70
  * Resolve dependencies using the tool-specific command and parser.
66
71
  *
@@ -145,6 +145,13 @@ export default class Base_pyproject {
145
145
  _cmdName() {
146
146
  throw new TypeError('_cmdName must be implemented');
147
147
  }
148
+ /**
149
+ * Returns the package manager name (e.g. pip, poetry, uv)
150
+ * @returns {string}
151
+ */
152
+ packageManagerName() {
153
+ return this._cmdName();
154
+ }
148
155
  /**
149
156
  * Resolve dependencies using the tool-specific command and parser.
150
157
  *
@@ -13,6 +13,7 @@ declare namespace _default {
13
13
  export { provideComponent };
14
14
  export { provideStack };
15
15
  export { readLicenseFromManifest };
16
+ export function packageManagerName(): string;
16
17
  }
17
18
  export default _default;
18
19
  export type Provided = import("../provider").Provided;
@@ -6,7 +6,7 @@ import Sbom from '../sbom.js';
6
6
  import { getCustom, getCustomPath, invokeCommand } from "../tools.js";
7
7
  import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js';
8
8
  import { getParser, getRequireQuery } from './gomod_parser.js';
9
- export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest };
9
+ export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest, packageManagerName() { return 'go'; } };
10
10
  /** @typedef {import('../provider').Provider} */
11
11
  /** @typedef {import('../provider').Provided} Provided */
12
12
  /** @typedef {{name: string, version: string}} Package */
@@ -181,6 +181,7 @@ export default class Java_maven extends Base_java {
181
181
  /** @type [Dependency] */
182
182
  let dependencies = this.#getDependencies(tmpEffectivePom)
183
183
  .filter(d => !this.#dependencyIn(d, ignored));
184
+ dependencies = this.#resolveVersionRanges(dependencies, manifestPath, opts);
184
185
  let sbom = new Sbom();
185
186
  let rootDependency = this.#getRootFromPom(tmpEffectivePom, manifestPath);
186
187
  let purlRoot = this.toPurl(rootDependency.groupId, rootDependency.artifactId, rootDependency.version);
@@ -289,6 +290,77 @@ export default class Java_maven extends Base_java {
289
290
  #dependencyIn(dep, deps) {
290
291
  return deps.filter(d => dep.artifactId === d.artifactId && dep.groupId === d.groupId && dep.scope === d.scope).length > 0;
291
292
  }
293
+ /**
294
+ * Returns true if the given version string is a Maven version range
295
+ * (starts with '[' or '(').
296
+ * @param {string} version
297
+ * @returns {boolean}
298
+ * @private
299
+ */
300
+ #isVersionRange(version) {
301
+ return typeof version === 'string' && (version.startsWith('[') || version.startsWith('('));
302
+ }
303
+ /**
304
+ * Resolves Maven version ranges in the given dependency list by running
305
+ * maven-dependency-plugin:tree and reading the concrete versions it selects.
306
+ * If no dependency uses a version range, returns the list unchanged.
307
+ * @param {Dependency[]} dependencies
308
+ * @param {string} manifestPath
309
+ * @param {object} opts
310
+ * @returns {Dependency[]}
311
+ * @private
312
+ */
313
+ #resolveVersionRanges(dependencies, manifestPath, opts = {}) {
314
+ // short-circuit if no dependency has a version range
315
+ if (!dependencies.some(dep => this.#isVersionRange(dep.version))) {
316
+ return dependencies;
317
+ }
318
+ const mvn = this.selectToolBinary(manifestPath, opts);
319
+ const mvnArgs = JSON.parse(getCustom('TRUSTIFY_DA_MVN_ARGS', '[]', opts));
320
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trustify_da_range_'));
321
+ const tmpDepTree = path.join(tmpDir, 'mvn_deptree_ranges.txt');
322
+ try {
323
+ this._invokeCommand(mvn, [
324
+ '-q',
325
+ 'org.apache.maven.plugins:maven-dependency-plugin:3.6.0:tree',
326
+ '-Dscope=compile',
327
+ '-DoutputType=text',
328
+ `-DoutputFile=${tmpDepTree}`,
329
+ ...mvnArgs
330
+ ], { cwd: path.dirname(manifestPath) });
331
+ const content = fs.readFileSync(tmpDepTree);
332
+ const lines = content.toString().split(EOL).filter(l => l.trim() !== '');
333
+ // Build a map of groupId:artifactId -> resolved version from depth-1 entries
334
+ /** @type {Map<string, string>} */
335
+ const resolvedVersions = new Map();
336
+ for (const line of lines) {
337
+ if (this._getDepth(line) === 1) {
338
+ const purl = this.parseDep(line);
339
+ resolvedVersions.set(`${purl.namespace}:${purl.name}`, purl.version);
340
+ }
341
+ }
342
+ // Replace version ranges with resolved concrete versions
343
+ return dependencies.map(dep => {
344
+ if (this.#isVersionRange(dep.version)) {
345
+ const key = `${dep.groupId}:${dep.artifactId}`;
346
+ const resolved = resolvedVersions.get(key);
347
+ if (resolved) {
348
+ return { ...dep, version: resolved };
349
+ }
350
+ }
351
+ return dep;
352
+ });
353
+ }
354
+ catch (error) {
355
+ if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
356
+ console.error("Failed to resolve Maven version ranges: " + error.message);
357
+ }
358
+ return dependencies;
359
+ }
360
+ finally {
361
+ fs.rmSync(tmpDir, { recursive: true, force: true });
362
+ }
363
+ }
292
364
  }
293
365
  const DEFAULT_MAVEN_DISCOVERY_IGNORE = [
294
366
  '**/target/**',
@@ -0,0 +1,10 @@
1
+ export default class Javascript_bun extends Base_javascript {
2
+ _listCmdArgs(): void;
3
+ _buildDependencyTree(includeTransitive: any, opts?: {}): {
4
+ name: any;
5
+ version: any;
6
+ dependencies: {};
7
+ };
8
+ #private;
9
+ }
10
+ import Base_javascript from './base_javascript.js';
@@ -0,0 +1,100 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parse } from 'jsonc-parser';
4
+ import Base_javascript from './base_javascript.js';
5
+ export default class Javascript_bun extends Base_javascript {
6
+ _lockFileName() {
7
+ return "bun.lock";
8
+ }
9
+ _cmdName() {
10
+ return "bun";
11
+ }
12
+ _listCmdArgs() {
13
+ throw new Error("not supported by Bun");
14
+ }
15
+ _updateLockFileCmdArgs() {
16
+ return ['install', '--lockfile-only'];
17
+ }
18
+ _buildDependencyTree(includeTransitive, opts = {}) {
19
+ this._version();
20
+ const manifestDir = path.dirname(this._getManifest().manifestPath);
21
+ const lockDir = this._findLockFileDir(manifestDir, opts) || manifestDir;
22
+ this._createLockFile(lockDir);
23
+ const lockContent = fs.readFileSync(path.join(lockDir, 'bun.lock'), 'utf-8');
24
+ const lockData = parse(lockContent);
25
+ const packages = lockData.packages || {};
26
+ const memberName = this._getManifest().name;
27
+ const workspaceEntry = this.#findWorkspaceEntry(lockData, lockDir, manifestDir, memberName);
28
+ const directDeps = workspaceEntry?.dependencies || {};
29
+ const tree = { name: memberName, version: this._getManifest().version, dependencies: {} };
30
+ const visited = new Set();
31
+ for (const depName of Object.keys(directDeps)) {
32
+ const resolved = this.#resolvePackage(depName, '', packages);
33
+ if (resolved) {
34
+ tree.dependencies[depName] = this.#buildNode(depName, resolved, packages, includeTransitive, visited);
35
+ }
36
+ }
37
+ return tree;
38
+ }
39
+ #findWorkspaceEntry(lockData, lockDir, manifestDir, memberName) {
40
+ const workspaces = lockData.workspaces || {};
41
+ const relPath = path.relative(lockDir, path.resolve(manifestDir));
42
+ if (!relPath || relPath === '.') {
43
+ return workspaces[''] || {};
44
+ }
45
+ const normalised = relPath.split(path.sep).join('/');
46
+ if (workspaces[normalised]) {
47
+ return workspaces[normalised];
48
+ }
49
+ for (const [wsPath, entry] of Object.entries(workspaces)) {
50
+ if (entry.name === memberName && wsPath !== '') {
51
+ return entry;
52
+ }
53
+ }
54
+ return workspaces[''] || {};
55
+ }
56
+ #resolvePackage(depName, parentKey, packages) {
57
+ if (parentKey) {
58
+ const scopedKey = `${parentKey}/${depName}`;
59
+ if (packages[scopedKey]) {
60
+ return packages[scopedKey];
61
+ }
62
+ }
63
+ return packages[depName] || null;
64
+ }
65
+ #buildNode(depName, resolved, packages, includeTransitive, visited) {
66
+ const resolvedId = Array.isArray(resolved) ? resolved[0] : resolved;
67
+ const version = this.#extractVersion(resolvedId);
68
+ const node = { version };
69
+ if (!includeTransitive) {
70
+ return node;
71
+ }
72
+ const metadata = Array.isArray(resolved) ? (resolved[2] || {}) : {};
73
+ const subDeps = metadata.dependencies || {};
74
+ if (Object.keys(subDeps).length > 0) {
75
+ const visitKey = `${depName}@${version}`;
76
+ if (visited.has(visitKey)) {
77
+ return node;
78
+ }
79
+ visited.add(visitKey);
80
+ node.dependencies = {};
81
+ for (const subName of Object.keys(subDeps)) {
82
+ const subResolved = this.#resolvePackage(subName, depName, packages);
83
+ if (subResolved) {
84
+ node.dependencies[subName] = this.#buildNode(subName, subResolved, packages, true, visited);
85
+ }
86
+ }
87
+ }
88
+ return node;
89
+ }
90
+ #extractVersion(resolvedId) {
91
+ if (typeof resolvedId !== 'string') {
92
+ return '0.0.0';
93
+ }
94
+ const atIdx = resolvedId.lastIndexOf('@');
95
+ if (atIdx > 0) {
96
+ return resolvedId.substring(atIdx + 1);
97
+ }
98
+ return '0.0.0';
99
+ }
100
+ }
@@ -19,6 +19,53 @@ function getPipShowOutput(depNames) {
19
19
  throw new Error('fail invoking \'pip show\' to fetch metadata for all installed packages in environment', { cause: error });
20
20
  }
21
21
  }
22
+ /**
23
+ * Get pip inspect output from env var override or by invoking pip inspect.
24
+ * Returns null if pip inspect is unavailable (pip < 22.2) or if freeze/show
25
+ * env vars are set without an inspect override (to avoid inconsistent data).
26
+ * @return {string|null} pip inspect JSON output, or null on failure
27
+ */
28
+ function getPipInspectOutput() {
29
+ if (environmentVariableIsPopulated("TRUSTIFY_DA_PIP_INSPECT")) {
30
+ return new Buffer.from(process.env["TRUSTIFY_DA_PIP_INSPECT"], 'base64').toString('ascii');
31
+ }
32
+ if (environmentVariableIsPopulated("TRUSTIFY_DA_PIP_FREEZE") || environmentVariableIsPopulated("TRUSTIFY_DA_PIP_SHOW")) {
33
+ return null;
34
+ }
35
+ try {
36
+ return invokeCommand(this.pathToPipBin, ['inspect']).toString();
37
+ }
38
+ catch (error) {
39
+ console.warn('pip inspect is not available (requires pip 22.2+), SBOM will be generated without hashes');
40
+ return null;
41
+ }
42
+ }
43
+ /**
44
+ * Parse pip inspect JSON output and build a hash lookup map.
45
+ * @param {string|null} inspectOutput - raw pip inspect JSON string
46
+ * @return {Map<string, Array<{alg: string, content: string}>>} map of lowercase package name to hashes
47
+ */
48
+ function parsePipInspectHashes(inspectOutput) {
49
+ if (!inspectOutput) {
50
+ return new Map();
51
+ }
52
+ try {
53
+ let inspectData = JSON.parse(inspectOutput);
54
+ let hashMap = new Map();
55
+ for (let pkg of (inspectData.installed || [])) {
56
+ let name = pkg.metadata?.name;
57
+ let sha256 = pkg.download_info?.archive_info?.hashes?.sha256;
58
+ if (name && sha256) {
59
+ hashMap.set(name.toLowerCase(), [{ alg: "SHA-256", content: sha256 }]);
60
+ }
61
+ }
62
+ return hashMap;
63
+ }
64
+ catch (error) {
65
+ console.warn('Failed to parse pip inspect output, SBOM will be generated without hashes');
66
+ return new Map();
67
+ }
68
+ }
22
69
  /** @typedef {{name: string, version: string, dependencies: DependencyEntry[], hashes?: Array<{alg: string, content: string}>}} DependencyEntry */
23
70
  export default class Python_controller {
24
71
  pythonEnvDir;
@@ -225,6 +272,8 @@ export default class Python_controller {
225
272
  CachedEnvironmentDeps[packageName.replace("_", "-")] = pipDepTreeEntryForCache;
226
273
  });
227
274
  }
275
+ const inspectOutput = getPipInspectOutput.call(this);
276
+ const hashMap = parsePipInspectHashes(inspectOutput);
228
277
  parsedRequirements.forEach(({ name: depName, version: manifestVersion, hasMarker }) => {
229
278
  if (hasMarker && CachedEnvironmentDeps[depName.toLowerCase()] === undefined) {
230
279
  return;
@@ -247,7 +296,7 @@ export default class Python_controller {
247
296
  }
248
297
  let path = [];
249
298
  path.push(depName.toLowerCase());
250
- bringAllDependencies(dependencies, depName, CachedEnvironmentDeps, includeTransitive, path, usePipDepTree);
299
+ bringAllDependencies(dependencies, depName, CachedEnvironmentDeps, includeTransitive, path, usePipDepTree, hashMap);
251
300
  });
252
301
  dependencies.sort((dep1, dep2) => {
253
302
  const DEP1 = dep1.name.toLowerCase();
@@ -327,8 +376,9 @@ function getDepsList(record) {
327
376
  * @param includeTransitive
328
377
  * @param usePipDepTree
329
378
  * @param {[string]}path array representing the path of the current branch in dependency tree, starting with a root dependency - that is - a given dependency in requirements.txt
379
+ * @param {Map<string, Array<{alg: string, content: string}>>} hashMap - map of lowercase package name to hashes
330
380
  */
331
- function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDeps, includeTransitive, path, usePipDepTree) {
381
+ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDeps, includeTransitive, path, usePipDepTree, hashMap) {
332
382
  if (dependencyName?.trim() === "") {
333
383
  return;
334
384
  }
@@ -350,7 +400,11 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
350
400
  directDeps = record.dependencies;
351
401
  }
352
402
  let targetDeps = [];
403
+ let hashes = hashMap?.get(depName.toLowerCase());
353
404
  let entry = { "name": depName, "version": version, "dependencies": [] };
405
+ if (hashes) {
406
+ entry.hashes = hashes;
407
+ }
354
408
  dependencies.push(entry);
355
409
  directDeps.forEach((dep) => {
356
410
  let depArray = [];
@@ -360,7 +414,7 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
360
414
  depArray.push(dep.toLowerCase());
361
415
  if (includeTransitive) {
362
416
  // send to recurrsion the array of all deps in path + the current dependency name which is not on the path.
363
- bringAllDependencies(targetDeps, dep, cachedEnvironmentDeps, includeTransitive, path.concat(depArray), usePipDepTree);
417
+ bringAllDependencies(targetDeps, dep, cachedEnvironmentDeps, includeTransitive, path.concat(depArray), usePipDepTree, hashMap);
364
418
  }
365
419
  }
366
420
  // sort ra
@@ -4,6 +4,7 @@ declare namespace _default {
4
4
  export { provideComponent };
5
5
  export { provideStack };
6
6
  export { readLicenseFromManifest };
7
+ export function packageManagerName(): string;
7
8
  }
8
9
  export default _default;
9
10
  export type DependencyEntry = {
@@ -5,7 +5,7 @@ import Sbom from '../sbom.js';
5
5
  import { environmentVariableIsPopulated, getCustom, getCustomPath, invokeCommand } from "../tools.js";
6
6
  import Python_controller from './python_controller.js';
7
7
  import { getParser, getIgnoreQuery, getPinnedVersionQuery } from './requirements_parser.js';
8
- export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest };
8
+ export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest, packageManagerName() { return 'pip'; } };
9
9
  /** @typedef {{name: string, version: string, dependencies: DependencyEntry[], hashes?: Array<{alg: string, content: string}>}} DependencyEntry */
10
10
  /**
11
11
  * @type {string} ecosystem for python-pip is 'pip'
@@ -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);
@@ -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 Provided = import("../provider").Provided;
@@ -46,7 +47,10 @@ declare function provideStack(manifest: string, opts?: {}): Provided;
46
47
  * Read project license from Cargo.toml, with fallback to LICENSE file.
47
48
  * Supports the `license` field under `[package]` (single crate / workspace
48
49
  * with root) and under `[workspace.package]` (virtual workspaces).
50
+ * When a member crate uses `license = { workspace = true }`, the actual
51
+ * license is resolved from the workspace root Cargo.toml.
49
52
  * @param {string} manifestPath - path to Cargo.toml
53
+ * @param {object} [metadata] - cargo metadata output (needed for workspace license resolution)
50
54
  * @returns {string|null} SPDX identifier or null
51
55
  */
52
- declare function readLicenseFromManifest(manifestPath: string): string | null;
56
+ declare function readLicenseFromManifest(manifestPath: string, metadata?: object): string | null;
@@ -5,7 +5,7 @@ import { parse as parseToml } from 'smol-toml';
5
5
  import { getLicense } from '../license/license_utils.js';
6
6
  import Sbom from '../sbom.js';
7
7
  import { getCustom, getCustomPath, invokeCommand } from '../tools.js';
8
- export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest };
8
+ export default { isSupported, validateLockFile, provideComponent, provideStack, readLicenseFromManifest, packageManagerName() { return 'cargo'; } };
9
9
  /** @typedef {import('../provider').Provider} */
10
10
  /** @typedef {import('../provider').Provided} Provided */
11
11
  /**
@@ -71,15 +71,22 @@ function isSupported(manifestName) {
71
71
  * Read project license from Cargo.toml, with fallback to LICENSE file.
72
72
  * Supports the `license` field under `[package]` (single crate / workspace
73
73
  * with root) and under `[workspace.package]` (virtual workspaces).
74
+ * When a member crate uses `license = { workspace = true }`, the actual
75
+ * license is resolved from the workspace root Cargo.toml.
74
76
  * @param {string} manifestPath - path to Cargo.toml
77
+ * @param {object} [metadata] - cargo metadata output (needed for workspace license resolution)
75
78
  * @returns {string|null} SPDX identifier or null
76
79
  */
77
- function readLicenseFromManifest(manifestPath) {
80
+ function readLicenseFromManifest(manifestPath, metadata) {
78
81
  let fromManifest = null;
79
82
  try {
80
83
  let content = fs.readFileSync(manifestPath, 'utf-8');
81
84
  let parsed = parseToml(content);
82
- fromManifest = parsed.package?.license
85
+ let packageLicense = parsed.package?.license;
86
+ if (typeof packageLicense === 'object' && packageLicense?.workspace === true) {
87
+ packageLicense = resolveWorkspaceLicense(parsed, metadata);
88
+ }
89
+ fromManifest = packageLicense
83
90
  || parsed.workspace?.package?.license
84
91
  || null;
85
92
  }
@@ -88,6 +95,26 @@ function readLicenseFromManifest(manifestPath) {
88
95
  }
89
96
  return getLicense(fromManifest, manifestPath);
90
97
  }
98
+ /**
99
+ * Resolve a workspace-inherited license from the workspace root Cargo.toml.
100
+ * @param {object} parsed - the parsed member crate Cargo.toml
101
+ * @param {object} [metadata] - cargo metadata output with workspace_root
102
+ * @returns {string|null} resolved license string or null
103
+ */
104
+ function resolveWorkspaceLicense(parsed, metadata) {
105
+ if (metadata?.workspace_root) {
106
+ let cargoTomlPath = path.join(metadata.workspace_root, 'Cargo.toml');
107
+ try {
108
+ let content = fs.readFileSync(cargoTomlPath, 'utf-8');
109
+ let workspaceParsed = parseToml(content);
110
+ return workspaceParsed.workspace?.package?.license || null;
111
+ }
112
+ catch (_) {
113
+ // fall through
114
+ }
115
+ }
116
+ return parsed.workspace?.package?.license || null;
117
+ }
91
118
  /**
92
119
  * Validates that Cargo.lock exists in the manifest directory or in a parent
93
120
  * workspace root directory. In Cargo workspaces the lock file always lives at
@@ -173,7 +200,7 @@ function getSBOM(manifest, opts = {}, includeTransitive) {
173
200
  let metadata = executeCargoMetadata(cargoBin, manifestDir);
174
201
  let ignoredDeps = getIgnoredDeps(manifest, metadata);
175
202
  let crateType = detectCrateType(metadata);
176
- let license = readLicenseFromManifest(manifest);
203
+ let license = readLicenseFromManifest(manifest, metadata);
177
204
  let sbom;
178
205
  if (crateType === CrateType.WORKSPACE_VIRTUAL) {
179
206
  sbom = handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransitive, opts, license);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustify-da/trustify-da-javascript-client",
3
- "version": "0.3.0-ea.e645720",
3
+ "version": "0.3.0-ea.f1e4e15",
4
4
  "description": "Code-Ready Dependency Analytics JavaScript API.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/guacsec/trustify-da-javascript-client#README.md",
@@ -55,6 +55,7 @@
55
55
  "help": "^3.0.2",
56
56
  "https-proxy-agent": "^7.0.6",
57
57
  "js-yaml": "^4.1.1",
58
+ "jsonc-parser": "^3.3.1",
58
59
  "micromatch": "^4.0.8",
59
60
  "node-fetch": "^3.3.2",
60
61
  "p-limit": "^4.0.0",