@trustify-da/trustify-da-javascript-client 0.2.4-ea.13

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 (57) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +482 -0
  3. package/config/config.properties +1 -0
  4. package/dist/package.json +106 -0
  5. package/dist/src/analysis.d.ts +43 -0
  6. package/dist/src/analysis.js +252 -0
  7. package/dist/src/cli.d.ts +2 -0
  8. package/dist/src/cli.js +102 -0
  9. package/dist/src/cyclone_dx_sbom.d.ts +77 -0
  10. package/dist/src/cyclone_dx_sbom.js +244 -0
  11. package/dist/src/index.d.ts +82 -0
  12. package/dist/src/index.js +194 -0
  13. package/dist/src/oci_image/images.d.ts +99 -0
  14. package/dist/src/oci_image/images.js +263 -0
  15. package/dist/src/oci_image/platform.d.ts +59 -0
  16. package/dist/src/oci_image/platform.js +138 -0
  17. package/dist/src/oci_image/utils.d.ts +42 -0
  18. package/dist/src/oci_image/utils.js +496 -0
  19. package/dist/src/provider.d.ts +29 -0
  20. package/dist/src/provider.js +47 -0
  21. package/dist/src/providers/base_java.d.ts +85 -0
  22. package/dist/src/providers/base_java.js +191 -0
  23. package/dist/src/providers/base_javascript.d.ts +127 -0
  24. package/dist/src/providers/base_javascript.js +350 -0
  25. package/dist/src/providers/golang_gomodules.d.ts +42 -0
  26. package/dist/src/providers/golang_gomodules.js +403 -0
  27. package/dist/src/providers/java_gradle.d.ts +35 -0
  28. package/dist/src/providers/java_gradle.js +399 -0
  29. package/dist/src/providers/java_gradle_groovy.d.ts +7 -0
  30. package/dist/src/providers/java_gradle_groovy.js +19 -0
  31. package/dist/src/providers/java_gradle_kotlin.d.ts +11 -0
  32. package/dist/src/providers/java_gradle_kotlin.js +23 -0
  33. package/dist/src/providers/java_maven.d.ts +52 -0
  34. package/dist/src/providers/java_maven.js +263 -0
  35. package/dist/src/providers/javascript_npm.d.ts +4 -0
  36. package/dist/src/providers/javascript_npm.js +15 -0
  37. package/dist/src/providers/javascript_pnpm.d.ts +5 -0
  38. package/dist/src/providers/javascript_pnpm.js +22 -0
  39. package/dist/src/providers/javascript_yarn.d.ts +11 -0
  40. package/dist/src/providers/javascript_yarn.js +39 -0
  41. package/dist/src/providers/manifest.d.ts +11 -0
  42. package/dist/src/providers/manifest.js +48 -0
  43. package/dist/src/providers/processors/yarn_berry_processor.d.ts +41 -0
  44. package/dist/src/providers/processors/yarn_berry_processor.js +130 -0
  45. package/dist/src/providers/processors/yarn_classic_processor.d.ts +37 -0
  46. package/dist/src/providers/processors/yarn_classic_processor.js +109 -0
  47. package/dist/src/providers/processors/yarn_processor.d.ts +9 -0
  48. package/dist/src/providers/processors/yarn_processor.js +20 -0
  49. package/dist/src/providers/python_controller.d.ts +31 -0
  50. package/dist/src/providers/python_controller.js +406 -0
  51. package/dist/src/providers/python_pip.d.ts +35 -0
  52. package/dist/src/providers/python_pip.js +227 -0
  53. package/dist/src/sbom.d.ts +59 -0
  54. package/dist/src/sbom.js +84 -0
  55. package/dist/src/tools.d.ts +74 -0
  56. package/dist/src/tools.js +159 -0
  57. package/package.json +106 -0
@@ -0,0 +1,191 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { PackageURL } from 'packageurl-js';
4
+ import { getCustomPath, getGitRootDir, getWrapperPreference, invokeCommand } from "../tools.js";
5
+ /** @typedef {import('../provider').Provider} */
6
+ /** @typedef {import('../provider').Provided} Provided */
7
+ /** @typedef {{name: string, version: string}} Package */
8
+ /** @typedef {{groupId: string, artifactId: string, version: string, scope: string, ignore: boolean}} Dependency */
9
+ /**
10
+ * @type {string} ecosystem for java maven packages.
11
+ * @private
12
+ */
13
+ export const ecosystem_maven = 'maven';
14
+ export const ecosystem_gradle = 'gradle';
15
+ export default class Base_Java {
16
+ DEP_REGEX = /(([-a-zA-Z0-9._]{2,})|[0-9])/g;
17
+ CONFLICT_REGEX = /.*omitted for conflict with (\S+)\)/;
18
+ globalBinary;
19
+ localWrapper;
20
+ /**
21
+ *
22
+ * @param {string} globalBinary name of the global binary
23
+ * @param {string} localWrapper name of the local wrapper filename
24
+ */
25
+ constructor(globalBinary, localWrapper) {
26
+ this.globalBinary = globalBinary;
27
+ this.localWrapper = localWrapper;
28
+ }
29
+ /**
30
+ * Recursively populates the SBOM instance with the parsed graph
31
+ * @param {string} src - Source dependency to start the calculations from
32
+ * @param {number} srcDepth - Current depth in the graph for the given source
33
+ * @param {Array} lines - Array containing the text files being parsed
34
+ * @param {Sbom} sbom - The SBOM where the dependencies are being added
35
+ */
36
+ parseDependencyTree(src, srcDepth, lines, sbom) {
37
+ if (lines.length === 0) {
38
+ return;
39
+ }
40
+ if ((lines.length === 1 && lines[0].trim() === "")) {
41
+ return;
42
+ }
43
+ let index = 0;
44
+ let target = lines[index];
45
+ let targetDepth = this.#getDepth(target);
46
+ while (targetDepth > srcDepth && index < lines.length) {
47
+ if (targetDepth === srcDepth + 1) {
48
+ let from = this.parseDep(src);
49
+ let to = this.parseDep(target);
50
+ let matchedScope = target.match(/:compile|:provided|:runtime|:test|:system|:import/g);
51
+ let matchedScopeSrc = src.match(/:compile|:provided|:runtime|:test|:system|:import/g);
52
+ // only add dependency to sbom if it's not with test scope or if it's root
53
+ if ((matchedScope && matchedScope[0] !== ":test" && (matchedScopeSrc && matchedScopeSrc[0] !== ":test")) || (srcDepth === 0 && matchedScope && matchedScope[0] !== ":test")) {
54
+ sbom.addDependency(from, to);
55
+ }
56
+ }
57
+ else {
58
+ this.parseDependencyTree(lines[index - 1], this.#getDepth(lines[index - 1]), lines.slice(index), sbom);
59
+ }
60
+ target = lines[++index];
61
+ targetDepth = this.#getDepth(target);
62
+ }
63
+ }
64
+ /**
65
+ * Calculates how deep in the graph is the given line
66
+ * @param {string} line - line to calculate the depth from
67
+ * @returns {number} The calculated depth
68
+ * @private
69
+ */
70
+ #getDepth(line) {
71
+ if (line === undefined) {
72
+ return -1;
73
+ }
74
+ return ((line.indexOf('-') - 1) / 3) + 1;
75
+ }
76
+ /**
77
+ * Create a PackageURL from any line in a Text Graph dependency tree for a manifest path.
78
+ * @param {string} line - line to parse from a dependencies.txt file
79
+ * @returns {PackageURL} The parsed packageURL
80
+ */
81
+ parseDep(line) {
82
+ let match = line.match(this.DEP_REGEX);
83
+ if (!match) {
84
+ throw new Error(`Unable generate SBOM from dependency tree. Line: ${line} cannot be parsed into a PackageURL`);
85
+ }
86
+ let version;
87
+ if (match.length >= 5 && ['compile', 'provided', 'runtime'].includes(match[5])) {
88
+ version = `${match[4]}-${match[3]}`;
89
+ }
90
+ else {
91
+ version = match[3];
92
+ }
93
+ let override = line.match(this.CONFLICT_REGEX);
94
+ if (override) {
95
+ version = override[1];
96
+ }
97
+ return this.toPurl(match[0], match[1], version);
98
+ }
99
+ /**
100
+ * Returns a PackageUrl For Java maven dependencies
101
+ * @param group
102
+ * @param artifact
103
+ * @param version
104
+ * @return {PackageURL}
105
+ */
106
+ toPurl(group, artifact, version) {
107
+ if (typeof version === "number") {
108
+ version = version.toString();
109
+ }
110
+ return new PackageURL('maven', group, artifact, version, undefined, undefined);
111
+ }
112
+ /** This method invokes command string in a process in a synchronous way.
113
+ * Exists for stubbing in tests.
114
+ * @param bin - the command to be invoked
115
+ * @param args - the args to pass to the binary
116
+ * @param {import('child_process').ExecFileOptionsWithStringEncoding} [opts={}]
117
+ * @protected
118
+ */
119
+ _invokeCommand(bin, args, opts = {}) { return invokeCommand(bin, args, opts); }
120
+ /**
121
+ *
122
+ * @param {string} manifestPath
123
+ * @param {{}} opts
124
+ * @returns string
125
+ */
126
+ selectToolBinary(manifestPath, opts) {
127
+ const manifestDir = path.dirname(manifestPath);
128
+ const toolPath = getCustomPath(this.globalBinary, opts);
129
+ const useWrapper = getWrapperPreference(this.globalBinary, opts);
130
+ if (useWrapper) {
131
+ const wrapper = this.traverseForWrapper(manifestPath);
132
+ if (wrapper !== undefined) {
133
+ try {
134
+ this._invokeCommand(wrapper, ['--version'], { cwd: manifestDir });
135
+ }
136
+ catch (error) {
137
+ throw new Error(`failed to check for ${this.localWrapper}`, { cause: error });
138
+ }
139
+ return wrapper;
140
+ }
141
+ }
142
+ // verify tool is accessible, if wrapper was not requested or not found
143
+ try {
144
+ this._invokeCommand(toolPath, ['--version'], { cwd: manifestDir });
145
+ }
146
+ catch (error) {
147
+ if (error.code === 'ENOENT') {
148
+ throw new Error((useWrapper ? `${this.localWrapper} not found and ` : '') + `${this.globalBinary === 'mvn' ? 'maven' : 'gradle'} not found at ${toolPath}`);
149
+ }
150
+ else {
151
+ throw new Error(`failed to check for ${this.globalBinary === 'mvn' ? 'maven' : 'gradle'}`, { cause: error });
152
+ }
153
+ }
154
+ return toolPath;
155
+ }
156
+ /**
157
+ *
158
+ * @param {string} startingManifest - the path of the manifest from which to start searching for the wrapper
159
+ * @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback
160
+ * to the root of the drive the manifest is on (assumes absolute path is given)
161
+ * @returns {string|undefined}
162
+ */
163
+ traverseForWrapper(startingManifest, repoRoot = undefined) {
164
+ const normalizedManifest = this.normalizePath(startingManifest);
165
+ const currentDir = this.normalizePath(path.dirname(normalizedManifest));
166
+ repoRoot = repoRoot || getGitRootDir(currentDir) || path.parse(normalizedManifest).root;
167
+ const wrapperPath = path.join(currentDir, this.localWrapper);
168
+ try {
169
+ fs.accessSync(wrapperPath, fs.constants.X_OK);
170
+ return wrapperPath;
171
+ }
172
+ catch (error) {
173
+ if (error.code === 'ENOENT') {
174
+ const rootDir = path.parse(currentDir).root;
175
+ if (currentDir === repoRoot || currentDir === rootDir) {
176
+ return undefined;
177
+ }
178
+ const parentDir = path.dirname(currentDir);
179
+ if (parentDir === currentDir || parentDir === rootDir) {
180
+ return undefined;
181
+ }
182
+ return this.traverseForWrapper(path.join(parentDir, path.basename(normalizedManifest)), repoRoot);
183
+ }
184
+ throw new Error(`failure searching for ${this.localWrapper}`, { cause: error });
185
+ }
186
+ }
187
+ normalizePath(thePath) {
188
+ const normalized = path.resolve(thePath).normalize();
189
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
190
+ }
191
+ }
@@ -0,0 +1,127 @@
1
+ /** @typedef {import('../provider.js').Provider} Provider */
2
+ /** @typedef {import('../provider.js').Provided} Provided */
3
+ /**
4
+ * The ecosystem identifier for JavaScript/npm packages
5
+ * @type {string}
6
+ */
7
+ export const purlType: string;
8
+ /**
9
+ * Base class for JavaScript package manager providers.
10
+ * This class provides common functionality for different JavaScript package managers
11
+ * (npm, pnpm, yarn) to generate SBOMs and handle package dependencies.
12
+ * @abstract
13
+ */
14
+ export default class Base_javascript {
15
+ /**
16
+ * Sets up the provider with the manifest path and options
17
+ * @param {string} manifestPath - Path to the package.json manifest file
18
+ * @param {Object} opts - Configuration options for the provider
19
+ * @protected
20
+ */
21
+ protected _setUp(manifestPath: string, opts: any): void;
22
+ /**
23
+ * Gets the current manifest object
24
+ * @returns {Manifest} The manifest object
25
+ * @protected
26
+ */
27
+ protected _getManifest(): Manifest;
28
+ /**
29
+ * Sets the ecosystem value
30
+ * @param {string} ecosystem - The ecosystem identifier
31
+ * @protected
32
+ */
33
+ protected _setEcosystem(ecosystem: string): void;
34
+ /**
35
+ * Returns the name of the lock file for the specific implementation
36
+ * @returns {string} The lock file name
37
+ * @abstract
38
+ * @protected
39
+ */
40
+ protected _lockFileName(): string;
41
+ /**
42
+ * Returns the command name to use for the specific JS package manager
43
+ * @returns {string} The command name
44
+ * @abstract
45
+ * @protected
46
+ */
47
+ protected _cmdName(): string;
48
+ /**
49
+ * Returns the command arguments for listing dependencies
50
+ * @returns {Array<string>} The command arguments
51
+ * @abstract
52
+ * @protected
53
+ */
54
+ protected _listCmdArgs(): Array<string>;
55
+ /**
56
+ * Returns the command arguments for updating the lock file
57
+ * @returns {Array<string>} The command arguments
58
+ * @abstract
59
+ * @protected
60
+ */
61
+ protected _updateLockFileCmdArgs(): Array<string>;
62
+ /**
63
+ * Checks if the provider supports the given manifest name
64
+ * @param {string} manifestName - The manifest name to check
65
+ * @returns {boolean} True if the manifest is supported
66
+ */
67
+ isSupported(manifestName: string): boolean;
68
+ /**
69
+ * Checks if a required lock file exists in the same path as the manifest
70
+ * @param {string} manifestDir - The base directory where the manifest is located
71
+ * @returns {boolean} True if the lock file exists
72
+ */
73
+ validateLockFile(manifestDir: string): boolean;
74
+ /**
75
+ * Provides content and content type for stack analysis
76
+ * @param {string} manifestPath - The manifest path or name
77
+ * @param {Object} [opts={}] - Optional configuration options
78
+ * @returns {Provided} The provided data for stack analysis
79
+ */
80
+ provideStack(manifestPath: string, opts?: any): Provided;
81
+ /**
82
+ * Provides content and content type for component analysis
83
+ * @param {string} manifestPath - Path to package.json for component report
84
+ * @param {Object} [opts={}] - Optional configuration options
85
+ * @returns {Provided} The provided data for component analysis
86
+ */
87
+ provideComponent(manifestPath: string, opts?: any): Provided;
88
+ /**
89
+ * Builds the dependency tree for the project
90
+ * @param {boolean} includeTransitive - Whether to include transitive dependencies
91
+ * @returns {Object} The dependency tree
92
+ * @protected
93
+ */
94
+ protected _buildDependencyTree(includeTransitive: boolean): any;
95
+ /**
96
+ * Recursively builds the Sbom from the JSON that npm listing returns
97
+ * @param {Sbom} sbom - The SBOM object to add dependencies to
98
+ * @param {Object} depTree - The current dependency tree
99
+ * @protected
100
+ */
101
+ protected _addDependenciesToSbom(sbom: Sbom, depTree: any): void;
102
+ /**
103
+ * Extracts root dependencies from the dependency tree
104
+ * @param {Object} depTree - The dependency tree object
105
+ * @returns {Map<string, PackageURL>} Map of dependency names to their PackageURL objects
106
+ * @protected
107
+ */
108
+ protected _getRootDependencies(depTree: any): Map<string, PackageURL>;
109
+ /**
110
+ * Gets the version of the package manager
111
+ * @returns {string} The version string of the package manager
112
+ * @protected
113
+ */
114
+ protected _version(): string;
115
+ /**
116
+ * Parses the dependency tree output
117
+ * @param {string} output - The output to parse
118
+ * @returns {string} The parsed output
119
+ * @protected
120
+ */
121
+ protected _parseDepTreeOutput(output: string): string;
122
+ #private;
123
+ }
124
+ export type Provider = import('../provider.js').Provider;
125
+ export type Provided = import('../provider.js').Provided;
126
+ import Manifest from './manifest.js';
127
+ import Sbom from '../sbom.js';
@@ -0,0 +1,350 @@
1
+ import fs from 'node:fs';
2
+ import os from "node:os";
3
+ import path from 'node:path';
4
+ import Sbom from '../sbom.js';
5
+ import { getCustom, getCustomPath, invokeCommand, toPurl, toPurlFromString } from "../tools.js";
6
+ import Manifest from './manifest.js';
7
+ /** @typedef {import('../provider.js').Provider} Provider */
8
+ /** @typedef {import('../provider.js').Provided} Provided */
9
+ /**
10
+ * The ecosystem identifier for JavaScript/npm packages
11
+ * @type {string}
12
+ */
13
+ export const purlType = 'npm';
14
+ /**
15
+ * Base class for JavaScript package manager providers.
16
+ * This class provides common functionality for different JavaScript package managers
17
+ * (npm, pnpm, yarn) to generate SBOMs and handle package dependencies.
18
+ * @abstract
19
+ */
20
+ export default class Base_javascript {
21
+ /** @type {Manifest} */
22
+ #manifest;
23
+ /** @type {string} */
24
+ #cmd;
25
+ /** @type {string} */
26
+ #ecosystem;
27
+ /**
28
+ * Sets up the provider with the manifest path and options
29
+ * @param {string} manifestPath - Path to the package.json manifest file
30
+ * @param {Object} opts - Configuration options for the provider
31
+ * @protected
32
+ */
33
+ _setUp(manifestPath, opts) {
34
+ this.#cmd = getCustomPath(this._cmdName(), opts);
35
+ this.#manifest = new Manifest(manifestPath);
36
+ this.#ecosystem = purlType;
37
+ }
38
+ /**
39
+ * Gets the current manifest object
40
+ * @returns {Manifest} The manifest object
41
+ * @protected
42
+ */
43
+ _getManifest() {
44
+ return this.#manifest;
45
+ }
46
+ /**
47
+ * Sets the ecosystem value
48
+ * @param {string} ecosystem - The ecosystem identifier
49
+ * @protected
50
+ */
51
+ _setEcosystem(ecosystem) {
52
+ this.#ecosystem = ecosystem;
53
+ }
54
+ /**
55
+ * Returns the name of the lock file for the specific implementation
56
+ * @returns {string} The lock file name
57
+ * @abstract
58
+ * @protected
59
+ */
60
+ _lockFileName() {
61
+ throw new TypeError("_lockFileName must be implemented");
62
+ }
63
+ /**
64
+ * Returns the command name to use for the specific JS package manager
65
+ * @returns {string} The command name
66
+ * @abstract
67
+ * @protected
68
+ */
69
+ _cmdName() {
70
+ throw new TypeError("_cmdName must be implemented");
71
+ }
72
+ /**
73
+ * Returns the command arguments for listing dependencies
74
+ * @returns {Array<string>} The command arguments
75
+ * @abstract
76
+ * @protected
77
+ */
78
+ _listCmdArgs() {
79
+ throw new TypeError("_listCmdArgs must be implemented");
80
+ }
81
+ /**
82
+ * Returns the command arguments for updating the lock file
83
+ * @returns {Array<string>} The command arguments
84
+ * @abstract
85
+ * @protected
86
+ */
87
+ _updateLockFileCmdArgs() {
88
+ throw new TypeError("_updateLockFileCmdArgs must be implemented");
89
+ }
90
+ /**
91
+ * Checks if the provider supports the given manifest name
92
+ * @param {string} manifestName - The manifest name to check
93
+ * @returns {boolean} True if the manifest is supported
94
+ */
95
+ isSupported(manifestName) {
96
+ return 'package.json' === manifestName;
97
+ }
98
+ /**
99
+ * Checks if a required lock file exists in the same path as the manifest
100
+ * @param {string} manifestDir - The base directory where the manifest is located
101
+ * @returns {boolean} True if the lock file exists
102
+ */
103
+ validateLockFile(manifestDir) {
104
+ const lock = path.join(manifestDir, this._lockFileName());
105
+ return fs.existsSync(lock);
106
+ }
107
+ /**
108
+ * Provides content and content type for stack analysis
109
+ * @param {string} manifestPath - The manifest path or name
110
+ * @param {Object} [opts={}] - Optional configuration options
111
+ * @returns {Provided} The provided data for stack analysis
112
+ */
113
+ provideStack(manifestPath, opts = {}) {
114
+ this._setUp(manifestPath, opts);
115
+ return {
116
+ ecosystem: this.#ecosystem,
117
+ content: this.#getSBOM(opts),
118
+ contentType: 'application/vnd.cyclonedx+json'
119
+ };
120
+ }
121
+ /**
122
+ * Provides content and content type for component analysis
123
+ * @param {string} manifestPath - Path to package.json for component report
124
+ * @param {Object} [opts={}] - Optional configuration options
125
+ * @returns {Provided} The provided data for component analysis
126
+ */
127
+ provideComponent(manifestPath, opts = {}) {
128
+ this._setUp(manifestPath, opts);
129
+ return {
130
+ ecosystem: this.#ecosystem,
131
+ content: this.#getDirectDependencySbom(opts),
132
+ contentType: 'application/vnd.cyclonedx+json'
133
+ };
134
+ }
135
+ /**
136
+ * Builds the dependency tree for the project
137
+ * @param {boolean} includeTransitive - Whether to include transitive dependencies
138
+ * @returns {Object} The dependency tree
139
+ * @protected
140
+ */
141
+ _buildDependencyTree(includeTransitive) {
142
+ this._version();
143
+ let manifestDir = path.dirname(this.#manifest.manifestPath);
144
+ this.#createLockFile(manifestDir);
145
+ let output = this.#executeListCmd(includeTransitive, manifestDir);
146
+ output = this._parseDepTreeOutput(output);
147
+ return JSON.parse(output);
148
+ }
149
+ /**
150
+ * Creates SBOM json string for npm Package
151
+ * @param {Object} [opts={}] - Optional configuration options
152
+ * @returns {string} The SBOM json content
153
+ * @private
154
+ */
155
+ #getSBOM(opts = {}) {
156
+ const depsObject = this._buildDependencyTree(true);
157
+ let mainComponent = toPurl(purlType, this.#manifest.name, this.#manifest.version);
158
+ let sbom = new Sbom();
159
+ sbom.addRoot(mainComponent);
160
+ this._addDependenciesToSbom(sbom, depsObject);
161
+ sbom.filterIgnoredDeps(this.#manifest.ignored);
162
+ return sbom.getAsJsonString(opts);
163
+ }
164
+ /**
165
+ * Recursively builds the Sbom from the JSON that npm listing returns
166
+ * @param {Sbom} sbom - The SBOM object to add dependencies to
167
+ * @param {Object} depTree - The current dependency tree
168
+ * @protected
169
+ */
170
+ _addDependenciesToSbom(sbom, depTree) {
171
+ const dependencies = depTree["dependencies"] || {};
172
+ Object.entries(dependencies)
173
+ .forEach(entry => {
174
+ const [name, artifact] = entry;
175
+ const target = toPurl(purlType, name, artifact.version);
176
+ const rootPurl = toPurl(purlType, this.#manifest.name, this.#manifest.version);
177
+ sbom.addDependency(rootPurl, target);
178
+ this.#addDependenciesOf(sbom, target, artifact);
179
+ });
180
+ }
181
+ /**
182
+ * Adds dependencies of a specific package to the SBOM
183
+ * @param {Sbom} sbom - The SBOM object to add dependencies to
184
+ * @param {PackageURL} from - The package URL to add dependencies for
185
+ * @param {Object} artifact - The artifact containing dependencies
186
+ * @private
187
+ */
188
+ #addDependenciesOf(sbom, from, artifact) {
189
+ const deps = artifact.dependencies || {};
190
+ Object.entries(deps)
191
+ .forEach(entry => {
192
+ const [name, depArtifact] = entry;
193
+ if (depArtifact.version !== undefined) {
194
+ const target = toPurl(purlType, name, depArtifact.version);
195
+ sbom.addDependency(from, target);
196
+ this.#addDependenciesOf(sbom, target, depArtifact);
197
+ }
198
+ });
199
+ }
200
+ /**
201
+ * Creates a SBOM containing only direct dependencies
202
+ * @param {Object} [opts={}] - Optional configuration options
203
+ * @returns {string} The SBOM as a JSON string
204
+ * @private
205
+ */
206
+ #getDirectDependencySbom(opts = {}) {
207
+ const depTree = this._buildDependencyTree(false);
208
+ let mainComponent = toPurl(purlType, this.#manifest.name, this.#manifest.version);
209
+ let sbom = new Sbom();
210
+ sbom.addRoot(mainComponent);
211
+ const rootDeps = this._getRootDependencies(depTree);
212
+ const sortedDepsKeys = Array
213
+ .from(rootDeps.keys())
214
+ .filter(key => this.#manifest.dependencies.includes(key))
215
+ .sort();
216
+ for (const key of sortedDepsKeys) {
217
+ const rootPurl = toPurlFromString(sbom.getRoot().purl);
218
+ sbom.addDependency(rootPurl, rootDeps.get(key));
219
+ }
220
+ sbom.filterIgnoredDeps(this.#manifest.ignored);
221
+ return sbom.getAsJsonString(opts);
222
+ }
223
+ /**
224
+ * Extracts root dependencies from the dependency tree
225
+ * @param {Object} depTree - The dependency tree object
226
+ * @returns {Map<string, PackageURL>} Map of dependency names to their PackageURL objects
227
+ * @protected
228
+ */
229
+ _getRootDependencies(depTree) {
230
+ if (!depTree.dependencies) {
231
+ return new Map();
232
+ }
233
+ return new Map(Object.entries(depTree.dependencies).map(([key, value]) => [key, toPurl(purlType, key, value.version)]));
234
+ }
235
+ /**
236
+ * Executes the list command to get dependencies
237
+ * @param {boolean} includeTransitive - Whether to include transitive dependencies
238
+ * @param {string} manifestDir - The manifest directory
239
+ * @returns {string} The command output
240
+ * @private
241
+ */
242
+ #executeListCmd(includeTransitive, manifestDir) {
243
+ const listArgs = this._listCmdArgs(includeTransitive, manifestDir);
244
+ return this.#invokeCommand(listArgs);
245
+ }
246
+ /**
247
+ * Gets the version of the package manager
248
+ * @returns {string} The version string of the package manager
249
+ * @protected
250
+ */
251
+ _version() {
252
+ return this.#invokeCommand(['--version']);
253
+ }
254
+ /**
255
+ * Creates or updates the lock file for the package manager
256
+ * @param {string} manifestDir - Directory containing the manifest file
257
+ * @private
258
+ */
259
+ #createLockFile(manifestDir) {
260
+ const originalDir = process.cwd();
261
+ const isWindows = os.platform() === 'win32';
262
+ 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
+ process.chdir(manifestDir);
266
+ }
267
+ try {
268
+ const args = this._updateLockFileCmdArgs(manifestDir);
269
+ this.#invokeCommand(args);
270
+ }
271
+ finally {
272
+ if (isWindows) {
273
+ process.chdir(originalDir);
274
+ }
275
+ }
276
+ }
277
+ /**
278
+ * Invokes a command with the given arguments
279
+ * @param {string[]} args - Command arguments
280
+ * @param {Object} [opts={}] - Optional configuration options
281
+ * @returns {string} Command output
282
+ * @throws {Error} If command execution fails or command is not found
283
+ * @private
284
+ */
285
+ #invokeCommand(args, opts = {}) {
286
+ try {
287
+ if (!opts.cwd) {
288
+ opts.cwd = path.dirname(this.#manifest.manifestPath);
289
+ }
290
+ // Add version manager paths for JavaScript package managers
291
+ if (process.platform !== 'win32') {
292
+ const versionManagerPaths = [];
293
+ // Add fnm path if available
294
+ const fnmDir = getCustom('FNM_DIR', null, opts);
295
+ if (fnmDir) {
296
+ versionManagerPaths.push(`${fnmDir}/current/bin`);
297
+ }
298
+ // Add nvm path if available
299
+ const nvmDir = getCustom('NVM_DIR', null, opts);
300
+ if (nvmDir) {
301
+ versionManagerPaths.push(`${nvmDir}/current/bin`);
302
+ }
303
+ // Add local node_modules/.bin path
304
+ const localBinPath = path.join(opts.cwd, 'node_modules', '.bin');
305
+ if (fs.existsSync(localBinPath)) {
306
+ versionManagerPaths.push(localBinPath);
307
+ }
308
+ if (versionManagerPaths.length > 0) {
309
+ opts = {
310
+ ...opts,
311
+ env: {
312
+ ...opts.env,
313
+ PATH: `${versionManagerPaths.join(path.delimiter)}${path.delimiter}${process.env.PATH}`
314
+ }
315
+ };
316
+ }
317
+ }
318
+ // Try to find the command in the following order:
319
+ // 1. Custom path from environment/opts (via getCustomPath)
320
+ // 2. Local node_modules/.bin
321
+ // 3. Global installation
322
+ let cmd = this.#cmd;
323
+ if (!fs.existsSync(cmd)) {
324
+ const localCmd = path.join(opts.cwd, 'node_modules', '.bin', this._cmdName());
325
+ if (fs.existsSync(localCmd)) {
326
+ cmd = localCmd;
327
+ }
328
+ }
329
+ return invokeCommand(cmd, args, opts);
330
+ }
331
+ catch (error) {
332
+ if (error.code === 'ENOENT') {
333
+ throw new Error(`${this.#cmd} is not accessible. Please ensure it is installed via npm, corepack, or your version manager.`);
334
+ }
335
+ if (error.code === 'EACCES') {
336
+ throw new Error(`Permission denied when executing ${this.#cmd}. Please check file permissions.`);
337
+ }
338
+ throw new Error(`Failed to execute ${this.#cmd} ${args.join(' ')}`, { cause: error });
339
+ }
340
+ }
341
+ /**
342
+ * Parses the dependency tree output
343
+ * @param {string} output - The output to parse
344
+ * @returns {string} The parsed output
345
+ * @protected
346
+ */
347
+ _parseDepTreeOutput(output) {
348
+ return output;
349
+ }
350
+ }