1- """  Package Malware Scanner 
1+ """Package Malware Scanner 
22
33CLI command that scans a package version for user-specified malware flags. 
44Includes rules based on package registry metadata and source code analysis. 
55""" 
66
77from  functools  import  reduce 
8- import  json  as  js 
98import  logging 
109import  os 
1110import  sys 
1413
1514import  click 
1615from  prettytable  import  PrettyTable 
17- from  termcolor  import  colored 
1816
1917from  guarddog .analyzer .metadata  import  get_metadata_detectors 
2018from  guarddog .analyzer .sourcecode  import  get_sourcecode_rules 
2119from  guarddog .ecosystems  import  ECOSYSTEM 
22- from  guarddog .reporters .sarif  import  report_verify_sarif 
20+ from  guarddog .reporters .reporter_factory  import  ReporterFactory , ReporterType 
21+ 
2322from  guarddog .scanners  import  get_package_scanner , get_project_scanner 
2423from  guarddog .utils .archives  import  safe_extract 
2524
@@ -127,7 +126,7 @@ def _get_all_rules(ecosystem: ECOSYSTEM) -> set[str]:
127126
128127def  _get_rule_param (
129128    rules : tuple [str , ...], exclude_rules : tuple [str , ...], ecosystem : ECOSYSTEM 
130- ) ->  Optional [set ]:
129+ ) ->  Optional [set [ str ] ]:
131130    """ 
132131    This function should return None if no rules are provided 
133132    Else a set of rules to be used for scanning 
@@ -162,28 +161,20 @@ def _verify(
162161        log .error (f"Command verify is not supported for ecosystem { ecosystem }  )
163162        exit (1 )
164163
165-     def  display_result (result : dict ) ->  None :
166-         identifier  =  (
167-             result ["dependency" ]
168-             if  result ["version" ] is  None 
169-             else  f"{ result ['dependency' ]} { result ['version' ]}  
170-         )
171-         if  output_format  is  None :
172-             print_scan_results (result .get ("result" ), identifier )
173- 
174-         if  len (result .get ("errors" , [])) >  0 :
175-             print_errors (result .get ("error" ), identifier )
164+     dependencies , results  =  scanner .scan_local (path = path , rules = rule_param )
176165
177-     results  =  scanner .scan_local (path , rule_param , display_result )
178-     if  output_format  ==  "json" :
179-         return_value  =  js .dumps (results )
166+     rule_docs  =  list (rule_param  or  _get_all_rules (ecosystem = ecosystem ))
180167
181-     if  output_format  ==  "sarif" :
182-         sarif_rules  =  _get_all_rules (ecosystem )
183-         return_value  =  report_verify_sarif (path , list (sarif_rules ), results , ecosystem )
168+     reporter  =  ReporterFactory .create_reporter (ReporterType .from_str (output_format ))
169+     stdout , stderr  =  reporter .render_verify (
170+         dependency_files = dependencies ,
171+         rule_names = rule_docs ,
172+         scan_results = results ,
173+         ecosystem = ecosystem ,
174+     )
184175
185-     if   output_format   is   not   None : 
186-          print ( return_value )
176+     sys . stdout . write ( stdout ) 
177+     sys . stderr . write ( stderr )
187178
188179    if  exit_non_zero_on_finding :
189180        exit_with_status_code ([result ["result" ] for  result  in  results ])
@@ -231,10 +222,10 @@ def _scan(
231222        log .error (f"Error occurred while scanning target { identifier } { e } \n " )
232223        sys .exit (1 )
233224
234-     if   output_format   ==   "json" : 
235-          print ( js . dumps (result ) )
236-     else : 
237-          print_scan_results ( result ,  result [ "package" ] )
225+     reporter   =   ReporterFactory . create_reporter ( ReporterType . from_str ( output_format )) 
226+     stdout ,  stderr   =   reporter . render_scan (result )
227+     sys . stdout . write ( stdout ) 
228+     sys . stderr . write ( stderr )
238229
239230    if  exit_non_zero_on_finding :
240231        exit_with_status_code ([result ])
@@ -262,6 +253,7 @@ class CliEcosystem(click.Group):
262253    Class that dynamically represents an ecosystem in click 
263254    It dynamically selects the ruleset to the instantiated ecosystem 
264255    """ 
256+ 
265257    def  __init__ (self , ecosystem : ECOSYSTEM ):
266258        super ().__init__ ()
267259        self .name  =  ecosystem .name .lower ()
@@ -288,7 +280,12 @@ def rule_options(fn):
288280        @scan_options  
289281        @rule_options  
290282        def  scan_ecosystem (
291-             target , version , rules , exclude_rules , output_format , exit_non_zero_on_finding 
283+             target ,
284+             version ,
285+             rules ,
286+             exclude_rules ,
287+             output_format ,
288+             exit_non_zero_on_finding ,
292289        ):
293290            return  _scan (
294291                target ,
@@ -304,7 +301,9 @@ def scan_ecosystem(
304301        @common_options  
305302        @verify_options  
306303        @rule_options  
307-         def  verify_ecosystem (target , rules , exclude_rules , output_format , exit_non_zero_on_finding ):
304+         def  verify_ecosystem (
305+             target , rules , exclude_rules , output_format , exit_non_zero_on_finding 
306+         ):
308307            return  _verify (
309308                target ,
310309                rules ,
@@ -314,7 +313,9 @@ def verify_ecosystem(target, rules, exclude_rules, output_format, exit_non_zero_
314313                self .ecosystem ,
315314            )
316315
317-         @click .command ("list-rules" , help = f"List available rules for { self .ecosystem .name }  ) 
316+         @click .command ( 
317+             "list-rules" , help = f"List available rules for { self .ecosystem .name }   
318+         ) 
318319        def  list_rules_ecosystem ():
319320            return  _list_rules (self .ecosystem )
320321
@@ -333,7 +334,7 @@ def list_rules_ecosystem():
333334@verify_options  
334335@legacy_rules_options  
335336def  verify (target , rules , exclude_rules , output_format , exit_non_zero_on_finding ):
336-     return  _verify (
337+     return  verify (
337338        target ,
338339        rules ,
339340        exclude_rules ,
@@ -361,81 +362,6 @@ def scan(
361362    )
362363
363364
364- # Pretty prints scan results for the console 
365- def  print_scan_results (results , identifier ):
366-     num_issues  =  results .get ("issues" )
367-     errors  =  results .get ("errors" , [])
368- 
369-     if  num_issues  ==  0 :
370-         print (
371-             "Found " 
372-             +  colored ("0 potentially malicious indicators" , "green" , attrs = ["bold" ])
373-             +  " scanning " 
374-             +  colored (identifier , None , attrs = ["bold" ])
375-         )
376-         print ()
377-     else :
378-         print (
379-             "Found " 
380-             +  colored (
381-                 str (num_issues ) +  " potentially malicious indicators" ,
382-                 "red" ,
383-                 attrs = ["bold" ],
384-             )
385-             +  " in " 
386-             +  colored (identifier , None , attrs = ["bold" ])
387-         )
388-         print ()
389- 
390-         findings  =  results .get ("results" , [])
391-         for  finding  in  findings :
392-             description  =  findings [finding ]
393-             if  isinstance (description , str ):  # package metadata 
394-                 print (colored (finding , None , attrs = ["bold" ]) +  ": "  +  description )
395-                 print ()
396-             elif  isinstance (description , list ):  # semgrep rule result: 
397-                 source_code_findings  =  description 
398-                 print (
399-                     colored (finding , None , attrs = ["bold" ])
400-                     +  ": found " 
401-                     +  str (len (source_code_findings ))
402-                     +  " source code matches" 
403-                 )
404-                 for  finding  in  source_code_findings :
405-                     print (
406-                         "  * " 
407-                         +  finding ["message" ]
408-                         +  " at " 
409-                         +  finding ["location" ]
410-                         +  "\n     " 
411-                         +  format_code_line_for_output (finding ["code" ])
412-                     )
413-                 print ()
414- 
415-     if  len (errors ) >  0 :
416-         print_errors (errors , identifier )
417-         print ("\n " )
418- 
419- 
420- def  print_errors (errors , identifier ):
421-     print (
422-         colored ("Some rules failed to run while scanning "  +  identifier  +  ":" , "yellow" )
423-     )
424-     print ()
425-     for  rule  in  errors :
426-         print (f"* { rule } { errors [rule ]}  )
427-     print ()
428- 
429- 
430- def  format_code_line_for_output (code ):
431-     return  "    "  +  colored (
432-         code .strip ().replace ("\n " , "\n     " ).replace ("\t " , "  " ),
433-         None ,
434-         "on_red" ,
435-         attrs = ["bold" ],
436-     )
437- 
438- 
439365# Given the results, exit with the appropriate status code 
440366def  exit_with_status_code (results ):
441367    for  result  in  results :
0 commit comments