diff --git a/contrib/descriptions/__init__.py b/contrib/descriptions/__init__.py index 91ca8bd963477fc009a4ead1f083268db6b41515..13b6727ab8b5a2550bf7da4c5288d456393d1d1a 100644 --- a/contrib/descriptions/__init__.py +++ b/contrib/descriptions/__init__.py @@ -1,2 +1,2 @@ -from .description_provider import VulnDescriptionProvider +from .description_provider import VulnDescription, VulnDescriptionProvider from .cveproject import CveProjectProvider diff --git a/contrib/descriptions/cveproject.py b/contrib/descriptions/cveproject.py index 794ef700b8abd207e250ecee4549d894071100f3..a984fc5ea80695731c72d39e1bed94a3805ceb45 100644 --- a/contrib/descriptions/cveproject.py +++ b/contrib/descriptions/cveproject.py @@ -1,7 +1,6 @@ from requests import Session, HTTPError -from contrib.descriptions import VulnDescriptionProvider - +from contrib.descriptions import VulnDescriptionProvider, VulnDescription __all__ = ['CveProjectProvider'] @@ -16,7 +15,7 @@ class CveProjectProvider(VulnDescriptionProvider): self.sess = session self.cache = {} - def get_description(self, vuln: str, vuln_type: str) -> str: + def get_description(self, vuln: str, vuln_type: str) -> VulnDescription: if vuln in self.cache: return self.cache[vuln] @@ -30,8 +29,8 @@ class CveProjectProvider(VulnDescriptionProvider): cve_json = response.json() description = cve_json['description']['description_data'][0]['value'] self.cache[vuln] = description - return description + return VulnDescription(description, url) except HTTPError as he: - return 'Description fetching error: ' + str(he) + return VulnDescription('', 'Description fetching error: ' + str(he)) - return '' + return VulnDescription('', '') diff --git a/contrib/descriptions/description_provider.py b/contrib/descriptions/description_provider.py index 26a6a8e39f76516f3151ecd6968a805edeb22363..4c6da2532713f5482e1038e97c1ec3fa25a5a657 100644 --- a/contrib/descriptions/description_provider.py +++ b/contrib/descriptions/description_provider.py @@ -1,6 +1,13 @@ import abc +from typing import Optional -__all__ = ['VulnDescriptionProvider'] +__all__ = ['VulnDescriptionProvider', 'VulnDescription'] + + +class VulnDescription: + def __init__(self, text: str, url: Optional[str] = None): + self.text = text + self.url = url class VulnDescriptionProvider(metaclass=abc.ABCMeta): @@ -8,5 +15,5 @@ class VulnDescriptionProvider(metaclass=abc.ABCMeta): Provides extended vulnerability description by vulnerablity identifier and type """ @abc.abstractmethod - def get_description(self, vuln: str, vuln_type: str) -> str: + def get_description(self, vuln: str, vuln_type: str) -> VulnDescription: pass diff --git a/contrib/internal_types/flan_types.py b/contrib/internal_types/flan_types.py index 8ea0a2b3684f954262bc9bef42dd377a79c01cee..f1c0da4c425162ed8090338e1a59ed025366e8b2 100644 --- a/contrib/internal_types/flan_types.py +++ b/contrib/internal_types/flan_types.py @@ -1,5 +1,5 @@ from collections import defaultdict - +from typing import List, Dict __all__ = ['SeverityLevels', 'Vuln', 'ScanResult'] @@ -46,5 +46,5 @@ class ScanResult: Scan result representation """ def __init__(self): - self.locations = defaultdict(list) - self.vulns = [] + self.locations = defaultdict(list) # type: Dict[str, List[str]] + self.vulns = [] # type: List[Vuln] diff --git a/contrib/report_builders/__init__.py b/contrib/report_builders/__init__.py index 6efa8430dad6dfd318e1fdd75ed5054d3c1528c1..5db235371c69a132b59d71227a0b2844a03f40fd 100644 --- a/contrib/report_builders/__init__.py +++ b/contrib/report_builders/__init__.py @@ -1,2 +1,3 @@ from .report_builder import ReportBuilder from .latex_report_builder import LatexReportBuilder +from .markdown_report_builder import MarkdownReportBuilder diff --git a/contrib/report_builders/latex_report_builder.py b/contrib/report_builders/latex_report_builder.py index d6f304b64f2e9adb734a735eaa48f604188cbce9..5f1cb618ec9d7cf60d3a052549007332c4dd6612 100644 --- a/contrib/report_builders/latex_report_builder.py +++ b/contrib/report_builders/latex_report_builder.py @@ -42,7 +42,8 @@ class LatexReportBuilder(ReportBuilder): locations = report.locations num_vulns = len(vulns) - for i, v in enumerate(vulns): + for v in vulns: + description = self.description_provider.get_description(v.name, v.vuln_type) severity_name = v.severity_str self._append('\\begin{figure}[h!]\n') self._append('\\begin{tabular}{|p{16cm}|}\\rowcolor[HTML]{' @@ -50,10 +51,10 @@ class LatexReportBuilder(ReportBuilder): + '} \\begin{tabular}{@{}p{15cm}>{\\raggedleft\\arraybackslash} p{0.5cm}@{}}\\textbf{' + v.name + ' ' + severity_name + ' (' + str(v.severity) - + ')} & \href{https://nvd.nist.gov/vuln/detail/' - + v.name + '}{\large \\faicon{link}}' + + ')} & \href{' + description.url + + '}{\large \\faicon{link}}' + '\end{tabular}\\\\\n Summary:' - + self.description_provider.get_description(v.name, v.vuln_type) + + description.text + '\\\\ \hline \end{tabular} ') self._append('\end{figure}\n') diff --git a/contrib/report_builders/markdown_report_builder.py b/contrib/report_builders/markdown_report_builder.py new file mode 100644 index 0000000000000000000000000000000000000000..f51b346a34072380b13e3fb76b86c3c45e1f60f4 --- /dev/null +++ b/contrib/report_builders/markdown_report_builder.py @@ -0,0 +1,95 @@ +from datetime import datetime +from typing import Any, Dict, List + +from contrib.descriptions import VulnDescriptionProvider +from contrib.internal_types import ScanResult +from contrib.report_builders import ReportBuilder + +__all__ = ['MarkdownReportBuilder'] + + +class MarkdownReportBuilder(ReportBuilder): + def __init__(self, description_provider: VulnDescriptionProvider): + self.description_provider = description_provider + self._buffer = '' + + def init_report(self, start_date: str, nmap_command: str): + self._append_line(self.header) + self._append_line('## {date:%B %d, %Y}'.format(date=datetime.utcnow())) + self._append_line('### **Summary**') + self._append_line('Flan Scan ran a network vulnerability scan with the following Nmap command on {date}' + .format(date=start_date)) + self._append_line('`{command}`'.format(command=nmap_command)) + + def build(self) -> Any: + return self._buffer + + def add_vulnerable_section(self): + self._append_line('### Services with vulnerabilities') + + def add_non_vulnerable_section(self): + self._append_line('### Services with no *known* vulnerabilities') + + def add_vulnerable_services(self, scan_results: Dict[str, ScanResult]): + for i, pair in enumerate(scan_results.items(), start=1): + app_name, report = pair # type: str, ScanResult + self._append_service(i, app_name) + num_vulns = len(report.vulns) + + for v in report.vulns: + description = self.description_provider.get_description(v.name, v.vuln_type) + self._append_line('- [**{name}** {severity} ({severity_num})]({link} "{title}")' + .format(name=v.name, severity=v.severity_str, severity_num=v.severity, + link=description.url, title=v.name), spaces=4) + self._append_line('```text', separators=1, spaces=6) + self._append_line(description.text, separators=1, spaces=6) + self._append_line('```', spaces=6) + + self._append_line('The above {num} vulnerabilities apply to these network locations'.format(num=num_vulns), + spaces=4) + self._append_line('```text', separators=1, spaces=4) + for addr, ports in report.locations.items(): + self._append_location(addr, ports, spaces=4) + self._append_line('```', spaces=4) + + def add_non_vulnerable_services(self, scan_results: Dict[str, ScanResult]): + for i, pair in enumerate(scan_results.items(), start=1): + app_name, report = pair # type: str, ScanResult + self._append_service(i, app_name) + + for addr, ports in report.locations.items(): + self._append_location(addr, ports, spaces=4) + self._append('\n') + + def initialize_section(self): + pass + + def add_ips_section(self): + self._append_line('### List of IPs Scanned') + + def add_ip_address(self, ip: str): + self._append_line('- {ip}'.format(ip=ip), separators=1) + + def finalize(self): + pass + + @property + def header(self) -> Any: + return '# Flan scan report' + + def _append(self, text: str, spaces: int = 0): + if spaces: + self._buffer += ' ' * spaces + self._buffer += text + + def _append_line(self, text: str, separators: int = 2, spaces: int = 0): + self._append(text, spaces) + self._append('\n' * separators) + + def _append_service(self, index: int, name: str, spaces: int = 0): + self._append_line('{index}. **{service}**'.format(index=index, service=name.strip()), spaces=spaces, + separators=1) + + def _append_location(self, address: str, ports: List[str], spaces: int): + self._append_line('- {address} Ports: {ports}'.format(address=address, ports=', '.join(ports)), spaces=spaces, + separators=1) diff --git a/output_report.py b/output_report.py index 239a13b372721d1ee9944068d1e980a9db075082..2f5b45e751f1d4d3e3edfddafba97a6a8703f6b6 100644 --- a/output_report.py +++ b/output_report.py @@ -6,7 +6,7 @@ from requests import Session from contrib.descriptions import CveProjectProvider from contrib.parsers import FlanXmlParser -from contrib.report_builders import ReportBuilder, LatexReportBuilder +from contrib.report_builders import ReportBuilder, LatexReportBuilder, MarkdownReportBuilder def create_report(parser: FlanXmlParser, builder: ReportBuilder, nmap_command: str, start_date: str, output_writer: IO, @@ -38,12 +38,15 @@ def parse_nmap_command(raw_command: str) -> str: return ' '.join(nmap_split) +def create_default_provider(): + return CveProjectProvider(Session()) + + def create_report_builder(report_type: str) -> ReportBuilder: if report_type == 'latex': - session = Session() - description_provider = CveProjectProvider(session) - report_bilder = LatexReportBuilder(description_provider) - return report_bilder + return LatexReportBuilder(create_default_provider()) + if report_type == 'md': + return MarkdownReportBuilder(create_default_provider()) raise NotImplementedError(report_type)