Skip to content

Commit 50df0b7

Browse files
authored
Merge pull request #42 from OPPIDA/feat/report-scoring
2 parents 0028ff2 + 4d164f9 commit 50df0b7

File tree

4 files changed

+92
-94
lines changed

4 files changed

+92
-94
lines changed

codesectools/sasts/all/parser.py

Lines changed: 54 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -132,83 +132,92 @@ def stats_by_scores(self) -> dict:
132132
for defect_file, defects in defect_files.items():
133133
defects_cwes = {d.cwe for d in defects if d.cwe.id != -1}
134134

135-
defects_same_cwe = 0
135+
same_cwe = 0
136136
for cwe in defects_cwes:
137137
cwes_sasts = {d.sast_name for d in defects if d.cwe == cwe}
138138
if set(self.sast_names) == cwes_sasts:
139-
defects_same_cwe += 1
139+
same_cwe += 1
140140
else:
141-
defects_same_cwe += (
142-
len(set(self.sast_names) & cwes_sasts) - 1
143-
) / len(self.sast_names)
141+
same_cwe += (len(set(self.sast_names) & cwes_sasts) - 1) / len(
142+
self.sast_names
143+
)
144144

145+
defects_severity = []
145146
defect_locations = {}
146147
for defect in defects:
148+
defects_severity.append(
149+
{"error": 1, "warning": 0.5, "note": 0.25, "none": 0.125}[
150+
defect.level
151+
]
152+
)
153+
147154
for line in defect.lines:
148155
if not defect_locations.get(line):
149156
defect_locations[line] = []
150157
defect_locations[line].append(defect)
151158

152-
defects_same_location = 0
153-
defects_same_location_same_cwe = 0
159+
same_location = 0
160+
same_location_same_cwe = 0
154161
for _, defects_ in defect_locations.items():
162+
same_location_coeff = 0
155163
if set(defect.sast_name for defect in defects_) == set(self.sast_names):
156-
defects_same_location += 1
157-
defects_by_cwe = {}
158-
for defect in defects_:
159-
if not defects_by_cwe.get(defect.cwe):
160-
defects_by_cwe[defect.cwe] = []
161-
defects_by_cwe[defect.cwe].append(defect)
162-
163-
for _, defects_ in defects_by_cwe.items():
164-
if set(defect.sast_name for defect in defects_) == set(
165-
self.sast_names
166-
):
167-
defects_same_location_same_cwe += 1
168-
else:
169-
defects_same_location_same_cwe += (
164+
same_location_coeff = 1
165+
else:
166+
same_location_coeff = (
167+
len(
168+
set(defect.sast_name for defect in defects_)
169+
& set(self.sast_names)
170+
)
171+
- 1
172+
) / len(set(self.sast_names))
173+
same_location += same_location_coeff
174+
175+
defects_by_cwe = {}
176+
for defect in defects_:
177+
if not defects_by_cwe.get(defect.cwe):
178+
defects_by_cwe[defect.cwe] = []
179+
defects_by_cwe[defect.cwe].append(defect)
180+
181+
for _, defects_ in defects_by_cwe.items():
182+
if set(defect.sast_name for defect in defects_) == set(
183+
self.sast_names
184+
):
185+
same_location_same_cwe += same_location_coeff * 1
186+
else:
187+
same_location_same_cwe += (
188+
same_location_coeff
189+
* (
170190
len(
171191
set(defect.sast_name for defect in defects_)
172192
& set(self.sast_names)
173193
)
174194
- 1
175-
) / len(self.sast_names)
195+
)
196+
/ len(self.sast_names)
197+
)
176198

177199
stats[defect_file] = {
178200
"score": {
179-
"defect_number": len(defects),
180-
"defects_same_cwe": defects_same_cwe * 2,
181-
"defects_same_location": defects_same_location * 4,
182-
"defects_same_location_same_cwe": defects_same_location_same_cwe
183-
* 8,
184-
},
185-
"count": {
186-
"defect_number": len(defects),
187-
"defects_same_cwe": defects_same_cwe,
188-
"defects_same_location": defects_same_location,
189-
"defects_same_location_same_cwe": defects_same_location_same_cwe,
201+
"severity": sum(defects_severity) / len(defects_severity),
202+
"same_cwe": same_cwe * 2,
203+
"same_location": same_location * 4,
204+
"same_location_same_cwe": same_location_same_cwe * 8,
190205
},
191206
}
192-
193207
return stats
194208

195209
def prepare_report_data(self) -> dict:
196210
"""Prepare data needed to generate a report."""
197-
report = {"score": {}, "files": {}}
211+
report = {}
198212
scores = self.stats_by_scores()
199213

200-
report["score"] = {k: 0 for k, _ in list(scores.values())[0]["score"].items()}
201-
202214
defect_files = {}
203215
for defect in self.defects:
204216
if defect.filepath_str not in defect_files:
205217
defect_files[defect.filepath_str] = []
206218
defect_files[defect.filepath_str].append(defect)
207219

208220
for defect_file, defects in defect_files.items():
209-
for k, v in scores[defect_file]["score"].items():
210-
report["score"][k] += v
211-
212221
locations = []
213222
for defect in defects:
214223
for group in group_successive(defect.lines):
@@ -217,19 +226,18 @@ def prepare_report_data(self) -> dict:
217226
(defect.sast_name, defect.cwe, defect.message, (start, end))
218227
)
219228

220-
report["files"][defect_file] = {
221-
"score": scores[defect_file]["score"],
222-
"count": scores[defect_file]["count"],
229+
report[defect_file] = {
230+
"score": sum(v for v in scores[defect_file]["score"].values()),
223231
"source_path": str(self.source_path / defect.filepath),
224232
"locations": locations,
225233
"defects": defects,
226234
}
227235

228-
report["files"] = {
236+
report = {
229237
k: v
230238
for k, v in sorted(
231-
report["files"].items(),
232-
key=lambda item: sum(v for v in item[1]["score"].values()),
239+
report.items(),
240+
key=lambda item: item[1]["score"],
233241
reverse=True,
234242
)
235243
}

codesectools/sasts/all/report.py

Lines changed: 36 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
from hashlib import sha256
55
from pathlib import Path
66

7-
from rich import print
8-
97
from codesectools.sasts.all.sast import AllSAST
10-
from codesectools.utils import group_successive, shorten_path
8+
from codesectools.utils import group_successive
119

1210

1311
class ReportEngine:
@@ -82,38 +80,17 @@ def __init__(self, project: str, all_sast: AllSAST) -> None:
8280
self.result = all_sast.parser.load_from_output_dir(project_name=project)
8381
self.report_data = self.result.prepare_report_data()
8482

85-
def generate_single_defect(self, file_data: dict) -> tuple:
83+
def generate_single_defect(self, defect_file: dict) -> str:
8684
"""Generate the HTML report for a single file with defects."""
8785
from rich.console import Console
8886
from rich.style import Style
8987
from rich.syntax import Syntax
9088
from rich.table import Table
9189
from rich.text import Text
9290

93-
file_report_name = (
94-
f"{sha256(file_data['source_path'].encode()).hexdigest()}.html"
95-
)
9691
file_page = Console(record=True, file=io.StringIO())
9792

98-
# Defect stat table
99-
file_stats_table = Table(title="")
100-
for key in list(self.report_data["files"].values())[0]["count"].keys():
101-
file_stats_table.add_column(key.replace("_", " ").title(), justify="center")
102-
103-
rendered_scores = []
104-
for v in file_data["count"].values():
105-
if isinstance(v, float):
106-
rendered_scores.append(f"~{v}")
107-
else:
108-
rendered_scores.append(str(v))
109-
110-
file_stats_table.add_row(*rendered_scores)
111-
file_page.print(file_stats_table)
112-
113-
file_report_redirect = Text(
114-
shorten_path(file_data["source_path"], 60),
115-
style=Style(link=file_report_name),
116-
)
93+
file_page.print(f"Score: {defect_file['score']:.2f}")
11794

11895
# Defect table
11996
defect_table = Table(title="", show_lines=True)
@@ -122,7 +99,7 @@ def generate_single_defect(self, file_data: dict) -> tuple:
12299
defect_table.add_column("CWE", justify="center")
123100
defect_table.add_column("Message")
124101
rows = []
125-
for defect in file_data["defects"]:
102+
for defect in defect_file["defects"]:
126103
groups = group_successive(defect.lines)
127104
if groups:
128105
for group in groups:
@@ -161,14 +138,14 @@ def generate_single_defect(self, file_data: dict) -> tuple:
161138
file_page.print(defect_table)
162139

163140
# Syntax
164-
if not Path(file_data["source_path"]).is_file():
141+
if not Path(defect_file["source_path"]).is_file():
165142
tippy_calls = ""
166-
print(f"Source file {file_data['source_path']} not found, skipping it...")
143+
print(f"Source file {defect_file['source_path']} not found, skipping it...")
167144
else:
168-
syntax = Syntax.from_path(file_data["source_path"], line_numbers=True)
145+
syntax = Syntax.from_path(defect_file["source_path"], line_numbers=True)
169146
tooltips = {}
170147
highlights = {}
171-
for location in file_data["locations"]:
148+
for location in defect_file["locations"]:
172149
sast, cwe, message, (start, end) = location
173150
for i in range(start, end + 1):
174151
text = (
@@ -199,13 +176,10 @@ def generate_single_defect(self, file_data: dict) -> tuple:
199176

200177
html_content = file_page.export_html(code_format=self.TEMPLATE)
201178
html_content = html_content.replace('href="HACK', 'id="')
202-
html_content = html_content.replace("[name]", file_data["source_path"])
179+
html_content = html_content.replace("[name]", defect_file["source_path"])
203180
html_content = html_content.replace("[tippy_calls]", tippy_calls)
204181

205-
report_file = self.report_dir / file_report_name
206-
report_file.write_text(html_content)
207-
208-
return file_report_redirect, rendered_scores
182+
return html_content
209183

210184
def generate(self) -> None:
211185
"""Generate the HTML report.
@@ -215,7 +189,9 @@ def generate(self) -> None:
215189
"""
216190
from rich.console import Console
217191
from rich.progress import track
192+
from rich.style import Style
218193
from rich.table import Table
194+
from rich.text import Text
219195

220196
self.TEMPLATE = self.TEMPLATE.replace(
221197
"[sasts]", ", ".join(sast_name for sast_name in self.result.sast_names)
@@ -224,24 +200,38 @@ def generate(self) -> None:
224200
home_page = Console(record=True, file=io.StringIO())
225201

226202
main_table = Table(title="")
203+
main_table.add_column("Score", justify="center")
227204
main_table.add_column("Files")
228-
for key in list(self.report_data["files"].values())[0]["score"].keys():
229-
main_table.add_column(
230-
key.replace("_", " ").title(), justify="center", no_wrap=True
231-
)
232205

233-
for file_data in track(
234-
self.report_data["files"].values(),
206+
for defect_file in track(
207+
self.report_data.values(),
235208
description="Generating report for source file with defects...",
236209
):
237-
file_report_redirect, rendered_scores = self.generate_single_defect(
238-
file_data
210+
html_content = self.generate_single_defect(defect_file)
211+
file_report_name = (
212+
f"{sha256(defect_file['source_path'].encode()).hexdigest()}.html"
213+
)
214+
file_report_redirect = Text(
215+
str(
216+
Path(defect_file["source_path"]).relative_to(
217+
self.result.source_path
218+
) # ty:ignore[no-matching-overload]
219+
),
220+
style=Style(link=file_report_name),
221+
)
222+
223+
report_file = self.report_dir / file_report_name
224+
report_file.write_text(html_content)
225+
226+
main_table.add_row(
227+
Text(f"{defect_file['score']:.2f}"), file_report_redirect
239228
)
240-
main_table.add_row(file_report_redirect, *rendered_scores)
241229

242230
home_page.print(main_table)
243231
html_content = home_page.export_html(code_format=self.TEMPLATE)
244-
html_content = html_content.replace("[name]", f"Project: {self.project}")
232+
html_content = html_content.replace(
233+
"[name]", f"Project: {self.result.source_path}"
234+
)
245235

246236
report_home_file = self.report_dir / "home.html"
247237
report_home_file.write_text(html_content)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "CodeSecTools"
3-
version = "0.15.0"
3+
version = "0.15.1"
44
description = "A framework for code security that provides abstractions for static analysis tools and datasets to support their integration, testing, and evaluation."
55
readme = "README.md"
66
license = "AGPL-3.0-only"

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)