# built-in imports
import json
import csv
from pathlib import Path
from typing import List, Dict, Tuple, Union
[docs]
class SASTAlert:
"""
Class representing an alert in a somewhat abstract SAST tool. Its
used to perform alert driven testing.
"""
def __init__(self):
# Static alert data
self.id: int = -1
#: Unique ID of the alert
self.type: str = ""
#: Type of the alert BoF, UaF (in the convention of the SAST)
self.params: list = []
#: Additional parameters of the alert (list)
self.taxonomy: str = ""
#: Taxonomy of the alert (e.g: CWE, CVE, MISRA checker, ..)
self.severity: str = ""
#: Severity of the alert (e.g: Review, Error, Critical ..)
self.file: str = ""
#: Source file impacted
self.line: int = -1
#: line of code (in the file)
self.function: str = ""
#: Function impacted
self.raw_line: str = ""
#: Raw alert extract taken from the report (in its own format)
# Analysis results
self.covered = False
#: Coverage: True if the alert has been covered (path leading there)
self.validated = False
#: Validation: True if the alert has been validated (as a true positive by a checker)
self.uncoverable = False
#: Reachability: True if the alert cannot be reached by any paths
[docs]
@staticmethod
def from_json(data: dict) -> 'SASTAlert':
"""
Create a SASTAlert object from the JSON data provided.
:param data: JSON data of the alert
:return: SASTAlert instance, initialized with the JSON
"""
alert = SASTAlert()
for name in ["id", "type", "params", "taxonomy", "severity", "file", "line", "function", "raw_line",
"covered", "validated", "uncoverable"]:
val = data.get(name)
if val:
setattr(alert, name, val)
return alert
[docs]
def to_dict(self) -> dict:
"""
Export the alert attribute to a valid JSON dictionnary
that can be written to file.
:return: JSON dict of the alert serialized
"""
return {x: getattr(self, x) for x in ["id", "type", "params", "taxonomy", "severity", "file", "line", "function",
"raw_line", "covered", "validated", "uncoverable"]}
def __repr__(self):
return f"<Alert id:{self.id}: {self.type} {self.function}:{self.line} ({Path(self.file).name})>"
[docs]
class SASTReport:
"""
SAST report. Manages a list of SAST alerts taken from a report.
"""
def __init__(self):
self.alerts: Dict[int, SASTAlert] = {}
#: Dictionnary of alerts indexed by their ID
[docs]
def iter_alerts(self) -> List[SASTAlert]:
"""
Iterate all the alerts of the report.
:return: list of alerts
"""
return list(self.alerts.values())
[docs]
def all_alerts_validated(self) -> bool:
"""
Checks if all alerts have been validated (and thus covered)
:return: True if all alerts are covered and vulns validated
"""
for alert in self.iter_alerts():
if not alert.covered:
return False
return True
[docs]
def add_alert(self, alert: SASTAlert) -> None:
"""
Add an alert in the report. This function is solely
meant to be used by the report parser
:param alert: Alert object to add in the report
"""
self.alerts[alert.id] = alert
[docs]
@staticmethod
def from_file(file: Union[str, Path]) -> 'SASTReport':
"""
Parse the given file into a SAST report object.
:param file: path to report
:return: SASTReport object
"""
data = Path(file).read_bytes()
return SASTReport.from_json(data)
[docs]
@staticmethod
def from_json(data: Union[str, bytes]) -> 'SASTReport':
"""
Parse the given string into a SAST report object.
:param data: serialized report in JSON
:return: SASTReport object
"""
data = json.loads(data)
report = SASTReport()
for it in data:
a = SASTAlert.from_json(it)
report.add_alert(a)
return report
[docs]
def to_json(self) -> str:
"""
Export the current state of the alerts within a JSON dictionnary.
:return: JSON serialized report
"""
return json.dumps([x.to_dict() for x in self.alerts.values()], indent=2)
[docs]
def write(self, out_file) -> None:
"""
Export the current state of the alerts within a JSON dictionary.
:param out_file: Output file path
"""
with open(out_file, "w") as f:
f.write(self.to_json())
[docs]
def get_stats(self) -> Tuple[int, int, int]:
"""
Get stats about the report. The results is a triple
with the number of alerts covered, validated and total.
:return: triple of covered, validated, totoal number of alerts
"""
covered = 0
validated = 0
total = 0
for alert in self.alerts.values():
covered += int(alert.covered)
validated += int(alert.validated)
total += 1
return covered, validated, total
[docs]
def write_csv(self, file: Path) -> None:
"""
Write the report as a csv into the given file.
:param file: CSV file to write
"""
with open(file, 'w', newline='') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=['id', 'type', 'covered', 'validated'])
writer.writeheader()
for a in self.iter_alerts():
writer.writerow({'id': a.id,
'type': a.type,
'covered': a.covered,
'validated': a.validated})