1 | # Copyright (C) 2014-2021 Greenbone Networks GmbH |
||
2 | # |
||
3 | # SPDX-License-Identifier: AGPL-3.0-or-later |
||
4 | # |
||
5 | # This program is free software: you can redistribute it and/or modify |
||
6 | # it under the terms of the GNU Affero General Public License as |
||
7 | # published by the Free Software Foundation, either version 3 of the |
||
8 | # License, or (at your option) any later version. |
||
9 | # |
||
10 | # This program is distributed in the hope that it will be useful, |
||
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
13 | # GNU Affero General Public License for more details. |
||
14 | # |
||
15 | # You should have received a copy of the GNU Affero General Public License |
||
16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
17 | |||
18 | """ OSP XML utils class. |
||
19 | """ |
||
20 | |||
21 | import re |
||
22 | |||
23 | from typing import List, Dict, Any, Union |
||
24 | |||
25 | from xml.sax.saxutils import escape, quoteattr |
||
26 | from xml.etree.ElementTree import tostring, Element |
||
27 | |||
28 | from ospd.misc import ResultType |
||
29 | |||
30 | |||
31 | r = re.compile( # pylint: disable=invalid-name |
||
32 | r'(.*?)(?:([^\x09\x0A\x0D\x20-\x7E\x85\xA0-\xFF' |
||
33 | + r'\u0100-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD])|([\n])|$)' |
||
34 | ) |
||
35 | |||
36 | |||
37 | def split_invalid_xml(result_text: str) -> Union[List[Union[str, int]], str]: |
||
38 | """Search for occurrence of non printable chars and replace them |
||
39 | with the integer representation the Unicode code. The original string |
||
40 | is splitted where a non printable char is found. |
||
41 | """ |
||
42 | splitted_string = [] |
||
43 | |||
44 | def replacer(match): |
||
45 | regex_g1 = match.group(1) |
||
46 | if len(regex_g1) > 0: |
||
47 | splitted_string.append(regex_g1) |
||
48 | regex_g2 = match.group(2) |
||
49 | if regex_g2 is not None: |
||
50 | splitted_string.append(ord(regex_g2)) |
||
51 | regex_g3 = match.group(3) |
||
52 | if regex_g3 is not None: |
||
53 | splitted_string.append(regex_g3) |
||
54 | return "" |
||
55 | |||
56 | re.sub(r, replacer, result_text) |
||
57 | return splitted_string |
||
58 | |||
59 | |||
60 | def escape_ctrl_chars(result_text): |
||
61 | """Replace non printable chars in result_text with an hexa code |
||
62 | in string format. |
||
63 | """ |
||
64 | escaped_str = '' |
||
65 | for fragment in split_invalid_xml(result_text): |
||
66 | if isinstance(fragment, int): |
||
67 | escaped_str += '\\x%04X' % fragment |
||
68 | else: |
||
69 | escaped_str += fragment |
||
70 | |||
71 | return escaped_str |
||
72 | |||
73 | |||
74 | def get_result_xml(result): |
||
75 | """Formats a scan result to XML format. |
||
76 | |||
77 | Arguments: |
||
78 | result (dict): Dictionary with a scan result. |
||
79 | |||
80 | Return: |
||
81 | Result as xml element object. |
||
82 | """ |
||
83 | |||
84 | result_xml = Element('result') |
||
85 | for name, value in [ |
||
86 | ('name', result['name']), |
||
87 | ('type', ResultType.get_str(result['type'])), |
||
88 | ('severity', result['severity']), |
||
89 | ('host', result['host']), |
||
90 | ('hostname', result['hostname']), |
||
91 | ('test_id', result['test_id']), |
||
92 | ('port', result['port']), |
||
93 | ('qod', result['qod']), |
||
94 | ('uri', result['uri']), |
||
95 | ]: |
||
96 | result_xml.set(name, escape(str(value))) |
||
97 | if result['value'] is not None: |
||
98 | result_xml.text = escape_ctrl_chars(result['value']) |
||
99 | |||
100 | return result_xml |
||
101 | |||
102 | |||
103 | def get_progress_xml(progress: Dict[str, int]): |
||
104 | """Formats a scan progress to XML format. |
||
105 | |||
106 | Arguments: |
||
107 | progress (dict): Dictionary with a scan progress. |
||
108 | |||
109 | Return: |
||
110 | Progress as xml element object. |
||
111 | """ |
||
112 | |||
113 | progress_xml = Element('progress') |
||
114 | for progress_item, value in progress.items(): |
||
115 | elem = None |
||
116 | if progress_item == 'current_hosts': |
||
117 | for host, h_progress in value.items(): |
||
118 | elem = Element('host') |
||
119 | elem.set('name', host) |
||
120 | elem.text = str(h_progress) |
||
121 | progress_xml.append(elem) |
||
122 | else: |
||
123 | elem = Element(progress_item) |
||
124 | elem.text = str(value) |
||
125 | progress_xml.append(elem) |
||
126 | return progress_xml |
||
127 | |||
128 | |||
129 | def simple_response_str( |
||
130 | command: str, |
||
131 | status: int, |
||
132 | status_text: str, |
||
133 | content: Union[str, Element, List[str], List[Element]] = "", |
||
134 | ) -> bytes: |
||
135 | """Creates an OSP response XML string. |
||
136 | |||
137 | Arguments: |
||
138 | command (str): OSP Command to respond to. |
||
139 | status (int): Status of the response. |
||
140 | status_text (str): Status text of the response. |
||
141 | content (str): Text part of the response XML element. |
||
142 | |||
143 | Return: |
||
144 | String of response in xml format. |
||
145 | """ |
||
146 | response = Element('%s_response' % command) |
||
147 | |||
148 | for name, value in [('status', str(status)), ('status_text', status_text)]: |
||
149 | response.set(name, escape(str(value))) |
||
150 | |||
151 | if isinstance(content, list): |
||
152 | for elem in content: |
||
153 | if isinstance(elem, Element): |
||
154 | response.append(elem) |
||
155 | elif isinstance(content, Element): |
||
156 | response.append(content) |
||
157 | elif content is not None: |
||
158 | response.text = escape_ctrl_chars(content) |
||
159 | |||
160 | return tostring(response, encoding='utf-8') |
||
161 | |||
162 | |||
163 | def get_elements_from_dict(data: Dict[str, Any]) -> List[Element]: |
||
164 | """Creates a list of etree elements from a dictionary |
||
165 | |||
166 | Args: |
||
167 | Dictionary of tags and their elements. |
||
168 | |||
169 | Return: |
||
170 | List of xml elements. |
||
171 | """ |
||
172 | |||
173 | responses = [] |
||
174 | |||
175 | for tag, value in data.items(): |
||
176 | elem = Element(tag) |
||
177 | |||
178 | if isinstance(value, dict): |
||
179 | for val in get_elements_from_dict(value): |
||
180 | elem.append(val) |
||
181 | elif isinstance(value, list): |
||
182 | elem.text = ', '.join(value) |
||
183 | elif value is not None: |
||
184 | elem.text = escape_ctrl_chars(value) |
||
185 | |||
186 | responses.append(elem) |
||
187 | |||
188 | return responses |
||
189 | |||
190 | |||
191 | def elements_as_text( |
||
192 | elements: Dict[str, Union[str, Dict]], indent: int = 2 |
||
193 | ) -> str: |
||
194 | """Returns the elements dictionary as formatted plain text.""" |
||
195 | |||
196 | text = "" |
||
197 | for elename, eledesc in elements.items(): |
||
198 | if isinstance(eledesc, dict): |
||
199 | desc_txt = elements_as_text(eledesc, indent + 2) |
||
200 | desc_txt = ''.join(['\n', desc_txt]) |
||
201 | elif isinstance(eledesc, str): |
||
202 | desc_txt = ''.join([eledesc, '\n']) |
||
203 | else: |
||
204 | assert False, "Only string or dictionary" |
||
205 | |||
206 | ele_txt = "\t{0}{1: <22} {2}".format(' ' * indent, elename, desc_txt) |
||
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
207 | |||
208 | text = ''.join([text, ele_txt]) |
||
209 | |||
210 | return text |
||
211 | |||
212 | |||
213 | class XmlStringHelper: |
||
214 | """Class with methods to help the creation of a xml object in |
||
215 | string format. |
||
216 | """ |
||
217 | |||
218 | def create_element(self, elem_name: str, end: bool = False) -> bytes: |
||
219 | """Get a name and create the open element of an entity. |
||
220 | |||
221 | Arguments: |
||
222 | elem_name (str): The name of the tag element. |
||
223 | end (bool): Create a initial tag if False, otherwise the end tag. |
||
224 | |||
225 | Return: |
||
226 | Encoded string representing a part of an xml element. |
||
227 | """ |
||
228 | if end: |
||
229 | ret = "</%s>" % elem_name |
||
230 | else: |
||
231 | ret = "<%s>" % elem_name |
||
232 | |||
233 | return ret.encode('utf-8') |
||
234 | |||
235 | def create_response(self, command: str, end: bool = False) -> bytes: |
||
236 | """Create or end an xml response. |
||
237 | |||
238 | Arguments: |
||
239 | command (str): The name of the command for the response element. |
||
240 | end (bool): Create a initial tag if False, otherwise the end tag. |
||
241 | |||
242 | Return: |
||
243 | Encoded string representing a part of an xml element. |
||
244 | """ |
||
245 | if not command: |
||
246 | return |
||
247 | |||
248 | if end: |
||
249 | return ('</%s_response>' % command).encode('utf-8') |
||
250 | |||
251 | return ('<%s_response status="200" status_text="OK">' % command).encode( |
||
252 | 'utf-8' |
||
253 | ) |
||
254 | |||
255 | def add_element( |
||
256 | self, |
||
257 | content: Union[Element, str, list], |
||
258 | xml_str: bytes = None, |
||
259 | end: bool = False, |
||
260 | ) -> bytes: |
||
261 | """Create the initial or ending tag for a subelement, or add |
||
262 | one or many xml elements |
||
263 | |||
264 | Arguments: |
||
265 | content (Element, str, list): Content to add. |
||
266 | xml_str (bytes): Initial string where content to be added to. |
||
267 | end (bool): Create a initial tag if False, otherwise the end tag. |
||
268 | It will be added to the xml_str. |
||
269 | |||
270 | Return: |
||
271 | Encoded string representing a part of an xml element. |
||
272 | """ |
||
273 | |||
274 | if not xml_str: |
||
275 | xml_str = b'' |
||
276 | |||
277 | if content: |
||
278 | if isinstance(content, list): |
||
279 | for elem in content: |
||
280 | xml_str = xml_str + tostring(elem, encoding='utf-8') |
||
281 | elif isinstance(content, Element): |
||
282 | xml_str = xml_str + tostring(content, encoding='utf-8') |
||
283 | else: |
||
284 | if end: |
||
285 | xml_str = xml_str + self.create_element(content, False) |
||
286 | else: |
||
287 | xml_str = xml_str + self.create_element(content) |
||
288 | |||
289 | return xml_str |
||
290 | |||
291 | def add_attr( |
||
292 | self, tag: bytes, attribute: str, value: Union[str, int] = None |
||
293 | ) -> bytes: |
||
294 | """Add an attribute to the beginning tag of an xml element. |
||
295 | Arguments: |
||
296 | tag (bytes): Tag to add the attribute to. |
||
297 | attribute (str): Attribute name |
||
298 | value (str): Attribute value |
||
299 | Return: |
||
300 | Tag in encoded string format with the given attribute |
||
301 | """ |
||
302 | if not tag: |
||
303 | return None |
||
304 | |||
305 | if not attribute: |
||
306 | return tag |
||
307 | |||
308 | if not value: |
||
309 | value = '' |
||
310 | |||
311 | return tag[:-1] + ( |
||
312 | " %s=%s>" % (attribute, quoteattr(str(value))) |
||
313 | ).encode('utf-8') |
||
314 |