@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.
Files changed (50) hide show
  1. package/README.md +73 -8
  2. package/dist/package.json +6 -5
  3. package/dist/src/analysis.js +3 -2
  4. package/dist/src/cli.js +51 -2
  5. package/dist/src/cyclone_dx_sbom.d.ts +14 -1
  6. package/dist/src/cyclone_dx_sbom.js +32 -5
  7. package/dist/src/index.d.ts +74 -3
  8. package/dist/src/index.js +87 -5
  9. package/dist/src/oci_image/utils.js +11 -2
  10. package/dist/src/provider.js +6 -0
  11. package/dist/src/providers/base_java.d.ts +0 -9
  12. package/dist/src/providers/base_java.js +2 -38
  13. package/dist/src/providers/base_javascript.d.ts +17 -7
  14. package/dist/src/providers/base_javascript.js +90 -17
  15. package/dist/src/providers/base_pyproject.d.ts +153 -0
  16. package/dist/src/providers/base_pyproject.js +315 -0
  17. package/dist/src/providers/golang_gomodules.d.ts +21 -12
  18. package/dist/src/providers/golang_gomodules.js +164 -118
  19. package/dist/src/providers/gomod_parser.d.ts +4 -0
  20. package/dist/src/providers/gomod_parser.js +16 -0
  21. package/dist/src/providers/java_gradle.d.ts +19 -0
  22. package/dist/src/providers/java_gradle.js +114 -0
  23. package/dist/src/providers/java_maven.d.ts +8 -0
  24. package/dist/src/providers/java_maven.js +93 -1
  25. package/dist/src/providers/javascript_npm.d.ts +1 -0
  26. package/dist/src/providers/javascript_npm.js +21 -0
  27. package/dist/src/providers/javascript_pnpm.js +6 -2
  28. package/dist/src/providers/manifest.d.ts +2 -0
  29. package/dist/src/providers/manifest.js +22 -4
  30. package/dist/src/providers/marker_evaluator.d.ts +14 -0
  31. package/dist/src/providers/marker_evaluator.js +191 -0
  32. package/dist/src/providers/processors/yarn_berry_processor.js +88 -5
  33. package/dist/src/providers/python_controller.d.ts +5 -1
  34. package/dist/src/providers/python_controller.js +8 -4
  35. package/dist/src/providers/python_pip.d.ts +4 -0
  36. package/dist/src/providers/python_pip.js +5 -5
  37. package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
  38. package/dist/src/providers/python_pip_pyproject.js +144 -0
  39. package/dist/src/providers/python_poetry.d.ts +75 -0
  40. package/dist/src/providers/python_poetry.js +238 -0
  41. package/dist/src/providers/python_uv.d.ts +55 -0
  42. package/dist/src/providers/python_uv.js +227 -0
  43. package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
  44. package/dist/src/sbom.d.ts +14 -1
  45. package/dist/src/sbom.js +13 -2
  46. package/dist/src/tools.d.ts +26 -0
  47. package/dist/src/tools.js +58 -0
  48. package/dist/src/workspace.d.ts +9 -0
  49. package/dist/src/workspace.js +1 -1
  50. 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 exist.`);
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}${File.pathSeparator}${podmanPath}`;
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
- return dockerSupplier(opts) || podmanSupplier(opts);
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
@@ -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, getGitRootDir, getWrapperPreference, invokeCommand } from "../tools.js";
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.traverseForWrapper(manifestPath);
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
- * Checks if a required lock file exists in the manifest directory or at the workspace root.
71
- * When TRUSTIFY_DA_WORKSPACE_DIR is provided (via env var or opts),
72
- * checks only that directory for the lock file.
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 {{TRUSTIFY_DA_WORKSPACE_DIR?: string}} [opts={}] - optional workspace root
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
- * Checks if a required lock file exists in the manifest directory or at the workspace root.
101
- * When TRUSTIFY_DA_WORKSPACE_DIR is provided (via env var or opts),
102
- * checks only that directory for the lock file.
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 {{TRUSTIFY_DA_WORKSPACE_DIR?: string}} [opts={}] - optional workspace root
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
- const workspaceDir = getCustom('TRUSTIFY_DA_WORKSPACE_DIR', null, opts);
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 workspaceDir = getCustom('TRUSTIFY_DA_WORKSPACE_DIR', null, opts);
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 = depTree["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
- if (!depTree.dependencies) {
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(depTree.dependencies).map(([key, value]) => [key, toPurl(purlType, key, value.version)]));
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';