ospd.xml   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 314
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 146
dl 0
loc 314
rs 8.64
c 0
b 0
f 0
wmc 47

7 Functions

Rating   Name   Duplication   Size   Complexity  
A escape_ctrl_chars() 0 12 3
A split_invalid_xml() 0 21 4
B get_elements_from_dict() 0 26 6
B simple_response_str() 0 32 7
A get_progress_xml() 0 24 4
A elements_as_text() 0 20 4
A get_result_xml() 0 27 3

4 Methods

Rating   Name   Duplication   Size   Complexity  
A XmlStringHelper.create_response() 0 18 3
B XmlStringHelper.add_element() 0 35 7
A XmlStringHelper.add_attr() 0 23 4
A XmlStringHelper.create_element() 0 16 2

How to fix   Complexity   

Complexity

Complex classes like ospd.xml often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
The variable desc_txt does not seem to be defined for all execution paths.
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