@trustify-da/trustify-da-javascript-client 0.3.0-ea.d349baa → 0.3.0-ea.d71f957

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 (53) hide show
  1. package/README.md +136 -10
  2. package/dist/package.json +18 -5
  3. package/dist/src/analysis.d.ts +16 -0
  4. package/dist/src/analysis.js +71 -78
  5. package/dist/src/batch_opts.d.ts +24 -0
  6. package/dist/src/batch_opts.js +35 -0
  7. package/dist/src/cli.js +192 -8
  8. package/dist/src/cyclone_dx_sbom.d.ts +10 -1
  9. package/dist/src/cyclone_dx_sbom.js +32 -5
  10. package/dist/src/index.d.ts +67 -4
  11. package/dist/src/index.js +270 -7
  12. package/dist/src/license/index.d.ts +28 -0
  13. package/dist/src/license/index.js +100 -0
  14. package/dist/src/license/license_utils.d.ts +40 -0
  15. package/dist/src/license/license_utils.js +134 -0
  16. package/dist/src/license/licenses_api.d.ts +34 -0
  17. package/dist/src/license/licenses_api.js +98 -0
  18. package/dist/src/license/project_license.d.ts +20 -0
  19. package/dist/src/license/project_license.js +62 -0
  20. package/dist/src/provider.d.ts +17 -5
  21. package/dist/src/provider.js +23 -5
  22. package/dist/src/providers/base_javascript.d.ts +29 -7
  23. package/dist/src/providers/base_javascript.js +129 -22
  24. package/dist/src/providers/golang_gomodules.d.ts +19 -12
  25. package/dist/src/providers/golang_gomodules.js +112 -114
  26. package/dist/src/providers/gomod_parser.d.ts +4 -0
  27. package/dist/src/providers/gomod_parser.js +16 -0
  28. package/dist/src/providers/java_gradle.d.ts +6 -0
  29. package/dist/src/providers/java_gradle.js +12 -2
  30. package/dist/src/providers/java_maven.d.ts +8 -1
  31. package/dist/src/providers/java_maven.js +32 -4
  32. package/dist/src/providers/javascript_pnpm.d.ts +1 -1
  33. package/dist/src/providers/javascript_pnpm.js +2 -2
  34. package/dist/src/providers/manifest.d.ts +2 -0
  35. package/dist/src/providers/manifest.js +22 -4
  36. package/dist/src/providers/processors/yarn_berry_processor.js +82 -3
  37. package/dist/src/providers/python_controller.d.ts +5 -2
  38. package/dist/src/providers/python_controller.js +56 -58
  39. package/dist/src/providers/python_pip.d.ts +11 -4
  40. package/dist/src/providers/python_pip.js +47 -54
  41. package/dist/src/providers/requirements_parser.d.ts +6 -0
  42. package/dist/src/providers/requirements_parser.js +24 -0
  43. package/dist/src/providers/rust_cargo.d.ts +52 -0
  44. package/dist/src/providers/rust_cargo.js +614 -0
  45. package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
  46. package/dist/src/providers/tree-sitter-requirements.wasm +0 -0
  47. package/dist/src/sbom.d.ts +10 -1
  48. package/dist/src/sbom.js +12 -2
  49. package/dist/src/tools.d.ts +18 -0
  50. package/dist/src/tools.js +56 -1
  51. package/dist/src/workspace.d.ts +61 -0
  52. package/dist/src/workspace.js +256 -0
  53. package/package.json +19 -6
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Client for the Trustify DA backend License Analysis API (POST /api/v5/licenses).
3
+ * The same license data shape is returned in the dependency analysis JSON report (result.licenses).
4
+ * @see https://github.com/guacsec/trustify-dependency-analytics#license-analysis-apiv5licenses
5
+ * @see https://github.com/guacsec/trustify-da-api-spec/blob/main/api/v5/openapi.yaml
6
+ */
7
+ import { PackageURL } from 'packageurl-js';
8
+ import { selectTrustifyDABackend } from '../index.js';
9
+ import { addProxyAgent, getTokenHeaders } from '../tools.js';
10
+ /**
11
+ * Fetch license details by SPDX identifier from the backend GET /api/v5/licenses/{spdx}.
12
+ * Returns detailed information about a specific license including category, name, and text.
13
+ *
14
+ * @param {string} spdxId - SPDX identifier (e.g., "Apache-2.0", "MIT")
15
+ * @param {import('../index.js').Options} [opts={}] - options (proxy, token, TRUSTIFY_DA_BACKEND_URL, etc.)
16
+ * @returns {Promise<Object|null>} License details or null if not found
17
+ */
18
+ export async function getLicenseDetails(spdxId, opts = {}) {
19
+ if (!spdxId) {
20
+ return null;
21
+ }
22
+ const url = selectTrustifyDABackend(opts);
23
+ const finalUrl = new URL(`${url}/api/v5/licenses/${encodeURIComponent(spdxId)}`);
24
+ const fetchOptions = addProxyAgent({
25
+ method: 'GET',
26
+ headers: {
27
+ 'Accept': 'application/json',
28
+ ...getTokenHeaders(opts)
29
+ },
30
+ }, opts);
31
+ try {
32
+ const resp = await fetch(finalUrl, fetchOptions);
33
+ if (!resp.ok) {
34
+ const errorText = await resp.text().catch(() => '');
35
+ throw new Error(`HTTP ${resp.status}: ${errorText || resp.statusText}`);
36
+ }
37
+ return await resp.json();
38
+ }
39
+ catch (err) {
40
+ throw new Error(`Failed to fetch license details: ${err.message}`);
41
+ }
42
+ }
43
+ function normalizePurlString(purl) {
44
+ const parsed = PackageURL.fromString(purl);
45
+ return new PackageURL(parsed.type, parsed.namespace, parsed.name, parsed.version, null, null).toString();
46
+ }
47
+ /**
48
+ * Normalize the LicensesResponse shape (array of LicenseProviderResult) into a map of purl -> license info.
49
+ * Each provider result has { status, summary, packages } where packages is { [purl]: { concluded, evidence } }.
50
+ * We merge the first successful provider's packages; concluded has identifiers[], category (PERMISSIVE | WEAK_COPYLEFT | STRONG_COPYLEFT | UNKNOWN).
51
+ *
52
+ * @param {unknown} data - LicensesResponse (array) or analysis report's licenses field
53
+ * @param {string[]} [purls] - optional list of purls to restrict to (for consistency with getLicensesByPurl)
54
+ * @returns {Map<string, { licenses: string[], category?: string }>}
55
+ */
56
+ export function normalizeLicensesResponse(data, purls = []) {
57
+ const map = new Map();
58
+ if (!data || !Array.isArray(data)) {
59
+ return map;
60
+ }
61
+ const normalizedPurlsSet = purls.length > 0 ? new Set(purls.map(normalizePurlString)) : null;
62
+ for (const providerResult of data) {
63
+ const packages = providerResult?.packages;
64
+ if (!packages || typeof packages !== 'object') {
65
+ continue;
66
+ }
67
+ for (const [purl, pkgLicense] of Object.entries(packages)) {
68
+ const concluded = pkgLicense?.concluded;
69
+ const identifiers = Array.isArray(concluded?.identifiers) ? concluded.identifiers : [];
70
+ const expression = concluded?.expression;
71
+ const licenses = identifiers.length > 0 ? identifiers : (expression ? [expression] : []);
72
+ const category = concluded?.category; // PERMISSIVE | WEAK_COPYLEFT | STRONG_COPYLEFT | UNKNOWN
73
+ const normalizedPurl = normalizePurlString(purl);
74
+ if (normalizedPurlsSet === null || normalizedPurlsSet.has(normalizedPurl)) {
75
+ map.set(normalizedPurl, { licenses: licenses.filter(Boolean), category });
76
+ }
77
+ }
78
+ // Use first provider that has packages; backend may return multiple (e.g. deps.dev)
79
+ if (map.size > 0) {
80
+ break;
81
+ }
82
+ }
83
+ return map;
84
+ }
85
+ /**
86
+ * Build license map from an analysis report that already includes license data (result.licenses).
87
+ * Use this when the dependency analysis response already contains the licenses array to avoid a second request.
88
+ *
89
+ * @param {import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport} analysisReport - full analysis JSON
90
+ * @param {string[]} [purls] - optional list of purls to restrict to
91
+ * @returns {Map<string, { licenses: string[], category?: string }>}
92
+ */
93
+ export function licensesFromReport(analysisReport, purls = []) {
94
+ if (!analysisReport?.licenses) {
95
+ return new Map();
96
+ }
97
+ return normalizeLicensesResponse(analysisReport.licenses, purls);
98
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Resolve project license from manifest and from LICENSE / LICENSE.md in manifest dir or git root.
3
+ * Uses local pattern matching for LICENSE file identification (synchronous).
4
+ * For more accurate backend-based identification, use identifyLicense() separately.
5
+ * @param {string} manifestPath - path to manifest
6
+ * @returns {{ fromManifest: string|null, fromFile: string|null, mismatch: boolean }}
7
+ */
8
+ export function getProjectLicense(manifestPath: string): {
9
+ fromManifest: string | null;
10
+ fromFile: string | null;
11
+ mismatch: boolean;
12
+ };
13
+ /**
14
+ * Call backend /licenses/identify endpoint to identify license from file.
15
+ * @param {string} licenseFilePath - path to LICENSE file
16
+ * @param {{}} [opts={}] - options (proxy, token, etc.)
17
+ * @returns {Promise<string|null>} - SPDX identifier or null
18
+ */
19
+ export function identifyLicense(licenseFilePath: string, opts?: {}): Promise<string | null>;
20
+ export { findLicenseFilePath, readLicenseFile } from "./license_utils.js";
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Resolves the project license from the manifest and from a LICENSE / LICENSE.md file.
3
+ * Used to report manifest-vs-file mismatch and as the baseline for dependency license compatibility.
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { selectTrustifyDABackend } from '../index.js';
8
+ import { matchForLicense, availableProviders } from '../provider.js';
9
+ import { addProxyAgent, getTokenHeaders } from '../tools.js';
10
+ import { normalizeSpdx, readLicenseFile } from './license_utils.js';
11
+ /**
12
+ * Resolve project license from manifest and from LICENSE / LICENSE.md in manifest dir or git root.
13
+ * Uses local pattern matching for LICENSE file identification (synchronous).
14
+ * For more accurate backend-based identification, use identifyLicense() separately.
15
+ * @param {string} manifestPath - path to manifest
16
+ * @returns {{ fromManifest: string|null, fromFile: string|null, mismatch: boolean }}
17
+ */
18
+ export function getProjectLicense(manifestPath) {
19
+ const resolved = path.resolve(manifestPath);
20
+ const provider = matchForLicense(resolved, availableProviders);
21
+ const fromManifest = provider.readLicenseFromManifest(resolved);
22
+ const fromFile = readLicenseFile(resolved);
23
+ const mismatch = Boolean(fromManifest && fromFile && normalizeSpdx(fromManifest) !== normalizeSpdx(fromFile));
24
+ return {
25
+ fromManifest: fromManifest || null,
26
+ fromFile: fromFile || null,
27
+ mismatch
28
+ };
29
+ }
30
+ export { findLicenseFilePath, readLicenseFile } from './license_utils.js';
31
+ /**
32
+ * Call backend /licenses/identify endpoint to identify license from file.
33
+ * @param {string} licenseFilePath - path to LICENSE file
34
+ * @param {{}} [opts={}] - options (proxy, token, etc.)
35
+ * @returns {Promise<string|null>} - SPDX identifier or null
36
+ */
37
+ export async function identifyLicense(licenseFilePath, opts = {}) {
38
+ try {
39
+ const fileContent = fs.readFileSync(licenseFilePath);
40
+ const backendUrl = selectTrustifyDABackend(opts);
41
+ const url = new URL(`${backendUrl}/api/v5/licenses/identify`);
42
+ const tokenHeaders = getTokenHeaders(opts);
43
+ const fetchOptions = addProxyAgent({
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/octet-stream',
47
+ ...tokenHeaders,
48
+ },
49
+ body: fileContent,
50
+ }, opts);
51
+ const resp = await fetch(url, fetchOptions);
52
+ if (!resp.ok) {
53
+ return null; // Fallback to local detection on error
54
+ }
55
+ const data = await resp.json();
56
+ // Extract SPDX identifier from backend response
57
+ return data?.license?.id || data?.spdx_id || data?.identifier || null;
58
+ }
59
+ catch {
60
+ return null; // Fallback to local detection on error
61
+ }
62
+ }
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Match a provider by manifest type only (no lock file check). Used for license reading.
3
+ * @param {string} manifestPath - path or name of the manifest
4
+ * @param {[Provider]} providers - list of providers to iterate over
5
+ * @returns {Provider}
6
+ * @throws {Error} when the manifest is not supported and no provider was matched
7
+ */
8
+ export function matchForLicense(manifestPath: string, providers: [Provider]): Provider;
1
9
  /**
2
10
  * Match a provider from a list or providers based on file type.
3
11
  * Each provider MUST export 'isSupported' taking a file name-type and returning true if supported.
@@ -5,12 +13,15 @@
5
13
  * Each provider MUST export 'provideStack' taking manifest path returning a {@link Provided}.
6
14
  * @param {string} manifest - the name-type or path of the manifest
7
15
  * @param {[Provider]} providers - list of providers to iterate over
16
+ * @param {{TRUSTIFY_DA_WORKSPACE_DIR?: string}} [opts={}] - optional; TRUSTIFY_DA_WORKSPACE_DIR overrides lock file location for workspaces
8
17
  * @returns {Provider}
9
18
  * @throws {Error} when the manifest is not supported and no provider was matched
10
19
  */
11
- export function match(manifest: string, providers: [Provider]): Provider;
20
+ export function match(manifest: string, providers: [Provider], opts?: {
21
+ TRUSTIFY_DA_WORKSPACE_DIR?: string;
22
+ }): Provider;
12
23
  /** @typedef {{ecosystem: string, contentType: string, content: string}} Provided */
13
- /** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string): void, provideComponent: function(string, {}): Provided, provideStack: function(string, {}): Provided}} 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): string | null}} Provider */
14
25
  /**
15
26
  * MUST include all providers here.
16
27
  * @type {[Provider]}
@@ -23,7 +34,8 @@ export type Provided = {
23
34
  };
24
35
  export type Provider = {
25
36
  isSupported: (arg0: string) => boolean;
26
- validateLockFile: (arg0: string) => void;
27
- provideComponent: (arg0: string, arg1: {}) => Provided;
28
- provideStack: (arg0: string, arg1: {}) => Provided;
37
+ validateLockFile: (arg0: string, arg1: any) => void;
38
+ provideComponent: (arg0: string, arg1: {}) => Provided | Promise<Provided>;
39
+ provideStack: (arg0: string, arg1: {}) => Provided | Promise<Provided>;
40
+ readLicenseFromManifest: (arg0: string) => string | null;
29
41
  };
@@ -7,8 +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 rustCargoProvider from './providers/rust_cargo.js';
10
11
  /** @typedef {{ecosystem: string, contentType: string, content: string}} Provided */
11
- /** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string): void, provideComponent: function(string, {}): Provided, provideStack: function(string, {}): Provided}} Provider */
12
+ /** @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 */
12
13
  /**
13
14
  * MUST include all providers here.
14
15
  * @type {[Provider]}
@@ -17,12 +18,28 @@ export const availableProviders = [
17
18
  new Java_maven(),
18
19
  new Java_gradle_groovy(),
19
20
  new Java_gradle_kotlin(),
20
- new Javascript_npm(),
21
21
  new Javascript_pnpm(),
22
22
  new Javascript_yarn(),
23
+ new Javascript_npm(),
23
24
  golangGomodulesProvider,
24
- pythonPipProvider
25
+ pythonPipProvider,
26
+ rustCargoProvider
25
27
  ];
28
+ /**
29
+ * Match a provider by manifest type only (no lock file check). Used for license reading.
30
+ * @param {string} manifestPath - path or name of the manifest
31
+ * @param {[Provider]} providers - list of providers to iterate over
32
+ * @returns {Provider}
33
+ * @throws {Error} when the manifest is not supported and no provider was matched
34
+ */
35
+ export function matchForLicense(manifestPath, providers) {
36
+ const base = path.parse(manifestPath).base;
37
+ const provider = providers.find(prov => prov.isSupported(base));
38
+ if (!provider) {
39
+ throw new Error(`${base} is not supported`);
40
+ }
41
+ return provider;
42
+ }
26
43
  /**
27
44
  * Match a provider from a list or providers based on file type.
28
45
  * Each provider MUST export 'isSupported' taking a file name-type and returning true if supported.
@@ -30,16 +47,17 @@ export const availableProviders = [
30
47
  * Each provider MUST export 'provideStack' taking manifest path returning a {@link Provided}.
31
48
  * @param {string} manifest - the name-type or path of the manifest
32
49
  * @param {[Provider]} providers - list of providers to iterate over
50
+ * @param {{TRUSTIFY_DA_WORKSPACE_DIR?: string}} [opts={}] - optional; TRUSTIFY_DA_WORKSPACE_DIR overrides lock file location for workspaces
33
51
  * @returns {Provider}
34
52
  * @throws {Error} when the manifest is not supported and no provider was matched
35
53
  */
36
- export function match(manifest, providers) {
54
+ export function match(manifest, providers, opts = {}) {
37
55
  const manifestPath = path.parse(manifest);
38
56
  const supported = providers.filter(prov => prov.isSupported(manifestPath.base));
39
57
  if (supported.length === 0) {
40
58
  throw new Error(`${manifestPath.base} is not supported`);
41
59
  }
42
- const provider = supported.find(prov => prov.validateLockFile(manifestPath.dir));
60
+ const provider = supported.find(prov => prov.validateLockFile(manifestPath.dir, opts));
43
61
  if (!provider) {
44
62
  throw new Error(`${manifestPath.base} requires a lock file. Use your preferred package manager to generate the lock file.`);
45
63
  }
@@ -1,5 +1,6 @@
1
- /** @typedef {import('../provider.js').Provider} Provider */
2
- /** @typedef {import('../provider.js').Provided} Provided */
1
+ export type purlType = import("../provider").Provider;
2
+ /** @typedef {import('../provider').Provider} */
3
+ /** @typedef {import('../provider').Provided} Provided */
3
4
  /**
4
5
  * The ecosystem identifier for JavaScript/npm packages
5
6
  * @type {string}
@@ -66,11 +67,26 @@ export default class Base_javascript {
66
67
  */
67
68
  isSupported(manifestName: string): boolean;
68
69
  /**
69
- * Checks if a required lock file exists in the same path as the manifest
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
+ /**
70
85
  * @param {string} manifestDir - The base directory where the manifest is located
86
+ * @param {Object} [opts={}] - optional; may contain TRUSTIFY_DA_WORKSPACE_DIR
71
87
  * @returns {boolean} True if the lock file exists
72
88
  */
73
- validateLockFile(manifestDir: string): boolean;
89
+ validateLockFile(manifestDir: string, opts?: any): boolean;
74
90
  /**
75
91
  * Provides content and content type for stack analysis
76
92
  * @param {string} manifestPath - The manifest path or name
@@ -85,13 +101,20 @@ export default class Base_javascript {
85
101
  * @returns {Provided} The provided data for component analysis
86
102
  */
87
103
  provideComponent(manifestPath: string, opts?: any): Provided;
104
+ /**
105
+ * Read license from manifest (package.json). Reused by npm, pnpm, yarn.
106
+ * @param {string} manifestPath - path to package.json
107
+ * @returns {string|null}
108
+ */
109
+ readLicenseFromManifest(manifestPath: string): string | null;
88
110
  /**
89
111
  * Builds the dependency tree for the project
90
112
  * @param {boolean} includeTransitive - Whether to include transitive dependencies
113
+ * @param {Object} [opts={}] - Configuration options; when `TRUSTIFY_DA_WORKSPACE_DIR` is set, commands run from workspace root
91
114
  * @returns {Object} The dependency tree
92
115
  * @protected
93
116
  */
94
- protected _buildDependencyTree(includeTransitive: boolean): any;
117
+ protected _buildDependencyTree(includeTransitive: boolean, opts?: any): any;
95
118
  /**
96
119
  * Recursively builds the Sbom from the JSON that npm listing returns
97
120
  * @param {Sbom} sbom - The SBOM object to add dependencies to
@@ -121,7 +144,6 @@ export default class Base_javascript {
121
144
  protected _parseDepTreeOutput(output: string): string;
122
145
  #private;
123
146
  }
124
- export type Provider = import("../provider.js").Provider;
125
- export type Provided = import("../provider.js").Provided;
147
+ export type Provided = import("../provider").Provided;
126
148
  import Manifest from './manifest.js';
127
149
  import Sbom from '../sbom.js';
@@ -1,11 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import os from "node:os";
3
3
  import path from 'node:path';
4
+ import { getLicense } from '../license/license_utils.js';
4
5
  import Sbom from '../sbom.js';
5
- import { getCustom, getCustomPath, invokeCommand, toPurl, toPurlFromString } from "../tools.js";
6
+ import { getCustom, getCustomPath, invokeCommand, toPurl, toPurlFromString } from '../tools.js';
6
7
  import Manifest from './manifest.js';
7
- /** @typedef {import('../provider.js').Provider} Provider */
8
- /** @typedef {import('../provider.js').Provided} Provided */
8
+ /** @typedef {import('../provider').Provider} */
9
+ /** @typedef {import('../provider').Provided} Provided */
9
10
  /**
10
11
  * The ecosystem identifier for JavaScript/npm packages
11
12
  * @type {string}
@@ -96,13 +97,63 @@ export default class Base_javascript {
96
97
  return 'package.json' === manifestName;
97
98
  }
98
99
  /**
99
- * Checks if a required lock file exists in the same path as the manifest
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
+ /**
100
151
  * @param {string} manifestDir - The base directory where the manifest is located
152
+ * @param {Object} [opts={}] - optional; may contain TRUSTIFY_DA_WORKSPACE_DIR
101
153
  * @returns {boolean} True if the lock file exists
102
154
  */
103
- validateLockFile(manifestDir) {
104
- const lock = path.join(manifestDir, this._lockFileName());
105
- return fs.existsSync(lock);
155
+ validateLockFile(manifestDir, opts = {}) {
156
+ return this._findLockFileDir(manifestDir, opts) !== null;
106
157
  }
107
158
  /**
108
159
  * Provides content and content type for stack analysis
@@ -132,17 +183,42 @@ export default class Base_javascript {
132
183
  contentType: 'application/vnd.cyclonedx+json'
133
184
  };
134
185
  }
186
+ /**
187
+ * Read license from manifest (package.json). Reused by npm, pnpm, yarn.
188
+ * @param {string} manifestPath - path to package.json
189
+ * @returns {string|null}
190
+ */
191
+ readLicenseFromManifest(manifestPath) {
192
+ let manifestLicense;
193
+ try {
194
+ const content = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
195
+ if (typeof content.license === 'string') {
196
+ manifestLicense = content.license.trim() || null;
197
+ }
198
+ else if (Array.isArray(content.licenses) && content.licenses.length > 0) {
199
+ const first = content.licenses[0];
200
+ const name = first.type || first.name;
201
+ manifestLicense = (typeof name === 'string' ? name.trim() : null);
202
+ }
203
+ }
204
+ catch {
205
+ manifestLicense = null;
206
+ }
207
+ return getLicense(manifestLicense, manifestPath);
208
+ }
135
209
  /**
136
210
  * Builds the dependency tree for the project
137
211
  * @param {boolean} includeTransitive - Whether to include transitive dependencies
212
+ * @param {Object} [opts={}] - Configuration options; when `TRUSTIFY_DA_WORKSPACE_DIR` is set, commands run from workspace root
138
213
  * @returns {Object} The dependency tree
139
214
  * @protected
140
215
  */
141
- _buildDependencyTree(includeTransitive) {
216
+ _buildDependencyTree(includeTransitive, opts = {}) {
142
217
  this._version();
143
- let manifestDir = path.dirname(this.#manifest.manifestPath);
144
- this.#createLockFile(manifestDir);
145
- let output = this.#executeListCmd(includeTransitive, manifestDir);
218
+ const manifestDir = path.dirname(this.#manifest.manifestPath);
219
+ const cmdDir = this._findLockFileDir(manifestDir, opts) || manifestDir;
220
+ this.#createLockFile(cmdDir);
221
+ let output = this.#executeListCmd(includeTransitive, cmdDir);
146
222
  output = this._parseDepTreeOutput(output);
147
223
  return JSON.parse(output);
148
224
  }
@@ -153,14 +229,38 @@ export default class Base_javascript {
153
229
  * @private
154
230
  */
155
231
  #getSBOM(opts = {}) {
156
- const depsObject = this._buildDependencyTree(true);
232
+ const depsObject = this._buildDependencyTree(true, opts);
157
233
  let mainComponent = toPurl(purlType, this.#manifest.name, this.#manifest.version);
234
+ const license = this.readLicenseFromManifest(this.#manifest.manifestPath);
158
235
  let sbom = new Sbom();
159
- sbom.addRoot(mainComponent);
236
+ sbom.addRoot(mainComponent, license);
160
237
  this._addDependenciesToSbom(sbom, depsObject);
238
+ this.#ensurePeerAndOptionalDeps(sbom);
161
239
  sbom.filterIgnoredDeps(this.#manifest.ignored);
162
240
  return sbom.getAsJsonString(opts);
163
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
+ }
164
264
  /**
165
265
  * Recursively builds the Sbom from the JSON that npm listing returns
166
266
  * @param {Sbom} sbom - The SBOM object to add dependencies to
@@ -168,7 +268,10 @@ export default class Base_javascript {
168
268
  * @protected
169
269
  */
170
270
  _addDependenciesToSbom(sbom, depTree) {
171
- const dependencies = depTree["dependencies"] || {};
271
+ const dependencies = {
272
+ ...depTree["dependencies"],
273
+ ...depTree["optionalDependencies"],
274
+ };
172
275
  Object.entries(dependencies)
173
276
  .forEach(entry => {
174
277
  const [name, artifact] = entry;
@@ -204,10 +307,11 @@ export default class Base_javascript {
204
307
  * @private
205
308
  */
206
309
  #getDirectDependencySbom(opts = {}) {
207
- const depTree = this._buildDependencyTree(false);
310
+ const depTree = this._buildDependencyTree(false, opts);
208
311
  let mainComponent = toPurl(purlType, this.#manifest.name, this.#manifest.version);
312
+ const license = this.readLicenseFromManifest(this.#manifest.manifestPath);
209
313
  let sbom = new Sbom();
210
- sbom.addRoot(mainComponent);
314
+ sbom.addRoot(mainComponent, license);
211
315
  const rootDeps = this._getRootDependencies(depTree);
212
316
  const sortedDepsKeys = Array
213
317
  .from(rootDeps.keys())
@@ -217,6 +321,7 @@ export default class Base_javascript {
217
321
  const rootPurl = toPurlFromString(sbom.getRoot().purl);
218
322
  sbom.addDependency(rootPurl, rootDeps.get(key));
219
323
  }
324
+ this.#ensurePeerAndOptionalDeps(sbom);
220
325
  sbom.filterIgnoredDeps(this.#manifest.ignored);
221
326
  return sbom.getAsJsonString(opts);
222
327
  }
@@ -227,10 +332,14 @@ export default class Base_javascript {
227
332
  * @protected
228
333
  */
229
334
  _getRootDependencies(depTree) {
230
- if (!depTree.dependencies) {
335
+ const allDeps = {
336
+ ...depTree.dependencies,
337
+ ...depTree.optionalDependencies,
338
+ };
339
+ if (Object.keys(allDeps).length === 0) {
231
340
  return new Map();
232
341
  }
233
- 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)]));
234
343
  }
235
344
  /**
236
345
  * Executes the list command to get dependencies
@@ -241,7 +350,7 @@ export default class Base_javascript {
241
350
  */
242
351
  #executeListCmd(includeTransitive, manifestDir) {
243
352
  const listArgs = this._listCmdArgs(includeTransitive, manifestDir);
244
- return this.#invokeCommand(listArgs);
353
+ return this.#invokeCommand(listArgs, { cwd: manifestDir });
245
354
  }
246
355
  /**
247
356
  * Gets the version of the package manager
@@ -260,13 +369,11 @@ export default class Base_javascript {
260
369
  const originalDir = process.cwd();
261
370
  const isWindows = os.platform() === 'win32';
262
371
  if (isWindows) {
263
- // On Windows, --prefix flag doesn't work as expected
264
- // Instead of installing from the prefix folder, it installs from current working directory
265
372
  process.chdir(manifestDir);
266
373
  }
267
374
  try {
268
375
  const args = this._updateLockFileCmdArgs(manifestDir);
269
- this.#invokeCommand(args);
376
+ this.#invokeCommand(args, { cwd: manifestDir });
270
377
  }
271
378
  finally {
272
379
  if (isWindows) {
@@ -3,6 +3,7 @@ declare namespace _default {
3
3
  export { validateLockFile };
4
4
  export { provideComponent };
5
5
  export { provideStack };
6
+ export { readLicenseFromManifest };
6
7
  }
7
8
  export default _default;
8
9
  export type Provided = import("../provider").Provided;
@@ -18,25 +19,31 @@ export type Dependency = {
18
19
  ignore: boolean;
19
20
  };
20
21
  /**
21
- * @param {string} manifestName - the subject manifest name-type
22
- * @returns {boolean} - return true if `pom.xml` is the manifest name-type
23
- */
22
+ * @param {string} manifestName the subject manifest name-type
23
+ * @returns {boolean} return true if `pom.xml` is the manifest name-type
24
+ */
24
25
  declare function isSupported(manifestName: string): boolean;
25
26
  /**
26
- * @param {string} manifestDir - the directory where the manifest lies
27
+ * @param {string} manifestDir the directory where the manifest lies
27
28
  */
28
29
  declare function validateLockFile(): boolean;
29
30
  /**
30
31
  * Provide content and content type for maven-maven component analysis.
31
- * @param {string} manifest - path to go.mod for component report
32
- * @param {{}} [opts={}] - optional various options to pass along the application
33
- * @returns {Provided}
32
+ * @param {string} manifest path to go.mod for component report
33
+ * @param {{}} [opts={}] optional various options to pass along the application
34
+ * @returns {Promise<Provided>}
34
35
  */
35
- declare function provideComponent(manifest: string, opts?: {}): Provided;
36
+ declare function provideComponent(manifest: string, opts?: {}): Promise<Provided>;
36
37
  /**
37
38
  * Provide content and content type for maven-maven stack analysis.
38
- * @param {string} manifest - the manifest path or name
39
- * @param {{}} [opts={}] - optional various options to pass along the application
40
- * @returns {Provided}
39
+ * @param {string} manifest the manifest path or name
40
+ * @param {{}} [opts={}] optional various options to pass along the application
41
+ * @returns {Promise<Provided>}
41
42
  */
42
- declare function provideStack(manifest: string, opts?: {}): Provided;
43
+ declare function provideStack(manifest: string, opts?: {}): Promise<Provided>;
44
+ /**
45
+ * Go modules have no standard license field in go.mod
46
+ * @param {string} manifestPath path to go.mod
47
+ * @returns {string|null}
48
+ */
49
+ declare function readLicenseFromManifest(manifestPath: string): string | null;