@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.
- package/LICENSE +201 -0
- package/README.md +482 -0
- package/config/config.properties +1 -0
- package/dist/package.json +106 -0
- package/dist/src/analysis.d.ts +43 -0
- package/dist/src/analysis.js +252 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +102 -0
- package/dist/src/cyclone_dx_sbom.d.ts +77 -0
- package/dist/src/cyclone_dx_sbom.js +244 -0
- package/dist/src/index.d.ts +82 -0
- package/dist/src/index.js +194 -0
- package/dist/src/oci_image/images.d.ts +99 -0
- package/dist/src/oci_image/images.js +263 -0
- package/dist/src/oci_image/platform.d.ts +59 -0
- package/dist/src/oci_image/platform.js +138 -0
- package/dist/src/oci_image/utils.d.ts +42 -0
- package/dist/src/oci_image/utils.js +496 -0
- package/dist/src/provider.d.ts +29 -0
- package/dist/src/provider.js +47 -0
- package/dist/src/providers/base_java.d.ts +85 -0
- package/dist/src/providers/base_java.js +191 -0
- package/dist/src/providers/base_javascript.d.ts +127 -0
- package/dist/src/providers/base_javascript.js +350 -0
- package/dist/src/providers/golang_gomodules.d.ts +42 -0
- package/dist/src/providers/golang_gomodules.js +403 -0
- package/dist/src/providers/java_gradle.d.ts +35 -0
- package/dist/src/providers/java_gradle.js +399 -0
- package/dist/src/providers/java_gradle_groovy.d.ts +7 -0
- package/dist/src/providers/java_gradle_groovy.js +19 -0
- package/dist/src/providers/java_gradle_kotlin.d.ts +11 -0
- package/dist/src/providers/java_gradle_kotlin.js +23 -0
- package/dist/src/providers/java_maven.d.ts +52 -0
- package/dist/src/providers/java_maven.js +263 -0
- package/dist/src/providers/javascript_npm.d.ts +4 -0
- package/dist/src/providers/javascript_npm.js +15 -0
- package/dist/src/providers/javascript_pnpm.d.ts +5 -0
- package/dist/src/providers/javascript_pnpm.js +22 -0
- package/dist/src/providers/javascript_yarn.d.ts +11 -0
- package/dist/src/providers/javascript_yarn.js +39 -0
- package/dist/src/providers/manifest.d.ts +11 -0
- package/dist/src/providers/manifest.js +48 -0
- package/dist/src/providers/processors/yarn_berry_processor.d.ts +41 -0
- package/dist/src/providers/processors/yarn_berry_processor.js +130 -0
- package/dist/src/providers/processors/yarn_classic_processor.d.ts +37 -0
- package/dist/src/providers/processors/yarn_classic_processor.js +109 -0
- package/dist/src/providers/processors/yarn_processor.d.ts +9 -0
- package/dist/src/providers/processors/yarn_processor.js +20 -0
- package/dist/src/providers/python_controller.d.ts +31 -0
- package/dist/src/providers/python_controller.js +406 -0
- package/dist/src/providers/python_pip.d.ts +35 -0
- package/dist/src/providers/python_pip.js +227 -0
- package/dist/src/sbom.d.ts +59 -0
- package/dist/src/sbom.js +84 -0
- package/dist/src/tools.d.ts +74 -0
- package/dist/src/tools.js +159 -0
- 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
|
+
}
|