@@ -132,33 +132,62 @@ const ScannerUI = () => {
132132 }
133133
134134 // Process findings to ensure proper structure
135- const processedFindings = allFindings . reduce ( ( acc , finding ) => {
135+ // Process and validate findings
136+ const processedFindings = { } ;
137+ for ( const finding of allFindings ) {
138+ console . log ( 'Processing finding:' , finding ) ; // Debug log
139+
140+ if ( ! finding || ! finding . type ) {
141+ console . warn ( 'Invalid finding structure:' , finding ) ;
142+ continue ;
143+ }
144+
136145 const key = finding . type ;
137- if ( ! acc [ key ] ) {
138- acc [ key ] = {
146+ if ( ! processedFindings [ key ] ) {
147+ processedFindings [ key ] = {
139148 type : finding . type ,
140149 severity : finding . severity || 'LOW' ,
141150 description : finding . description || 'No description provided' ,
142- allLineNumbers : { [ finding . file ] : finding . lineNumbers || [ ] }
151+ allLineNumbers : { }
143152 } ;
144- } else {
145- // Merge line numbers if same type
146- const file = finding . file ;
147- if ( ! acc [ key ] . allLineNumbers [ file ] ) {
148- acc [ key ] . allLineNumbers [ file ] = finding . lineNumbers || [ ] ;
149- } else {
150- const merged = new Set ( [ ...acc [ key ] . allLineNumbers [ file ] , ...finding . lineNumbers ] ) ;
151- acc [ key ] . allLineNumbers [ file ] = Array . from ( merged ) . sort ( ( a , b ) => a - b ) ;
153+ }
154+
155+ // Ensure file and line numbers exist
156+ if ( finding . file ) {
157+ if ( ! processedFindings [ key ] . allLineNumbers [ finding . file ] ) {
158+ processedFindings [ key ] . allLineNumbers [ finding . file ] = [ ] ;
159+ }
160+
161+ if ( Array . isArray ( finding . lineNumbers ) ) {
162+ const merged = new Set ( [
163+ ...processedFindings [ key ] . allLineNumbers [ finding . file ] ,
164+ ...finding . lineNumbers
165+ ] ) ;
166+ processedFindings [ key ] . allLineNumbers [ finding . file ] = Array . from ( merged ) . sort ( ( a , b ) => a - b ) ;
152167 }
153168 }
154- return acc ;
155- } , { } ) ;
169+ }
156170
157- const report = scanner . generateReport ( allFindings ) ;
158- setScanResults ( {
159- findings : processedFindings ,
160- summary : report . summary || { }
161- } ) ;
171+ // Debug log processed findings
172+ console . log ( 'Processed findings:' , processedFindings ) ;
173+
174+ try {
175+ const report = scanner . generateReport ( allFindings ) ;
176+ console . log ( 'Generated report:' , report ) ; // Debug log
177+
178+ if ( ! report || ! report . summary ) {
179+ throw new Error ( 'Invalid report structure generated' ) ;
180+ }
181+
182+ setScanResults ( {
183+ findings : processedFindings ,
184+ summary : report . summary
185+ } ) ;
186+ } catch ( err ) {
187+ console . error ( 'Error generating report:' , err ) ;
188+ setError ( `Error generating report: ${ err . message } ` ) ;
189+ return ;
190+ }
162191
163192 const { criticalIssues = 0 , highIssues = 0 , mediumIssues = 0 , lowIssues = 0 } = report . summary || { } ;
164193 setSuccessMessage (
@@ -248,49 +277,120 @@ const ScannerUI = () => {
248277
249278 return (
250279 < div className = "p-8 bg-white min-h-screen" >
251- < div className = "flex flex-col gap-8 p-8" >
252- < div className = "flex flex-col gap-4" >
253- { /* File Upload Section */ }
254- < div className = "border-2 border-dashed border-gray-300 rounded-lg p-8 text-center" >
280+ { /* Main Input Section */ }
281+ < div className = "max-w-3xl mx-auto mb-8" >
282+ < h1 className = "text-3xl font-bold text-gray-900 mb-4 flex items-center" >
283+ < Shield className = "h-8 w-8 mr-2" />
284+ SecurityLens
285+ </ h1 >
286+ < p className = "text-gray-600 mb-6 max-w-2xl" >
287+ Scans code for security vulnerabilities including code injection, authentication bypass,
288+ SQL injection, XSS, buffer issues, sensitive data exposure, and more. Supports JavaScript,
289+ TypeScript, Python, and other languages.
290+ </ p >
291+
292+ { /* URL Input Section */ }
293+ < div className = "bg-gray-50 p-6 rounded-lg shadow-sm mb-4" >
294+ < h2 className = "text-lg font-semibold text-gray-700 mb-4" > Scan Repository</ h2 >
295+ < div className = "flex gap-4" >
296+ < input
297+ type = "text"
298+ value = { urlInput }
299+ onChange = { ( e ) => setUrlInput ( e . target . value ) }
300+ placeholder = "Enter GitHub repository URL"
301+ className = "flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
302+ />
303+ < button
304+ onClick = { handleUrlScan }
305+ disabled = { scanning || ! urlInput }
306+ className = { `px-6 py-2 rounded-md text-white font-medium ${
307+ scanning || ! urlInput
308+ ? 'bg-gray-400 cursor-not-allowed'
309+ : 'bg-blue-600 hover:bg-blue-700'
310+ } `}
311+ >
312+ { scanning ? 'Scanning...' : 'Scan Repository' }
313+ </ button >
314+ </ div >
315+ </ div >
316+
317+ { /* File Upload Section */ }
318+ < div className = "bg-gray-50 p-6 rounded-lg shadow-sm" >
319+ < h2 className = "text-lg font-semibold text-gray-700 mb-4" > Scan Local Files</ h2 >
320+ < div className = "flex justify-center" >
255321 < input
256322 type = "file"
323+ id = "fileInput"
257324 multiple
258325 onChange = { handleFileUpload }
259326 className = "hidden"
260- id = "file-upload"
261327 />
262328 < label
263- htmlFor = "file-upload "
264- className = "cursor-pointer text -gray-600 "
329+ htmlFor = "fileInput "
330+ className = "inline-flex flex-col items-center justify-center px-4 py-6 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300 cursor-pointer hover:bg -gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 "
265331 >
266- Drag and drop files here, or click to select files
267- < div className = "text-sm text-gray-500 mt-2" >
268- Supported files: js, jsx, ts, tsx, py, java, and more
332+ < div className = "mb-2" >
333+ < svg className = "w-12 h-12 text-gray-400" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
334+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = "2"
335+ d = "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
336+ />
337+ </ svg >
269338 </ div >
339+ < p className = "text-gray-600" >
340+ Drag and drop files here, or click to select files
341+ </ p >
342+ < p className = "text-sm text-gray-500 mt-1" >
343+ Supported files: .js, .jsx, .ts, .tsx, .py, and more
344+ </ p >
270345 </ label >
271346 </ div >
347+ </ div >
272348
273- { /* Progress and Status */ }
274- { scanning && (
275- < div className = "text-center" >
276- < div className = "animate-pulse text-blue-600" >
277- Scanning... { progress . current } of { progress . total } files
278- </ div >
349+ { /* Progress Bar */ }
350+ { scanning && progress . total > 0 && (
351+ < div className = "mb-6" >
352+ < div className = "w-full bg-gray-300 rounded-full h-3" >
353+ < div
354+ className = "bg-blue-600 h-3 rounded-full transition-all duration-300"
355+ style = { { width : `${ ( progress . current / progress . total ) * 100 } %` } }
356+ > </ div >
279357 </ div >
280- ) }
281358
282- { error && (
283- < div className = "text-red-600 text-center" >
284- { error }
359+ < div className = "text-sm text-gray-700 mt-2 text-center" >
360+ { progress . current === progress . total
361+ ? 'Processing results...'
362+ : `Scanning file ${ progress . current } of ${ progress . total } `
363+ }
285364 </ div >
286- ) }
365+ </ div >
366+ ) }
287367
288- { successMessage && (
289- < div className = "text-green-600 text-center" >
290- { successMessage }
291- </ div >
292- ) }
293- </ div >
368+ { /* Success Message */ }
369+ { successMessage && (
370+ < Alert className = "mb-4" variant = "default" >
371+ < AlertDescription > { successMessage } </ AlertDescription >
372+ </ Alert >
373+ ) }
374+
375+ { /* Error Message */ }
376+ { error && (
377+ < Alert className = "mb-4" variant = "error" >
378+ < AlertDescription >
379+ < AlertTriangle className = "h-4 w-4 inline-block mr-2" />
380+ { error }
381+ </ AlertDescription >
382+ </ Alert >
383+ ) }
384+
385+ { /* Rate Limit Info */ }
386+ { rateLimitInfo && rateLimitInfo . remaining < 10 && (
387+ < Alert className = "mb-4" variant = "warning" >
388+ < AlertDescription >
389+ Rate limit: { rateLimitInfo . remaining } requests remaining.
390+ Resets at { new Date ( rateLimitInfo . reset * 1000 ) . toLocaleTimeString ( ) }
391+ </ AlertDescription >
392+ </ Alert >
393+ ) }
294394
295395 { /* Scan Results */ }
296396 { scanResults && ! scanning && (
@@ -302,7 +402,70 @@ const ScannerUI = () => {
302402 } }
303403 />
304404 ) }
405+
406+ { /* If user has no token, show a quick form to set one */ }
407+ { ! githubToken && (
408+ < div className = "bg-gray-50 p-6 rounded-lg shadow-sm mb-4" >
409+ < h2 className = "text-lg font-semibold text-gray-700 mb-4" > GitHub Access Token</ h2 >
410+ < p className = "text-sm text-gray-600 mb-4" >
411+ To scan repositories, you'll need a GitHub personal access token.
412+ This stays in your browser and is never sent to any server.
413+ </ p >
414+ < input
415+ type = "password"
416+ placeholder = "GitHub token"
417+ onChange = { ( e ) => handleTokenSubmit ( e . target . value ) }
418+ className = "w-full px-4 py-2 border rounded"
419+ />
420+ < a
421+ href = "https://github.com/settings/tokens/new"
422+ target = "_blank"
423+ className = "text-sm text-blue-600 hover:underline mt-2 inline-block"
424+ >
425+ Generate a token
426+ </ a >
427+ </ div >
428+ ) }
305429 </ div >
430+
431+ { /* Token Dialog */ }
432+ < AlertDialog open = { showTokenDialog } onClose = { ( ) => setShowTokenDialog ( false ) } >
433+ < AlertDialogContent >
434+ < AlertDialogHeader >
435+ < h2 className = "text-lg font-semibold" > GitHub Token Required</ h2 >
436+ </ AlertDialogHeader >
437+ < div className = "space-y-4" >
438+ < div className = "bg-blue-50 border border-blue-200 rounded p-3 text-sm" >
439+ < strong > 🔒 Security Note:</ strong > Your token is stored only in your browser's local storage.
440+ It never leaves your device and is not sent to any external servers.
441+ </ div >
442+ < p className = "text-sm text-gray-600" >
443+ To scan GitHub repositories, you'll need a Personal Access Token. Here's how to get one:
444+ </ p >
445+ < ol className = "list-decimal list-inside space-y-2 text-sm" >
446+ < li >
447+ Go to < a
448+ href = "https://github.com/settings/tokens/new"
449+ target = "_blank"
450+ rel = "noopener noreferrer"
451+ className = "text-blue-600 hover:underline"
452+ >
453+ GitHub Token Settings
454+ </ a >
455+ </ li >
456+ < li > Select either "Classic" or "Fine-grained" token</ li >
457+ < li > Enable "repo" access permissions</ li >
458+ < li > Generate and copy the token</ li >
459+ </ ol >
460+ < input
461+ type = "password"
462+ placeholder = "Paste your GitHub token here"
463+ className = "w-full px-4 py-2 border rounded"
464+ onChange = { ( e ) => handleTokenSubmit ( e . target . value ) }
465+ />
466+ </ div >
467+ </ AlertDialogContent >
468+ </ AlertDialog >
306469 </ div >
307470 ) ;
308471} ;
0 commit comments