@@ -11,11 +11,14 @@ import { generateMarkdownReport } from './reporting/markdown';
11
11
import path from 'path' ;
12
12
import fs from 'fs' ;
13
13
import chalk from 'chalk' ;
14
+ import { scanConfigFile , ConfigFinding } from './scanners/configuration' ;
15
+ import { scanForUnvalidatedUploads , UploadFinding } from './scanners/uploads' ;
16
+ import { scanForExposedEndpoints , EndpointFinding } from './scanners/endpoints' ;
14
17
15
18
// Define a combined finding type if needed later
16
19
17
20
// Helper for coloring severities
18
- function colorSeverity ( severity : FindingSeverity | SecretFinding [ 'severity' ] ) : string {
21
+ function colorSeverity ( severity : FindingSeverity | SecretFinding [ 'severity' ] | UploadFinding [ 'severity' ] ) : string {
19
22
switch ( severity ) {
20
23
case 'Critical' : return chalk . red . bold ( severity ) ;
21
24
case 'High' : return chalk . red ( severity ) ;
@@ -37,7 +40,7 @@ program.command('scan')
37
40
. description ( 'Scan a directory for potential security issues.' )
38
41
. argument ( '[directory]' , 'Directory to scan' , '.' )
39
42
. option ( '-o, --output <file>' , 'Specify JSON output file path (e.g., report.json)' )
40
- . option ( '-r, --report < file> ' , 'Specify Markdown report file path (e.g., report .md)' )
43
+ . option ( '-r, --report [ file] ' , 'Specify Markdown report file path (defaults to VIBESAFE-REPORT .md)' )
41
44
. option ( '--high-only' , 'Only report high severity issues' )
42
45
. action ( async ( directory , options ) => {
43
46
const rootDir = path . resolve ( directory ) ;
@@ -46,10 +49,21 @@ program.command('scan')
46
49
console . log ( '(--high-only flag detected)' ) ;
47
50
}
48
51
if ( options . output ) {
49
- console . log ( `Output will be written to: ${ options . output } ` ) ;
52
+ console . log ( `JSON output will be written to: ${ options . output } ` ) ;
50
53
}
51
- if ( options . report ) {
52
- console . log ( `Markdown report will be written to: ${ options . report } ` ) ;
54
+
55
+ // Determine report path based on options
56
+ let reportPath : string | null = null ;
57
+ if ( options . report ) { // Check if -r or --report was used
58
+ if ( typeof options . report === 'string' ) {
59
+ // User provided a specific filename
60
+ reportPath = path . resolve ( options . report ) ;
61
+ console . log ( `Markdown report will be written to: ${ reportPath } ` ) ;
62
+ } else {
63
+ // User used the flag without a filename, use default
64
+ reportPath = path . join ( rootDir , 'VIBESAFE-REPORT.md' ) ;
65
+ console . log ( `Markdown report will be written to default location: ${ reportPath } ` ) ;
66
+ }
53
67
}
54
68
55
69
// --- Moved: Check .gitignore Status ---
@@ -59,9 +73,13 @@ program.command('scan')
59
73
// --- Findings Aggregation ---
60
74
let allSecretFindings : SecretFinding [ ] = [ ] ;
61
75
let allDependencyFindings : DependencyFinding [ ] = [ ] ;
76
+ let allConfigFindings : ConfigFinding [ ] = [ ] ;
77
+ let allUploadFindings : UploadFinding [ ] = [ ] ;
78
+ let allEndpointFindings : EndpointFinding [ ] = [ ] ;
62
79
63
80
// --- File Traversal (Phase 2.2) ---
64
81
const filesToScan = getFilesToScan ( directory ) ;
82
+ const configFilesToScan = filesToScan . filter ( f => / \. ( j s o n | y a ? m l ) $ / i. test ( f ) ) ;
65
83
66
84
// --- Detect Package Manager (Phase 3.1) ---
67
85
const detectedManagers = detectPackageManagers ( filesToScan , rootDir ) ;
@@ -93,6 +111,48 @@ program.command('scan')
93
111
console . log ( 'Skipping CVE lookup as no dependencies were parsed.' ) ;
94
112
}
95
113
114
+ // --- Configuration Scan (Phase 6.1) ---
115
+ console . log ( `Scanning ${ configFilesToScan . length } potential config files...` ) ;
116
+ configFilesToScan . forEach ( filePath => {
117
+ const findings = scanConfigFile ( filePath ) ;
118
+ const relativeFindings = findings . map ( f => ( { ...f , file : path . relative ( rootDir , f . file ) } ) ) ;
119
+ allConfigFindings = allConfigFindings . concat ( relativeFindings ) ;
120
+ } ) ;
121
+
122
+ // --- Upload Scan (Phase 6.2) ---
123
+ // Define file extensions relevant for upload checks
124
+ const UPLOAD_SCAN_EXTENSIONS = new Set ( [ '.js' , '.ts' , '.jsx' , '.tsx' , '.vue' , '.html' ] ) ;
125
+ const filesForUploadScan = filesToScan . filter ( f => UPLOAD_SCAN_EXTENSIONS . has ( path . extname ( f ) . toLowerCase ( ) ) ) ;
126
+ console . log ( `Scanning ${ filesForUploadScan . length } files for potential upload issues...` ) ;
127
+ filesForUploadScan . forEach ( filePath => {
128
+ try {
129
+ const content = fs . readFileSync ( filePath , 'utf-8' ) ;
130
+ const findings = scanForUnvalidatedUploads ( filePath , content ) ;
131
+ const relativeFindings = findings . map ( f => ( { ...f , file : path . relative ( rootDir , f . file ) } ) ) ;
132
+ allUploadFindings = allUploadFindings . concat ( relativeFindings ) ;
133
+ } catch ( error : any ) {
134
+ // Avoid crashing if a single file fails (e.g., read permission)
135
+ console . warn ( chalk . yellow ( `Could not scan ${ path . relative ( rootDir , filePath ) } for uploads: ${ error . message } ` ) ) ;
136
+ }
137
+ } ) ;
138
+
139
+ // --- Endpoint Scan (Phase 6.3) ---
140
+ // Define file extensions relevant for endpoint checks (JS/TS files)
141
+ const ENDPOINT_SCAN_EXTENSIONS = new Set ( [ '.js' , '.ts' , '.jsx' , '.tsx' ] ) ;
142
+ const filesForEndpointScan = filesToScan . filter ( f => ENDPOINT_SCAN_EXTENSIONS . has ( path . extname ( f ) . toLowerCase ( ) ) ) ;
143
+ console . log ( `Scanning ${ filesForEndpointScan . length } files for potentially exposed endpoints...` ) ;
144
+ filesForEndpointScan . forEach ( filePath => {
145
+ try {
146
+ const content = fs . readFileSync ( filePath , 'utf-8' ) ;
147
+ const findings = scanForExposedEndpoints ( filePath , content ) ;
148
+ const relativeFindings = findings . map ( f => ( { ...f , file : path . relative ( rootDir , f . file ) } ) ) ;
149
+ allEndpointFindings = allEndpointFindings . concat ( relativeFindings ) ;
150
+ } catch ( error : any ) {
151
+ // Avoid crashing if a single file fails (e.g., read permission)
152
+ console . warn ( chalk . yellow ( `Could not scan ${ path . relative ( rootDir , filePath ) } for endpoints: ${ error . message } ` ) ) ;
153
+ }
154
+ } ) ;
155
+
96
156
// Separate Info findings
97
157
const infoSecretFindings = allSecretFindings . filter ( f => f . severity === 'Info' ) ;
98
158
const standardSecretFindings = allSecretFindings . filter ( f => f . severity !== 'Info' ) ;
@@ -106,13 +166,31 @@ program.command('scan')
106
166
? allDependencyFindings . filter ( dep => ( dep . maxSeverity === 'High' || dep . maxSeverity === 'Critical' ) ) // Exclude errors when highOnly
107
167
: allDependencyFindings . filter ( dep => dep . vulnerabilities . length > 0 || dep . error ) ;
108
168
169
+ // Filter config findings based on high-only flag if needed (e.g., only High CORS)
170
+ const reportConfigFindings = options . highOnly
171
+ ? allConfigFindings . filter ( f => f . severity === 'High' || f . severity === 'Critical' )
172
+ : allConfigFindings ;
173
+
174
+ // Filter upload findings (adjust severity filtering as needed)
175
+ const reportUploadFindings = options . highOnly
176
+ ? allUploadFindings . filter ( f => f . severity === 'High' || f . severity === 'Critical' || f . severity === 'Medium' ) // Example: Include Medium for uploads even with --high-only?
177
+ : allUploadFindings ;
178
+
179
+ // Filter endpoint findings (e.g., keep Medium+ for high-only)
180
+ const reportEndpointFindings = options . highOnly
181
+ ? allEndpointFindings . filter ( f => f . severity === 'High' || f . severity === 'Critical' || f . severity === 'Medium' )
182
+ : allEndpointFindings ;
183
+
109
184
// --- NOW Check Gitignore Status ---
110
185
gitignoreWarnings = checkGitignoreStatus ( rootDir ) ;
111
186
112
187
// --- Output Generation ---
113
188
const reportData = {
114
- secretFindings : reportSecretFindings , // Report only standard secrets
115
- dependencyFindings : reportDependencyFindings
189
+ secretFindings : reportSecretFindings ,
190
+ dependencyFindings : reportDependencyFindings ,
191
+ configFindings : reportConfigFindings ,
192
+ uploadFindings : reportUploadFindings ,
193
+ endpointFindings : reportEndpointFindings
116
194
} ;
117
195
118
196
// Generate JSON if requested
@@ -121,6 +199,9 @@ program.command('scan')
121
199
const outputJsonData = {
122
200
secrets : reportSecretFindings ,
123
201
dependencies : reportDependencyFindings ,
202
+ configuration : reportConfigFindings ,
203
+ uploads : reportUploadFindings ,
204
+ endpoints : reportEndpointFindings
124
205
}
125
206
fs . writeFileSync ( options . output , JSON . stringify ( outputJsonData , null , 2 ) ) ;
126
207
console . log ( `JSON results successfully written to ${ options . output } ` ) ;
@@ -131,19 +212,19 @@ program.command('scan')
131
212
}
132
213
133
214
// Generate Markdown Report if requested
134
- if ( options . report ) {
215
+ if ( reportPath ) {
135
216
try {
136
217
// Await the async report generation
137
218
const markdownContent = await generateMarkdownReport ( reportData ) ;
138
- fs . writeFileSync ( options . report , markdownContent ) ;
139
- console . log ( `Markdown report successfully written to ${ options . report } ` ) ;
219
+ fs . writeFileSync ( reportPath , markdownContent ) ;
220
+ console . log ( `Markdown report successfully written to ${ reportPath } ` ) ;
140
221
} catch ( error ) {
141
- console . error ( `Error writing Markdown report file ${ options . report } :` , error ) ;
222
+ console . error ( `Error writing Markdown report file ${ reportPath } :` , error ) ;
142
223
}
143
224
}
144
225
145
226
// Print to console ONLY if neither JSON nor Markdown output was specified
146
- if ( ! options . output && ! options . report ) {
227
+ if ( ! options . output && ! reportPath ) {
147
228
148
229
// Print Configuration Warnings FIRST (after scans, before results)
149
230
if ( gitignoreWarnings . length > 0 ) {
@@ -164,11 +245,14 @@ program.command('scan')
164
245
} ) ;
165
246
}
166
247
167
- // Check if any standard findings exist
248
+ // Check if any standard findings exist (including config)
168
249
const hasStandardSecrets = reportSecretFindings . length > 0 ;
169
250
const hasDependencyIssues = reportDependencyFindings . length > 0 ;
251
+ const hasConfigIssues = reportConfigFindings . length > 0 ;
252
+ const hasUploadIssues = reportUploadFindings . length > 0 ;
253
+ const hasEndpointIssues = reportEndpointFindings . length > 0 ;
170
254
171
- if ( hasStandardSecrets || hasDependencyIssues ) {
255
+ if ( hasStandardSecrets || hasDependencyIssues || hasConfigIssues || hasUploadIssues || hasEndpointIssues ) {
172
256
// Print standard secrets to console if found
173
257
if ( hasStandardSecrets ) {
174
258
console . log ( chalk . bold ( '\nPotential Secrets Found:' ) ) ;
@@ -192,6 +276,43 @@ program.command('scan')
192
276
}
193
277
} ) ;
194
278
}
279
+
280
+ // Print config findings to console if found
281
+ if ( hasConfigIssues ) {
282
+ console . log ( chalk . bold ( '\nConfiguration Issues Found:' ) ) ;
283
+ reportConfigFindings . sort ( ( a , b ) => severityToSortOrder ( b . severity ) - severityToSortOrder ( a . severity ) ) ;
284
+ reportConfigFindings . forEach ( finding => {
285
+ console . log ( ` - [${ colorSeverity ( finding . severity ) } ] ${ finding . type } : ${ chalk . cyan ( finding . file ) } - Key: ${ chalk . magenta ( finding . key ) } , Value: ${ chalk . yellow ( JSON . stringify ( finding . value ) ) } ` ) ;
286
+ console . log ( chalk . dim ( ` > ${ finding . message } ` ) ) ;
287
+ } ) ;
288
+ }
289
+
290
+ // Print upload findings to console if found
291
+ if ( hasUploadIssues ) {
292
+ console . log ( chalk . bold ( '\nPotential Upload Issues Found:' ) ) ;
293
+ reportUploadFindings . sort ( ( a , b ) => severityToSortOrder ( b . severity ) - severityToSortOrder ( a . severity ) ) ;
294
+ reportUploadFindings . forEach ( finding => {
295
+ // Customize console output for upload findings
296
+ console . log ( ` - [${ colorSeverity ( finding . severity ) } ] ${ finding . type } in ${ chalk . cyan ( finding . file ) } :${ chalk . yellow ( String ( finding . line ) ) } ` ) ;
297
+ console . log ( chalk . dim ( ` > ${ finding . message } ` ) ) ;
298
+ if ( finding . details ) {
299
+ console . log ( chalk . dim ( ` ${ finding . details } ` ) ) ;
300
+ }
301
+ } ) ;
302
+ }
303
+
304
+ // Print endpoint findings to console if found
305
+ if ( hasEndpointIssues ) {
306
+ console . log ( chalk . bold ( '\nPotentially Exposed Endpoints Found:' ) ) ;
307
+ reportEndpointFindings . sort ( ( a , b ) => severityToSortOrder ( b . severity ) - severityToSortOrder ( a . severity ) ) ;
308
+ reportEndpointFindings . forEach ( finding => {
309
+ console . log ( ` - [${ colorSeverity ( finding . severity ) } ] ${ finding . type } in ${ chalk . cyan ( finding . file ) } :${ chalk . yellow ( String ( finding . line ) ) } ` ) ;
310
+ console . log ( chalk . dim ( ` > Path: ${ chalk . magenta ( finding . path ) } - ${ finding . message } ` ) ) ;
311
+ if ( finding . details ) {
312
+ console . log ( chalk . dim ( ` Context: ${ finding . details } ` ) ) ;
313
+ }
314
+ } ) ;
315
+ }
195
316
} else {
196
317
// All Clear! Print positive message.
197
318
// Check if we actually scanned for dependencies before saying no vulns found
@@ -210,8 +331,11 @@ program.command('scan')
210
331
// Info findings should NOT affect exit code
211
332
const highSeveritySecrets = reportSecretFindings . some ( f => f . severity === 'High' ) ;
212
333
const highSeverityDeps = reportDependencyFindings . some ( d => d . maxSeverity === 'High' || d . maxSeverity === 'Critical' ) ;
334
+ const highSeverityConfig = reportConfigFindings . some ( f => f . severity === 'High' || f . severity === 'Critical' ) ;
335
+ const highSeverityUploads = reportUploadFindings . some ( f => f . severity === 'High' || f . severity === 'Critical' || f . severity === 'Medium' ) ;
336
+ const highSeverityEndpoints = reportEndpointFindings . some ( f => f . severity === 'High' || f . severity === 'Critical' || f . severity === 'Medium' ) ;
213
337
214
- if ( options . highOnly && ( highSeveritySecrets || highSeverityDeps ) ) {
338
+ if ( options . highOnly && ( highSeveritySecrets || highSeverityDeps || highSeverityConfig || highSeverityUploads || highSeverityEndpoints ) ) {
215
339
console . log ( 'Exiting with code 1 due to High/Critical severity findings (--high-only specified).' ) ;
216
340
process . exit ( 1 ) ;
217
341
}
0 commit comments