@@ -2,109 +2,193 @@ import React, { useState } from "react";
22import { toast } from "react-toastify" ;
33import { DocumentDuplicateIcon } from "@heroicons/react/24/outline" ;
44
5- const ResultDisplay = ( { result, item_name} ) => {
6- const [ activeTab , setActiveTab ] = useState ( "vulnerabilities" ) ; // Default tab: Vulnerabilities
5+ const ResultDisplay = ( { result, item_name } ) => {
6+ const [ activeTab , setActiveTab ] = useState ( "vulnerabilities" ) ;
77 console . log ( result ) ;
88
9- if ( ! result ) return < p className = "text-gray-400" > Result Not available </ p > ;
9+ if ( ! result ) return < p className = "text-gray-400" > Result Not available</ p > ;
1010
1111 const handleCopy = ( ) => {
1212 navigator . clipboard . writeText ( JSON . stringify ( result , null , 2 ) ) . then ( ( ) => {
1313 toast . success ( "Result copied" , { position : "top-right" , autoClose : 2000 , theme : "light" } ) ;
1414 } ) ;
1515 } ;
1616
17- const renderResults = ( results ) => {
18- // Handle array of results (legacy support for multi-repo)
19- if ( Array . isArray ( results ) ) {
20- return results . map ( ( item , index ) => (
21- < div key = { index } className = "mb-4" >
22- < h3 className = "text-lg font-semibold text-gray-100" > { item . repo } </ h3 >
17+ // -------- Normalization --------
18+ const normalize = ( raw ) => {
19+ // If array: legacy multi-repo
20+ if ( Array . isArray ( raw ) ) {
21+ return raw . map ( ( item ) => ( {
22+ repo : item . repo ,
23+ ...( item . error
24+ ? { error : item . error , vulnerabilities : [ ] , summary : { total : 0 } , files_scanned : [ ] }
25+ : normalize ( item . data ) ) ,
26+ } ) ) ;
27+ }
28+
29+ // If {repo, data}: single repo wrapper
30+ if ( raw && raw . repo && raw . data ) {
31+ const inner = normalize ( raw . data ) ;
32+ return { repo : raw . repo , ...inner } ;
33+ }
34+
35+ // If single-file shape { file_path, vulnerabilities }
36+ if ( raw && raw . file_path ) {
37+ const vulns = ( raw . vulnerabilities || [ ] ) . map ( ( v ) => ( {
38+ id : v . rule_id || v . id || v . check_id ,
39+ message : v . message || v . title ,
40+ severity : ( v . severity || "" ) . toUpperCase ( ) ,
41+ file_path : v . file_path || raw . file_path ,
42+ line : v . line ?? ( Array . isArray ( v . file_line_range ) ? v . file_line_range [ 0 ] : undefined ) ,
43+ fix : v . fix || v . recommendation || v . suggestion ,
44+ } ) ) ;
45+ return {
46+ vulnerabilities : vulns ,
47+ summary : makeSummary ( vulns ) ,
48+ files_scanned : [ raw . file_path ] ,
49+ } ;
50+ }
51+
52+ // If semgrep-like { results: { failed_checks: [...] } } OR directly { failed_checks: [...] }
53+ const failedChecks =
54+ raw ?. results ?. failed_checks ||
55+ raw ?. failed_checks ||
56+ raw ?. results ?. vulnerabilities || // fallback if your backend renames later
57+ raw ?. vulnerabilities ||
58+ [ ] ;
59+
60+ const vulns = failedChecks . map ( ( fc ) => ( {
61+ id : fc . rule_id || fc . id || fc . check_id ,
62+ message : fc . message || fc . title ,
63+ severity : ( fc . severity || "" ) . toUpperCase ( ) , // e.g., WARNING / LOW / MEDIUM / HIGH
64+ file_path : fc . file_path || fc . path || "unknown" ,
65+ line : fc . line ?? ( Array . isArray ( fc . file_line_range ) ? fc . file_line_range [ 0 ] : undefined ) ,
66+ fix : fc . fix || fc . recommendation || fc . suggestion ,
67+ } ) ) ;
68+
69+ // Files scanned (unique)
70+ const files_scanned = Array . from ( new Set ( vulns . map ( ( v ) => v . file_path ) . filter ( Boolean ) ) ) ;
71+
72+ return {
73+ vulnerabilities : vulns ,
74+ summary : makeSummary ( vulns ) ,
75+ files_scanned,
76+ } ;
77+ } ;
78+
79+ const makeSummary = ( vulns ) => {
80+ const s = { total : vulns . length , high : 0 , medium : 0 , low : 0 , warning : 0 } ;
81+ vulns . forEach ( ( v ) => {
82+ const sev = ( v . severity || "" ) . toUpperCase ( ) ;
83+ if ( sev === "HIGH" || sev === "CRITICAL" ) s . high += 1 ;
84+ else if ( sev === "MEDIUM" ) s . medium += 1 ;
85+ else if ( sev === "LOW" ) s . low += 1 ;
86+ else if ( sev === "WARNING" ) s . warning += 1 ;
87+ } ) ;
88+ return s ;
89+ } ;
90+
91+ const normalized = normalize ( result ) ;
92+
93+ const renderResults = ( resultsLike ) => {
94+ // Array => multi repo
95+ if ( Array . isArray ( resultsLike ) ) {
96+ return resultsLike . map ( ( item , idx ) => (
97+ < div key = { idx } className = "mb-4" >
98+ { item . repo && < h3 className = "text-lg font-semibold text-gray-100" > { item . repo } </ h3 > }
2399 { item . error ? (
24- < p className = "text-red-400" > Error : { item . error } </ p >
100+ < p className = "text-red-400" > Error: { item . error } </ p >
25101 ) : (
26- renderSingleResult ( item . data )
102+ renderSingleResult ( item )
27103 ) }
28104 </ div >
29105 ) ) ;
30106 }
31-
32- // Handle single result from SelectedReposVulnerabilityScanner
33- if ( results . repo && results . data ) {
34- return (
35- < div className = "mb-4" >
36- < h3 className = "text-lg font-semibold text-gray-100" > { results . repo } </ h3 >
37- { renderSingleResult ( results . data ) }
38- </ div >
39- ) ;
40- }
41-
42- // Handle raw vulnerability scan result
43- return renderSingleResult ( results ) ;
107+ return renderSingleResult ( resultsLike ) ;
44108 } ;
45109
46110 const renderSingleResult = ( data ) => {
47- // Handle single file result
48- if ( data . file_path ) {
49- const groupedVulns = {
50- [ data . file_path ] : data . vulnerabilities || [ ] ,
51- } ;
52- return renderGroupedResults ( { vulnerabilities : data . vulnerabilities , files_scanned : [ data . file_path ] } , groupedVulns ) ;
53- }
111+ const results = data ; // already normalized => { vulnerabilities, summary, files_scanned, [repo]? }
54112
55- // Handle directory/repo result
56- const results = data . results || data ;
57- if ( ! results . vulnerabilities && ! results . summary ) {
113+ if ( ! results ?. vulnerabilities ?. length && ! results ?. summary ?. total ) {
58114 return < p className = "text-gray-400" > No vulnerabilities found.</ p > ;
59115 }
60116
61- // Group vulnerabilities by file_path
62- const groupedVulns = { } ;
63- ( results . vulnerabilities || [ ] ) . forEach ( ( vuln ) => {
64- const filePath = vuln . file_path || vuln . path ;
65- if ( ! groupedVulns [ filePath ] ) {
66- groupedVulns [ filePath ] = [ ] ;
67- }
68- groupedVulns [ filePath ] . push ( vuln ) ;
117+ // Group by file
118+ const grouped = { } ;
119+ ( results . vulnerabilities || [ ] ) . forEach ( ( v ) => {
120+ const fp = v . file_path || "unknown" ;
121+ if ( ! grouped [ fp ] ) grouped [ fp ] = [ ] ;
122+ grouped [ fp ] . push ( v ) ;
69123 } ) ;
70124
71- return renderGroupedResults ( results , groupedVulns ) ;
125+ return renderGroupedResults ( results , grouped , data . repo ) ;
72126 } ;
73127
74- const renderGroupedResults = ( results , groupedVulns ) => {
75- // Extract repository name from repo_url
128+ const renderGroupedResults = ( results , groupedVulns , repoName ) => {
129+ // repo url → item name (if you pass repo_url on parent `result`)
76130 const repoUrl = result ?. repo_url ;
77- const ItemName = ( repoUrl != null ) ? repoUrl . split ( "/" ) . slice ( - 2 ) . join ( "/" )
78- : ( item_name ? item_name : "File" ) ;
131+ const ItemName =
132+ repoUrl != null
133+ ? repoUrl . split ( "/" ) . slice ( - 2 ) . join ( "/" )
134+ : repoName || ( item_name ? item_name : "File" ) ;
135+
136+ const sevBadge = ( sev ) => {
137+ const s = ( sev || "" ) . toUpperCase ( ) ;
138+ const base = "ml-2 px-2 py-1 rounded text-xs" ;
139+ if ( s === "HIGH" || s === "CRITICAL" ) return `${ base } bg-red-600` ;
140+ if ( s === "MEDIUM" ) return `${ base } bg-yellow-600` ;
141+ if ( s === "LOW" ) return `${ base } bg-blue-600` ;
142+ if ( s === "WARNING" ) return `${ base } bg-orange-600` ;
143+ return `${ base } bg-gray-600` ;
144+ } ;
79145
80146 return (
81147 < div >
82- { /* Repository Name */ }
83- { ItemName !== "File" && < h3 className = "text-lg font-semibold text-gray-100 mb-4" > { ItemName } </ h3 > }
148+ { ItemName !== "File" && (
149+ < h3 className = "text-lg font-semibold text-gray-100 mb-4" > { ItemName } </ h3 >
150+ ) }
84151
85152 { /* Summary */ }
86153 < div className = "mb-4" >
87- < p className = "text-red-400" > Vulnerabilities Found: { results . summary ?. total || results . vulnerabilities ?. length || 0 } </ p >
88- { results . summary ?. high && < p className = "text-red-600" > High Severity: { results . summary . high } </ p > }
89- { results . summary ?. medium && < p className = "text-yellow-500" > Medium Severity: { results . summary . medium } </ p > }
90- { results . summary ?. low && < p className = "text-blue-400" > Low Severity: { results . summary . low } </ p > }
91- { results . score !== undefined && < p className = "text-gray-100" > Security Score: { results . score } %</ p > }
154+ < p className = "text-red-400" >
155+ Vulnerabilities Found: { results . summary ?. total || results . vulnerabilities ?. length || 0 }
156+ </ p >
157+ { results . summary ?. high > 0 && (
158+ < p className = "text-red-600" > High Severity: { results . summary . high } </ p >
159+ ) }
160+ { results . summary ?. medium > 0 && (
161+ < p className = "text-yellow-500" > Medium Severity: { results . summary . medium } </ p >
162+ ) }
163+ { results . summary ?. low > 0 && (
164+ < p className = "text-blue-400" > Low Severity: { results . summary . low } </ p >
165+ ) }
166+ { results . summary ?. warning > 0 && (
167+ < p className = "text-orange-400" > Warnings: { results . summary . warning } </ p >
168+ ) }
169+ { results . score !== undefined && (
170+ < p className = "text-gray-100" > Security Score: { results . score } %</ p >
171+ ) }
92172 </ div >
93173
94174 { /* Tabs */ }
95175 < div className = "mb-4" >
96176 < div className = "flex border-b border-gray-700" >
97177 < button
98178 className = { `px-4 py-2 font-semibold ${
99- activeTab === "vulnerabilities" ? "border-b-2 border-red-500 text-red-400" : "text-gray-400"
179+ activeTab === "vulnerabilities"
180+ ? "border-b-2 border-red-500 text-red-400"
181+ : "text-gray-400"
100182 } `}
101183 onClick = { ( ) => setActiveTab ( "vulnerabilities" ) }
102184 >
103185 Vulnerabilities
104186 </ button >
105187 < button
106188 className = { `px-4 py-2 font-semibold ${
107- activeTab === "recommendations" ? "border-b-2 border-red-500 text-red-400" : "text-gray-400"
189+ activeTab === "recommendations"
190+ ? "border-b-2 border-red-500 text-red-400"
191+ : "text-gray-400"
108192 } `}
109193 onClick = { ( ) => setActiveTab ( "recommendations" ) }
110194 >
@@ -123,14 +207,11 @@ const ResultDisplay = ({ result, item_name}) => {
123207 < div key = { filePath } className = "mt-4" >
124208 < h5 className = "text-red-500 font-medium" > ----------------{ filePath } ----------------</ h5 >
125209 < ul className = "list-disc list-inside text-gray-300" >
126- { vulns . map ( ( vuln , idx ) => (
210+ { vulns . map ( ( v , idx ) => (
127211 < li key = { idx } >
128- < span className = "text-red-400" > { vuln . rule_id || vuln . id } </ span > - { vuln . message || vuln . title }
129- { vuln . severity && < span className = { `ml-2 px-2 py-1 rounded text-xs ${
130- vuln . severity === 'HIGH' ? 'bg-red-600' :
131- vuln . severity === 'MEDIUM' ? 'bg-yellow-600' : 'bg-blue-600'
132- } `} > { vuln . severity } </ span > }
133- { vuln . line && < span > (Line: { vuln . line } )</ span > }
212+ < span className = "text-red-400" > { v . id } </ span > - { v . message }
213+ { v . severity && < span className = { sevBadge ( v . severity ) } > { v . severity } </ span > }
214+ { v . line != null && < span > (Line: { v . line } )</ span > }
134215 </ li >
135216 ) ) }
136217 </ ul >
@@ -148,11 +229,11 @@ const ResultDisplay = ({ result, item_name}) => {
148229 < div key = { filePath } className = "mt-4" >
149230 < h5 className = "text-red-700 font-medium" > ----------------{ filePath } ----------------</ h5 >
150231 < ul className = "list-disc list-inside text-gray-300" >
151- { vulns . map ( ( vuln , idx ) => (
232+ { vulns . map ( ( v , idx ) => (
152233 < li key = { idx } className = "mb-2" >
153- < span className = "text-red-400" > { vuln . rule_id || vuln . id } </ span > - { vuln . message || vuln . title }
234+ < span className = "text-red-400" > { v . id } </ span > - { v . message }
154235 < div className = "text-slate-400 mt-1" >
155- Recommendation: { vuln . fix || vuln . recommendation || "Review and fix this vulnerability" }
236+ Recommendation: { v . fix || "Review and fix this vulnerability" }
156237 </ div >
157238 </ li >
158239 ) ) }
@@ -181,22 +262,20 @@ const ResultDisplay = ({ result, item_name}) => {
181262 } ;
182263
183264 return (
184- < div className = "mt-6 w-full max-w-260 relative h-50" >
185- < label htmlFor = "vulnerability-result" className = "text-gray-400 text-sm mb-2 block" >
186- Vulnerability Scan Result:
187- </ label >
188- < div
189- className = "p-4 bg-slate-800 text-red-300 rounded-lg shadow-inner max-h-96 overflow-y-auto font-mono text-sm" >
190- { renderResults ( result ) }
191- < button
192- onClick = { handleCopy }
193- className = "absolute top-8 right-6 bg-gray-700 text-white px-2 py-1 rounded hover:bg-gray-600"
194- >
195- < DocumentDuplicateIcon className = "h-4 w-4" />
196- </ button >
197- </ div >
198-
265+ < div className = "mt-6 w-full max-w-260 relative h-50" >
266+ < label htmlFor = "vulnerability-result" className = "text-gray-400 text-sm mb-2 block" >
267+ Vulnerability Scan Result:
268+ </ label >
269+ < div className = "p-4 bg-slate-800 text-red-300 rounded-lg shadow-inner max-h-96 overflow-y-auto font-mono text-sm" >
270+ { renderResults ( normalized ) }
271+ < button
272+ onClick = { handleCopy }
273+ className = "absolute top-8 right-6 bg-gray-700 text-white px-2 py-1 rounded hover:bg-gray-600"
274+ >
275+ < DocumentDuplicateIcon className = "h-4 w-4" />
276+ </ button >
199277 </ div >
278+ </ div >
200279 ) ;
201280} ;
202281
0 commit comments