@trustify-da/trustify-da-javascript-client 0.3.0-ea.7144952 → 0.3.0-ea.76e1fef
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/README.md +73 -8
- package/dist/package.json +6 -5
- package/dist/src/analysis.js +3 -2
- package/dist/src/cli.js +51 -2
- package/dist/src/cyclone_dx_sbom.d.ts +14 -1
- package/dist/src/cyclone_dx_sbom.js +32 -5
- package/dist/src/index.d.ts +74 -3
- package/dist/src/index.js +87 -5
- package/dist/src/oci_image/utils.js +11 -2
- package/dist/src/provider.js +6 -0
- package/dist/src/providers/base_java.d.ts +0 -9
- package/dist/src/providers/base_java.js +2 -38
- package/dist/src/providers/base_javascript.d.ts +17 -7
- package/dist/src/providers/base_javascript.js +90 -17
- package/dist/src/providers/base_pyproject.d.ts +153 -0
- package/dist/src/providers/base_pyproject.js +315 -0
- package/dist/src/providers/golang_gomodules.d.ts +21 -12
- package/dist/src/providers/golang_gomodules.js +164 -118
- package/dist/src/providers/gomod_parser.d.ts +4 -0
- package/dist/src/providers/gomod_parser.js +16 -0
- package/dist/src/providers/java_gradle.d.ts +19 -0
- package/dist/src/providers/java_gradle.js +114 -0
- package/dist/src/providers/java_maven.d.ts +8 -0
- package/dist/src/providers/java_maven.js +93 -1
- package/dist/src/providers/javascript_npm.d.ts +1 -0
- package/dist/src/providers/javascript_npm.js +21 -0
- package/dist/src/providers/javascript_pnpm.js +6 -2
- package/dist/src/providers/manifest.d.ts +2 -0
- package/dist/src/providers/manifest.js +22 -4
- package/dist/src/providers/marker_evaluator.d.ts +14 -0
- package/dist/src/providers/marker_evaluator.js +191 -0
- package/dist/src/providers/processors/yarn_berry_processor.js +88 -5
- package/dist/src/providers/python_controller.d.ts +5 -1
- package/dist/src/providers/python_controller.js +8 -4
- package/dist/src/providers/python_pip.d.ts +4 -0
- package/dist/src/providers/python_pip.js +5 -5
- package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
- package/dist/src/providers/python_pip_pyproject.js +144 -0
- package/dist/src/providers/python_poetry.d.ts +75 -0
- package/dist/src/providers/python_poetry.js +238 -0
- package/dist/src/providers/python_uv.d.ts +55 -0
- package/dist/src/providers/python_uv.js +227 -0
- package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
- package/dist/src/sbom.d.ts +14 -1
- package/dist/src/sbom.js +13 -2
- package/dist/src/tools.d.ts +26 -0
- package/dist/src/tools.js +58 -0
- package/dist/src/workspace.d.ts +9 -0
- package/dist/src/workspace.js +1 -1
- package/package.json +7 -6
package/dist/src/index.js
CHANGED
|
@@ -6,14 +6,18 @@ import analysis from './analysis.js';
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import { getCustom } from "./tools.js";
|
|
8
8
|
import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js';
|
|
9
|
+
import { discoverMavenModules } from './providers/java_maven.js';
|
|
10
|
+
import { discoverGradleSubprojects } from './providers/java_gradle.js';
|
|
11
|
+
import { discoverGoWorkspaceModules } from './providers/golang_gomodules.js';
|
|
12
|
+
import { discoverUvWorkspaceMembers } from './providers/python_uv.js';
|
|
9
13
|
import { discoverWorkspaceCrates, discoverWorkspacePackages, filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore, validatePackageJson, } from './workspace.js';
|
|
10
14
|
import.meta.dirname;
|
|
11
15
|
import * as url from 'url';
|
|
12
16
|
export { parseImageRef } from "./oci_image/utils.js";
|
|
13
17
|
export { ImageRef } from "./oci_image/images.js";
|
|
14
18
|
export { getProjectLicense, findLicenseFilePath, identifyLicense, getLicenseDetails, licensesFromReport, normalizeLicensesResponse, runLicenseCheck, getCompatibility } from "./license/index.js";
|
|
15
|
-
export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnalysis, validateToken };
|
|
16
|
-
export { discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson, resolveWorkspaceDiscoveryIgnore, filterManifestPathsByDiscoveryIgnore, resolveContinueOnError, resolveBatchMetadata, };
|
|
19
|
+
export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnalysis, validateToken, generateSbom };
|
|
20
|
+
export { discoverMavenModules, discoverGradleSubprojects, discoverGoWorkspaceModules, discoverUvWorkspaceMembers, discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson, resolveWorkspaceDiscoveryIgnore, filterManifestPathsByDiscoveryIgnore, resolveContinueOnError, resolveBatchMetadata, };
|
|
17
21
|
/**
|
|
18
22
|
* @typedef {{
|
|
19
23
|
* TRUSTIFY_DA_CARGO_PATH?: string | undefined,
|
|
@@ -56,13 +60,15 @@ export { discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson
|
|
|
56
60
|
* TRUSTIFY_DA_CONTINUE_ON_ERROR?: string | undefined,
|
|
57
61
|
* batchMetadata?: boolean | undefined,
|
|
58
62
|
* TRUSTIFY_DA_BATCH_METADATA?: string | undefined,
|
|
63
|
+
* TRUSTIFY_DA_UV_PATH?: string | undefined,
|
|
64
|
+
* TRUSTIFY_DA_POETRY_PATH?: string | undefined,
|
|
59
65
|
* [key: string]: string | number | boolean | string[] | undefined,
|
|
60
66
|
* }} Options
|
|
61
67
|
*/
|
|
62
68
|
/**
|
|
63
69
|
* @typedef {{
|
|
64
70
|
* workspaceRoot: string,
|
|
65
|
-
* ecosystem: 'javascript' | 'cargo' | 'unknown',
|
|
71
|
+
* ecosystem: 'javascript' | 'cargo' | 'pyproject' | 'unknown',
|
|
66
72
|
* total: number,
|
|
67
73
|
* successful: number,
|
|
68
74
|
* failed: number,
|
|
@@ -235,6 +241,22 @@ function buildBatchAnalysisMetadata(root, ecosystem, totalSbomAttempts, successf
|
|
|
235
241
|
errors: [...errors],
|
|
236
242
|
};
|
|
237
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Generate a CycloneDX SBOM from a manifest file. No backend HTTP request is made.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} manifestPath - path to the manifest file (e.g. pom.xml, package.json)
|
|
248
|
+
* @param {Options} [opts={}] - optional options (e.g. workspace dir, tool paths)
|
|
249
|
+
* @returns {Promise<object>} parsed CycloneDX SBOM JSON object
|
|
250
|
+
* @throws {Error} if the manifest is unsupported or SBOM generation fails
|
|
251
|
+
*/
|
|
252
|
+
export async function generateSbom(manifestPath, opts = {}) {
|
|
253
|
+
fs.accessSync(manifestPath, fs.constants.R_OK);
|
|
254
|
+
const result = await generateOneSbom(manifestPath, opts);
|
|
255
|
+
if (!result.ok) {
|
|
256
|
+
throw new Error(`Failed to generate SBOM for ${result.manifestPath}: ${result.reason}`);
|
|
257
|
+
}
|
|
258
|
+
return result.sbom;
|
|
259
|
+
}
|
|
238
260
|
/**
|
|
239
261
|
* @typedef {{ ok: true, purl: string, sbom: object } | { ok: false, manifestPath: string, reason: string }} SbomResult
|
|
240
262
|
*/
|
|
@@ -261,16 +283,43 @@ async function generateOneSbom(manifestPath, workspaceOpts) {
|
|
|
261
283
|
*
|
|
262
284
|
* @param {string} root - Resolved workspace root
|
|
263
285
|
* @param {Options} opts
|
|
264
|
-
* @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'unknown', manifestPaths: string[] }>}
|
|
286
|
+
* @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'maven' | 'gradle' | 'gomodules' | 'pyproject' | 'unknown', manifestPaths: string[] }>}
|
|
265
287
|
* @private
|
|
266
288
|
*/
|
|
267
289
|
async function detectWorkspaceManifests(root, opts) {
|
|
268
290
|
const cargoToml = path.join(root, 'Cargo.toml');
|
|
269
291
|
const cargoLock = path.join(root, 'Cargo.lock');
|
|
270
292
|
const packageJson = path.join(root, 'package.json');
|
|
293
|
+
const pomXml = path.join(root, 'pom.xml');
|
|
271
294
|
if (fs.existsSync(cargoToml) && fs.existsSync(cargoLock)) {
|
|
272
295
|
return { ecosystem: 'cargo', manifestPaths: await discoverWorkspaceCrates(root, opts) };
|
|
273
296
|
}
|
|
297
|
+
if (fs.existsSync(pomXml)) {
|
|
298
|
+
const manifestPaths = await discoverMavenModules(root, opts);
|
|
299
|
+
if (manifestPaths.length > 0) {
|
|
300
|
+
return { ecosystem: 'maven', manifestPaths };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const hasGradleSettings = fs.existsSync(path.join(root, 'settings.gradle'))
|
|
304
|
+
|| fs.existsSync(path.join(root, 'settings.gradle.kts'));
|
|
305
|
+
if (hasGradleSettings) {
|
|
306
|
+
const manifestPaths = await discoverGradleSubprojects(root, opts);
|
|
307
|
+
if (manifestPaths.length > 0) {
|
|
308
|
+
return { ecosystem: 'gradle', manifestPaths };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (fs.existsSync(path.join(root, 'go.work'))) {
|
|
312
|
+
const manifestPaths = await discoverGoWorkspaceModules(root, opts);
|
|
313
|
+
if (manifestPaths.length > 0) {
|
|
314
|
+
return { ecosystem: 'gomodules', manifestPaths };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (fs.existsSync(path.join(root, 'pyproject.toml')) && fs.existsSync(path.join(root, 'uv.lock'))) {
|
|
318
|
+
const manifestPaths = await discoverUvWorkspaceMembers(root, opts);
|
|
319
|
+
if (manifestPaths.length > 0) {
|
|
320
|
+
return { ecosystem: 'pyproject', manifestPaths };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
274
323
|
const hasJsLock = fs.existsSync(path.join(root, 'pnpm-lock.yaml'))
|
|
275
324
|
|| fs.existsSync(path.join(root, 'yarn.lock'))
|
|
276
325
|
|| fs.existsSync(path.join(root, 'package-lock.json'));
|
|
@@ -380,12 +429,45 @@ function batchError(message, wantMetadata, metadata) {
|
|
|
380
429
|
}
|
|
381
430
|
return err;
|
|
382
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* @overload
|
|
434
|
+
* @param {string} workspaceRoot
|
|
435
|
+
* @param {true} html
|
|
436
|
+
* @param {Options & { batchMetadata: true }} opts
|
|
437
|
+
* @returns {Promise<{ analysis: string, metadata: BatchAnalysisMetadata }>}
|
|
438
|
+
* @throws {Error}
|
|
439
|
+
*/
|
|
440
|
+
/**
|
|
441
|
+
* @overload
|
|
442
|
+
* @param {string} workspaceRoot
|
|
443
|
+
* @param {true} html
|
|
444
|
+
* @param {Options & { batchMetadata?: false }} [opts={}]
|
|
445
|
+
* @returns {Promise<string>}
|
|
446
|
+
* @throws {Error}
|
|
447
|
+
*/
|
|
448
|
+
/**
|
|
449
|
+
* @overload
|
|
450
|
+
* @param {string} workspaceRoot
|
|
451
|
+
* @param {false} html
|
|
452
|
+
* @param {Options & { batchMetadata: true }} opts
|
|
453
|
+
* @returns {Promise<{ analysis: Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>, metadata: BatchAnalysisMetadata }>}
|
|
454
|
+
* @throws {Error}
|
|
455
|
+
*/
|
|
456
|
+
/**
|
|
457
|
+
* @overload
|
|
458
|
+
* @param {string} workspaceRoot
|
|
459
|
+
* @param {false} html
|
|
460
|
+
* @param {Options & { batchMetadata?: false }} [opts={}]
|
|
461
|
+
* @returns {Promise<Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>>}
|
|
462
|
+
* @throws {Error}
|
|
463
|
+
*/
|
|
383
464
|
/**
|
|
384
465
|
* Get stack analysis for all workspace packages/crates (batch).
|
|
385
466
|
* Detects ecosystem from workspace root: Cargo (Cargo.toml + Cargo.lock) or JS/TS (package.json + lock file).
|
|
386
467
|
* SBOMs are generated in parallel (see `batchConcurrency`) unless `continueOnError: false` (fail-fast sequential).
|
|
387
468
|
* With `opts.batchMetadata` / `TRUSTIFY_DA_BATCH_METADATA`, returns `{ analysis, metadata }` including validation and SBOM errors.
|
|
388
469
|
*
|
|
470
|
+
* @overload
|
|
389
471
|
* @param {string} workspaceRoot - Path to workspace root (containing lock file and workspace config)
|
|
390
472
|
* @param {boolean} [html=false] - true returns HTML, false returns JSON report
|
|
391
473
|
* @param {Options} [opts={}] - `batchConcurrency`, discovery ignores, `continueOnError` (default true), `batchMetadata` (default false)
|
|
@@ -416,7 +498,7 @@ async function stackAnalysisBatch(workspaceRoot, html = false, opts = {}) {
|
|
|
416
498
|
}
|
|
417
499
|
}
|
|
418
500
|
if (manifestPaths.length === 0) {
|
|
419
|
-
throw new Error(`No workspace manifests found at ${root}. Ensure Cargo.toml+Cargo.lock or package.json+lock file
|
|
501
|
+
throw new Error(`No workspace manifests found at ${root}. Ensure a supported workspace root exists (Cargo.toml+Cargo.lock, pom.xml, build.gradle, go.work, pyproject.toml+uv.lock, or package.json+lock file).`);
|
|
420
502
|
}
|
|
421
503
|
const workspaceOpts = { ...opts, TRUSTIFY_DA_WORKSPACE_DIR: root };
|
|
422
504
|
const concurrency = resolveBatchConcurrency(opts);
|
|
@@ -135,7 +135,7 @@ function execSyft(imageRef, opts = {}) {
|
|
|
135
135
|
function getSyftEnvs(dockerPath, podmanPath) {
|
|
136
136
|
let path = null;
|
|
137
137
|
if (dockerPath && podmanPath) {
|
|
138
|
-
path = `${dockerPath}${
|
|
138
|
+
path = `${dockerPath}${delimiter}${podmanPath}`;
|
|
139
139
|
}
|
|
140
140
|
else if (dockerPath) {
|
|
141
141
|
path = dockerPath;
|
|
@@ -276,7 +276,16 @@ function podmanGetVariant(opts = {}) {
|
|
|
276
276
|
* @returns {string} - The information
|
|
277
277
|
*/
|
|
278
278
|
function dockerPodmanInfo(dockerSupplier, podmanSupplier, opts = {}) {
|
|
279
|
-
|
|
279
|
+
try {
|
|
280
|
+
const result = dockerSupplier(opts);
|
|
281
|
+
if (result) {
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (_) {
|
|
286
|
+
// docker not available, fall through to podman
|
|
287
|
+
}
|
|
288
|
+
return podmanSupplier(opts);
|
|
280
289
|
}
|
|
281
290
|
/**
|
|
282
291
|
* Gets the digests for an image
|
package/dist/src/provider.js
CHANGED
|
@@ -7,6 +7,9 @@ import Javascript_npm from './providers/javascript_npm.js';
|
|
|
7
7
|
import Javascript_pnpm from './providers/javascript_pnpm.js';
|
|
8
8
|
import Javascript_yarn from './providers/javascript_yarn.js';
|
|
9
9
|
import pythonPipProvider from './providers/python_pip.js';
|
|
10
|
+
import Python_pip_pyproject from './providers/python_pip_pyproject.js';
|
|
11
|
+
import Python_poetry from './providers/python_poetry.js';
|
|
12
|
+
import Python_uv from './providers/python_uv.js';
|
|
10
13
|
import rustCargoProvider from './providers/rust_cargo.js';
|
|
11
14
|
/** @typedef {{ecosystem: string, contentType: string, content: string}} Provided */
|
|
12
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 */
|
|
@@ -23,6 +26,9 @@ export const availableProviders = [
|
|
|
23
26
|
new Javascript_npm(),
|
|
24
27
|
golangGomodulesProvider,
|
|
25
28
|
pythonPipProvider,
|
|
29
|
+
new Python_poetry(),
|
|
30
|
+
new Python_uv(),
|
|
31
|
+
new Python_pip_pyproject(),
|
|
26
32
|
rustCargoProvider
|
|
27
33
|
];
|
|
28
34
|
/**
|
|
@@ -57,15 +57,6 @@ export default class Base_Java {
|
|
|
57
57
|
* @returns string
|
|
58
58
|
*/
|
|
59
59
|
selectToolBinary(manifestPath: string, opts: {}): string | null;
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
* @param {string} startingManifest - the path of the manifest from which to start searching for the wrapper
|
|
63
|
-
* @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback
|
|
64
|
-
* to the root of the drive the manifest is on (assumes absolute path is given)
|
|
65
|
-
* @returns {string|undefined}
|
|
66
|
-
*/
|
|
67
|
-
traverseForWrapper(startingManifest: string, repoRoot?: string): string | undefined;
|
|
68
|
-
normalizePath(thePath: any): string;
|
|
69
60
|
#private;
|
|
70
61
|
}
|
|
71
62
|
export type Provided = import("../provider").Provided;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import { PackageURL } from 'packageurl-js';
|
|
4
|
-
import { getCustomPath,
|
|
3
|
+
import { getCustomPath, getWrapperPreference, invokeCommand, traverseForWrapper } from "../tools.js";
|
|
5
4
|
/** @typedef {import('../provider').Provider} */
|
|
6
5
|
/** @typedef {import('../provider').Provided} Provided */
|
|
7
6
|
/** @typedef {{name: string, version: string}} Package */
|
|
@@ -131,7 +130,7 @@ export default class Base_Java {
|
|
|
131
130
|
const toolPath = getCustomPath(this.globalBinary, opts);
|
|
132
131
|
const useWrapper = getWrapperPreference(this.globalBinary, opts);
|
|
133
132
|
if (useWrapper) {
|
|
134
|
-
const wrapper = this.
|
|
133
|
+
const wrapper = traverseForWrapper(manifestDir, this.localWrapper);
|
|
135
134
|
if (wrapper !== undefined) {
|
|
136
135
|
try {
|
|
137
136
|
this._invokeCommand(wrapper, ['--version'], { cwd: manifestDir });
|
|
@@ -156,39 +155,4 @@ export default class Base_Java {
|
|
|
156
155
|
}
|
|
157
156
|
return toolPath;
|
|
158
157
|
}
|
|
159
|
-
/**
|
|
160
|
-
*
|
|
161
|
-
* @param {string} startingManifest - the path of the manifest from which to start searching for the wrapper
|
|
162
|
-
* @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback
|
|
163
|
-
* to the root of the drive the manifest is on (assumes absolute path is given)
|
|
164
|
-
* @returns {string|undefined}
|
|
165
|
-
*/
|
|
166
|
-
traverseForWrapper(startingManifest, repoRoot = undefined) {
|
|
167
|
-
const normalizedManifest = this.normalizePath(startingManifest);
|
|
168
|
-
const currentDir = this.normalizePath(path.dirname(normalizedManifest));
|
|
169
|
-
repoRoot = repoRoot || getGitRootDir(currentDir) || path.parse(normalizedManifest).root;
|
|
170
|
-
const wrapperPath = path.join(currentDir, this.localWrapper);
|
|
171
|
-
try {
|
|
172
|
-
fs.accessSync(wrapperPath, fs.constants.X_OK);
|
|
173
|
-
return wrapperPath;
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
176
|
-
if (error.code === 'ENOENT') {
|
|
177
|
-
const rootDir = path.parse(currentDir).root;
|
|
178
|
-
if (currentDir === repoRoot || currentDir === rootDir) {
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
|
-
const parentDir = path.dirname(currentDir);
|
|
182
|
-
if (parentDir === currentDir || parentDir === rootDir) {
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
return this.traverseForWrapper(path.join(parentDir, path.basename(normalizedManifest)), repoRoot);
|
|
186
|
-
}
|
|
187
|
-
throw new Error(`failure searching for ${this.localWrapper}`, { cause: error });
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
normalizePath(thePath) {
|
|
191
|
-
const normalized = path.resolve(thePath).normalize();
|
|
192
|
-
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
193
|
-
}
|
|
194
158
|
}
|
|
@@ -67,16 +67,26 @@ export default class Base_javascript {
|
|
|
67
67
|
*/
|
|
68
68
|
isSupported(manifestName: string): boolean;
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* Walks up the directory tree from manifestDir looking for the lock file.
|
|
71
|
+
* Stops when the lock file is found, when a package.json with a "workspaces"
|
|
72
|
+
* field is encountered without a lock file (workspace root boundary), or
|
|
73
|
+
* when the filesystem root is reached.
|
|
74
|
+
*
|
|
75
|
+
* When TRUSTIFY_DA_WORKSPACE_DIR is set, checks only that directory (no walk-up).
|
|
76
|
+
*
|
|
77
|
+
* @param {string} manifestDir - The directory to start searching from
|
|
78
|
+
* @param {Object} [opts={}] - optional; may contain TRUSTIFY_DA_WORKSPACE_DIR
|
|
79
|
+
* @returns {string|null} The directory containing the lock file, or null
|
|
80
|
+
* @protected
|
|
81
|
+
*/
|
|
82
|
+
protected _isWorkspaceRoot(dir: any): string | null;
|
|
83
|
+
_findLockFileDir(manifestDir: any, opts?: {}): string | null;
|
|
84
|
+
/**
|
|
73
85
|
* @param {string} manifestDir - The base directory where the manifest is located
|
|
74
|
-
* @param {
|
|
86
|
+
* @param {Object} [opts={}] - optional; may contain TRUSTIFY_DA_WORKSPACE_DIR
|
|
75
87
|
* @returns {boolean} True if the lock file exists
|
|
76
88
|
*/
|
|
77
|
-
validateLockFile(manifestDir: string, opts?:
|
|
78
|
-
TRUSTIFY_DA_WORKSPACE_DIR?: string;
|
|
79
|
-
}): boolean;
|
|
89
|
+
validateLockFile(manifestDir: string, opts?: any): boolean;
|
|
80
90
|
/**
|
|
81
91
|
* Provides content and content type for stack analysis
|
|
82
92
|
* @param {string} manifestPath - The manifest path or name
|
|
@@ -97,18 +97,63 @@ export default class Base_javascript {
|
|
|
97
97
|
return 'package.json' === manifestName;
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
100
|
+
* Walks up the directory tree from manifestDir looking for the lock file.
|
|
101
|
+
* Stops when the lock file is found, when a package.json with a "workspaces"
|
|
102
|
+
* field is encountered without a lock file (workspace root boundary), or
|
|
103
|
+
* when the filesystem root is reached.
|
|
104
|
+
*
|
|
105
|
+
* When TRUSTIFY_DA_WORKSPACE_DIR is set, checks only that directory (no walk-up).
|
|
106
|
+
*
|
|
107
|
+
* @param {string} manifestDir - The directory to start searching from
|
|
108
|
+
* @param {Object} [opts={}] - optional; may contain TRUSTIFY_DA_WORKSPACE_DIR
|
|
109
|
+
* @returns {string|null} The directory containing the lock file, or null
|
|
110
|
+
* @protected
|
|
111
|
+
*/
|
|
112
|
+
_isWorkspaceRoot(dir) {
|
|
113
|
+
if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
const pkgJsonPath = path.join(dir, 'package.json');
|
|
117
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
118
|
+
try {
|
|
119
|
+
const content = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
120
|
+
if (content.workspaces) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (_) {
|
|
125
|
+
// ignore parse errors
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
_findLockFileDir(manifestDir, opts = {}) {
|
|
131
|
+
const workspaceDir = getCustom('TRUSTIFY_DA_WORKSPACE_DIR', null, opts);
|
|
132
|
+
if (workspaceDir) {
|
|
133
|
+
const dir = path.resolve(workspaceDir);
|
|
134
|
+
return fs.existsSync(path.join(dir, this._lockFileName())) ? dir : null;
|
|
135
|
+
}
|
|
136
|
+
let dir = path.resolve(manifestDir);
|
|
137
|
+
let parent = dir;
|
|
138
|
+
do {
|
|
139
|
+
dir = parent;
|
|
140
|
+
if (fs.existsSync(path.join(dir, this._lockFileName()))) {
|
|
141
|
+
return dir;
|
|
142
|
+
}
|
|
143
|
+
if (this._isWorkspaceRoot(dir)) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
parent = path.dirname(dir);
|
|
147
|
+
} while (parent !== dir);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
103
151
|
* @param {string} manifestDir - The base directory where the manifest is located
|
|
104
|
-
* @param {
|
|
152
|
+
* @param {Object} [opts={}] - optional; may contain TRUSTIFY_DA_WORKSPACE_DIR
|
|
105
153
|
* @returns {boolean} True if the lock file exists
|
|
106
154
|
*/
|
|
107
155
|
validateLockFile(manifestDir, opts = {}) {
|
|
108
|
-
|
|
109
|
-
const dirToCheck = workspaceDir ? path.resolve(workspaceDir) : manifestDir;
|
|
110
|
-
const lock = path.join(dirToCheck, this._lockFileName());
|
|
111
|
-
return fs.existsSync(lock);
|
|
156
|
+
return this._findLockFileDir(manifestDir, opts) !== null;
|
|
112
157
|
}
|
|
113
158
|
/**
|
|
114
159
|
* Provides content and content type for stack analysis
|
|
@@ -171,8 +216,7 @@ export default class Base_javascript {
|
|
|
171
216
|
_buildDependencyTree(includeTransitive, opts = {}) {
|
|
172
217
|
this._version();
|
|
173
218
|
const manifestDir = path.dirname(this.#manifest.manifestPath);
|
|
174
|
-
const
|
|
175
|
-
const cmdDir = workspaceDir ? path.resolve(workspaceDir) : manifestDir;
|
|
219
|
+
const cmdDir = this._findLockFileDir(manifestDir, opts) || manifestDir;
|
|
176
220
|
this.#createLockFile(cmdDir);
|
|
177
221
|
let output = this.#executeListCmd(includeTransitive, cmdDir);
|
|
178
222
|
output = this._parseDepTreeOutput(output);
|
|
@@ -191,9 +235,32 @@ export default class Base_javascript {
|
|
|
191
235
|
let sbom = new Sbom();
|
|
192
236
|
sbom.addRoot(mainComponent, license);
|
|
193
237
|
this._addDependenciesToSbom(sbom, depsObject);
|
|
238
|
+
this.#ensurePeerAndOptionalDeps(sbom);
|
|
194
239
|
sbom.filterIgnoredDeps(this.#manifest.ignored);
|
|
195
240
|
return sbom.getAsJsonString(opts);
|
|
196
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Ensures peer and optional dependencies declared in the manifest are
|
|
244
|
+
* present in the SBOM, even when the package manager does not resolve them
|
|
245
|
+
* (e.g. yarn does not include peer deps in its dependency listing).
|
|
246
|
+
* @param {Sbom} sbom - The SBOM to supplement
|
|
247
|
+
* @private
|
|
248
|
+
*/
|
|
249
|
+
#ensurePeerAndOptionalDeps(sbom) {
|
|
250
|
+
const rootPurl = toPurl(purlType, this.#manifest.name, this.#manifest.version);
|
|
251
|
+
const depSources = [this.#manifest.peerDependencies, this.#manifest.optionalDependencies];
|
|
252
|
+
for (const source of depSources) {
|
|
253
|
+
for (const [name, version] of Object.entries(source)) {
|
|
254
|
+
// Build the purl prefix for exact matching (e.g. "pkg:npm/minimist@"
|
|
255
|
+
// or "pkg:npm/%40hapi/joi@") to avoid substring false positives
|
|
256
|
+
const probe = toPurl(purlType, name, version);
|
|
257
|
+
const purlPrefix = probe.toString().replace(/@[^@]*$/, '@');
|
|
258
|
+
if (!sbom.checkDependsOnByPurlPrefix(rootPurl, purlPrefix)) {
|
|
259
|
+
sbom.addDependency(rootPurl, probe);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
197
264
|
/**
|
|
198
265
|
* Recursively builds the Sbom from the JSON that npm listing returns
|
|
199
266
|
* @param {Sbom} sbom - The SBOM object to add dependencies to
|
|
@@ -201,7 +268,10 @@ export default class Base_javascript {
|
|
|
201
268
|
* @protected
|
|
202
269
|
*/
|
|
203
270
|
_addDependenciesToSbom(sbom, depTree) {
|
|
204
|
-
const dependencies =
|
|
271
|
+
const dependencies = {
|
|
272
|
+
...depTree["dependencies"],
|
|
273
|
+
...depTree["optionalDependencies"],
|
|
274
|
+
};
|
|
205
275
|
Object.entries(dependencies)
|
|
206
276
|
.forEach(entry => {
|
|
207
277
|
const [name, artifact] = entry;
|
|
@@ -251,6 +321,7 @@ export default class Base_javascript {
|
|
|
251
321
|
const rootPurl = toPurlFromString(sbom.getRoot().purl);
|
|
252
322
|
sbom.addDependency(rootPurl, rootDeps.get(key));
|
|
253
323
|
}
|
|
324
|
+
this.#ensurePeerAndOptionalDeps(sbom);
|
|
254
325
|
sbom.filterIgnoredDeps(this.#manifest.ignored);
|
|
255
326
|
return sbom.getAsJsonString(opts);
|
|
256
327
|
}
|
|
@@ -261,10 +332,14 @@ export default class Base_javascript {
|
|
|
261
332
|
* @protected
|
|
262
333
|
*/
|
|
263
334
|
_getRootDependencies(depTree) {
|
|
264
|
-
|
|
335
|
+
const allDeps = {
|
|
336
|
+
...depTree.dependencies,
|
|
337
|
+
...depTree.optionalDependencies,
|
|
338
|
+
};
|
|
339
|
+
if (Object.keys(allDeps).length === 0) {
|
|
265
340
|
return new Map();
|
|
266
341
|
}
|
|
267
|
-
return new Map(Object.entries(
|
|
342
|
+
return new Map(Object.entries(allDeps).map(([key, value]) => [key, toPurl(purlType, key, value.version)]));
|
|
268
343
|
}
|
|
269
344
|
/**
|
|
270
345
|
* Executes the list command to get dependencies
|
|
@@ -275,7 +350,7 @@ export default class Base_javascript {
|
|
|
275
350
|
*/
|
|
276
351
|
#executeListCmd(includeTransitive, manifestDir) {
|
|
277
352
|
const listArgs = this._listCmdArgs(includeTransitive, manifestDir);
|
|
278
|
-
return this.#invokeCommand(listArgs);
|
|
353
|
+
return this.#invokeCommand(listArgs, { cwd: manifestDir });
|
|
279
354
|
}
|
|
280
355
|
/**
|
|
281
356
|
* Gets the version of the package manager
|
|
@@ -294,13 +369,11 @@ export default class Base_javascript {
|
|
|
294
369
|
const originalDir = process.cwd();
|
|
295
370
|
const isWindows = os.platform() === 'win32';
|
|
296
371
|
if (isWindows) {
|
|
297
|
-
// On Windows, --prefix flag doesn't work as expected
|
|
298
|
-
// Instead of installing from the prefix folder, it installs from current working directory
|
|
299
372
|
process.chdir(manifestDir);
|
|
300
373
|
}
|
|
301
374
|
try {
|
|
302
375
|
const args = this._updateLockFileCmdArgs(manifestDir);
|
|
303
|
-
this.#invokeCommand(args);
|
|
376
|
+
this.#invokeCommand(args, { cwd: manifestDir });
|
|
304
377
|
}
|
|
305
378
|
finally {
|
|
306
379
|
if (isWindows) {
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** @typedef {{name: string, version: string, children: string[], hashes?: Array<{alg: string, content: string}>}} GraphEntry */
|
|
2
|
+
/** @typedef {{name: string, version: string, dependencies: DepTreeEntry[]}} DepTreeEntry */
|
|
3
|
+
/** @typedef {{directDeps: string[], graph: Map<string, GraphEntry>}} DependencyData */
|
|
4
|
+
/** @typedef {{ecosystem: string, content: string, contentType: string}} Provided */
|
|
5
|
+
export default class Base_pyproject {
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} manifestName
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
isSupported(manifestName: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} manifestDir
|
|
13
|
+
* @param {Object} [opts={}]
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
validateLockFile(manifestDir: string, opts?: any): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Walk up from manifestDir to find the directory containing the lock file.
|
|
19
|
+
* Follows the same pattern as Base_javascript._findLockFileDir().
|
|
20
|
+
* @param {string} manifestDir
|
|
21
|
+
* @param {Object} [opts={}]
|
|
22
|
+
* @returns {string|null}
|
|
23
|
+
* @protected
|
|
24
|
+
*/
|
|
25
|
+
protected _findLockFileDir(manifestDir: string, opts?: any): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Detect workspace root boundaries.
|
|
28
|
+
* Currently only uv has native workspace support ([tool.uv.workspace] in pyproject.toml).
|
|
29
|
+
* Poetry has no workspace/monorepo support (python-poetry/poetry#2270), so each
|
|
30
|
+
* poetry project is treated independently — see Python_poetry._findLockFileDir().
|
|
31
|
+
* @param {string} dir
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
* @protected
|
|
34
|
+
*/
|
|
35
|
+
protected _isWorkspaceRoot(dir: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Read project license from pyproject.toml, with fallback to LICENSE file.
|
|
38
|
+
* @param {string} manifestPath
|
|
39
|
+
* @returns {string|null}
|
|
40
|
+
*/
|
|
41
|
+
readLicenseFromManifest(manifestPath: string): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} manifest - path to pyproject.toml
|
|
44
|
+
* @param {Object} [opts={}]
|
|
45
|
+
* @returns {Promise<Provided>}
|
|
46
|
+
*/
|
|
47
|
+
provideStack(manifest: string, opts?: any): Promise<Provided>;
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} manifest - path to pyproject.toml
|
|
50
|
+
* @param {Object} [opts={}]
|
|
51
|
+
* @returns {Promise<Provided>}
|
|
52
|
+
*/
|
|
53
|
+
provideComponent(manifest: string, opts?: any): Promise<Provided>;
|
|
54
|
+
/**
|
|
55
|
+
* @returns {string}
|
|
56
|
+
* @protected
|
|
57
|
+
*/
|
|
58
|
+
protected _lockFileName(): string;
|
|
59
|
+
/**
|
|
60
|
+
* @returns {string}
|
|
61
|
+
* @protected
|
|
62
|
+
*/
|
|
63
|
+
protected _cmdName(): string;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve dependencies using the tool-specific command and parser.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} manifestDir - directory containing the target pyproject.toml
|
|
68
|
+
* @param {string} workspaceDir - workspace root (where the lock file lives);
|
|
69
|
+
* only used by providers that need workspace-level resolution (e.g. uv)
|
|
70
|
+
* @param {object} parsed - parsed pyproject.toml
|
|
71
|
+
* @param {Object} opts
|
|
72
|
+
* @returns {Promise<DependencyData>}
|
|
73
|
+
* @protected
|
|
74
|
+
*/
|
|
75
|
+
protected _getDependencyData(manifestDir: string, workspaceDir: string, parsed: object, opts: any): Promise<DependencyData>;
|
|
76
|
+
/**
|
|
77
|
+
* Canonicalize a Python package name per PEP 503.
|
|
78
|
+
* @param {string} name
|
|
79
|
+
* @returns {string}
|
|
80
|
+
* @protected
|
|
81
|
+
*/
|
|
82
|
+
protected _canonicalize(name: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* Get the project name from pyproject.toml.
|
|
85
|
+
* @param {object} parsed
|
|
86
|
+
* @returns {string|null}
|
|
87
|
+
* @protected
|
|
88
|
+
*/
|
|
89
|
+
protected _getProjectName(parsed: object): string | null;
|
|
90
|
+
/**
|
|
91
|
+
* Get the project version from pyproject.toml.
|
|
92
|
+
* @param {object} parsed
|
|
93
|
+
* @returns {string|null}
|
|
94
|
+
* @protected
|
|
95
|
+
*/
|
|
96
|
+
protected _getProjectVersion(parsed: object): string | null;
|
|
97
|
+
/**
|
|
98
|
+
* Scan raw pyproject.toml text for dependencies with ignore markers.
|
|
99
|
+
* @param {string} manifestPath
|
|
100
|
+
* @returns {Set<string>}
|
|
101
|
+
* @protected
|
|
102
|
+
*/
|
|
103
|
+
protected _getIgnoredDeps(manifestPath: string): Set<string>;
|
|
104
|
+
/**
|
|
105
|
+
* Compute the set of graph nodes reachable from direct deps, excluding ignored.
|
|
106
|
+
* @param {Map<string, GraphEntry>} graph
|
|
107
|
+
* @param {string[]} directDeps
|
|
108
|
+
* @param {Set<string>} ignoredDeps
|
|
109
|
+
* @returns {Set<string>}
|
|
110
|
+
* @protected
|
|
111
|
+
*/
|
|
112
|
+
protected _reachableNodes(graph: Map<string, GraphEntry>, directDeps: string[], ignoredDeps: Set<string>): Set<string>;
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} name
|
|
115
|
+
* @param {string} version
|
|
116
|
+
* @returns {PackageURL}
|
|
117
|
+
* @protected
|
|
118
|
+
*/
|
|
119
|
+
protected _toPurl(name: string, version: string): PackageURL;
|
|
120
|
+
/**
|
|
121
|
+
* Create SBOM json string for a pyproject.toml project.
|
|
122
|
+
* @param {string} manifest - path to pyproject.toml
|
|
123
|
+
* @param {Object} opts
|
|
124
|
+
* @param {boolean} includeTransitive
|
|
125
|
+
* @returns {Promise<string>}
|
|
126
|
+
* @private
|
|
127
|
+
*/
|
|
128
|
+
private _createSbom;
|
|
129
|
+
}
|
|
130
|
+
export type GraphEntry = {
|
|
131
|
+
name: string;
|
|
132
|
+
version: string;
|
|
133
|
+
children: string[];
|
|
134
|
+
hashes?: Array<{
|
|
135
|
+
alg: string;
|
|
136
|
+
content: string;
|
|
137
|
+
}>;
|
|
138
|
+
};
|
|
139
|
+
export type DepTreeEntry = {
|
|
140
|
+
name: string;
|
|
141
|
+
version: string;
|
|
142
|
+
dependencies: DepTreeEntry[];
|
|
143
|
+
};
|
|
144
|
+
export type DependencyData = {
|
|
145
|
+
directDeps: string[];
|
|
146
|
+
graph: Map<string, GraphEntry>;
|
|
147
|
+
};
|
|
148
|
+
export type Provided = {
|
|
149
|
+
ecosystem: string;
|
|
150
|
+
content: string;
|
|
151
|
+
contentType: string;
|
|
152
|
+
};
|
|
153
|
+
import { PackageURL } from 'packageurl-js';
|