@trustify-da/trustify-da-javascript-client 0.3.0-ea.e645720 → 0.3.0-ea.e660b02
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 +1 -0
- package/dist/src/cyclone_dx_sbom.js +11 -3
- package/dist/src/index.js +5 -2
- package/dist/src/provider.d.ts +3 -2
- package/dist/src/provider.js +3 -1
- package/dist/src/providers/base_java.d.ts +12 -1
- package/dist/src/providers/base_java.js +16 -6
- package/dist/src/providers/base_javascript.d.ts +11 -0
- package/dist/src/providers/base_javascript.js +10 -3
- package/dist/src/providers/base_pyproject.d.ts +5 -0
- package/dist/src/providers/base_pyproject.js +7 -0
- package/dist/src/providers/golang_gomodules.d.ts +1 -0
- package/dist/src/providers/golang_gomodules.js +1 -1
- package/dist/src/providers/java_maven.js +72 -0
- package/dist/src/providers/javascript_bun.d.ts +10 -0
- package/dist/src/providers/javascript_bun.js +100 -0
- package/dist/src/providers/python_controller.js +57 -3
- package/dist/src/providers/python_pip.d.ts +1 -0
- package/dist/src/providers/python_pip.js +1 -1
- package/dist/src/providers/python_pip_pyproject.js +3 -1
- package/dist/src/providers/python_poetry.d.ts +30 -7
- package/dist/src/providers/python_poetry.js +62 -14
- package/dist/src/providers/python_uv.d.ts +4 -13
- package/dist/src/providers/python_uv.js +44 -2
- package/dist/src/providers/rust_cargo.d.ts +5 -1
- package/dist/src/providers/rust_cargo.js +31 -4
- package/package.json +2 -1
package/dist/package.json
CHANGED
|
@@ -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
|
-
|
|
44
|
+
const validLicenses = licenseArray
|
|
45
|
+
.map(lic => {
|
|
45
46
|
if (typeof lic === 'string') {
|
|
46
47
|
return { license: { id: lic } };
|
|
47
48
|
}
|
|
48
|
-
|
|
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
|
-
|
|
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, '
|
|
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) {
|
package/dist/src/provider.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/src/provider.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
74
|
+
* @protected
|
|
68
75
|
*/
|
|
69
|
-
|
|
70
|
-
if (line ===
|
|
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
|
|
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
|
-
* @
|
|
373
|
+
* @protected
|
|
367
374
|
*/
|
|
368
|
-
|
|
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
|
|
@@ -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
|
-
|
|
79
|
+
let sha256 = pkg.download_info?.archive_info?.hashes?.sha256;
|
|
80
|
+
let hashes = sha256 ? [{ alg: "SHA-256", content: sha256 }] : undefined;
|
|
81
|
+
graph.set(key, { name, version, children: [], hashes });
|
|
80
82
|
}
|
|
81
83
|
for (let pkg of nonRootPackages) {
|
|
82
84
|
let key = this._canonicalize(pkg.metadata.name);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export default class Python_poetry extends Base_pyproject {
|
|
2
|
+
_verifyPoetryAccessible(poetryBin: any): void;
|
|
2
3
|
/**
|
|
3
4
|
* @param {string} manifestDir
|
|
4
5
|
* @param {string} _workspaceDir - unused (poetry has no workspace support)
|
|
@@ -18,17 +19,17 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
18
19
|
* Get poetry show --tree output.
|
|
19
20
|
* @param {string} manifestDir
|
|
20
21
|
* @param {boolean} hasDevGroup
|
|
21
|
-
* @param {
|
|
22
|
+
* @param {string} poetryBin
|
|
22
23
|
* @returns {string}
|
|
23
24
|
*/
|
|
24
|
-
_getPoetryShowTreeOutput(manifestDir: string, hasDevGroup: boolean,
|
|
25
|
+
_getPoetryShowTreeOutput(manifestDir: string, hasDevGroup: boolean, poetryBin: string): string;
|
|
25
26
|
/**
|
|
26
27
|
* Get poetry show --all output (flat list with resolved versions).
|
|
27
28
|
* @param {string} manifestDir
|
|
28
|
-
* @param {
|
|
29
|
+
* @param {string} poetryBin
|
|
29
30
|
* @returns {string}
|
|
30
31
|
*/
|
|
31
|
-
_getPoetryShowAllOutput(manifestDir: string,
|
|
32
|
+
_getPoetryShowAllOutput(manifestDir: string, poetryBin: string): string;
|
|
32
33
|
/**
|
|
33
34
|
* Parse poetry show --all output into a version map.
|
|
34
35
|
* Lines look like: "name (!) 1.2.3 Description text..."
|
|
@@ -46,29 +47,51 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
46
47
|
* → transitiveMarkers['click']['colorama'] = "sys_platform == 'win32'"
|
|
47
48
|
* @param {string|null} lockDir
|
|
48
49
|
* @param {object} parsed - parsed pyproject.toml
|
|
49
|
-
* @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}}
|
|
50
|
+
* @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>, hashMap: Map<string, Array<{alg: string, content: string}>>}}
|
|
50
51
|
*/
|
|
51
52
|
_extractMarkerData(lockDir: string | null, parsed: object): {
|
|
52
53
|
directMarkers: Map<string, string>;
|
|
53
54
|
transitiveMarkers: Map<string, Map<string, string>>;
|
|
55
|
+
hashMap: Map<string, Array<{
|
|
56
|
+
alg: string;
|
|
57
|
+
content: string;
|
|
58
|
+
}>>;
|
|
54
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* Extract a SHA-256 hex digest from poetry.lock files array.
|
|
62
|
+
* Prefers the sdist (.tar.gz) hash; falls back to the first wheel hash.
|
|
63
|
+
* @param {Array<{file: string, hash: string}>} [files]
|
|
64
|
+
* @returns {string|null}
|
|
65
|
+
*/
|
|
66
|
+
_extractSha256FromFiles(files?: Array<{
|
|
67
|
+
file: string;
|
|
68
|
+
hash: string;
|
|
69
|
+
}>): string | null;
|
|
55
70
|
/**
|
|
56
71
|
* Parse poetry show --tree output into a dependency graph structure.
|
|
57
72
|
*
|
|
58
73
|
* @param {string} treeOutput
|
|
59
74
|
* @param {Map<string, string>} versionMap - canonical name -> resolved version
|
|
60
|
-
* @param {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}} markerData
|
|
61
|
-
* @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}}
|
|
75
|
+
* @param {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>, hashMap: Map<string, Array<{alg: string, content: string}>>}} markerData
|
|
76
|
+
* @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[], hashes?: Array<{alg: string, content: string}>}>}}
|
|
62
77
|
*/
|
|
63
78
|
_parsePoetryTree(treeOutput: string, versionMap: Map<string, string>, markerData: {
|
|
64
79
|
directMarkers: Map<string, string>;
|
|
65
80
|
transitiveMarkers: Map<string, Map<string, string>>;
|
|
81
|
+
hashMap: Map<string, Array<{
|
|
82
|
+
alg: string;
|
|
83
|
+
content: string;
|
|
84
|
+
}>>;
|
|
66
85
|
}): {
|
|
67
86
|
directDeps: string[];
|
|
68
87
|
graph: Map<string, {
|
|
69
88
|
name: string;
|
|
70
89
|
version: string;
|
|
71
90
|
children: string[];
|
|
91
|
+
hashes?: Array<{
|
|
92
|
+
alg: string;
|
|
93
|
+
content: string;
|
|
94
|
+
}>;
|
|
72
95
|
}>;
|
|
73
96
|
};
|
|
74
97
|
}
|
|
@@ -32,6 +32,17 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
32
32
|
_cmdName() {
|
|
33
33
|
return 'poetry';
|
|
34
34
|
}
|
|
35
|
+
_verifyPoetryAccessible(poetryBin) {
|
|
36
|
+
try {
|
|
37
|
+
invokeCommand(poetryBin, ['--version']);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code === 'ENOENT') {
|
|
41
|
+
throw new Error(`poetry is not accessible at "${poetryBin}"`, { cause: error });
|
|
42
|
+
}
|
|
43
|
+
throw new Error('failed to check for poetry binary', { cause: error });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
35
46
|
/**
|
|
36
47
|
* @param {string} manifestDir
|
|
37
48
|
* @param {string} _workspaceDir - unused (poetry has no workspace support)
|
|
@@ -41,9 +52,15 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
41
52
|
*/
|
|
42
53
|
// eslint-disable-next-line no-unused-vars
|
|
43
54
|
async _getDependencyData(manifestDir, _workspaceDir, parsed, opts) {
|
|
55
|
+
let poetryBin = getCustomPath('poetry', opts);
|
|
56
|
+
let envBypass = environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_TREE')
|
|
57
|
+
&& environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_ALL');
|
|
58
|
+
if (!envBypass) {
|
|
59
|
+
this._verifyPoetryAccessible(poetryBin);
|
|
60
|
+
}
|
|
44
61
|
let hasDevGroup = !!(parsed.tool?.poetry?.group?.dev || parsed.tool?.poetry?.['dev-dependencies']);
|
|
45
|
-
let treeOutput = this._getPoetryShowTreeOutput(manifestDir, hasDevGroup,
|
|
46
|
-
let showAllOutput = this._getPoetryShowAllOutput(manifestDir,
|
|
62
|
+
let treeOutput = this._getPoetryShowTreeOutput(manifestDir, hasDevGroup, poetryBin);
|
|
63
|
+
let showAllOutput = this._getPoetryShowAllOutput(manifestDir, poetryBin);
|
|
47
64
|
let versionMap = this._parsePoetryShowAll(showAllOutput);
|
|
48
65
|
let lockDir = this._findLockFileDir(manifestDir, opts);
|
|
49
66
|
let markerData = this._extractMarkerData(lockDir, parsed);
|
|
@@ -53,14 +70,13 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
53
70
|
* Get poetry show --tree output.
|
|
54
71
|
* @param {string} manifestDir
|
|
55
72
|
* @param {boolean} hasDevGroup
|
|
56
|
-
* @param {
|
|
73
|
+
* @param {string} poetryBin
|
|
57
74
|
* @returns {string}
|
|
58
75
|
*/
|
|
59
|
-
_getPoetryShowTreeOutput(manifestDir, hasDevGroup,
|
|
76
|
+
_getPoetryShowTreeOutput(manifestDir, hasDevGroup, poetryBin) {
|
|
60
77
|
if (environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_TREE')) {
|
|
61
78
|
return Buffer.from(process.env['TRUSTIFY_DA_POETRY_SHOW_TREE'], 'base64').toString('utf-8');
|
|
62
79
|
}
|
|
63
|
-
let poetryBin = getCustomPath('poetry', opts);
|
|
64
80
|
let args = ['show', '--tree', '--no-ansi'];
|
|
65
81
|
if (hasDevGroup) {
|
|
66
82
|
args.push('--without', 'dev');
|
|
@@ -70,14 +86,13 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
70
86
|
/**
|
|
71
87
|
* Get poetry show --all output (flat list with resolved versions).
|
|
72
88
|
* @param {string} manifestDir
|
|
73
|
-
* @param {
|
|
89
|
+
* @param {string} poetryBin
|
|
74
90
|
* @returns {string}
|
|
75
91
|
*/
|
|
76
|
-
_getPoetryShowAllOutput(manifestDir,
|
|
92
|
+
_getPoetryShowAllOutput(manifestDir, poetryBin) {
|
|
77
93
|
if (environmentVariableIsPopulated('TRUSTIFY_DA_POETRY_SHOW_ALL')) {
|
|
78
94
|
return Buffer.from(process.env['TRUSTIFY_DA_POETRY_SHOW_ALL'], 'base64').toString('utf-8');
|
|
79
95
|
}
|
|
80
|
-
let poetryBin = getCustomPath('poetry', opts);
|
|
81
96
|
return invokeCommand(poetryBin, ['show', '--no-ansi', '--all'], { cwd: manifestDir }).toString();
|
|
82
97
|
}
|
|
83
98
|
/**
|
|
@@ -111,11 +126,12 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
111
126
|
* → transitiveMarkers['click']['colorama'] = "sys_platform == 'win32'"
|
|
112
127
|
* @param {string|null} lockDir
|
|
113
128
|
* @param {object} parsed - parsed pyproject.toml
|
|
114
|
-
* @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}}
|
|
129
|
+
* @returns {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>, hashMap: Map<string, Array<{alg: string, content: string}>>}}
|
|
115
130
|
*/
|
|
116
131
|
_extractMarkerData(lockDir, parsed) {
|
|
117
132
|
let directMarkers = new Map();
|
|
118
133
|
let transitiveMarkers = new Map();
|
|
134
|
+
let hashMap = new Map();
|
|
119
135
|
// Extract markers from PEP 621 dependency strings: "name[extras]>=ver ; marker"
|
|
120
136
|
let deps = parsed.project?.dependencies || [];
|
|
121
137
|
for (let dep of deps) {
|
|
@@ -142,18 +158,40 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
142
158
|
transitiveMarkers.get(pkgKey).set(this._canonicalize(depName), markers);
|
|
143
159
|
}
|
|
144
160
|
}
|
|
161
|
+
let sha256Hex = this._extractSha256FromFiles(pkg.files);
|
|
162
|
+
if (sha256Hex) {
|
|
163
|
+
hashMap.set(pkgKey, [{ alg: "SHA-256", content: sha256Hex }]);
|
|
164
|
+
}
|
|
145
165
|
}
|
|
146
166
|
}
|
|
147
167
|
}
|
|
148
|
-
return { directMarkers, transitiveMarkers };
|
|
168
|
+
return { directMarkers, transitiveMarkers, hashMap };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Extract a SHA-256 hex digest from poetry.lock files array.
|
|
172
|
+
* Prefers the sdist (.tar.gz) hash; falls back to the first wheel hash.
|
|
173
|
+
* @param {Array<{file: string, hash: string}>} [files]
|
|
174
|
+
* @returns {string|null}
|
|
175
|
+
*/
|
|
176
|
+
_extractSha256FromFiles(files) {
|
|
177
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
let sdist = files.find(f => f.file.endsWith('.tar.gz'));
|
|
181
|
+
let entry = sdist || files[0];
|
|
182
|
+
let hash = entry.hash;
|
|
183
|
+
if (hash && hash.startsWith('sha256:')) {
|
|
184
|
+
return hash.slice(7);
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
149
187
|
}
|
|
150
188
|
/**
|
|
151
189
|
* Parse poetry show --tree output into a dependency graph structure.
|
|
152
190
|
*
|
|
153
191
|
* @param {string} treeOutput
|
|
154
192
|
* @param {Map<string, string>} versionMap - canonical name -> resolved version
|
|
155
|
-
* @param {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>}} markerData
|
|
156
|
-
* @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}}
|
|
193
|
+
* @param {{directMarkers: Map<string, string>, transitiveMarkers: Map<string, Map<string, string>>, hashMap: Map<string, Array<{alg: string, content: string}>>}} markerData
|
|
194
|
+
* @returns {{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[], hashes?: Array<{alg: string, content: string}>}>}}
|
|
157
195
|
*/
|
|
158
196
|
_parsePoetryTree(treeOutput, versionMap, markerData) {
|
|
159
197
|
let lines = treeOutput.split(/\r?\n/);
|
|
@@ -179,7 +217,12 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
179
217
|
}
|
|
180
218
|
directDeps.push(key);
|
|
181
219
|
if (!graph.has(key)) {
|
|
182
|
-
|
|
220
|
+
let entry = { name, version, children: [] };
|
|
221
|
+
let hashes = markerData.hashMap.get(key);
|
|
222
|
+
if (hashes) {
|
|
223
|
+
entry.hashes = hashes;
|
|
224
|
+
}
|
|
225
|
+
graph.set(key, entry);
|
|
183
226
|
}
|
|
184
227
|
currentDirectDep = key;
|
|
185
228
|
stack = [{ key, depth: -1 }];
|
|
@@ -223,7 +266,12 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
223
266
|
throw new Error(`poetry: package '${depName}' has no resolved version`);
|
|
224
267
|
}
|
|
225
268
|
if (!graph.has(depKey)) {
|
|
226
|
-
|
|
269
|
+
let entry = { name: depName, version, children: [] };
|
|
270
|
+
let hashes = markerData.hashMap.get(depKey);
|
|
271
|
+
if (hashes) {
|
|
272
|
+
entry.hashes = hashes;
|
|
273
|
+
}
|
|
274
|
+
graph.set(depKey, entry);
|
|
227
275
|
}
|
|
228
276
|
if (parentKey) {
|
|
229
277
|
let parentEntry = graph.get(parentKey);
|
|
@@ -13,20 +13,11 @@ export function discoverUvWorkspaceMembers(workspaceRoot: string, opts?: {
|
|
|
13
13
|
}): Promise<string[]>;
|
|
14
14
|
export default class Python_uv extends Base_pyproject {
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
* @param {string}
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {Object} opts
|
|
20
|
-
* @returns {Promise<{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}>}
|
|
16
|
+
* Parse uv.lock and attach SHA-256 hashes to graph entries.
|
|
17
|
+
* @param {string} lockFilePath - path to uv.lock
|
|
18
|
+
* @param {Map<string, import('./base_pyproject.js').GraphEntry>} graph
|
|
21
19
|
*/
|
|
22
|
-
|
|
23
|
-
directDeps: string[];
|
|
24
|
-
graph: Map<string, {
|
|
25
|
-
name: string;
|
|
26
|
-
version: string;
|
|
27
|
-
children: string[];
|
|
28
|
-
}>;
|
|
29
|
-
}>;
|
|
20
|
+
_attachHashesFromLockFile(lockFilePath: string, graph: Map<string, import("./base_pyproject.js").GraphEntry>): Promise<void>;
|
|
30
21
|
/**
|
|
31
22
|
* Get the uv export output, either from env var or by running the command.
|
|
32
23
|
* @param {string} manifestDir
|
|
@@ -21,12 +21,54 @@ export default class Python_uv extends Base_pyproject {
|
|
|
21
21
|
* @param {string} workspaceDir - workspace root (for resolving editable install paths)
|
|
22
22
|
* @param {object} parsed - parsed pyproject.toml
|
|
23
23
|
* @param {Object} opts
|
|
24
|
-
* @returns {Promise<{directDeps: string[], graph: Map<string,
|
|
24
|
+
* @returns {Promise<{directDeps: string[], graph: Map<string, import('./base_pyproject.js').GraphEntry>}>}
|
|
25
25
|
*/
|
|
26
26
|
async _getDependencyData(manifestDir, workspaceDir, parsed, opts) {
|
|
27
27
|
let projectName = this._getProjectName(parsed);
|
|
28
28
|
let uvOutput = this._getUvExportOutput(manifestDir, opts);
|
|
29
|
-
|
|
29
|
+
let { directDeps, graph } = await this._parseUvExport(uvOutput, projectName, workspaceDir);
|
|
30
|
+
await this._attachHashesFromLockFile(path.join(workspaceDir, 'uv.lock'), graph);
|
|
31
|
+
return { directDeps, graph };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse uv.lock and attach SHA-256 hashes to graph entries.
|
|
35
|
+
* @param {string} lockFilePath - path to uv.lock
|
|
36
|
+
* @param {Map<string, import('./base_pyproject.js').GraphEntry>} graph
|
|
37
|
+
*/
|
|
38
|
+
async _attachHashesFromLockFile(lockFilePath, graph) {
|
|
39
|
+
let lockContent;
|
|
40
|
+
try {
|
|
41
|
+
lockContent = await fs.promises.readFile(lockFilePath, 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = parseToml(lockContent);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let packages = parsed.package;
|
|
54
|
+
if (!Array.isArray(packages)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (let pkg of packages) {
|
|
58
|
+
if (!pkg.name) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
let hashStr = pkg.sdist?.hash || pkg.wheels?.[0]?.hash;
|
|
62
|
+
if (!hashStr || !hashStr.startsWith('sha256:')) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
let key = this._canonicalize(pkg.name);
|
|
66
|
+
let entry = graph.get(key);
|
|
67
|
+
if (!entry || entry.hashes) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
entry.hashes = [{ alg: "SHA-256", content: hashStr.slice(7) }];
|
|
71
|
+
}
|
|
30
72
|
}
|
|
31
73
|
/**
|
|
32
74
|
* Get the uv export output, either from env var or by running the command.
|
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.0-ea.e660b02",
|
|
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",
|