@@ -3,7 +3,7 @@ import os from "node:os";
3
3
import path from 'node:path'
4
4
import { PackageURL } from 'packageurl-js'
5
5
6
- import { invokeCommand } from "../tools.js" ;
6
+ import { getCustomPath , invokeCommand } from "../tools.js" ;
7
7
import Sbom from '../sbom.js'
8
8
9
9
/** @typedef {import('../provider.js').Provider } */
@@ -15,197 +15,200 @@ const defaultVersion = 'v0.0.0'
15
15
16
16
export default class Base_javascript {
17
17
18
- /**
18
+ // Resolved cmd to use
19
+ #cmd;
20
+
21
+ /**
19
22
* @returns {string } the name of the lock file name for the specific implementation
20
23
*/
21
- _lockFileName ( ) {
22
- throw new TypeError ( "_lockFileName must be implemented" ) ;
23
- }
24
+ _lockFileName ( ) {
25
+ throw new TypeError ( "_lockFileName must be implemented" ) ;
26
+ }
24
27
25
- /**
28
+ /**
26
29
* @returns {string } the command name to use for the specific JS package manager
27
30
*/
28
- _cmdName ( ) {
29
- throw new TypeError ( "_cmdName must be implemented" ) ;
30
- }
31
-
32
- /**
33
- * @returns {Array<string> }
34
- */
35
- _listCmdArgs ( ) {
36
- throw new TypeError ( "_listCmdArgs must be implemented" ) ;
37
- }
38
-
39
- /**
40
- * @returns {Array<string> }
41
- */
42
- _updateLockFileCmdArgs ( ) {
43
- throw new TypeError ( "_updateLockFileCmdArgs must be implemented" ) ;
44
- }
45
-
46
- /**
31
+ _cmdName ( ) {
32
+ throw new TypeError ( "_cmdName must be implemented" ) ;
33
+ }
34
+
35
+ /**
36
+ * @returns {Array<string> }
37
+ */
38
+ _listCmdArgs ( ) {
39
+ throw new TypeError ( "_listCmdArgs must be implemented" ) ;
40
+ }
41
+
42
+ /**
43
+ * @returns {Array<string> }
44
+ */
45
+ _updateLockFileCmdArgs ( ) {
46
+ throw new TypeError ( "_updateLockFileCmdArgs must be implemented" ) ;
47
+ }
48
+
49
+ /**
47
50
* @param {string } manifestName - the subject manifest name-type
48
51
* @returns {boolean } - return true if `pom.xml` is the manifest name-type
49
52
*/
50
- isSupported ( manifestName ) {
51
- return 'package.json' === manifestName ;
52
- }
53
+ isSupported ( manifestName ) {
54
+ return 'package.json' === manifestName ;
55
+ }
53
56
54
- /**
57
+ /**
55
58
* Checks if a required lock file exists in the same path as the manifest
56
59
*
57
60
* @param {string } manifestDir - The base directory where the manifest is located
58
61
* @returns {boolean } - True if the lock file exists
59
62
*/
60
- validateLockFile ( manifestDir ) {
61
- const lock = path . join ( manifestDir , this . _lockFileName ( ) ) ;
62
- return fs . existsSync ( lock ) ;
63
- }
63
+ validateLockFile ( manifestDir ) {
64
+ const lock = path . join ( manifestDir , this . _lockFileName ( ) ) ;
65
+ return fs . existsSync ( lock ) ;
66
+ }
64
67
65
- /**
68
+ /**
66
69
* Provide content and content type for maven-maven stack analysis.
67
70
* @param {string } manifest - the manifest path or name
68
71
* @param {{} } [opts={}] - optional various options to pass along the application
69
72
* @returns {Provided }
70
73
*/
71
- provideStack ( manifest , opts = { } ) {
72
- return {
73
- ecosystem,
74
- content : this . #getSBOM( manifest , opts , true ) ,
75
- contentType : 'application/vnd.cyclonedx+json'
76
- }
77
- }
78
-
79
- /**
74
+ provideStack ( manifest , opts = { } ) {
75
+ return {
76
+ ecosystem,
77
+ content : this . #getSBOM( manifest , opts , true ) ,
78
+ contentType : 'application/vnd.cyclonedx+json'
79
+ }
80
+ }
81
+
82
+ /**
80
83
* Provide content and content type for maven-maven component analysis.
81
84
* @param {string } manifest - path to pom.xml for component report
82
85
* @param {{} } [opts={}] - optional various options to pass along the application
83
86
* @returns {Provided }
84
87
*/
85
- provideComponent ( manifest , opts = { } ) {
86
- return {
87
- ecosystem,
88
- content : this . #getSBOM( manifest , opts , false ) ,
89
- contentType : 'application/vnd.cyclonedx+json'
90
- }
91
- }
92
-
93
- /**
94
- * Utility function for creating Purl String
95
-
96
- * @param name the name of the artifact, can include a namespace(group) or not - namespace/artifactName.
97
- * @param version the version of the artifact
98
- * @returns {PackageURL|null } PackageUrl Object ready to be used in SBOM
99
- */
100
- #toPurl( name , version ) {
101
- let parts = name . split ( "/" ) ;
102
- var purlNs , purlName ;
103
- if ( parts . length === 2 ) {
104
- purlNs = parts [ 0 ] ;
105
- purlName = parts [ 1 ] ;
106
- } else {
107
- purlName = parts [ 0 ] ;
108
- }
109
- return new PackageURL ( 'npm' , purlNs , purlName , version , undefined , undefined ) ;
110
- }
111
-
112
- _buildDependencyTree ( includeTransitive , manifest ) {
113
- this . #version( ) ;
114
- let manifestDir = path . dirname ( manifest )
115
- this . #createLockFile( manifestDir ) ;
116
-
117
- let npmOutput = this . #executeListCmd( includeTransitive , manifestDir ) ;
118
- return JSON . parse ( npmOutput ) ;
119
- }
120
-
121
- /**
88
+ provideComponent ( manifest , opts = { } ) {
89
+ return {
90
+ ecosystem,
91
+ content : this . #getSBOM( manifest , opts , false ) ,
92
+ contentType : 'application/vnd.cyclonedx+json'
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Utility function for creating Purl String
98
+ * @param name the name of the artifact, can include a namespace(group) or not - namespace/artifactName.
99
+ * @param version the version of the artifact
100
+ * @returns {PackageURL|null } PackageUrl Object ready to be used in SBOM
101
+ */
102
+ #toPurl( name , version ) {
103
+ let parts = name . split ( "/" ) ;
104
+ var purlNs , purlName ;
105
+ if ( parts . length === 2 ) {
106
+ purlNs = parts [ 0 ] ;
107
+ purlName = parts [ 1 ] ;
108
+ } else {
109
+ purlName = parts [ 0 ] ;
110
+ }
111
+ return new PackageURL ( 'npm' , purlNs , purlName , version , undefined , undefined ) ;
112
+ }
113
+
114
+ _buildDependencyTree ( includeTransitive , manifest ) {
115
+ this . #version( ) ;
116
+ let manifestDir = path . dirname ( manifest )
117
+ this . #createLockFile( manifestDir ) ;
118
+
119
+ let npmOutput = this . #executeListCmd( includeTransitive , manifestDir ) ;
120
+ return JSON . parse ( npmOutput ) ;
121
+ }
122
+
123
+ /**
122
124
* Create SBOM json string for npm Package.
123
125
* @param {string } manifest - path for package.json
124
126
* @param {{} } [opts={}] - optional various options to pass along the application
125
127
* @returns {string } the SBOM json content
126
128
* @private
127
129
*/
128
- #getSBOM( manifest , opts = { } , includeTransitive ) {
129
- const depsObject = this . _buildDependencyTree ( includeTransitive , manifest ) ;
130
- let rootName = depsObject [ "name" ]
131
- let rootVersion = depsObject [ "version" ]
132
- if ( ! rootVersion ) {
133
- rootVersion = defaultVersion
134
- }
135
- let mainComponent = this . #toPurl( rootName , rootVersion ) ;
136
-
137
- let sbom = new Sbom ( ) ;
138
- sbom . addRoot ( mainComponent )
139
-
140
- let dependencies = depsObject [ "dependencies" ] || { } ;
141
- this . #addAllDependencies( sbom , sbom . getRoot ( ) , dependencies )
142
- let packageJson = fs . readFileSync ( manifest ) . toString ( )
143
- let packageJsonObject = JSON . parse ( packageJson ) ;
144
- if ( packageJsonObject . exhortignore !== undefined ) {
145
- let ignoredDeps = Array . from ( packageJsonObject . exhortignore ) ;
146
- sbom . filterIgnoredDeps ( ignoredDeps )
147
- }
148
- return sbom . getAsJsonString ( opts )
149
- }
150
-
151
- /**
130
+ #getSBOM( manifest , opts = { } , includeTransitive ) {
131
+ this . #cmd = getCustomPath ( this . _cmdName ( ) , opts ) ;
132
+ const depsObject = this . _buildDependencyTree ( includeTransitive , manifest , opts ) ;
133
+ let rootName = depsObject [ "name" ]
134
+ let rootVersion = depsObject [ "version" ]
135
+ if ( ! rootVersion ) {
136
+ rootVersion = defaultVersion
137
+ }
138
+ let mainComponent = this . #toPurl( rootName , rootVersion ) ;
139
+
140
+ let sbom = new Sbom ( ) ;
141
+ sbom . addRoot ( mainComponent )
142
+
143
+ let dependencies = depsObject [ "dependencies" ] || { } ;
144
+ this . #addAllDependencies( sbom , sbom . getRoot ( ) , dependencies )
145
+ let packageJson = fs . readFileSync ( manifest ) . toString ( )
146
+ let packageJsonObject = JSON . parse ( packageJson ) ;
147
+ if ( packageJsonObject . exhortignore !== undefined ) {
148
+ let ignoredDeps = Array . from ( packageJsonObject . exhortignore ) ;
149
+ sbom . filterIgnoredDeps ( ignoredDeps )
150
+ }
151
+ return sbom . getAsJsonString ( opts )
152
+ }
153
+
154
+ /**
152
155
* This function recursively build the Sbom from the JSON that npm listing returns
153
156
* @param sbom this is the sbom object
154
157
* @param from this is the current component in bom (Should start with root/main component of SBOM) for which we want to add all its dependencies.
155
158
* @param dependencies the current dependency list (initially it's the list of the root component)
156
159
* @private
157
160
*/
158
- #addAllDependencies( sbom , from , dependencies ) {
159
- Object . entries ( dependencies )
160
- . filter ( entry => entry [ 1 ] . version !== undefined )
161
- . forEach ( entry => {
162
- let [ name , artifact ] = entry ;
163
- let purl = this . #toPurl( name , artifact . version ) ;
164
- sbom . addDependency ( from , purl )
165
- let transitiveDeps = artifact . dependencies
166
- if ( transitiveDeps !== undefined ) {
167
- this . #addAllDependencies( sbom , sbom . purlToComponent ( purl ) , transitiveDeps )
168
- }
169
- } ) ;
170
- }
171
-
172
- #executeListCmd( includeTransitive , manifestDir ) {
173
- const listArgs = this . _listCmdArgs ( includeTransitive , manifestDir ) ;
174
- try {
175
- return invokeCommand ( this . _cmdName ( ) , listArgs )
176
- } catch ( error ) {
177
- throw new Error ( `failed to list dependencies via " ${ this . _cmdName ( ) } ${ listArgs . join ( ' ' ) } " - Error: ${ error } ` , { cause : error } ) ;
178
- }
179
- }
180
-
181
- #version ( ) {
182
- try {
183
- invokeCommand ( this . _cmdName ( ) , [ '--version' ] , { stdio : 'ignore' } ) ;
184
- } catch ( error ) {
185
- if ( error . code === 'ENOENT ' ) {
186
- throw new Error ( ` ${ this . _cmdName ( ) } is not accessible` ) ;
187
- }
188
- throw new Error ( `failed to check for package manager binary at ${ this . _cmdName ( ) } ` , { cause : error } )
189
- }
190
- }
191
-
192
- #createLockFile ( manifestDir ) {
193
- // in windows os, --prefix flag doesn't work, it behaves really weird , instead of installing the package.json fromm the prefix folder,
194
- // it's installing package.json (placed in current working directory of process) into prefix directory, so
195
- let originalDir = process . cwd ( )
196
- if ( os . platform ( ) === 'win32' ) {
197
- process . chdir ( manifestDir )
198
- }
199
- const args = this . _updateLockFileCmdArgs ( manifestDir ) ;
200
- try {
201
- invokeCommand ( this . _cmdName ( ) , args )
202
- } catch ( error ) {
203
- throw new Error ( `failed to create lockfile " ${ args } " - Error: ${ error } ` , { cause : error } ) ;
204
- } finally {
205
- if ( os . platform ( ) === 'win32' ) {
206
- process . chdir ( originalDir )
207
- }
208
- }
209
- }
161
+ #addAllDependencies( sbom , from , dependencies ) {
162
+ Object . entries ( dependencies )
163
+ . filter ( entry => entry [ 1 ] . version !== undefined )
164
+ . forEach ( entry => {
165
+ let [ name , artifact ] = entry ;
166
+ let purl = this . #toPurl( name , artifact . version ) ;
167
+ sbom . addDependency ( from , purl )
168
+ let transitiveDeps = artifact . dependencies
169
+ if ( transitiveDeps !== undefined ) {
170
+ this . #addAllDependencies( sbom , sbom . purlToComponent ( purl ) , transitiveDeps )
171
+ }
172
+ } ) ;
173
+ }
174
+
175
+ #executeListCmd( includeTransitive , manifestDir ) {
176
+ const listArgs = this . _listCmdArgs ( includeTransitive , manifestDir ) ;
177
+ return this . #invokeCommand ( listArgs ) ;
178
+ }
179
+
180
+ #version ( ) {
181
+ this . #invokeCommand ( [ '--version' ] , { stdio : 'ignore' } ) ;
182
+ }
183
+
184
+ #createLockFile ( manifestDir ) {
185
+ // in windows os, --prefix flag doesn't work, it behaves really weird , instead of installing the package.json fromm the prefix folder,
186
+ // it's installing package.json (placed in current working directory of process) into prefix directory, so
187
+ let originalDir = process . cwd ( )
188
+ if ( os . platform ( ) === 'win32 ' ) {
189
+ process . chdir ( manifestDir )
190
+ }
191
+ const args = this . _updateLockFileCmdArgs ( manifestDir ) ;
192
+ try {
193
+ this . #invokeCommand ( args ) ;
194
+ } catch ( error ) {
195
+ throw new Error ( `failed to create lockfile " ${ args } "` , { cause : error } ) ;
196
+ } finally {
197
+ if ( os . platform ( ) === 'win32' ) {
198
+ process . chdir ( originalDir )
199
+ }
200
+ }
201
+ }
202
+
203
+ #invokeCommand ( args , opts = { } ) {
204
+ try {
205
+ return invokeCommand ( this . #cmd , args , opts ) ;
206
+ } catch ( error ) {
207
+ if ( error . code === 'ENOENT' ) {
208
+ throw new Error ( ` ${ this . #cmd } is not accessible` ) ;
209
+ }
210
+ throw new Error ( `failed to execute ${ this . #cmd } ${ args } ` , { cause : error } )
211
+ }
212
+ }
210
213
}
211
214
0 commit comments