| 1 | + | import os |
1 | 2 | | import sys |
2 | | - | import json |
3 | | - | import urllib.request as urllib |
4 | | - | import os |
5 | | - | import xmltodict |
| 3 | + | from typing import IO |
6 | 4 | | |
7 | | - | results = {} |
8 | | - | vulnerable_services = [] |
9 | | - | colors = {'High': 'FD6864', 'Medium': 'F8A102', 'Low': '34CDF9'} |
| 5 | + | from requests import Session |
10 | 6 | | |
| 7 | + | from contrib.descriptions import CveProjectProvider |
| 8 | + | from contrib.parsers import FlanXmlParser |
| 9 | + | from contrib.report_builders import ReportBuilder, LatexReportBuilder, MarkdownReportBuilder |
11 | 10 | | |
12 | | - | def parse_vuln(ip_addr, port, app_name, vuln): |
13 | | - | vuln_name = '' |
14 | | - | severity = '' |
15 | | - | type = '' |
16 | | - | for field in vuln: |
17 | | - | if field['@key'] == 'cvss': |
18 | | - | severity = float(field['#text']) |
19 | | - | elif field['@key'] == 'id': |
20 | | - | vuln_name = field['#text'] |
21 | | - | elif field['@key'] == 'type': |
22 | | - | type = field['#text'] |
23 | | - | if 'vulns'in results[app_name].keys(): |
24 | | - | results[app_name]['vulns'].append({'name': vuln_name, |
25 | | - | 'type': type, |
26 | | - | 'severity': severity}) |
27 | | - | else: |
28 | | - | results[app_name]['vulns'] = [{'name': vuln_name, |
29 | | - | 'type': type, |
30 | | - | 'severity': severity}] |
31 | 11 | | |
| 12 | + | def create_report(parser: FlanXmlParser, builder: ReportBuilder, nmap_command: str, start_date: str, output_writer: IO, |
| 13 | + | ip_reader: IO): |
32 | 14 | | |
33 | | - | def parse_script(ip_addr, port, app_name, script): |
34 | | - | if 'table' in script.keys(): |
35 | | - | vulnerable_services.append(app_name) |
36 | | - | script_table = script['table']['table'] |
37 | | - | if isinstance(script_table, list): |
38 | | - | for vuln in script_table: |
39 | | - | parse_vuln(ip_addr, port, app_name, vuln['elem']) |
40 | | - | else: |
41 | | - | parse_vuln(ip_addr, port, app_name, script_table['elem']) |
42 | | - | else: |
43 | | - | print('ERROR in script: ' + script['@output'] + " at location: " + ip_addr + " port: " + port + " app: " + app_name) |
44 | | - | |
45 | | - | |
46 | | - | def get_app_name(service): |
47 | | - | app_name = '' |
48 | | - | if '@product' in service.keys(): |
49 | | - | app_name += service['@product'] + " " |
50 | | - | if '@version' in service.keys(): |
51 | | - | app_name += service['@version'] + " " |
52 | | - | elif '@name' in service.keys(): |
53 | | - | app_name += service['@name'] + " " |
54 | | - | |
55 | | - | if('cpe' in service.keys()): |
56 | | - | if isinstance(service['cpe'], list): |
57 | | - | for cpe in service['cpe']: |
58 | | - | app_name += '(' + cpe + ") " |
59 | | - | else: |
60 | | - | app_name += '(' + service['cpe'] + ") " |
61 | | - | return app_name |
62 | | - | |
63 | | - | |
64 | | - | def parse_port(ip_addr, port): |
65 | | - | if port['state']['@state'] == 'closed': |
66 | | - | return |
67 | | - | app_name = get_app_name(port['service']) |
68 | | - | |
69 | | - | port_num = port['@portid'] |
70 | | - | |
71 | | - | if app_name in results.keys(): |
72 | | - | if ip_addr in results[app_name]['locations'].keys(): |
73 | | - | results[app_name]['locations'][ip_addr].append(port_num) |
74 | | - | else: |
75 | | - | results[app_name]['locations'][ip_addr] = [port_num] |
76 | | - | else: |
77 | | - | results[app_name] = {'locations': {ip_addr: [port_num]}} |
78 | | - | if 'script' in port.keys(): |
79 | | - | scripts = port['script'] |
80 | | - | if isinstance(scripts, list): |
81 | | - | for s in scripts: |
82 | | - | if s['@id'] == 'vulners': |
83 | | - | parse_script(ip_addr, port_num, app_name, s) |
84 | | - | else: |
85 | | - | if scripts['@id'] == 'vulners': |
86 | | - | parse_script(ip_addr, port_num, app_name, scripts) |
87 | | - | |
88 | | - | |
89 | | - | def parse_host(host): |
90 | | - | addresses = host['address'] |
91 | | - | if isinstance(addresses, list): |
92 | | - | for addr in addresses: |
93 | | - | if "ip" in addr['@addrtype']: |
94 | | - | ip_addr = addr['@addr'] |
95 | | - | else: |
96 | | - | ip_addr = addresses['@addr'] |
97 | | - | |
98 | | - | if host['status']['@state'] == 'up' and 'port' in host['ports'].keys(): |
99 | | - | ports = host['ports']['port'] |
100 | | - | if isinstance(ports, list): |
101 | | - | for p in ports: |
102 | | - | parse_port(ip_addr, p) |
103 | | - | else: |
104 | | - | parse_port(ip_addr, ports) |
105 | | - | |
106 | | - | |
107 | | - | def parse_results(data): |
108 | | - | if 'host' in data['nmaprun'].keys(): |
109 | | - | hosts = data['nmaprun']['host'] |
110 | | - | |
111 | | - | if isinstance(hosts, list): |
112 | | - | for h in hosts: |
113 | | - | parse_host(h) |
114 | | - | else: |
115 | | - | parse_host(hosts) |
116 | | - | |
117 | | - | |
118 | | - | def convert_severity(sev): |
119 | | - | if sev < 4: |
120 | | - | return 'Low' |
121 | | - | elif sev < 7: |
122 | | - | return 'Medium' |
123 | | - | else: |
124 | | - | return 'High' |
125 | | - | |
126 | | - | |
127 | | - | def get_description(vuln, type): |
128 | | - | if type == 'cve': |
129 | | - | year = vuln[4:8] |
130 | | - | section = vuln[9:-3] + 'xxx' |
131 | | - | url = """https://raw.githubusercontent.com/CVEProject/cvelist/master/{}/{}/{}.json""".format(year, section, vuln) |
132 | | - | cve_json = json.loads(urllib.urlopen(url).read().decode("utf-8")) |
133 | | - | return cve_json["description"]["description_data"][0]["value"] |
134 | | - | else: |
135 | | - | return '' |
| 15 | + | builder.init_report(start_date, nmap_command) |
136 | 16 | | |
| 17 | + | if parser.vulnerable_services: |
| 18 | + | builder.add_vulnerable_section() |
| 19 | + | builder.initialize_section() |
| 20 | + | builder.add_vulnerable_services(parser.vulnerable_dict) |
137 | 21 | | |
138 | | - | def create_latex(nmap_command, start_date): |
139 | | - | f = open('./latex_header.tex') |
140 | | - | write_buffer = f.read() |
141 | | - | f.close() |
| 22 | + | if parser.non_vuln_services: |
| 23 | + | builder.add_non_vulnerable_section() |
| 24 | + | builder.initialize_section() |
| 25 | + | builder.add_non_vulnerable_services(parser.non_vulnerable_dict) |
142 | 26 | | |
143 | | - | output_file = sys.argv[2] |
144 | | - | ip_file = sys.argv[3] |
| 27 | + | builder.add_ips_section() |
| 28 | + | for ip in ip_reader: |
| 29 | + | builder.add_ip_address(ip) |
145 | 30 | | |
146 | | - | write_buffer += "Flan Scan ran a network vulnerability scan with the following Nmap command on " \ |
147 | | - | + start_date \ |
148 | | - | + "UTC.\n\\begin{lstlisting}\n" \ |
149 | | - | + nmap_command \ |
150 | | - | + "\n\end{lstlisting}\nTo find out what IPs were scanned see the end of this report.\n" |
151 | | - | write_buffer += "\section*{Services with Vulnerabilities}" |
152 | | - | if vulnerable_services: |
153 | | - | write_buffer += """\\begin{enumerate}[wide, labelwidth=!, labelindent=0pt, |
154 | | - | label=\\textbf{\large \\arabic{enumi} \large}]\n""" |
155 | | - | for s in vulnerable_services: |
156 | | - | write_buffer += '\item \\textbf{\large ' + s + ' \large}' |
157 | | - | vulns = results[s]['vulns'] |
158 | | - | locations = results[s]['locations'] |
159 | | - | num_vulns = len(vulns) |
| 31 | + | builder.finalize() |
| 32 | + | output_writer.write(builder.build()) |
160 | 33 | | |
161 | | - | for i, v in enumerate(vulns): |
162 | | - | write_buffer += '\\begin{figure}[h!]\n' |
163 | | - | severity_name = convert_severity(v['severity']) |
164 | | - | write_buffer += '\\begin{tabular}{|p{16cm}|}\\rowcolor[HTML]{' \ |
165 | | - | + colors[severity_name] \ |
166 | | - | + """} \\begin{tabular}{@{}p{15cm}>{\\raggedleft\\arraybackslash} |
167 | | - | p{0.5cm}@{}}\\textbf{""" \ |
168 | | - | + v['name'] + ' ' + severity_name + ' (' \ |
169 | | - | + str(v['severity']) \ |
170 | | - | + ')} & \href{https://nvd.nist.gov/vuln/detail/' \ |
171 | | - | + v['name'] + '}{\large \\faicon{link}}' \ |
172 | | - | + '\end{tabular}\\\\\n Summary:' \ |
173 | | - | + get_description(v['name'], v['type']) \ |
174 | | - | + '\\\\ \hline \end{tabular} ' |
175 | | - | write_buffer += '\end{figure}\n' |
176 | 34 | | |
177 | | - | write_buffer += '\FloatBarrier\n\\textbf{The above ' \ |
178 | | - | + str(num_vulns) \ |
179 | | - | + """ vulnerabilities apply to these network locations:}\n |
180 | | - | \\begin{itemize}\n""" |
181 | | - | for addr in locations.keys(): |
182 | | - | write_buffer += '\item ' + addr + ' Ports: ' + str(locations[addr])+ '\n' |
183 | | - | write_buffer += '\\\\ \\\\ \n \end{itemize}\n' |
184 | | - | write_buffer += '\end{enumerate}\n' |
| 35 | + | def parse_nmap_command(raw_command: str) -> str: |
| 36 | + | nmap_split = raw_command.split()[:-1] # remove last element, ip address |
| 37 | + | nmap_split[3] = '<output-file>' |
| 38 | + | return ' '.join(nmap_split) |
185 | 39 | | |
186 | | - | non_vuln_services = list(set(results.keys()) - set(vulnerable_services)) |
187 | | - | write_buffer += '\section*{Services With No Known Vulnerabilities}' |
188 | 40 | | |
189 | | - | if non_vuln_services: |
190 | | - | write_buffer += """\\begin{enumerate}[wide, labelwidth=!, labelindent=0pt, |
191 | | - | label=\\textbf{\large \\arabic{enumi} \large}]\n""" |
192 | | - | for ns in non_vuln_services: |
193 | | - | write_buffer += '\item \\textbf{\large ' + ns \ |
194 | | - | + ' \large}\n\\begin{itemize}\n' |
195 | | - | locations = results[ns]['locations'] |
196 | | - | for addr in locations.keys(): |
197 | | - | write_buffer += '\item ' + addr + ' Ports: ' + str(locations[addr])+ '\n' |
198 | | - | write_buffer += '\end{itemize}\n' |
199 | | - | write_buffer += '\end{enumerate}\n' |
| 41 | + | def create_default_provider(): |
| 42 | + | return CveProjectProvider(Session()) |
200 | 43 | | |
201 | | - | write_buffer += '\section*{List of IPs Scanned}' |
202 | | - | write_buffer += '\\begin{itemize}\n' |
203 | | - | f = open(ip_file) |
204 | | - | for line in f: |
205 | | - | write_buffer += '\item ' + line + '\n' |
206 | | - | f.close() |
207 | | - | write_buffer += '\end{itemize}\n' |
208 | 44 | | |
209 | | - | write_buffer += '\end{document}' |
210 | | - | latex_file = open(output_file, "w+") |
211 | | - | latex_file.write(write_buffer) |
212 | | - | latex_file.close() |
| 45 | + | def create_report_builder(report_type: str) -> ReportBuilder: |
| 46 | + | if report_type == 'latex': |
| 47 | + | return LatexReportBuilder(create_default_provider()) |
| 48 | + | if report_type == 'md': |
| 49 | + | return MarkdownReportBuilder(create_default_provider()) |
| 50 | + | raise NotImplementedError(report_type) |
213 | 51 | | |
214 | | - | def parse_nmap_command(raw_command): |
215 | | - | nmap_split = raw_command.split()[:-1] #remove last element, ip address |
216 | | - | nmap_split[3] = "<output-file>" |
217 | | - | return " ".join(nmap_split) |
218 | 52 | | |
219 | | - | def main(): |
220 | | - | dirname = sys.argv[1] |
221 | | - | nmap_command = "" |
222 | | - | start_date = "" |
| 53 | + | def main(dirname: str, output_file: str, ip_file: str, report_type: str = 'latex'): |
| 54 | + | nmap_command = '' |
| 55 | + | start_date = '' |
| 56 | + | builder = create_report_builder(report_type) |
| 57 | + | parser = FlanXmlParser() |
223 | 58 | | |
224 | | - | for i, filename in enumerate(os.listdir(dirname)): |
225 | | - | f = open(dirname + "/" + filename) |
226 | | - | xml_content = f.read() |
227 | | - | f.close() |
228 | | - | data = xmltodict.parse(xml_content) |
229 | | - | parse_results(data) |
230 | | - | if i == 0: |
231 | | - | nmap_command = parse_nmap_command(data['nmaprun']['@args']) |
232 | | - | start_date = data['nmaprun']['@startstr'] |
| 59 | + | for entry in os.scandir(dirname): # type: os.DirEntry |
| 60 | + | if not (entry.is_file() and entry.name.endswith('.xml')): |
| 61 | + | continue |
| 62 | + | data = parser.read_xml_file(entry.path) |
| 63 | + | parser.parse(data) |
| 64 | + | nmap_command = parse_nmap_command(data['nmaprun']['@args']) |
| 65 | + | start_date = data['nmaprun']['@startstr'] |
233 | 66 | | |
234 | | - | create_latex(nmap_command, start_date) |
| 67 | + | with open(output_file, 'w+') as output, open(ip_file) as ip_source: |
| 68 | + | create_report(parser, builder, nmap_command, start_date, output, ip_source) |
235 | 69 | | |
236 | 70 | | |
237 | | - | if __name__ == "__main__": |
238 | | - | main() |
| 71 | + | if __name__ == '__main__': |
| 72 | + | main(*sys.argv[1:4], report_type='latex') |
239 | 73 | | |