SpyGuard/analysis/classes/report.py

476 lines
29 KiB
Python
Raw Permalink Normal View History

2022-11-06 15:51:33 +01:00
import weasyprint
import os
import json
import hashlib
import re
import sys
from weasyprint import HTML
from pathlib import Path
from datetime import datetime
from utils import get_config
class Report(object):
def __init__(self, capture_directory, analysis_duration):
self.capture_directory = capture_directory
self.alerts = self.read_json(os.path.join(capture_directory, "assets/alerts.json"))
self.records = self.read_json(os.path.join(capture_directory, "assets/records.json"))
self.methods = self.read_json(os.path.join(capture_directory, "assets/detection_methods.json"))
self.device = self.read_json(os.path.join(capture_directory, "assets/device.json"))
self.capinfos = self.read_json(os.path.join(capture_directory, "assets/capinfos.json"))
self.instance = self.read_json(os.path.join(capture_directory, "assets/instance.json"))
self.analysis_duration = analysis_duration
with open(os.path.join(self.capture_directory, "capture.pcap"), "rb") as f:
self.capture_sha1 = hashlib.sha1(f.read()).hexdigest()
self.userlang = get_config(("frontend", "user_lang"))
# Load template language
if not re.match("^[a-z]{2,3}$", self.userlang):
self.userlang = "en"
with open(os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "locales/{}.json".format(self.userlang))) as f:
self.template = json.load(f)["report"]
def read_json(self, json_path):
"""Read a JSON
Args:
json_path (_type_): _description_
Returns:
_type_: _description_
"""
with open(json_path, "r") as json_file:
return json.load(json_file)
def generate_report(self):
"""Generate the full report and save it as report.pdf """
content = self.generate_page_header()
content += self.generate_header()
content += self.generate_warning()
content += self.generate_alerts()
content += self.generate_suspect_conns_block()
content += self.generate_uncat_conns_block()
content += self.generate_whitelist_block()
htmldoc = HTML(string=content, base_url="").write_pdf()
Path(os.path.join(self.capture_directory,
"report.pdf")).write_bytes(htmldoc)
def generate_warning(self):
"""Generate the main warning message on the report
Returns:
string: HTML code of the main warning message.
"""
if len(self.alerts["high"]):
msg = "<div class=\"warning high\">"
msg += self.template["high_msg"].format(
self.nb_translate(len(self.alerts["high"])))
msg += "</div>"
return msg
elif len(self.alerts["moderate"]):
msg = "<div class=\"warning moderate\">"
msg += self.template["moderate_msg"].format(
self.nb_translate(len(self.alerts["moderate"])))
msg += "</div>"
return msg
elif len(self.alerts["low"]):
msg = "<div class=\"warning low\">"
msg += self.template["low_msg"].format(
self.nb_translate(len(self.alerts["low"])))
msg += "</div>"
return msg
else:
msg = "<div class=\"warning low\">"
msg += self.template["none_msg"]
msg += "</div>"
return msg
def nb_translate(self, nb):
""" Translate a number in a string.
Args:
nb (int): integer
Returns:
string: A translated string related to the integer
"""
a = self.template["numbers"]
return a[nb-1] if nb <= 9 else str(nb)
def generate_suspect_conns_block(self):
"""Generate the block and the table of communications categorized as suspect.
Returns:
str: HTML block of the suspect communications
"""
tbody = ""
for record in self.records:
if record["suspicious"] == True:
tbody += "<tr>"
tbody += f"<td>{', '.join([p['name'] for p in record['protocols']])}</td>"
tbody += f"<td>{', '.join(record['domains'])}</td>"
tbody += f"<td>{record['ip_dst']}</td>"
tbody += f"<td>{', '.join([str(p['port']) if p['port'] != -1 else '--' for p in record['protocols']])}</td>"
tbody += "</tr>"
if len(tbody):
title = f"<h2>{self.template['suspect_title']}</h2>"
table = "<table>"
table += " <thead>"
table += " <tr>"
table += f" <th>{self.template['protocol']}</th>"
table += f" <th>{self.template['domain']}</th>"
table += f" <th>{self.template['dst_ip']}</th>"
table += f" <th>{self.template['dst_port']}</th>"
table += " </tr>"
table += " </thead>"
table += "<tbody>"
table += tbody
table += "</tbody></table>"
return title + table
else:
return ""
def generate_uncat_conns_block(self):
"""Generate the block and the table of the uncategorized communications
Returns:
str: HTML block of the uncategorized communications
"""
tbody = ""
for record in self.records:
if record["suspicious"] == False and record["whitelisted"] == False:
tbody += "<tr>"
tbody += f"<td>{', '.join([p['name'] for p in record['protocols']])}</td>"
tbody += f"<td>{', '.join(record['domains'])}</td>"
tbody += f"<td>{record['ip_dst']}</td>"
tbody += f"<td>{', '.join([str(p['port']) if p['port'] != -1 else '--' for p in record['protocols']])}</td>"
tbody += "</tr>"
if len(tbody):
title = "<h2>{}</h2>".format(self.template["uncat_title"])
table = "<table>"
table += " <thead>"
table += " <tr>"
table += f" <th>{self.template['protocol']}</th>"
table += f" <th>{self.template['domain']}</th>"
table += f" <th>{self.template['dst_ip']}</th>"
table += f" <th>{self.template['dst_port']}</th>"
table += " </tr>"
table += " </thead>"
table += "<tbody>"
table += tbody
table += "</tbody></table>"
return title + table
else:
return ""
def generate_whitelist_block(self):
"""Generate the block and the table of the whitelisted communications
Returns:
str: HTML block of the whitelisted communications
"""
tbody = ""
for record in self.records:
if record["whitelisted"] == True:
tbody += "<tr>"
tbody += f"<td>{', '.join([p['name'] for p in record['protocols']])}</td>"
tbody += f"<td>{', '.join(record['domains'])}</td>"
tbody += f"<td>{record['ip_dst']}</td>"
tbody += f"<td>{', '.join([str(p['port']) if p['port'] != -1 else '--' for p in record['protocols']])}</td>"
tbody += "</tr>"
if len(tbody):
title = "<h2>{}</h2>".format(self.template["whitelist_title"])
table = "<table>"
table += " <thead>"
table += " <tr>"
table += f" <th>{self.template['protocol']}</th>"
table += f" <th>{self.template['domain']}</th>"
table += f" <th>{self.template['dst_ip']}</th>"
table += f" <th>{self.template['dst_port']}</th>"
table += " </tr>"
table += " </thead>"
table += "<tbody>"
table += tbody
table += "</tbody></table>"
return title + table
else:
return ""
def generate_header(self):
"""Generate the report headers with the capture's metadata.
Returns:
str: HTML block containing the data.
"""
header = "<div class=\"header\">"
header += "<div class=\"logo\"></div>"
header += f"<p><br /><strong>{self.template['device_mac']}: {self.device['mac_address']}</strong><br />"
header += f"{self.template['detection_methods']}: {'' if self.methods['iocs'] else ''} IOCs {'' if self.methods['heuristics'] else ''} Heuristics {'' if self.methods['active'] else ''} Active analysis <br />"
header += f"{self.template['capture_sha1']}: {self.capture_sha1}<br />"
header += f"{self.template['instance_uuid']}: {self.instance['instance_uuid']}<br />"
header += f"{self.template['report_generated_on']} {datetime.now().strftime('%d/%m/%Y - %H:%M:%S')}<br />"
if self.capinfos is not None:
header += f"{self.template['capture_duration']}: {self.capinfos['Capture duration'].split(' ')[0]} {self.template['seconds']}<br />"
header += f"{self.template['analysis_duration']}: {self.analysis_duration} {self.template['seconds']}<br />"
header += f"{self.template['packets_number']}: {self.capinfos['Number of packets']}<br />"
header += "</p>"
header += "</div>"
return header
def generate_alerts(self):
"""Generate a block embedding the alerts triggered during the analysis.
Returns:
str: HTML block containing the data.
"""
alerts = "<ul class=\"alerts\">"
for alert in self.alerts["high"]:
alerts += "<li class =\"alert\">"
alerts += "<span class=\"high-label\">High</span>"
alerts += "<span class=\"alert-id\">{}</span>".format(alert["id"])
alerts += "<div class = \"alert-body\">"
alerts += "<span class=\"title\">{}</span>".format(alert["title"])
alerts += "<p class=\"description\">{}</p>".format(
alert["description"])
alerts += "</div>"
alerts += "</li>"
for alert in self.alerts["moderate"]:
alerts += "<li class =\"alert\">"
alerts += "<span class=\"moderate-label\">moderate</span>"
alerts += "<span class=\"alert-id\">{}</span>".format(alert["id"])
alerts += "<div class = \"alert-body\">"
alerts += "<span class=\"title\">{}</span>".format(alert["title"])
alerts += "<p class=\"description\">{}</p>".format(
alert["description"])
alerts += "</div>"
alerts += "</li>"
for alert in self.alerts["low"]:
alerts += "<li class =\"alert\">"
alerts += "<span class=\"low-label\">low</span>"
alerts += "<span class=\"alert-id\">{}</span>".format(alert["id"])
alerts += "<div class = \"alert-body\">"
alerts += "<span class=\"title\">{}</span>".format(alert["title"])
alerts += "<p class=\"description\">{}</p>".format(
alert["description"])
alerts += "</div>"
alerts += "</li>"
alerts += "</ul>"
return alerts
def generate_page_footer(self):
"""Generate the page footer
Returns:
str: HTML block closing the page
"""
return "</body></html>"
def generate_page_header(self):
"""Generate the page header
Returns:
str: HTML block containing the page header with the CSS.
"""
return """<html
<head>
<style>
* {
font-family: Arial, Helvetica, sans-serif;
}
h2 {
padding-top: 30px;
font-weight: 400;
font-size: 18px;
}
td {
width: auto;
padding: 10px;
}
table {
background: #FFF;
border: 2px solid #FAFAFA;
border-radius: 5px;
border-collapse: separate;
border-spacing: 0px;
width: 100%;
font-size: 12px;
}
p {
font-size: 13px;
}
thead tr th {
border-bottom: 1px solid #CCC;
border-collapse: separate;
border-spacing: 5px 5px;
background-color: #FFF;
padding: 10px;
text-align: left;
}
tbody tr#first td {
border-top: 3px solid #4d4d4d;
border-collapse: separate;
border-spacing: 5px 5px;
}
tr:nth-of-type(odd) {
background-color: #fafafa;
}
.logo {
background-image: url("data:image/svg+xml;base64;base64,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
width: 230px;
height: 60px;
background-size: cover;
position: absolute;
right: 0px;
}
.warning {
padding: 10px;
text-align: center;
border-radius: 5px;
color: #FFF;
margin-top: 40px;
margin-bottom: 40px;
font-weight:900;
}
.high {
background-color: #F44336;
}
.moderate {
background-color: #ff7e33;
}
.low {
background-color: #4fce0e;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
.alert {
margin-top: 15px;
}
.alert-body {
background-color: #FFF;
list-style: none;
padding: 10px;
border-radius: 5px;
border: 1px solid #EEE;
margin-top: 3px;
}
.alert-body>.title {
display: block;
padding: 5px 5px 5px 10px;
font-size: 13px;
}
.high-label {
background-color: #F44336;
padding: 5px;
text-transform: uppercase;
font-size: 10px;
font-weight: bold;
border-radius: 3px 0px 0px 0px;
margin: 0px;
color: #FFF;
margin-left: 10px;
}
.moderate-label {
background-color: #ff7e33;
padding: 5px;
text-transform: uppercase;
font-size: 10px;
font-weight: bold;
border-radius: 3px 0px 0px 0px;
margin: 0px;
color: #FFF;
margin-left: 10px;
}
.low-label {
background-color: #4fce0e;
padding: 5px;
text-transform: uppercase;
font-size: 10px;
font-weight: bold;
border-radius: 3px 0px 0px 0px;
margin: 0px;
color: #FFF;
margin-left: 10px;
}
.description {
margin: 0;
padding: 10px;
color:#333;
font-size:12px;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
.alert-id {
background-color: #636363;
padding: 5px;
text-transform: uppercase;
font-size: 10px;
font-weight: bold;
border-radius: 0px 3px 0px 0px;
margin: 0px;
color: #FFF;
margin-right: 10px;
}
.header>p {
font-size:12px;
}
@page {
@top-center {
content: "REPORT_HEADER - Page " counter(page) " / " counter(pages) ".";
font-size:12px;
color:#CCC;
}
@bottom-center {
content: "REPORT_FOOTER";
font-size:12px;
color:#CCC;
}
}
</style>
</head>
<body>""".replace("REPORT_HEADER", "{} {}".format(self.template["report_for_the_capture"], self.capture_sha1)).replace("REPORT_FOOTER", self.template["report_footer"])