22
33# Copyright 2026 Cloudsmith Ltd
44
5+ from concurrent .futures import ThreadPoolExecutor , as_completed
6+
57import click
68from rich .console import Console
79from rich .progress import BarColumn , Progress , SpinnerColumn , TextColumn
@@ -170,13 +172,60 @@ def _print_repo_summary_table(package_rows, severity_filter=None):
170172 console .print (f"\n Total Vulnerabilities: [bold]{ grand_total } [/bold]\n " )
171173
172174
175+ _SCAN_WORKERS = 10
176+
177+
173178def _collect_repo_scan_data (opts , owner , repo , slugs , severity_filter , fixable ):
174179 """Silently collect scan data for all packages with a progress bar.
175180
176181 Returns list of (slug, label, counts, status) tuples where status is one of
177182 "vulnerable", "safe", or "no_scan". Sorted: vulnerable (by count desc),
178183 then safe, then no_scan.
179184 """
185+
186+ def _scan_one (slug , pkg_name_fallback , pkg_version_fallback ):
187+ fallback_label = (
188+ f"{ pkg_name_fallback } :{ pkg_version_fallback } "
189+ if pkg_version_fallback
190+ else pkg_name_fallback
191+ )
192+ try :
193+ data = get_package_scan_result (
194+ opts = opts ,
195+ owner = owner ,
196+ repo = repo ,
197+ package = slug ,
198+ show_assessment = False ,
199+ severity_filter = severity_filter ,
200+ fixable = fixable ,
201+ )
202+ except Exception : # pylint: disable=broad-exception-caught
203+ return (slug , fallback_label , {}, "no_scan" )
204+
205+ # Build label from scan response metadata, fall back to list_packages data
206+ pkg_data = getattr (data , "package" , None ) if data else None
207+ pkg_name = (
208+ getattr (pkg_data , "name" , pkg_name_fallback )
209+ if pkg_data
210+ else pkg_name_fallback
211+ )
212+ pkg_version = (
213+ getattr (pkg_data , "version" , pkg_version_fallback )
214+ if pkg_data
215+ else pkg_version_fallback
216+ )
217+ label = f"{ pkg_name } :{ pkg_version } " if pkg_version else pkg_name
218+
219+ if not data or not _has_scan_results (data ):
220+ return (slug , label , {}, "no_scan" )
221+
222+ if severity_filter or fixable is not None :
223+ _apply_filters (data , severity_filter , fixable )
224+
225+ counts = _aggregate_severity_counts (data , severity_filter )
226+ status = "vulnerable" if sum (counts .values ()) > 0 else "no_issues_found"
227+ return (slug , label , counts , status )
228+
180229 rows = []
181230 console = Console (stderr = True )
182231
@@ -191,60 +240,14 @@ def _collect_repo_scan_data(opts, owner, repo, slugs, severity_filter, fixable):
191240 ) as progress :
192241 task = progress .add_task ("Scanning packages..." , total = len (slugs ))
193242
194- for slug , pkg_name_fallback , pkg_version_fallback in slugs :
195- progress .update (task , description = f"Processing { slug } ..." )
196- fallback_label = (
197- f"{ pkg_name_fallback } :{ pkg_version_fallback } "
198- if pkg_version_fallback
199- else pkg_name_fallback
200- )
201-
202- try :
203- data = get_package_scan_result (
204- opts = opts ,
205- owner = owner ,
206- repo = repo ,
207- package = slug ,
208- show_assessment = False ,
209- severity_filter = severity_filter ,
210- fixable = fixable ,
211- )
212- except Exception : # pylint: disable=broad-exception-caught
213- rows .append ((slug , fallback_label , {}, "no_scan" ))
214- progress .advance (task )
215- continue
216-
217- # Build label from scan response metadata, fall back to list_packages data
218- pkg_data = getattr (data , "package" , None ) if data else None
219- pkg_name = (
220- getattr (pkg_data , "name" , pkg_name_fallback )
221- if pkg_data
222- else pkg_name_fallback
223- )
224- pkg_version = (
225- getattr (pkg_data , "version" , pkg_version_fallback )
226- if pkg_data
227- else pkg_version_fallback
228- )
229- label = f"{ pkg_name } :{ pkg_version } " if pkg_version else pkg_name
230-
231- if not data or not _has_scan_results (data ):
232- rows .append ((slug , label , {}, "no_scan" ))
243+ with ThreadPoolExecutor (max_workers = _SCAN_WORKERS ) as executor :
244+ futures = {
245+ executor .submit (_scan_one , slug , name , version ): slug
246+ for slug , name , version in slugs
247+ }
248+ for future in as_completed (futures ):
249+ rows .append (future .result ())
233250 progress .advance (task )
234- continue
235-
236- # Apply filters if active
237- if severity_filter or fixable is not None :
238- _apply_filters (data , severity_filter , fixable )
239-
240- counts = _aggregate_severity_counts (data , severity_filter )
241-
242- if sum (counts .values ()) > 0 :
243- rows .append ((slug , label , counts , "vulnerable" ))
244- else :
245- rows .append ((slug , label , counts , "no_issues_found" ))
246-
247- progress .advance (task )
248251
249252 # Sort: vulnerable first (by total desc), then safe, then no_scan
250253 # When filters are active, only return packages with matching vulnerabilities
0 commit comments