New upstream version 1.5.2
Sophie Brun
2 years ago
0 | cwe, capec, references, tags, impact, resolution, easeofresolution |
0 | add os openvas⏎ |
0 | Jul 27th, 2021 |
0 | [FIX] Fix improt of CSV with big fields |
0 | Fix sslyze json bug with port |
0 | Only show report name in command data |
0 | Aug 9th, 2021 |
0 | add new structure acunetix ⏎ |
0 | 1.5.2 [Aug 9th, 2021]: | |
1 | --- | |
2 | * add new structure acunetix | |
3 | ||
4 | 1.5.1 [Jul 27th, 2021]: | |
5 | --- | |
6 | * cwe, capec, references, tags, impact, resolution, easeofresolution | |
7 | * add os openvas | |
8 | * [FIX] Fix improt of CSV with big fields | |
9 | * Fix sslyze json bug with port | |
10 | * Only show report name in command data | |
11 | ||
0 | 12 | 1.5.0 [Jun 28th, 2021]: |
1 | 13 | --- |
2 | 14 | * Add Nipper Plugin |
5 | 5 | import shlex |
6 | 6 | import subprocess |
7 | 7 | import sys |
8 | from pathlib import Path | |
8 | 9 | |
9 | 10 | import click |
10 | 11 | from tabulate import tabulate |
79 | 80 | if not plugin: |
80 | 81 | click.echo(click.style(f"Failed to detect report: {report_file}", fg="red"), err=True) |
81 | 82 | return |
82 | plugin.processReport(report_file, getpass.getuser()) | |
83 | plugin.processReport(Path(report_file), getpass.getuser()) | |
83 | 84 | if summary: |
84 | 85 | click.echo(json.dumps(plugin.get_summary(), indent=4)) |
85 | 86 | else: |
13 | 13 | import zipfile |
14 | 14 | from collections import defaultdict |
15 | 15 | from datetime import datetime |
16 | from pathlib import Path | |
16 | 17 | |
17 | 18 | import pytz |
18 | 19 | import simplejson as json |
276 | 277 | params = " ".join(command_string.split()[2:]) |
277 | 278 | else: |
278 | 279 | params = " ".join(command_string.split()[1:]) |
279 | self.vulns_data["command"]["params"] = params | |
280 | self.vulns_data["command"]["params"] = params if not self.ignore_info else f"{params} (Info ignored)" | |
280 | 281 | self.vulns_data["command"]["user"] = username |
281 | 282 | self.vulns_data["command"]["import_source"] = "shell" |
282 | 283 | if self._use_temp_file: |
299 | 300 | |
300 | 301 | def processOutput(self, command_output): |
301 | 302 | if self.has_custom_output(): |
302 | self._parse_filename(self.get_custom_file_path()) | |
303 | self._parse_filename(Path(self.get_custom_file_path())) | |
303 | 304 | else: |
304 | 305 | self.parseOutputString(command_output) |
305 | 306 | |
306 | def _parse_filename(self, filename): | |
307 | with open(filename, **self.open_options) as output: | |
307 | def _parse_filename(self, filename: Path): | |
308 | with filename.open(**self.open_options) as output: | |
308 | 309 | self.parseOutputString(output.read()) |
309 | 310 | if self._delete_temp_file: |
310 | 311 | try: |
311 | if os.path.isfile(filename): | |
312 | if filename.is_file(): | |
312 | 313 | os.remove(filename) |
313 | elif os.path.isdir(filename): | |
314 | elif filename.is_dir(): | |
314 | 315 | shutil.rmtree(filename) |
315 | 316 | except Exception as e: |
316 | 317 | self.logger.error("Error on delete file: (%s) [%s]", filename, e) |
317 | 318 | |
318 | def processReport(self, filepath, user="faraday"): | |
319 | if os.path.isfile(filepath): | |
320 | self.vulns_data["command"]["params"] = filepath if not self.ignore_info else f"{filepath} (Info ignored)" | |
319 | def processReport(self, filepath: Path, user="faraday"): | |
320 | if type(filepath) == str: # TODO workaround for compatibility, remove in the future | |
321 | filepath = Path(filepath) | |
322 | if filepath.is_file(): | |
323 | self.vulns_data["command"]["params"] = filepath.name if not self.ignore_info else f"{filepath.name} (Info ignored)" | |
321 | 324 | self.vulns_data["command"]["user"] = user |
322 | 325 | self.vulns_data["command"]["import_source"] = "report" |
323 | 326 | self._parse_filename(filepath) |
565 | 568 | def processOutput(self, term_output): |
566 | 569 | # we discard the term_output since it's not necessary |
567 | 570 | # for this type of plugins |
568 | self.processReport(self._output_file_path) | |
571 | self.processReport(Path(self._output_file_path)) | |
569 | 572 | |
570 | 573 | |
571 | 574 | class PluginByExtension(PluginBase): |
0 | from typing import List | |
1 | ||
2 | ||
3 | class Technicaldetails: | |
4 | def __init__(self, node): | |
5 | self.node = node | |
6 | ||
7 | @property | |
8 | def request(self) -> str: | |
9 | if not self.node: | |
10 | return '' | |
11 | return self.node.findtext('Request', '') | |
12 | ||
13 | @property | |
14 | def response(self) -> str: | |
15 | if not self.node: | |
16 | return '' | |
17 | return self.node.findtext('Response', '') | |
18 | ||
19 | ||
20 | class Cwe: | |
21 | def __init__(self, node): | |
22 | self.node = node | |
23 | ||
24 | @property | |
25 | def id_attr(self) -> str: | |
26 | return self.node.findtext('id', '') | |
27 | ||
28 | @property | |
29 | def text(self) -> str: | |
30 | return self.node.findtext('#text', '') | |
31 | ||
32 | ||
33 | class Cwelist: | |
34 | def __init__(self, node): | |
35 | self.node = node | |
36 | ||
37 | @property | |
38 | def cwe(self) -> Cwe: | |
39 | return Cwe(self.node.find('CWE')) | |
40 | ||
41 | ||
42 | class Cvss: | |
43 | def __init__(self, node): | |
44 | self.node = node | |
45 | ||
46 | @property | |
47 | def descriptor(self) -> str: | |
48 | return self.node.findtext('Descriptor', '') | |
49 | ||
50 | @property | |
51 | def score(self) -> str: | |
52 | return self.node.findtext('Score', '') | |
53 | ||
54 | @property | |
55 | def av(self) -> str: | |
56 | return self.node.findtext('AV', '') | |
57 | ||
58 | @property | |
59 | def ac(self) -> str: | |
60 | return self.node.findtext('AC', '') | |
61 | ||
62 | @property | |
63 | def au(self) -> str: | |
64 | return self.node.findtext('Au', '') | |
65 | ||
66 | @property | |
67 | def c(self) -> str: | |
68 | return self.node.findtext('C', '') | |
69 | ||
70 | @property | |
71 | def i(self) -> str: | |
72 | return self.node.findtext('I', '') | |
73 | ||
74 | @property | |
75 | def a(self) -> str: | |
76 | return self.node.findtext('A', '') | |
77 | ||
78 | @property | |
79 | def e(self): | |
80 | return self.node.find('E') | |
81 | ||
82 | @property | |
83 | def rl(self): | |
84 | return self.node.find('RL') | |
85 | ||
86 | @property | |
87 | def rc(self): | |
88 | return self.node.find('RC') | |
89 | ||
90 | ||
91 | class Cvss3: | |
92 | def __init__(self, node): | |
93 | self.node = node | |
94 | ||
95 | @property | |
96 | def descriptor(self) -> str: | |
97 | return self.node.findtext('Descriptor', '') | |
98 | ||
99 | @property | |
100 | def score(self) -> str: | |
101 | return self.node.findtext('Score', '') | |
102 | ||
103 | @property | |
104 | def tempscore(self): | |
105 | return self.node.find('TempScore') | |
106 | ||
107 | @property | |
108 | def envscore(self): | |
109 | return self.node.find('EnvScore') | |
110 | ||
111 | @property | |
112 | def av(self) -> str: | |
113 | return self.node.find('AV', '') | |
114 | ||
115 | @property | |
116 | def ac(self) -> str: | |
117 | return self.node.find('AC', '') | |
118 | ||
119 | @property | |
120 | def pr(self) -> str: | |
121 | return self.node.find('PR', '') | |
122 | ||
123 | @property | |
124 | def ui(self) -> str: | |
125 | return self.node.find('UI', '') | |
126 | ||
127 | @property | |
128 | def s(self) -> str: | |
129 | return self.node.find('S', '') | |
130 | ||
131 | @property | |
132 | def c(self) -> str: | |
133 | return self.node.find('C', '') | |
134 | ||
135 | @property | |
136 | def i(self) -> str: | |
137 | return self.node.findtext('I', '') | |
138 | ||
139 | @property | |
140 | def a(self) -> str: | |
141 | return self.node.findtext('A', '') | |
142 | ||
143 | @property | |
144 | def e(self): | |
145 | return self.node.find('E') | |
146 | ||
147 | @property | |
148 | def rl(self): | |
149 | return self.node.find('RL') | |
150 | ||
151 | @property | |
152 | def rc(self): | |
153 | return self.node.find('RC') | |
154 | ||
155 | ||
156 | class Reference: | |
157 | def __init__(self, node): | |
158 | self.node = node | |
159 | ||
160 | @property | |
161 | def database(self) -> str: | |
162 | return self.node.findtext('Database', '') | |
163 | ||
164 | @property | |
165 | def url(self) -> str: | |
166 | return self.node.findtext('URL', '') | |
167 | ||
168 | ||
169 | class References: | |
170 | def __init__(self, node): | |
171 | self.node = node | |
172 | ||
173 | @property | |
174 | def reference(self) -> List[Reference]: | |
175 | return [Reference(i) for i in self.node.findall('Reference', [])] | |
176 | ||
177 | ||
178 | class Reportitem: | |
179 | def __init__(self, node): | |
180 | self.node = node | |
181 | ||
182 | @property | |
183 | def id_attr(self) -> str: | |
184 | return self.node.findtext('id', '') | |
185 | ||
186 | @property | |
187 | def color_attr(self) -> str: | |
188 | return self.node.findtext('color', '') | |
189 | ||
190 | @property | |
191 | def name(self) -> str: | |
192 | return self.node.findtext('Name', '') | |
193 | ||
194 | @property | |
195 | def modulename(self) -> str: | |
196 | return self.node.findtext('ModuleName', '') | |
197 | ||
198 | @property | |
199 | def details(self) -> str: | |
200 | return self.node.findtext('Details', '') | |
201 | ||
202 | @property | |
203 | def affects(self) -> str: | |
204 | return self.node.findtext('Affects', '') | |
205 | ||
206 | @property | |
207 | def parameter(self) -> str: | |
208 | return self.node.findtext('Parameter') | |
209 | ||
210 | @property | |
211 | def aop_sourcefile(self): | |
212 | return self.node.find('AOP_SourceFile') | |
213 | ||
214 | @property | |
215 | def aop_sourceline(self): | |
216 | return self.node.find('AOP_SourceLine') | |
217 | ||
218 | @property | |
219 | def aop_additional(self): | |
220 | return self.node.find('AOP_Additional') | |
221 | ||
222 | @property | |
223 | def isfalsepositive(self): | |
224 | return self.node.find('IsFalsePositive') | |
225 | ||
226 | @property | |
227 | def severity(self) -> str: | |
228 | return self.node.findtext('Severity', '') | |
229 | ||
230 | @property | |
231 | def type(self) -> str: | |
232 | return self.node.findtext('Type', '') | |
233 | ||
234 | @property | |
235 | def impact(self) -> str: | |
236 | return self.node.findtext('Impact', '') | |
237 | ||
238 | @property | |
239 | def description(self) -> str: | |
240 | return self.node.findtext('Description', '') | |
241 | ||
242 | @property | |
243 | def recommendation(self) -> str: | |
244 | return self.node.findtext('Recommendation', '') | |
245 | ||
246 | @property | |
247 | def technicaldetails(self) -> Technicaldetails: | |
248 | return Technicaldetails(self.node.find('TechnicalDetails')) | |
249 | ||
250 | @property | |
251 | def cwelist(self) -> Cwelist: | |
252 | return Cwelist(self.node.find('CWEList')) | |
253 | ||
254 | @property | |
255 | def cvelist(self): | |
256 | return self.node.find('CVEList') | |
257 | ||
258 | @property | |
259 | def cvss(self) -> Cvss: | |
260 | return Cvss(self.node.find('cvss')) | |
261 | ||
262 | @property | |
263 | def cvss3(self) -> Cvss3: | |
264 | return Cvss3(self.node.find('cvss3')) | |
265 | ||
266 | @property | |
267 | def references(self) -> References: | |
268 | return References(self.node.find('References')) | |
269 | ||
270 | ||
271 | class Reportitems: | |
272 | def __init__(self, node): | |
273 | self.node = node | |
274 | ||
275 | @property | |
276 | def reportitem(self) -> List[Reportitem]: | |
277 | return [Reportitem(i) for i in self.node.findall('ReportItem', [])] | |
278 | ||
279 | ||
280 | class Crawler: | |
281 | def __init__(self, node): | |
282 | self.node = node | |
283 | ||
284 | @property | |
285 | def start_url_attr(self) -> str: | |
286 | return self.node.get('StartUrl', '') | |
287 | ||
288 | ||
289 | class Scan: | |
290 | def __init__(self, node): | |
291 | self.node = node | |
292 | ||
293 | @property | |
294 | def reportitems(self) -> Reportitems: | |
295 | return Reportitems(self.node.find('ReportItems')) | |
296 | ||
297 | @property | |
298 | def start_url(self) -> str: | |
299 | return self.node.findtext("StartURL", "") | |
300 | ||
301 | @property | |
302 | def crawler(self) -> Crawler: | |
303 | return Crawler(self.node.find('Crawler')) | |
304 | ||
305 | @property | |
306 | def os(self) -> str: | |
307 | if not self.node.findtext("Os", "unknown"): | |
308 | return "unknown" | |
309 | return self.node.findtext("Os", "unknown") | |
310 | ||
311 | @property | |
312 | def banner(self) -> str: | |
313 | if not self.node.findtext('Banner'): | |
314 | return None | |
315 | return self.node.findtext("Banner") | |
316 | ||
317 | @property | |
318 | def start_url_new(self) -> str: | |
319 | return self.node.findtext("", "") | |
320 | ||
321 | ||
322 | class Acunetix: | |
323 | def __init__(self, node): | |
324 | self.node = node | |
325 | ||
326 | @property | |
327 | def exportedon_attr(self) -> str: | |
328 | return self.node.get('ExportedOn') | |
329 | ||
330 | @property | |
331 | def scan(self) -> List[Scan]: | |
332 | return [Scan(i) for i in self.node.findall('Scan', [])] |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | |
5 | 5 | """ |
6 | from re import findall | |
6 | 7 | from urllib.parse import urlsplit |
7 | 8 | |
8 | 9 | from lxml import etree |
9 | 10 | |
10 | 11 | from faraday_plugins.plugins.plugin import PluginXMLFormat |
11 | 12 | from faraday_plugins.plugins.plugins_utils import resolve_hostname |
13 | from faraday_plugins.plugins.repo.acunetix.DTO import Acunetix, Scan | |
12 | 14 | |
13 | 15 | __author__ = "Francisco Amato" |
14 | 16 | __copyright__ = "Copyright (c) 2013, Infobyte LLC" |
33 | 35 | """ |
34 | 36 | |
35 | 37 | def __init__(self, xml_output): |
38 | ||
36 | 39 | tree = self.parse_xml(xml_output) |
37 | if len(tree): | |
38 | self.sites = list(self.get_items(tree)) | |
39 | else: | |
40 | self.sites = [] | |
40 | self.acunetix = Acunetix(tree) | |
41 | 41 | |
42 | 42 | @staticmethod |
43 | 43 | def parse_xml(xml_output): |
49 | 49 | |
50 | 50 | @return xml_tree An xml tree instance. None if error. |
51 | 51 | """ |
52 | ||
52 | 53 | try: |
53 | 54 | parser = etree.XMLParser(recover=True) |
54 | 55 | tree = etree.fromstring(xml_output, parser=parser) |
57 | 58 | return None |
58 | 59 | |
59 | 60 | return tree |
60 | ||
61 | @staticmethod | |
62 | def get_items(tree): | |
63 | """ | |
64 | @return items A list of Host instances | |
65 | """ | |
66 | ||
67 | for node in tree.findall('Scan'): | |
68 | yield Site(node) | |
69 | ||
70 | ||
71 | def get_attrib_from_subnode(xml_node, subnode_xpath_expr, attrib_name): | |
72 | """ | |
73 | Finds a subnode in the item node and the retrieves a value from it | |
74 | ||
75 | @return An attribute value | |
76 | """ | |
77 | ||
78 | node = xml_node.find(subnode_xpath_expr) | |
79 | ||
80 | if node is not None: | |
81 | return node.get(attrib_name) | |
82 | ||
83 | return None | |
84 | ||
85 | ||
86 | class Site: | |
87 | ||
88 | def __init__(self, item_node): | |
89 | self.node = item_node | |
90 | url_data = self.get_url(self.node) | |
91 | ||
92 | self.protocol = url_data.scheme | |
93 | if url_data.hostname: | |
94 | self.host = url_data.hostname | |
95 | else: | |
96 | self.host = None | |
97 | # Use the port in the URL if it is defined, or 80 or 443 by default | |
98 | self.port = url_data.port or (443 if url_data.scheme == "https" else 80) | |
99 | self.ip = resolve_hostname(self.host) | |
100 | self.os = self.get_text_from_subnode('Os') | |
101 | self.banner = self.get_text_from_subnode('Banner') | |
102 | self.items = [] | |
103 | for alert in self.node.findall('ReportItems/ReportItem'): | |
104 | self.items.append(Item(alert)) | |
105 | ||
106 | def get_text_from_subnode(self, subnode_xpath_expr): | |
107 | """ | |
108 | Finds a subnode in the host node and the retrieves a value from it. | |
109 | ||
110 | @return An attribute value | |
111 | """ | |
112 | sub_node = self.node.find(subnode_xpath_expr) | |
113 | if sub_node is not None: | |
114 | return sub_node.text | |
115 | ||
116 | return None | |
117 | ||
118 | def get_url(self, node): | |
119 | url = self.get_text_from_subnode('StartURL') | |
120 | if not url.startswith('http'): | |
121 | url = f'http://{url}' | |
122 | url_data = urlsplit(url) | |
123 | if not url_data.scheme: | |
124 | # Getting url from subnode 'Crawler' | |
125 | url_aux = get_attrib_from_subnode(node, 'Crawler', 'StartUrl') | |
126 | url_data = urlsplit(url_aux) | |
127 | ||
128 | return url_data | |
129 | ||
130 | ||
131 | class Item: | |
132 | """ | |
133 | An abstract representation of a Item | |
134 | ||
135 | ||
136 | @param item_node A item_node taken from an acunetix xml tree | |
137 | """ | |
138 | ||
139 | def __init__(self, item_node): | |
140 | self.node = item_node | |
141 | self.name = self.get_text_from_subnode('Name') | |
142 | self.severity = self.get_text_from_subnode('Severity') | |
143 | self.request = self.get_text_from_subnode('TechnicalDetails/Request') | |
144 | self.response = self.get_text_from_subnode('TechnicalDetails/Response') | |
145 | self.parameter = self.get_text_from_subnode('Parameter') | |
146 | self.uri = self.get_text_from_subnode('Affects') | |
147 | ||
148 | if self.get_text_from_subnode('Description'): | |
149 | self.desc = self.get_text_from_subnode('Description') | |
150 | else: | |
151 | self.desc = "" | |
152 | ||
153 | if self.get_text_from_subnode('Recommendation'): | |
154 | self.resolution = self.get_text_from_subnode('Recommendation') | |
155 | else: | |
156 | self.resolution = "" | |
157 | ||
158 | if self.get_text_from_subnode('reference'): | |
159 | self.desc += "\nDetails: " + self.get_text_from_subnode('Details') | |
160 | ||
161 | # Add path and params to the description to create different IDs if at | |
162 | # least one of this fields is different | |
163 | if self.uri: | |
164 | self.desc += '\nPath: ' + self.uri | |
165 | if self.parameter: | |
166 | self.desc += '\nParameter: ' + self.parameter | |
167 | ||
168 | self.ref = [] | |
169 | for n in item_node.findall('References/Reference'): | |
170 | n2 = n.find('URL') | |
171 | self.ref.append(n2.text) | |
172 | ||
173 | def get_text_from_subnode(self, subnode_xpath_expr): | |
174 | """ | |
175 | Finds a subnode in the host node and the retrieves a value from it. | |
176 | ||
177 | @return An attribute value | |
178 | """ | |
179 | sub_node = self.node.find(subnode_xpath_expr) | |
180 | if sub_node is not None: | |
181 | return sub_node.text | |
182 | ||
183 | return None | |
184 | 61 | |
185 | 62 | |
186 | 63 | class AcunetixPlugin(PluginXMLFormat): |
210 | 87 | """ |
211 | 88 | parser = AcunetixXmlParser(output) |
212 | 89 | |
213 | for site in parser.sites: | |
214 | if site.ip is None: | |
90 | for site in parser.acunetix.scan: | |
91 | url_data = self.get_domain(site) | |
92 | if not url_data: | |
215 | 93 | continue |
94 | if url_data.hostname: | |
95 | self.old_structure(url_data, site) | |
96 | else: | |
97 | self.new_structure(site) | |
216 | 98 | |
217 | if site.host != site.ip and site.host is not None: | |
218 | hostnames = [site.host] | |
219 | else: | |
220 | hostnames = None | |
221 | h_id = self.createAndAddHost(site.ip, site.os, hostnames=hostnames) | |
99 | def new_structure(self, site): | |
100 | for item in site.reportitems.reportitem: | |
101 | host = item.technicaldetails.request | |
102 | host = findall('Host: (.*)', host)[0] | |
103 | url = f'http://{host}' | |
104 | url_data = urlsplit(url) | |
105 | site_ip = resolve_hostname(host) | |
106 | h_id = self.createAndAddHost(site_ip, site.os, hostnames=[host]) | |
222 | 107 | s_id = self.createAndAddServiceToHost( |
223 | 108 | h_id, |
224 | 109 | "http", |
225 | 110 | "tcp", |
226 | ports=[site.port], | |
111 | ports=['443'], | |
227 | 112 | version=site.banner, |
228 | 113 | status='open') |
229 | for item in site.items: | |
114 | self.create_vul(item, h_id, s_id, url_data) | |
230 | 115 | |
231 | if item.desc is None: | |
232 | self.createAndAddVulnWebToService( | |
233 | h_id, | |
234 | s_id, | |
235 | item.name, | |
236 | desc="", | |
237 | website=site.host, | |
238 | severity=item.severity, | |
239 | resolution=item.resolution, | |
240 | path=item.uri, | |
241 | params=item.parameter, | |
242 | request=item.request, | |
243 | response=item.response, | |
244 | ref=item.ref) | |
245 | else: | |
246 | self.createAndAddVulnWebToService( | |
247 | h_id, | |
248 | s_id, | |
249 | item.name, | |
250 | item.desc, | |
251 | website=site.host, | |
252 | severity=item.severity, | |
253 | resolution=item.resolution, | |
254 | path=item.uri, | |
255 | params=item.parameter, | |
256 | request=item.request, | |
257 | response=item.response, | |
258 | ref=item.ref) | |
259 | del parser | |
116 | def old_structure(self, url_data, site: Scan): | |
117 | site_ip = resolve_hostname(url_data.hostname) | |
118 | if url_data.hostname: | |
119 | hostnames = [url_data.hostname] | |
120 | else: | |
121 | hostnames = None | |
122 | port = url_data.port or (443 if url_data.scheme == "https" else 80) | |
123 | ||
124 | h_id = self.createAndAddHost(site_ip, site.os, hostnames=hostnames) | |
125 | s_id = self.createAndAddServiceToHost( | |
126 | h_id, | |
127 | "http", | |
128 | "tcp", | |
129 | ports=[port], | |
130 | version=site.banner, | |
131 | status='open') | |
132 | for item in site.reportitems.reportitem: | |
133 | self.create_vul(item, h_id, s_id, url_data) | |
134 | ||
135 | def create_vul(self, item, h_id, s_id, url_data): | |
136 | description = item.description | |
137 | if item.affects: | |
138 | description += f'\nPath: {item.affects}' | |
139 | if item.parameter: | |
140 | description += f'\nParameter: {item.parameter}' | |
141 | self.createAndAddVulnWebToService( | |
142 | h_id, | |
143 | s_id, | |
144 | item.name, | |
145 | description, | |
146 | website=url_data.hostname, | |
147 | severity=item.severity, | |
148 | resolution=item.recommendation, | |
149 | path=item.affects, | |
150 | params=item.parameter, | |
151 | request=item.technicaldetails.request, | |
152 | response=item.technicaldetails.response, | |
153 | ref=[i.url for i in item.references.reference]) | |
154 | ||
155 | @staticmethod | |
156 | def get_domain(scan: Scan): | |
157 | url = scan.start_url | |
158 | if not url.startswith('http'): | |
159 | url = f'http://{url}' | |
160 | url_data = urlsplit(url) | |
161 | if not url_data.scheme: | |
162 | url_data = urlsplit(scan.crawler.start_url_attr) | |
163 | return url_data | |
260 | 164 | |
261 | 165 | |
262 | 166 | def createPlugin(ignore_info=False): |
0 | from typing import List | |
1 | ||
2 | ||
3 | class InfoVul: | |
4 | def __init__(self, node): | |
5 | self.node = node | |
6 | ||
7 | @property | |
8 | def vt_id(self) -> str: | |
9 | if not self.node: | |
10 | return '' | |
11 | return self.node.get('vt_id', '') | |
12 | ||
13 | @property | |
14 | def request(self) -> str: | |
15 | if not self.node: | |
16 | return '' | |
17 | return self.node.get('request', '') | |
18 | ||
19 | ||
20 | class Vulnerabilities: | |
21 | def __init__(self, node): | |
22 | self.node = node | |
23 | ||
24 | @property | |
25 | def info(self) -> InfoVul: | |
26 | return InfoVul(self.node.get('info')) | |
27 | ||
28 | @property | |
29 | def response(self) -> str: | |
30 | if not self.node: | |
31 | return '' | |
32 | return self.node.get('response', '') | |
33 | ||
34 | ||
35 | class VulnerabilityTypes: | |
36 | def __init__(self, node): | |
37 | self.node = node | |
38 | ||
39 | @property | |
40 | def vt_id(self) -> str: | |
41 | if not self.node: | |
42 | return '' | |
43 | return self.node.get('vt_id', '') | |
44 | ||
45 | @property | |
46 | def name(self) -> str: | |
47 | if not self.node: | |
48 | return '' | |
49 | return self.node.get('name', '') | |
50 | ||
51 | @property | |
52 | def description(self) -> str: | |
53 | if not self.node: | |
54 | return '' | |
55 | return self.node.get('description', '') | |
56 | ||
57 | @property | |
58 | def severity(self) -> int: | |
59 | return self.node.get('severity', '') | |
60 | ||
61 | @property | |
62 | def recommendation(self) -> str: | |
63 | if not self.node: | |
64 | return '' | |
65 | return self.node.get('recommendation', '') | |
66 | ||
67 | @property | |
68 | def app_id(self) -> str: | |
69 | if not self.node: | |
70 | return '' | |
71 | return self.node.get('app_id', '') | |
72 | ||
73 | @property | |
74 | def use_ssl(self) -> bool: | |
75 | return self.node.get('use_ssl', '') | |
76 | ||
77 | ||
78 | class Info: | |
79 | def __init__(self, node): | |
80 | self.node = node | |
81 | ||
82 | @property | |
83 | def host(self) -> str: | |
84 | if not self.node: | |
85 | return '' | |
86 | return self.node.get('host', '') | |
87 | ||
88 | ||
89 | class Scan: | |
90 | def __init__(self, node): | |
91 | self.node = node | |
92 | ||
93 | @property | |
94 | def info(self) -> Info: | |
95 | return Info(self.node.get('info')) | |
96 | ||
97 | @property | |
98 | def vul_types(self) -> List[VulnerabilityTypes]: | |
99 | return [VulnerabilityTypes(i) for i in self.node.get('vulnerability_types', [])] | |
100 | ||
101 | @property | |
102 | def vulnerabilities(self) -> List[Vulnerabilities]: | |
103 | return [Vulnerabilities(i) for i in self.node.get('vulnerabilities', [])] | |
104 | ||
105 | ||
106 | class Export: | |
107 | def __init__(self, node): | |
108 | self.node = node | |
109 | ||
110 | @property | |
111 | def scans(self) -> List[Scan]: | |
112 | return [Scan(i) for i in self.node.get('scans', [])] | |
113 | ||
114 | @property | |
115 | def lang(self) -> str: | |
116 | if not self.node: | |
117 | return '' | |
118 | return self.node.get('scans', '') | |
119 | ||
120 | ||
121 | class AcunetixJsonParser: | |
122 | def __init__(self, node): | |
123 | self.node = node | |
124 | ||
125 | @property | |
126 | def export(self) -> Export: | |
127 | return Export(self.node.get('export'))⏎ |
0 | """ | |
1 | Faraday Penetration Test IDE | |
2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) | |
3 | See the file 'doc/LICENSE' for the license information | |
4 | ||
5 | """ | |
6 |
0 | """ | |
1 | Faraday Penetration Test IDE | |
2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) | |
3 | See the file 'doc/LICENSE' for the license information | |
4 | ||
5 | """ | |
6 | from urllib.parse import urlsplit | |
7 | ||
8 | from lxml import etree | |
9 | ||
10 | from faraday_plugins.plugins.plugin import PluginXMLFormat, PluginJsonFormat | |
11 | from faraday_plugins.plugins.plugins_utils import resolve_hostname | |
12 | from faraday_plugins.plugins.repo.acunetix.DTO import Acunetix, Scan | |
13 | from json import loads | |
14 | ||
15 | __author__ = "Francisco Amato" | |
16 | __copyright__ = "Copyright (c) 2013, Infobyte LLC" | |
17 | __credits__ = ["Francisco Amato"] | |
18 | __version__ = "1.0.0" | |
19 | __maintainer__ = "Francisco Amato" | |
20 | __email__ = "[email protected]" | |
21 | __status__ = "Development" | |
22 | ||
23 | from faraday_plugins.plugins.repo.acunetix_json.DTO import AcunetixJsonParser, Vulnerabilities, \ | |
24 | VulnerabilityTypes | |
25 | ||
26 | ||
27 | class AcunetixXmlParser: | |
28 | """ | |
29 | The objective of this class is to parse an xml file generated by | |
30 | the acunetix tool. | |
31 | ||
32 | TODO: Handle errors. | |
33 | TODO: Test acunetix output version. Handle what happens if | |
34 | the parser doesn't support it. | |
35 | TODO: Test cases. | |
36 | ||
37 | @param acunetix_xml_filepath A proper xml generated by acunetix | |
38 | """ | |
39 | ||
40 | def __init__(self, xml_output): | |
41 | ||
42 | tree = self.parse_xml(xml_output) | |
43 | self.acunetix = Acunetix(tree) | |
44 | ||
45 | @staticmethod | |
46 | def parse_xml(xml_output): | |
47 | """ | |
48 | Open and parse an xml file. | |
49 | ||
50 | TODO: Write custom parser to just read the nodes that we need instead | |
51 | of reading the whole file. | |
52 | ||
53 | @return xml_tree An xml tree instance. None if error. | |
54 | """ | |
55 | ||
56 | try: | |
57 | parser = etree.XMLParser(recover=True) | |
58 | tree = etree.fromstring(xml_output, parser=parser) | |
59 | except SyntaxError as err: | |
60 | print("SyntaxError: %s. %s", err, xml_output) | |
61 | return None | |
62 | ||
63 | return tree | |
64 | ||
65 | ||
66 | class AcunetixJsonPlugin(PluginJsonFormat): | |
67 | ||
68 | def __init__(self, *arg, **kwargs): | |
69 | super().__init__(*arg, **kwargs) | |
70 | self.id = "Acunetix_Json" | |
71 | self.name = "Acunetix JSON Output Plugin" | |
72 | self.plugin_version = "0.1" | |
73 | self.version = "9" | |
74 | self.json_keys = {'export'} | |
75 | self.framework_version = "1.0.0" | |
76 | self._temp_file_extension = "json" | |
77 | """ | |
78 | Example plugin to parse acunetix output. | |
79 | """ | |
80 | ||
81 | def parseOutputString(self, output): | |
82 | """ | |
83 | This method will discard the output the shell sends, it will read it | |
84 | from the xml where it expects it to be present. | |
85 | ||
86 | NOTE: if 'debug' is true then it is being run from a test case and the | |
87 | output being sent is valid. | |
88 | """ | |
89 | parser = AcunetixJsonParser(loads(output)) | |
90 | for site in parser.export.scans: | |
91 | self.new_structure(site) | |
92 | ||
93 | def new_structure(self, site: Scan): | |
94 | start_url = site.info.host | |
95 | url_data = urlsplit(start_url) | |
96 | site_ip = resolve_hostname(url_data.hostname) | |
97 | ports = '443' if (url_data.scheme == 'https') else '80' | |
98 | vulnerability_type = {i.vt_id: i for i in site.vul_types} | |
99 | h_id = self.createAndAddHost(site_ip, None, hostnames=[url_data.hostname]) | |
100 | s_id = self.createAndAddServiceToHost( | |
101 | h_id, | |
102 | "http", | |
103 | "tcp", | |
104 | ports=[ports], | |
105 | version=None, | |
106 | status='open') | |
107 | for i in site.vulnerabilities: | |
108 | vul_type = vulnerability_type[i.info.vt_id] | |
109 | self.create_vul(i, vul_type, h_id, s_id, url_data) | |
110 | ||
111 | def create_vul(self, vul: Vulnerabilities, vul_type: VulnerabilityTypes, h_id, s_id, url_data): | |
112 | self.createAndAddVulnWebToService( | |
113 | h_id, | |
114 | s_id, | |
115 | vul_type.name, | |
116 | vul_type.description, | |
117 | website=url_data.hostname, | |
118 | severity=vul_type.severity, | |
119 | resolution=vul_type.recommendation, | |
120 | request=vul.info.request, | |
121 | response=vul.response) | |
122 | ||
123 | @staticmethod | |
124 | def get_domain(scan: Scan): | |
125 | url = scan.start_url | |
126 | if not url.startswith('http'): | |
127 | url = f'http://{url}' | |
128 | url_data = urlsplit(url) | |
129 | if not url_data.scheme: | |
130 | url_data = urlsplit(scan.crawler.start_url_attr) | |
131 | return url_data | |
132 | ||
133 | ||
134 | def createPlugin(ignore_info=False): | |
135 | return AcunetixJsonPlugin(ignore_info=ignore_info) |
2 | 2 | Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) |
3 | 3 | See the file 'doc/LICENSE' for the license information |
4 | 4 | """ |
5 | import sys | |
5 | 6 | import re |
6 | 7 | import csv |
7 | 8 | from ast import literal_eval |
59 | 60 | |
60 | 61 | def parse_csv(self, output): |
61 | 62 | items = [] |
63 | csv.field_size_limit(sys.maxsize) | |
62 | 64 | reader = csv.DictReader(output, delimiter=',') |
63 | 65 | obj_to_import = self.check_objects_to_import(reader.fieldnames) |
64 | 66 | if not obj_to_import: |
4 | 4 | |
5 | 5 | """ |
6 | 6 | import socket |
7 | import re | |
7 | 8 | import json |
8 | 9 | import dateutil |
9 | 10 | from collections import defaultdict |
15 | 16 | __copyright__ = "Copyright (c) 2021, Infobyte LLC" |
16 | 17 | __credits__ = ["Nicolas Rebagliati"] |
17 | 18 | __license__ = "" |
18 | __version__ = "0.0.1" | |
19 | __version__ = "1.0.0" | |
19 | 20 | __maintainer__ = "Nicolas Rebagliati" |
20 | 21 | __email__ = "[email protected]" |
21 | 22 | __status__ = "Development" |
30 | 31 | super().__init__(*arg, **kwargs) |
31 | 32 | self.id = "nuclei" |
32 | 33 | self.name = "Nuclei" |
33 | self.plugin_version = "0.1" | |
34 | self.version = "2.3.0" | |
34 | self.plugin_version = "1.0.0" | |
35 | self.version = "2.3.8" | |
35 | 36 | self.json_keys = {"matched", "templateID", "host"} |
36 | 37 | |
37 | 38 | def parseOutputString(self, output, debug=False): |
59 | 60 | description='web server') |
60 | 61 | matched = vuln_dict.get('matched') |
61 | 62 | matched_data = urlparse(matched) |
62 | references = [f"author: {vuln_dict['info'].get('author', '')}"] | |
63 | reference = vuln_dict["info"].get('reference', []) | |
64 | if reference: | |
65 | if isinstance(reference, str): | |
66 | if re.match('^- ', reference): | |
67 | reference = list(filter(None, [re.sub('^- ','', elem) for elem in reference.split('\n')])) | |
68 | else: | |
69 | reference = [reference] | |
70 | references = vuln_dict["info"].get('references', []) | |
71 | if references: | |
72 | if isinstance(references, str): | |
73 | if re.match('^- ', references): | |
74 | references = list(filter(None, [re.sub('^- ','', elem) for elem in references.split('\n')])) | |
75 | else: | |
76 | references = [references] | |
77 | cwe = vuln_dict['info'].get('cwe', []) | |
78 | capec = vuln_dict['info'].get('capec', []) | |
79 | refs = list(set(reference + references + cwe + capec)).sort() | |
80 | tags = vuln_dict['info'].get('tags', '').split(',') | |
81 | impact = vuln_dict['info'].get('impact') | |
82 | resolution = vuln_dict['info'].get('resolution', '') | |
83 | easeofresolution = vuln_dict['info'].get('easeofresolution') | |
63 | 84 | request = vuln_dict.get('request', '') |
64 | 85 | if request: |
65 | 86 | method = request.split(" ")[0] |
78 | 99 | service_id, |
79 | 100 | name=name, |
80 | 101 | desc=vuln_dict["info"].get("description", name), |
81 | ref=references, | |
102 | ref=refs, | |
82 | 103 | severity=vuln_dict["info"].get('severity'), |
104 | tags=tags, | |
105 | impact=impact, | |
106 | resolution=resolution, | |
107 | easeofresolution=easeofresolution, | |
83 | 108 | website=host, |
84 | 109 | request=request, |
85 | 110 | response=vuln_dict.get('response', ''), |
321 | 321 | from the xml where it expects it to be present. |
322 | 322 | """ |
323 | 323 | parser = OpenvasXmlParser(output, self.logger) |
324 | web = False | |
325 | 324 | ids = {} |
326 | 325 | # The following threats values will not be taken as vulns |
327 | 326 | self.ignored_severities = ['Log', 'Debug'] |
328 | 327 | for ip, values in parser.hosts.items(): |
329 | 328 | # values contains: ip details and ip hostnames |
329 | os_report = values['details'].get('best_os_txt') | |
330 | 330 | h_id = self.createAndAddHost( |
331 | 331 | ip, |
332 | os_report[0] if os_report else '', | |
332 | 333 | hostnames=values['hostnames'] |
333 | 334 | ) |
334 | 335 | ids[ip] = h_id |
74 | 74 | hostname = server_location.get('hostname', None) |
75 | 75 | ip = server_location.get('ip_address', resolve_hostname(hostname)) |
76 | 76 | if port != 443: |
77 | url = 'https://' + hostname + ':' + port | |
77 | url = f"https://{hostname}:{port}" | |
78 | 78 | else: |
79 | url = 'https://' + hostname | |
79 | url = f"https://{hostname}" | |
80 | 80 | |
81 | 81 | json_host = { |
82 | 82 | "name": 'https', |
1 | 1 | import socket |
2 | 2 | import json |
3 | 3 | import pytest |
4 | from pathlib import Path | |
4 | 5 | from faraday_plugins.plugins.manager import PluginsManager, ReportAnalyzer |
5 | 6 | from faraday_plugins.plugins.plugin import PluginBase |
6 | 7 | from faraday.server.api.modules.bulk_create import BulkCreateSchema |
44 | 45 | if not plugin_json: |
45 | 46 | plugin = get_plugin_from_cache(report_file) |
46 | 47 | if plugin: |
47 | plugin.processReport(report_file) | |
48 | plugin.processReport(Path(report_file)) | |
48 | 49 | plugin_json = json.loads(plugin.get_json()) |
49 | 50 | REPORTS_JSON_CACHE[report_file] = plugin_json |
50 | 51 | else: |