11import logging
22import sys
3+ from typing import NoReturn
34
45from twyn .__version__ import __version__
56from twyn .base .constants import (
1314from twyn .main import check_dependencies
1415from twyn .trusted_packages .cache_handler import CacheHandler
1516from twyn .trusted_packages .constants import CACHE_DIR
17+ from twyn .trusted_packages .models import TyposquatCheckResults
1618
1719try :
1820 import click
1921 from rich .console import Console
2022 from rich .logging import RichHandler
23+ from rich .table import Table
2124
2225 from twyn .base .exceptions import CliError
2326except ImportError :
@@ -104,6 +107,12 @@ def entry_point() -> None:
104107 default = False ,
105108 help = "Display the results in json format. It implies --no-track." ,
106109)
110+ @click .option (
111+ "--table" ,
112+ is_flag = True ,
113+ default = False ,
114+ help = "Display the results in a table format. It implies --no-track." ,
115+ )
107116@click .option (
108117 "-r" ,
109118 "--recursive" ,
@@ -131,33 +140,94 @@ def run( # noqa: C901
131140 no_cache : bool | None ,
132141 no_track : bool ,
133142 json : bool ,
143+ table : bool ,
134144 package_ecosystem : str | None ,
135145 recursive : bool ,
136146 pypi_source : str | None ,
137147 npm_source : str | None ,
138- ) -> int :
139- if vv :
140- logger .setLevel (logging .DEBUG )
141- elif v :
142- logger .setLevel (logging .INFO )
148+ ) -> None :
149+ set_logging_level (v = v , vv = vv )
150+ check_args (
151+ dependency_file = dependency_file ,
152+ dependency = dependency ,
153+ json = json ,
154+ table = table ,
155+ )
143156
144- if dependency and dependency_file :
145- raise click .UsageError (
146- "Only one of --dependency or --dependency-file can be set at a time." , ctx = click .get_current_context ()
147- )
157+ possible_typos = get_typos (
158+ config = config ,
159+ dependency_file = dependency_file ,
160+ dependency = dependency ,
161+ selector_method = selector_method ,
162+ no_cache = no_cache ,
163+ no_track = no_track ,
164+ json = json ,
165+ table = table ,
166+ package_ecosystem = package_ecosystem ,
167+ recursive = recursive ,
168+ pypi_source = pypi_source ,
169+ npm_source = npm_source ,
170+ )
171+ display_output_and_exit (json = json , table = table , possible_typos = possible_typos )
148172
149- for dep_file in dependency_file :
150- if dep_file and not any (dep_file .endswith (key ) for key in DEPENDENCY_FILE_MAPPING ):
151- raise click .UsageError (f"Dependency file name { dep_file } not supported." , ctx = click .get_current_context ())
152173
174+ def display_output_and_exit (json : bool , table : bool , possible_typos : TyposquatCheckResults ) -> NoReturn :
175+ if json :
176+ click .echo (possible_typos .model_dump_json ())
177+ sys .exit (int (bool (possible_typos )))
178+ elif table :
179+ if not possible_typos :
180+ click .echo ("✅ No typosquats detected" )
181+ sys .exit (0 )
182+
183+ console = Console ()
184+ table_obj = Table (title = "❌ Twyn Detection Results" )
185+ table_obj .add_column ("Source" )
186+ table_obj .add_column ("Dependency" )
187+ table_obj .add_column ("Similar trusted packages" )
188+
189+ for possible_typosquats in possible_typos .results :
190+ for error in possible_typosquats .errors :
191+ table_obj .add_row (str (possible_typosquats .source ), error .dependency , ", " .join (error .similars ))
192+
193+ console .print (table_obj )
194+ sys .exit (1 )
195+ elif possible_typos :
196+ for possible_typosquats in possible_typos .results :
197+ for error in possible_typosquats .errors :
198+ click .echo (
199+ click .style ("Possible typosquat detected: " , fg = "red" ) + f"`{ error .dependency } `, "
200+ f"did you mean any of [{ ', ' .join (error .similars )} ]?" ,
201+ color = True ,
202+ )
203+ sys .exit (1 )
204+ else :
205+ click .echo (click .style ("No typosquats detected" , fg = "green" ), color = True )
206+ sys .exit (0 )
207+
208+
209+ def get_typos (
210+ config : str ,
211+ dependency_file : tuple [str ],
212+ dependency : tuple [str ],
213+ selector_method : str ,
214+ no_cache : bool | None ,
215+ no_track : bool ,
216+ json : bool ,
217+ table : bool ,
218+ package_ecosystem : str | None ,
219+ recursive : bool ,
220+ pypi_source : str | None ,
221+ npm_source : str | None ,
222+ ) -> TyposquatCheckResults :
153223 try :
154- possible_typos = check_dependencies (
224+ return check_dependencies (
155225 selector_method = selector_method ,
156226 dependencies = set (dependency ) or None ,
157227 config_file = config ,
158228 dependency_files = set (dependency_file ) or None ,
159229 use_cache = not no_cache if no_cache is not None else no_cache ,
160- show_progress_bar = False if json else not no_track ,
230+ show_progress_bar = False if json or table else not no_track ,
161231 load_config_from_file = True ,
162232 package_ecosystem = package_ecosystem ,
163233 recursive = recursive ,
@@ -169,21 +239,26 @@ def run( # noqa: C901
169239 except Exception as e :
170240 raise CliError ("Unhandled exception occured." ) from e
171241
172- if json :
173- click .echo (possible_typos .model_dump_json ())
174- sys .exit (int (bool (possible_typos )))
175- elif possible_typos :
176- for possible_typosquats in possible_typos .results :
177- for error in possible_typosquats .errors :
178- click .echo (
179- click .style ("Possible typosquat detected: " , fg = "red" ) + f"`{ error .dependency } `, "
180- f"did you mean any of [{ ', ' .join (error .similars )} ]?" ,
181- color = True ,
182- )
183- sys .exit (1 )
184- else :
185- click .echo (click .style ("No typosquats detected" , fg = "green" ), color = True )
186- sys .exit (0 )
242+
243+ def set_logging_level (v : bool , vv : bool ) -> None :
244+ if vv :
245+ logger .setLevel (logging .DEBUG )
246+ elif v :
247+ logger .setLevel (logging .INFO )
248+
249+
250+ def check_args (dependency_file : tuple [str ], dependency : tuple [str ], json : bool , table : bool ) -> None :
251+ if dependency and dependency_file :
252+ raise click .UsageError (
253+ "Only one of --dependency or --dependency-file can be set at a time." , ctx = click .get_current_context ()
254+ )
255+
256+ if json and table :
257+ raise click .UsageError ("`--json` and `--table` are mutually exclusive. Select only one." )
258+
259+ for dep_file in dependency_file :
260+ if dep_file and not any (dep_file .endswith (key ) for key in DEPENDENCY_FILE_MAPPING ):
261+ raise click .UsageError (f"Dependency file name { dep_file } not supported." , ctx = click .get_current_context ())
187262
188263
189264@entry_point .group ()
0 commit comments