11"""Defines the command-line interface for running all available SAST tools."""
22
3- import io
43import shutil
5- from hashlib import sha256
64from pathlib import Path
75
86import typer
1311from codesectools .datasets import DATASETS_ALL
1412from codesectools .datasets .core .dataset import FileDataset , GitRepoDataset
1513from codesectools .sasts import SASTS_ALL
14+ from codesectools .sasts .all .report import ReportEngine
1615from codesectools .sasts .all .sast import AllSAST
1716from codesectools .sasts .core .sast import PrebuiltBuildlessSAST , PrebuiltSAST
18- from codesectools .utils import group_successive , shorten_path
1917
2018
2119def build_cli () -> typer .Typer :
@@ -85,7 +83,14 @@ def analyze(
8583 ),
8684 ] = False ,
8785 ) -> None :
88- """Run analysis on the current project with all available SAST tools."""
86+ """Run analysis on the current project with all available SAST tools.
87+
88+ Args:
89+ lang: The source code language to analyze.
90+ artifacts: The path to pre-built artifacts (for PrebuiltSAST only).
91+ overwrite: If True, overwrite existing analysis results for the current project.
92+
93+ """
8994 for sast in all_sast .sasts_by_lang .get (lang , []):
9095 if isinstance (sast , PrebuiltBuildlessSAST ) and artifacts is None :
9196 print (
@@ -140,7 +145,14 @@ def benchmark(
140145 ),
141146 ] = False ,
142147 ) -> None :
143- """Run a benchmark on a dataset using all available SAST tools."""
148+ """Run a benchmark on a dataset using all available SAST tools.
149+
150+ Args:
151+ dataset: The name of the dataset to benchmark.
152+ overwrite: If True, overwrite existing results.
153+ testing: If True, run benchmark over a single dataset unit for testing.
154+
155+ """
144156 dataset_name , lang = dataset .split ("_" )
145157 for sast in all_sast .sasts_by_dataset .get (DATASETS_ALL [dataset_name ], []):
146158 dataset = DATASETS_ALL [dataset_name ](lang )
@@ -205,7 +217,14 @@ def plot(
205217 typer .Option ("--format" , help = "Figures export format" ),
206218 ] = "png" ,
207219 ) -> None :
208- """Generate and display plots for a project's aggregated analysis results."""
220+ """Generate and display plots for a project's aggregated analysis results.
221+
222+ Args:
223+ project: The name of the project to visualize.
224+ overwrite: If True, overwrite existing figures.
225+ format: The export format for the figures.
226+
227+ """
209228 from codesectools .sasts .all .graphics import ProjectGraphics
210229
211230 project_graphics = ProjectGraphics (project_name = project )
@@ -228,14 +247,13 @@ def report(
228247 ),
229248 ] = False ,
230249 ) -> None :
231- """Generate an HTML report for a project's aggregated analysis results."""
232- from rich .console import Console
233- from rich .progress import track
234- from rich .style import Style
235- from rich .syntax import Syntax
236- from rich .table import Table
237- from rich .text import Text
250+ """Generate an HTML report for a project's aggregated analysis results.
251+
252+ Args:
253+ project: The name of the project to report on.
254+ overwrite: If True, overwrite existing results.
238255
256+ """
239257 report_dir = all_sast .output_dir / project / "report"
240258 if report_dir .is_dir ():
241259 if overwrite :
@@ -247,197 +265,8 @@ def report(
247265
248266 report_dir .mkdir (parents = True )
249267
250- result = all_sast .parser .load_from_output_dir (project_name = project )
251- report_data = result .prepare_report_data ()
252-
253- template = """
254- <!DOCTYPE html>
255- <html>
256- <head>
257- <meta charset="UTF-8">
258- <style>
259- {stylesheet}
260- body {{
261- color: {foreground};
262- background-color: {background};
263- font-family: Menlo, 'DejaVu Sans Mono', consolas, 'Courier New', monospace;
264- }}
265- .tippy-box {{
266- background-color: white;
267- color: black;
268- }}
269- img {{
270- display: block;
271- margin: auto;
272- border: solid black 1px;
273- }}
274- #top {{
275- position: fixed;
276- bottom: 20px;
277- right: 30px;
278- background-color: white;
279- padding: 10px;
280- border: solid black 5px;
281- }}
282- </style>
283- </head>
284- <body>
285- <a href="./home.html"><h1>CodeSecTools All SAST Tools Report</h1></a>
286- <h3>SAST Tools used: [sasts]</h3>
287- <h2>[name]</h2>
288- <pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><code style="font-family:inherit">{code}</code></pre>
289- <script src="https://unpkg.com/@popperjs/core@2"></script>
290- <script src="https://unpkg.com/tippy.js@6"></script>
291- <script>[tippy_calls]</script>
292- <a href="#" id="top">^</a>
293- </body>
294- </html>
295- """
296- template = template .replace (
297- "[sasts]" , ", " .join (sast_name for sast_name in result .sast_names )
298- )
299-
300- home_page = Console (record = True , file = io .StringIO ())
301-
302- main_table = Table (title = "" )
303- main_table .add_column ("Files" )
304- for key in list (report_data ["defects" ].values ())[0 ]["score" ].keys ():
305- main_table .add_column (
306- key .replace ("_" , " " ).title (), justify = "center" , no_wrap = True
307- )
308-
309- for defect_data in track (
310- report_data ["defects" ].values (),
311- description = "Generating report for source file with defects..." ,
312- ):
313- defect_report_name = (
314- f"{ sha256 (defect_data ['source_path' ].encode ()).hexdigest ()} .html"
315- )
316- defect_page = Console (record = True , file = io .StringIO ())
317-
318- # Defect stat table
319- defect_stats_table = Table (title = "" )
320- for key in list (report_data ["defects" ].values ())[0 ]["score" ].keys ():
321- defect_stats_table .add_column (
322- key .replace ("_" , " " ).title (), justify = "center"
323- )
324-
325- rendered_scores = []
326- for v in defect_data ["score" ].values ():
327- if isinstance (v , float ):
328- rendered_scores .append (f"~{ v } " )
329- else :
330- rendered_scores .append (str (v ))
331-
332- defect_stats_table .add_row (* rendered_scores )
333- defect_page .print (defect_stats_table )
334-
335- defect_report_redirect = Text (
336- shorten_path (defect_data ["source_path" ], 60 ),
337- style = Style (link = defect_report_name ),
338- )
339-
340- main_table .add_row (defect_report_redirect , * rendered_scores )
341-
342- # Defect table
343- defect_table = Table (title = "" , show_lines = True )
344- defect_table .add_column ("Location" , justify = "center" )
345- defect_table .add_column ("SAST" , justify = "center" )
346- defect_table .add_column ("CWE" , justify = "center" )
347- defect_table .add_column ("Message" )
348- rows = []
349- for defect in defect_data ["raw" ]:
350- groups = group_successive (defect .lines )
351- if groups :
352- for group in groups :
353- start , end = group [0 ], group [- 1 ]
354- shortcut = Text (f"{ start } " , style = Style (link = f"#L{ start } " ))
355- cwe_link = (
356- Text (
357- f"CWE-{ defect .cwe .id } " ,
358- style = Style (
359- link = f"https://cwe.mitre.org/data/definitions/{ defect .cwe .id } .html"
360- ),
361- )
362- if defect .cwe .id != - 1
363- else "None"
364- )
365- rows .append (
366- (start , shortcut , defect .sast , cwe_link , defect .message )
367- )
368- else :
369- cwe_link = (
370- Text (
371- f"CWE-{ defect .cwe .id } " ,
372- style = Style (
373- link = f"https://cwe.mitre.org/data/definitions/{ defect .cwe .id } .html"
374- ),
375- )
376- if defect .cwe .id != - 1
377- else "None"
378- )
379- rows .append (
380- (float ("inf" ), "None" , defect .sast , cwe_link , defect .message )
381- )
382-
383- for row in sorted (rows , key = lambda r : r [0 ]):
384- defect_table .add_row (* row [1 :])
385- defect_page .print (defect_table )
386-
387- # Syntax
388- if not Path (defect_data ["source_path" ]).is_file ():
389- tippy_calls = ""
390- print (
391- f"Source file { defect_data ['source_path' ]} not found, skipping it..."
392- )
393- else :
394- syntax = Syntax .from_path (defect_data ["source_path" ], line_numbers = True )
395- tooltips = {}
396- highlights = {}
397- for location in defect_data ["locations" ]:
398- sast , cwe , message , (start , end ) = location
399- for i in range (start , end + 1 ):
400- text = (
401- f"<b>{ sast } </b>: <i>{ message } (CWE-{ cwe .id } )</i>"
402- if cwe .id != - 1
403- else f"<b>{ sast } </b>: <i>{ message } </i>"
404- )
405- if highlights .get (i ):
406- highlights [i ].add (text )
407- else :
408- highlights [i ] = {text }
409-
410- for line , texts in highlights .items ():
411- element_id = f"L{ line } "
412- bgcolor = "red" if len (texts ) > 1 else "yellow"
413- syntax .stylize_range (
414- Style (bgcolor = bgcolor , link = f"HACK{ element_id } " ),
415- start = (line , 0 ),
416- end = (line + 1 , 0 ),
417- )
418- tooltips [element_id ] = "<hr>" .join (text for text in texts )
419-
420- tippy_calls = ""
421- for element_id , content in tooltips .items ():
422- tippy_calls += f"""tippy('#{ element_id } ', {{ content: `{ content .replace ("`" , "\\ `" )} `, allowHTML: true, interactive: true }});\n """
423-
424- defect_page .print (syntax )
425-
426- html_content = defect_page .export_html (code_format = template )
427- html_content = html_content .replace ('href="HACK' , 'id="' )
428- html_content = html_content .replace ("[name]" , defect_data ["source_path" ])
429- html_content = html_content .replace ("[tippy_calls]" , tippy_calls )
430-
431- report_defect_file = report_dir / defect_report_name
432- report_defect_file .write_text (html_content )
433-
434- home_page .print (main_table )
435- html_content = home_page .export_html (code_format = template )
436- html_content = html_content .replace ("[name]" , f"Project: { project } " )
437-
438- report_home_file = report_dir / "home.html"
439- report_home_file .write_text (html_content )
440-
268+ report_engine = ReportEngine (project = project , all_sast = all_sast )
269+ report_engine .generate ()
441270 print (f"Report generated at { report_dir .resolve ()} " )
442271
443272 return cli
0 commit comments