Skip to content

Commit 7f0b20a

Browse files
authored
Merge pull request #25 from OPPIDA/refactor/allsast-report
2 parents b50ce78 + c81cc79 commit 7f0b20a

5 files changed

Lines changed: 294 additions & 211 deletions

File tree

codesectools/sasts/all/cli.py

Lines changed: 33 additions & 204 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""Defines the command-line interface for running all available SAST tools."""
22

3-
import io
43
import shutil
5-
from hashlib import sha256
64
from pathlib import Path
75

86
import typer
@@ -13,9 +11,9 @@
1311
from codesectools.datasets import DATASETS_ALL
1412
from codesectools.datasets.core.dataset import FileDataset, GitRepoDataset
1513
from codesectools.sasts import SASTS_ALL
14+
from codesectools.sasts.all.report import ReportEngine
1615
from codesectools.sasts.all.sast import AllSAST
1716
from codesectools.sasts.core.sast import PrebuiltBuildlessSAST, PrebuiltSAST
18-
from codesectools.utils import group_successive, shorten_path
1917

2018

2119
def 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

codesectools/sasts/all/parser.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,13 +192,19 @@ def stats_by_scores(self) -> dict:
192192
"defects_same_location_same_cwe": defects_same_location_same_cwe
193193
* 8,
194194
},
195+
"count": {
196+
"defect_number": len(defects),
197+
"defects_same_cwe": defects_same_cwe,
198+
"defects_same_location": defects_same_location,
199+
"defects_same_location_same_cwe": defects_same_location_same_cwe,
200+
},
195201
}
196202

197203
return stats
198204

199205
def prepare_report_data(self) -> dict:
200206
"""Prepare data needed to generate a report."""
201-
report = {"score": {}, "defects": {}}
207+
report = {"score": {}, "files": {}}
202208
scores = self.stats_by_scores()
203209

204210
report["score"] = {k: 0 for k, _ in list(scores.values())[0]["score"].items()}
@@ -221,17 +227,18 @@ def prepare_report_data(self) -> dict:
221227
(defect.sast, defect.cwe, defect.message, (start, end))
222228
)
223229

224-
report["defects"][defect_file] = {
230+
report["files"][defect_file] = {
225231
"score": scores[defect_file]["score"],
232+
"count": scores[defect_file]["count"],
226233
"source_path": str(self.source_path / defect.filepath),
227234
"locations": locations,
228-
"raw": defects,
235+
"defects": defects,
229236
}
230237

231-
report["defects"] = {
238+
report["files"] = {
232239
k: v
233240
for k, v in sorted(
234-
report["defects"].items(),
241+
report["files"].items(),
235242
key=lambda item: (sum(v for v in item[1]["score"].values())),
236243
reverse=True,
237244
)

0 commit comments

Comments
 (0)