Completed
Push — master ( e52b6b...0fab9f )
by
unknown
18s queued 10s
created

ospd.ospd.OSPDError.as_xml()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 3
Ratio 100 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 3
loc 3
rs 10
c 0
b 0
f 0
1
# Copyright (C) 2014-2018 Greenbone Networks GmbH
0 ignored issues
show
coding-style introduced by
Too many lines in module (1645/1000)
Loading history...
2
#
3
# SPDX-License-Identifier: GPL-2.0-or-later
4
#
5
# This program is free software; you can redistribute it and/or
6
# modify it under the terms of the GNU General Public License
7
# as published by the Free Software Foundation; either version 2
8
# of the 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 General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18
19
""" OSP Daemon core class.
20
"""
21
22
23
# This is needed for older pythons as our current module is called the same
24
# as the package we are in ...
25
# Another solution would be to rename that file.
26
from __future__ import absolute_import
27
28
import logging
29
import socket
30
import ssl
31
import multiprocessing
32
import re
33
import time
34
from xml.etree.ElementTree import tostring, Element, SubElement
35
import defusedxml.ElementTree as secET
36
import os
0 ignored issues
show
introduced by
standard import "import os" should be placed before "import defusedxml.ElementTree as secET"
Loading history...
37
38
from ospd import __version__
39
from ospd.misc import ScanCollection, ResultType, target_str_to_list
40
from ospd.misc import resolve_hostname, valid_uuid
41
42
logger = logging.getLogger(__name__)
0 ignored issues
show
Coding Style Naming introduced by
The name logger does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
43
44
PROTOCOL_VERSION = "1.2"
45
46
BASE_SCANNER_PARAMS = {
47
    'debug_mode': {
48
        'type': 'boolean',
49
        'name': 'Debug Mode',
50
        'default': 0,
51
        'mandatory': 0,
52
        'description': 'Whether to get extra scan debug information.',
53
    },
54
    'dry_run': {
55
        'type': 'boolean',
56
        'name': 'Dry Run',
57
        'default': 0,
58
        'mandatory': 0,
59
        'description': 'Whether to dry run scan.',
60
    },
61
}
62
63
COMMANDS_TABLE = {
64
    'start_scan': {
65
        'description': 'Start a new scan.',
66
        'attributes': {
67
            'target': 'Target host to scan',
68
            'ports': 'Ports list to scan',
69
            'scan_id': 'Optional UUID value to use as scan ID',
70
            'parallel': 'Optional nummer of parallel target to scan',
71
        },
72
        'elements': None
73
    },
74
    'stop_scan': {
75
        'description': 'Stop a currently running scan.',
76
        'attributes': {
77
            'scan_id': 'ID of scan to stop.'
78
        },
79
        'elements': None
80
    },
81
    'help': {
82
        'description': 'Print the commands help.',
83
        'attributes': {
84
            'format': 'Help format. Could be text or xml.'
85
        },
86
        'elements': None
87
    },
88
    'get_scans': {
89
        'description': 'List the scans in buffer.',
90
        'attributes': {
91
            'scan_id': 'ID of a specific scan to get.',
92
            'details': 'Whether to return the full scan report.'
93
        },
94
        'elements': None
95
    },
96
    'get_vts': {
97
        'description': 'List of available vulnerability tests.',
98
        'attributes': {
99
            'vt_id': 'ID of a specific vulnerability test to get.'
100
        },
101
        'elements': None
102
    },
103
    'delete_scan': {
104
        'description': 'Delete a finished scan.',
105
        'attributes': {
106
            'scan_id': 'ID of scan to delete.'
107
        },
108
        'elements': None
109
    },
110
    'get_version': {
111
        'description': 'Return various versions.',
112
        'attributes': None,
113
        'elements': None
114
    },
115
    'get_scanner_details': {
116
        'description': 'Return scanner description and parameters',
117
        'attributes': None,
118
        'elements': None
119
    }
120
}
121
122
123 View Code Duplication
def get_result_xml(result):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
124
    """ Formats a scan result to XML format. """
125
    result_xml = Element('result')
126
    for name, value in [('name', result['name']),
127
                        ('type', ResultType.get_str(result['type'])),
128
                        ('severity', result['severity']),
129
                        ('host', result['host']),
130
                        ('test_id', result['test_id']),
131
                        ('port', result['port']),
132
                        ('qod', result['qod'])]:
133
        result_xml.set(name, str(value))
134
    result_xml.text = result['value']
135
    return result_xml
136
137
138 View Code Duplication
def simple_response_str(command, status, status_text, content=""):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
139
    """ Creates an OSP response XML string.
140
141
    @param: OSP Command to respond to.
142
    @param: Status of the response.
143
    @param: Status text of the response.
144
    @param: Text part of the response XML element.
145
146
    @return: String of response in xml format.
147
    """
148
    response = Element('%s_response' % command)
149
    for name, value in [('status', str(status)), ('status_text', status_text)]:
150
        response.set(name, str(value))
151
    if isinstance(content, list):
152
        for elem in content:
153
            response.append(elem)
154
    elif isinstance(content, Element):
155
        response.append(content)
156
    else:
157
        response.text = content
158
    return tostring(response)
159
160
161 View Code Duplication
class OSPDError(Exception):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
162
163
    """ This is an exception that will result in an error message to the
164
    client """
165
166
    def __init__(self, message, command='osp', status=400):
167
        super(OSPDError, self).__init__()
168
        self.message = message
169
        self.command = command
170
        self.status = status
171
172
    def as_xml(self):
173
        """ Return the error in xml format. """
174
        return simple_response_str(self.command, self.status, self.message)
175
176
177 View Code Duplication
def bind_socket(address, port):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
178
    """ Returns a socket bound on (address:port). """
179
180
    assert address
181
    assert port
182
    bindsocket = socket.socket()
183
    try:
184
        bindsocket.bind((address, port))
185
    except socket.error:
186
        logger.error("Couldn't bind socket on %s:%s", address, port)
187
        return None
188
189
    logger.info('Listening on %s:%s', address, port)
190
    bindsocket.listen(0)
191
    return bindsocket
192
193 View Code Duplication
def bind_unix_socket(path):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
194
    """ Returns a unix file socket bound on (path). """
195
196
    assert path
197
    bindsocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
198
    try:
199
        os.unlink(path)
200
    except OSError:
201
        if os.path.exists(path):
202
            raise
203
    try:
204
        bindsocket.bind(path)
205
    except socket.error:
206
        logger.error("Couldn't bind socket on %s", path)
207
        return None
208
209
    logger.info('Listening on %s', path)
210
    bindsocket.listen(0)
211
    return bindsocket
212
213
214 View Code Duplication
def close_client_stream(client_stream, unix_path):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
215
    """ Closes provided client stream """
216
    try:
217
        client_stream.shutdown(socket.SHUT_RDWR)
218
        if unix_path:
219
            logger.debug('%s: Connection closed', unix_path)
220
        else:
221
            peer = client_stream.getpeername()
222
            logger.debug('%s:%s: Connection closed', peer[0], peer[1])
223
    except (socket.error, OSError) as exception:
224
        logger.debug('Connection closing error: %s', exception)
225
    client_stream.close()
226
227
228 View Code Duplication
class OSPDaemon(object):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
best-practice introduced by
Too many instance attributes (12/7)
Loading history...
best-practice introduced by
Too many public methods (86/20)
Loading history...
229
230
    """ Daemon class for OSP traffic handling.
231
232
    Every scanner wrapper should subclass it and make necessary additions and
233
    changes.
234
    * Add any needed parameters in __init__.
235
    * Implement check() method which verifies scanner availability and other
236
      environment related conditions.
237
    * Implement process_scan_params and exec_scan methods which are
238
      specific to handling the <start_scan> command, executing the wrapped
239
      scanner and storing the results.
240
    * exec_scan() should return 0 if host is dead or not reached, 1 if host is
241
      alive and 2 if scan error or status is unknown.
242
    * Implement other methods that assert to False such as get_scanner_name,
243
      get_scanner_version.
244
    * Use Call set_command_attributes at init time to add scanner command
245
      specific options eg. the w3af profile for w3af wrapper.
246
    """
247
248
    def __init__(self, certfile, keyfile, cafile):
249
        """ Initializes the daemon's internal data. """
250
        # @todo: Actually it makes sense to move the certificate params to
251
        #        a separate function because it is not mandatory anymore to
252
        #        use a TLS setup (unix file socket is an alternative).
253
        #        However, changing this makes it mandatory for any ospd scanner
254
        #        to change the function calls as well. So this breaks the API
255
        #        and should only be done with a major release.
256
        self.certs = dict()
257
        self.certs['cert_file'] = certfile
258
        self.certs['key_file'] = keyfile
259
        self.certs['ca_file'] = cafile
260
        self.scan_collection = ScanCollection()
261
        self.scan_processes = dict()
262
        self.daemon_info = dict()
263
        self.daemon_info['name'] = "OSPd"
264
        self.daemon_info['version'] = __version__
265
        self.daemon_info['description'] = "No description"
266
        self.scanner_info = dict()
267
        self.scanner_info['name'] = 'No name'
268
        self.scanner_info['version'] = 'No version'
269
        self.scanner_info['description'] = 'No description'
270
        self.server_version = None  # Set by the subclass.
271
        self.protocol_version = PROTOCOL_VERSION
272
        self.commands = COMMANDS_TABLE
273
        self.scanner_params = dict()
274
        for name, param in BASE_SCANNER_PARAMS.items():
275
            self.add_scanner_param(name, param)
276
        self.vts = dict()
277
        self.vt_id_pattern = re.compile("[0-9a-zA-Z_\-:.]{1,80}")
0 ignored issues
show
Bug introduced by
A suspicious escape sequence \- was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
278
        self.vts_version = None
279
280
    def set_command_attributes(self, name, attributes):
281
        """ Sets the xml attributes of a specified command. """
282
        if self.command_exists(name):
283
            command = self.commands.get(name)
284
            command['attributes'] = attributes
285
286
    def add_scanner_param(self, name, scanner_param):
287
        """ Add a scanner parameter. """
288
289
        assert name
290
        assert scanner_param
291
        self.scanner_params[name] = scanner_param
292
        command = self.commands.get('start_scan')
293
        command['elements'] = {
294
            'scanner_params':
295
                {k: v['name'] for k, v in self.scanner_params.items()}}
296
297
    def add_vt(self, vt_id, name=None, vt_params=None, vt_refs=None,
0 ignored issues
show
best-practice introduced by
Too many arguments (19/5)
Loading history...
Comprehensibility introduced by
This function exceeds the maximum number of variables (20/15).
Loading history...
298
               custom=None, vt_creation_time=None, vt_modification_time=None,
299
               vt_dependencies=None, summary=None, impact=None, affected=None,
300
               insight=None, solution=None, solution_t=None, detection=None,
301
               qod_t=None, qod_v=None, severities=None):
302
        """ Add a vulnerability test information.
303
304
        Returns: The new number of stored VTs.
305
        -1 in case the VT ID was already present and thus the
306
        new VT was not considered.
307
        -2 in case the vt_id was invalid.
308
        """
309
310
        if not vt_id:
311
            return -2  # no valid vt_id
312
313
        if self.vt_id_pattern.fullmatch(vt_id) is None:
314
            return -2  # no valid vt_id
315
316
        if vt_id in self.vts:
317
            return -1  # The VT was already in the list.
318
319
        if name is None:
320
            name = ''
321
322
        self.vts[vt_id] = {'name': name}
323
        if custom is not None:
324
            self.vts[vt_id]["custom"] = custom
325
        if vt_params is not None:
326
            self.vts[vt_id]["vt_params"] = vt_params
327
        if vt_refs is not None:
328
            self.vts[vt_id]["vt_refs"] = vt_refs
329
        if vt_dependencies is not None:
330
            self.vts[vt_id]["vt_dependencies"] = vt_dependencies
331
        if vt_creation_time is not None:
332
            self.vts[vt_id]["creation_time"] = vt_creation_time
333
        if vt_modification_time is not None:
334
            self.vts[vt_id]["modification_time"] = vt_modification_time
335
        if summary is not None:
336
            self.vts[vt_id]["summary"] = summary
337
        if impact is not None:
338
            self.vts[vt_id]["impact"] = impact
339
        if affected is not None:
340
            self.vts[vt_id]["affected"] = affected
341
        if insight is not None:
342
            self.vts[vt_id]["insight"] = insight
343
        if solution is not None:
344
            self.vts[vt_id]["solution"] = solution
345
            if solution_t is not None:
346
                self.vts[vt_id]["solution_type"] = solution_t
347
        if detection is not None:
348
            self.vts[vt_id]["detection"] = detection
349
        if qod_t is not None:
350
            self.vts[vt_id]["qod_type"] = qod_t
351
        elif qod_v is not None:
352
            self.vts[vt_id]["qod"] = qod_v
353
        if severities is not None:
354
            self.vts[vt_id]["severities"] = severities
355
356
        return len(self.vts)
357
358
    def set_vts_version(self, vts_version):
359
        """ Add into the vts dictionary an entry to identify the
360
        vts version.
361
362
        Parameters:
363
            vts_version (str): Identifies a unique vts version.
364
        """
365
        if not vts_version:
366
            raise OSPDError('A vts_version parameter is required',
367
                            'set_vts_version')
368
        self.vts_version = vts_version
369
370
    def get_vts_version(self):
371
        """Return the vts version.
372
        """
373
        return self.vts_version
374
375
    def command_exists(self, name):
376
        """ Checks if a commands exists. """
377
        return name in self.commands.keys()
378
379
    def get_scanner_name(self):
380
        """ Gives the wrapped scanner's name. """
381
        return self.scanner_info['name']
382
383
    def get_scanner_version(self):
384
        """ Gives the wrapped scanner's version. """
385
        return self.scanner_info['version']
386
387
    def get_scanner_description(self):
388
        """ Gives the wrapped scanner's description. """
389
        return self.scanner_info['description']
390
391
    def get_server_version(self):
392
        """ Gives the specific OSP server's version. """
393
        assert self.server_version
394
        return self.server_version
395
396
    def get_protocol_version(self):
397
        """ Gives the OSP's version. """
398
        return self.protocol_version
399
400
    def _preprocess_scan_params(self, xml_params):
401
        """ Processes the scan parameters. """
402
        params = {}
403
        for param in xml_params:
404
            params[param.tag] = param.text or ''
405
        # Set default values.
406
        for key in self.scanner_params:
407
            if key not in params:
408
                params[key] = self.get_scanner_param_default(key)
409
                if self.get_scanner_param_type(key) == 'selection':
410
                    params[key] = params[key].split('|')[0]
411
        # Validate values.
412
        for key in params:
413
            param_type = self.get_scanner_param_type(key)
414
            if not param_type:
415
                continue
416
            if param_type in ['integer', 'boolean']:
417
                try:
418
                    params[key] = int(params[key])
419
                except ValueError:
420
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
421
            if param_type == 'boolean':
422
                if params[key] not in [0, 1]:
423
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
424
            elif param_type == 'selection':
425
                selection = self.get_scanner_param_default(key).split('|')
426
                if params[key] not in selection:
427
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
428
            if self.get_scanner_param_mandatory(key) and params[key] == '':
429
                    raise OSPDError('Mandatory %s value is missing' % key,
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 16 spaces were expected, but 20 were found.
Loading history...
430
                                    'start_scan')
431
        return params
432
433
    def process_scan_params(self, params):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
434
        """ This method is to be overridden by the child classes if necessary
435
        """
436
        return params
437
438
    def process_vts_params(self, scanner_vts):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
439
        """ Receive an XML object with the Vulnerability Tests an their
440
        parameters to be use in a scan and return a dictionary.
441
442
        @param: XML element with vt subelements. Each vt has an
443
                id attribute. Optional parameters can be included
444
                as vt child.
445
                Example form:
446
                <vt_selection>
447
                  <vt_single id='vt1' />
448
                  <vt_single id='vt2'>
449
                    <vt_value id='param1'>value</vt_value>
450
                  </vt_single>
451
                  <vt_group filter='family=debian'/>
452
                  <vt_group filter='family=general'/>
453
                </vt_selection>
454
455
        @return: Dictionary containing the vts attribute and subelements,
456
                 like the VT's id and VT's parameters.
457
                 Example form:
458
                 {'vt1': {},
459
                  'vt2': {'value_id': 'value'},
460
                  'vt_groups': ['family=debian', 'family=general']}
461
        """
462
        vt_selection = {}
463
        filters = list()
464
        for vt in scanner_vts:
0 ignored issues
show
Coding Style Naming introduced by
The name vt does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
465
            if vt.tag == 'vt_single':
466
                vt_id = vt.attrib.get('id')
467
                vt_selection[vt_id] = {}
468
                for vt_value in vt:
469
                    if not vt_value.attrib.get('id'):
470
                        raise OSPDError('Invalid VT preference. No attribute id',
471
                                        'start_scan')
472
                    vt_value_id = vt_value.attrib.get('id')
473
                    vt_value_value = vt_value.text if vt_value.text else ''
474
                    vt_selection[vt_id][vt_value_id] = vt_value_value
475
            if vt.tag == 'vt_group':
476
                vts_filter = vt.attrib.get('filter', None)
477
                if vts_filter is None:
478
                    raise OSPDError('Invalid VT group. No filter given.',
479
                                    'start_scan')
480
                filters.append(vts_filter)
481
        vt_selection['vt_groups'] = filters
482
        return vt_selection
483
484
    @staticmethod
485
    def process_credentials_elements(cred_tree):
486
        """ Receive an XML object with the credentials to run
487
        a scan against a given target.
488
489
        @param:
490
        <credentials>
491
          <credential type="up" service="ssh" port="22">
492
            <username>scanuser</username>
493
            <password>mypass</password>
494
          </credential>
495
          <credential type="up" service="smb">
496
            <username>smbuser</username>
497
            <password>mypass</password>
498
          </credential>
499
        </credentials>
500
501
        @return: Dictionary containing the credentials for a given target.
502
                 Example form:
503
                 {'ssh': {'type': type,
504
                          'port': port,
505
                          'username': username,
506
                          'password': pass,
507
                        },
508
                  'smb': {'type': type,
509
                          'username': username,
510
                          'password': pass,
511
                         },
512
                   }
513
        """
514
        credentials = {}
515
        for credential in cred_tree:
516
            service = credential.attrib.get('service')
517
            credentials[service] = {}
518
            credentials[service]['type'] = credential.attrib.get('type')
519
            if service == 'ssh':
520
                credentials[service]['port'] = credential.attrib.get('port')
521
            for param in credential:
522
                credentials[service][param.tag] = param.text
523
524
        return credentials
525
526
    @classmethod
527
    def process_targets_element(cls, scanner_target):
528
        """ Receive an XML object with the target, ports and credentials to run
529
        a scan against.
530
531
        @param: XML element with target subelements. Each target has <hosts>
532
        and <ports> subelements. Hosts can be a single host, a host range,
533
        a comma-separated host list or a network address.
534
        <ports> and  <credentials> are optional. Therefore each ospd-scanner
535
        should check for a valid ones if needed.
536
537
                Example form:
538
                <targets>
539
                  <target>
540
                    <hosts>localhosts</hosts>
541
                    <ports>80,443</ports>
542
                  </target>
543
                  <target>
544
                    <hosts>192.168.0.0/24</hosts>
545
                    <ports>22</ports>
546
                    <credentials>
547
                      <credential type="up" service="ssh" port="22">
548
                        <username>scanuser</username>
549
                        <password>mypass</password>
550
                      </credential>
551
                      <credential type="up" service="smb">
552
                        <username>smbuser</username>
553
                        <password>mypass</password>
554
                      </credential>
555
                    </credentials>
556
                  </target>
557
                </targets>
558
559
        @return: A list of (hosts, port) tuples.
560
                 Example form:
561
                 [['localhost', '80,43'],
562
                  ['192.168.0.0/24', '22', {'smb': {'type': type,
563
                                                    'port': port,
564
                                                    'username': username,
565
                                                    'password': pass,
566
                                                   }}]]
567
        """
568
569
        target_list = []
570
        for target in scanner_target:
571
            ports = ''
572
            credentials = {}
573
            for child in target:
574
                if child.tag == 'hosts':
575
                    hosts = child.text
576
                if child.tag == 'ports':
577
                    ports = child.text
578
                if child.tag == 'credentials':
579
                    credentials = cls.process_credentials_elements(child)
580
            if hosts:
0 ignored issues
show
introduced by
The variable hosts does not seem to be defined for all execution paths.
Loading history...
581
                target_list.append([hosts, ports, credentials])
582
            else:
583
                raise OSPDError('No target to scan', 'start_scan')
584
585
        return target_list
586
587
    def handle_start_scan_command(self, scan_et):
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (18/15).
Loading history...
588
        """ Handles <start_scan> command.
589
590
        @return: Response string for <start_scan> command.
591
        """
592
593
        target_str = scan_et.attrib.get('target')
594
        ports_str = scan_et.attrib.get('ports')
595
        # For backward compatibility, if target and ports attributes are set,
596
        # <targets> element is ignored.
597
        if target_str is None or ports_str is None:
598
            target_list = scan_et.find('targets')
599
            if target_list is None or not target_list:
600
                raise OSPDError('No targets or ports', 'start_scan')
601
            else:
602
                scan_targets = self.process_targets_element(target_list)
603
        else:
604
            scan_targets = []
605
            for single_target in target_str_to_list(target_str):
606
                scan_targets.append([single_target, ports_str, ''])
607
608
        scan_id = scan_et.attrib.get('scan_id')
609
        if scan_id is not None and scan_id != '' and not valid_uuid(scan_id):
610
            raise OSPDError('Invalid scan_id UUID', 'start_scan')
611
612
        try:
613
            parallel = int(scan_et.attrib.get('parallel', '1'))
614
            if parallel < 1 or parallel > 20:
615
                parallel = 1
616
        except ValueError:
617
            raise OSPDError('Invalid value for parallel scans. '
618
                            'It must be a number', 'start_scan')
619
620
        scanner_params = scan_et.find('scanner_params')
621
        if scanner_params is None:
622
            raise OSPDError('No scanner_params element', 'start_scan')
623
624
        params = self._preprocess_scan_params(scanner_params)
625
626
        # VTS is an optional element. If present should not be empty.
627
        vt_selection = {}
628
        scanner_vts = scan_et.find('vt_selection')
629
        if scanner_vts is not None:
630
            if not scanner_vts:
631
                raise OSPDError('VTs list is empty', 'start_scan')
632
            else:
633
                vt_selection = self.process_vts_params(scanner_vts)
634
635
        # Dry run case.
636
        if 'dry_run' in params and int(params['dry_run']):
637
            scan_func = self.dry_run_scan
638
            scan_params = None
639
        else:
640
            scan_func = self.start_scan
641
            scan_params = self.process_scan_params(params)
642
643
        scan_id = self.create_scan(scan_id, scan_targets,
644
                                   target_str, scan_params,
645
                                   vt_selection)
646
        scan_process = multiprocessing.Process(target=scan_func,
647
                                               args=(scan_id,
648
                                                     scan_targets,
649
                                                     parallel))
650
        self.scan_processes[scan_id] = scan_process
651
        scan_process.start()
652
        id_ = Element('id')
653
        id_.text = scan_id
654
        return simple_response_str('start_scan', 200, 'OK', id_)
655
656
    def handle_stop_scan_command(self, scan_et):
657
        """ Handles <stop_scan> command.
658
659
        @return: Response string for <stop_scan> command.
660
        """
661
662
        scan_id = scan_et.attrib.get('scan_id')
663
        if scan_id is None or scan_id == '':
664
            raise OSPDError('No scan_id attribute', 'stop_scan')
665
        scan_process = self.scan_processes.get(scan_id)
666
        if not scan_process:
667
            raise OSPDError('Scan not found {0}.'.format(scan_id), 'stop_scan')
668
        if not scan_process.is_alive():
669
            raise OSPDError('Scan already stopped or finished.', 'stop_scan')
670
671
        self.set_scan_status(scan_id, "stopped")
672
        logger.info('%s: Scan stopping %s.', scan_id, scan_process.ident)
673
        self.stop_scan(scan_id)
674
        scan_process.terminate()
675
        os.killpg(os.getpgid(scan_process.ident), 15)
676
        scan_process.join()
677
        self.set_scan_progress(scan_id, 100)
678
        self.add_scan_log(scan_id, name='', host='', value='Scan stopped.')
679
        logger.info('%s: Scan stopped.', scan_id)
680
        return simple_response_str('stop_scan', 200, 'OK')
681
682
    @staticmethod
683
    def stop_scan(scan_id):
684
        """ Should be implemented by subclass in case of a clean up before
685
        terminating is needed. """
686
687
    def exec_scan(self, scan_id, target):
688
        """ Asserts to False. Should be implemented by subclass. """
689
        raise NotImplementedError
690
691
    def finish_scan(self, scan_id):
692
        """ Sets a scan as finished. """
693
        self.set_scan_progress(scan_id, 100)
694
        logger.info("%s: Scan finished.", scan_id)
695
696
    def get_daemon_name(self):
697
        """ Gives osp daemon's name. """
698
        return self.daemon_info['name']
699
700
    def get_daemon_version(self):
701
        """ Gives osp daemon's version. """
702
        return self.daemon_info['version']
703
704
    def get_scanner_param_type(self, param):
705
        """ Returns type of a scanner parameter. """
706
        assert isinstance(param, str)
707
        entry = self.scanner_params.get(param)
708
        if not entry:
709
            return None
710
        return entry.get('type')
711
712
    def get_scanner_param_mandatory(self, param):
713
        """ Returns if a scanner parameter is mandatory. """
714
        assert isinstance(param, str)
715
        entry = self.scanner_params.get(param)
716
        if not entry:
717
            return False
718
        return entry.get('mandatory')
719
720
    def get_scanner_param_default(self, param):
721
        """ Returns default value of a scanner parameter. """
722
        assert isinstance(param, str)
723
        entry = self.scanner_params.get(param)
724
        if not entry:
725
            return None
726
        return entry.get('default')
727
728
    def get_scanner_params_xml(self):
729
        """ Returns the OSP Daemon's scanner params in xml format. """
730
        scanner_params = Element('scanner_params')
731
        for param_id, param in self.scanner_params.items():
732
            param_xml = SubElement(scanner_params, 'scanner_param')
733
            for name, value in [('id', param_id),
734
                                ('type', param['type'])]:
735
                param_xml.set(name, value)
736
            for name, value in [('name', param['name']),
737
                                ('description', param['description']),
738
                                ('default', param['default']),
739
                                ('mandatory', param['mandatory'])]:
740
                elem = SubElement(param_xml, name)
741
                elem.text = str(value)
742
        return scanner_params
743
744
    def new_client_stream(self, sock):
745
        """ Returns a new ssl client stream from bind_socket. """
746
747
        assert sock
748
        newsocket, fromaddr = sock.accept()
749
        logger.debug("New connection from"
750
                     " %s:%s", fromaddr[0], fromaddr[1])
751
        # NB: Despite the name, ssl.PROTOCOL_SSLv23 selects the highest
752
        # protocol version that both the client and server support. In modern
753
        # Python versions (>= 3.4) it supports TLS >= 1.0 with SSLv2 and SSLv3
754
        # being disabled. For Python >=3.5, PROTOCOL_SSLv23 is an alias for
755
        # PROTOCOL_TLS which should be used once compatibility with Python 3.4
756
        # is no longer desired.
757
        try:
758
            ssl_socket = ssl.wrap_socket(newsocket, cert_reqs=ssl.CERT_REQUIRED,
759
                                         server_side=True,
760
                                         certfile=self.certs['cert_file'],
761
                                         keyfile=self.certs['key_file'],
762
                                         ca_certs=self.certs['ca_file'],
763
                                         ssl_version=ssl.PROTOCOL_SSLv23)
764
        except (ssl.SSLError, socket.error) as message:
765
            logger.error(message)
766
            return None
767
        return ssl_socket
768
769
    @staticmethod
770
    def write_to_stream(stream, response, block_len=1024):
771
        """
772
        Send the response in blocks of the given len using the
773
        passed method dependending on the socket type.
774
        """
775
        try:
776
            i_start = 0
777
            i_end = block_len
778
            while True:
779
                if i_end > len(response):
780
                    stream(response[i_start:])
781
                    break
782
                stream(response[i_start:i_end])
783
                i_start = i_end
784
                i_end += block_len
785
        except (socket.timeout, socket.error) as exception:
786
            logger.debug('Error sending response to the client: %s', exception)
787
788
    def handle_client_stream(self, stream, is_unix=False):
789
        """ Handles stream of data received from client. """
790
791
        assert stream
792
        data = []
793
        stream.settimeout(2)
794
        while True:
795
            try:
796
                if is_unix:
797
                    buf = stream.recv(1024)
798
                else:
799
                    buf = stream.read(1024)
800
                if not buf:
801
                    break
802
                data.append(buf)
803
            except (AttributeError, ValueError) as message:
804
                logger.error(message)
805
                return
806
            except (ssl.SSLError) as exception:
807
                logger.debug('Error: %s', exception[0])
808
                break
809
            except (socket.timeout) as exception:
810
                logger.debug('Error: %s', exception)
811
                break
812
        data = b''.join(data)
813
        if len(data) <= 0:
814
            logger.debug("Empty client stream")
815
            return
816
        try:
817
            response = self.handle_command(data)
818
        except OSPDError as exception:
819
            response = exception.as_xml()
820
            logger.debug('Command error: %s', exception.message)
821
        except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
822
            logger.exception('While handling client command:')
823
            exception = OSPDError('Fatal error', 'error')
824
            response = exception.as_xml()
825
        if is_unix:
826
            send_method = stream.send
827
        else:
828
            send_method = stream.write
829
        self.write_to_stream(send_method, response)
830
831
    def parallel_scan(self, scan_id, target):
832
        """ Starts the scan with scan_id. """
833
        try:
834
            ret = self.exec_scan(scan_id, target)
835
            if ret == 0:
836
                self.add_scan_host_detail(scan_id, name='host_status',
837
                                          host=target, value='0')
838
            elif ret == 1:
839
                self.add_scan_host_detail(scan_id, name='host_status',
840
                                          host=target, value='1')
841
            elif ret == 2:
842
                self.add_scan_host_detail(scan_id, name='host_status',
843
                                          host=target, value='2')
844
            else:
845
                logger.debug('%s: No host status returned', target)
846
        except Exception as e:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
Coding Style Naming introduced by
The name e does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
847
            self.add_scan_error(scan_id, name='', host=target,
848
                                value='Host process failure (%s).' % e)
849
            logger.exception('While scanning %s:', target)
850
        else:
851
            logger.info("%s: Host scan finished.", target)
852
853
    def check_pending_target(self, scan_id, multiscan_proc):
854
        """ Check if a scan process is still alive. In case the process
855
        finished, removes the process from the multiscan_process list.
856
        In case of the progress is not set from the wrapper for a single
857
        target, it will be automatically set if the target scan finished.
858
859
        @input scan_id        Scan_id of the whole scan.
860
        @input multiscan_proc A list with the scan process which
861
                              may still be alive.
862
863
        @return Actualized list with current runnging scan processes."""
864
865
        for running_target in multiscan_proc:
866
            if not running_target[0].is_alive():
867
                target_prog = self.get_scan_target_progress(scan_id)
868
                if target_prog[running_target[1]] < 100:
869
                    self.set_scan_target_progress(scan_id,
870
                                                  running_target[1],
871
                                                  100)
872
                multiscan_proc.remove(running_target)
873
        return multiscan_proc
874
875
    def calculate_progress(self, scan_id):
876
        """ Calculate the total scan progress from the
877
        partial target progress. """
878
879
        target_progress = self.get_scan_target_progress(scan_id)
880
        return sum(target_progress.values())/len(target_progress)
881
882
    def start_scan(self, scan_id, targets, parallel=1):
883
        """ Handle N parallel scans if 'parallel' is greater than 1. """
884
885
        os.setsid()
886
        multiscan_proc = []
887
        logger.info("%s: Scan started.", scan_id)
888
        target_list = targets
889
        if target_list is None or not target_list:
890
            raise OSPDError('Erroneous targets list', 'start_scan')
891
892
        for index, target in enumerate(target_list):
0 ignored issues
show
Unused Code introduced by
The variable index seems to be unused.
Loading history...
893
            while len(multiscan_proc) >= parallel:
894
                multiscan_proc = self.check_pending_target(scan_id,
895
                                                           multiscan_proc)
896
                progress = self.calculate_progress(scan_id)
897
                self.set_scan_progress(scan_id, progress)
898
                time.sleep(1)
899
900
            if self.get_scan_status(scan_id) == "stopped":
901
                self.finish_scan(scan_id)
902
                return
903
904
            logger.info("%s: Host scan started on ports %s.", target[0], target[1])
905
            scan_process = multiprocessing.Process(target=self.parallel_scan,
906
                                                   args=(scan_id, target[0]))
907
            multiscan_proc.append((scan_process, target[0]))
908
            scan_process.start()
909
910
        # Wait until all single target were scanned
911
        while multiscan_proc:
912
            multiscan_proc = self.check_pending_target(scan_id, multiscan_proc)
913
            if multiscan_proc:
914
                progress = self.calculate_progress(scan_id)
915
                self.set_scan_progress(scan_id, progress)
916
            time.sleep(1)
917
918
        self.finish_scan(scan_id)
919
920
    def dry_run_scan(self, scan_id, targets):
921
        """ Dry runs a scan. """
922
923
        os.setsid()
924
        #target_list = target_str_to_list(target_str)
925
        for _, target in enumerate(targets):
926
            host = resolve_hostname(target[0])
927
            if host is None:
928
                logger.info("Couldn't resolve %s.", target[0])
929
                continue
930
            port = self.get_scan_ports(scan_id, target=target[0])
931
            logger.info("%s:%s: Dry run mode.", host, port)
932
            self.add_scan_log(scan_id, name='', host=host,
933
                              value='Dry run result')
934
        self.finish_scan(scan_id)
935
936
    def handle_timeout(self, scan_id, host):
937
        """ Handles scanner reaching timeout error. """
938
        self.add_scan_error(scan_id, host=host, name="Timeout",
939
                            value="{0} exec timeout."
940
                            .format(self.get_scanner_name()))
941
942
    def set_scan_progress(self, scan_id, progress):
943
        """ Sets scan_id scan's progress which is a number between 0 and 100. """
944
        self.scan_collection.set_progress(scan_id, progress)
945
946
    def set_scan_target_progress(self, scan_id, target, progress):
947
        """ Sets target's progress. """
948
        self.scan_collection.set_target_progress(scan_id, target, progress)
949
950
    def set_scan_status(self, scan_id, status):
951
        """ Set the scan's status."""
952
        self.scan_collection.set_status(scan_id, status)
953
954
    def get_scan_status(self, scan_id):
955
        """ Get scan_id scans's status."""
956
        return self.scan_collection.get_status(scan_id)
957
958
    def scan_exists(self, scan_id):
959
        """ Checks if a scan with ID scan_id is in collection.
960
961
        @return: 1 if scan exists, 0 otherwise.
962
        """
963
        return self.scan_collection.id_exists(scan_id)
964
965
    def handle_get_scans_command(self, scan_et):
966
        """ Handles <get_scans> command.
967
968
        @return: Response string for <get_scans> command.
969
        """
970
971
        scan_id = scan_et.attrib.get('scan_id')
972
        details = scan_et.attrib.get('details')
973
        pop_res = scan_et.attrib.get('pop_results')
974
        if details and details == '0':
975
            details = False
976
        else:
977
            details = True
978
            if pop_res and pop_res == '1':
0 ignored issues
show
Unused Code introduced by
The if statement can be replaced with 'var = bool(test)'
Loading history...
979
                pop_res = True
980
            else:
981
                pop_res = False
982
983
        responses = []
984
        if scan_id and scan_id in self.scan_collection.ids_iterator():
985
            self.check_scan_process(scan_id)
986
            scan = self.get_scan_xml(scan_id, details, pop_res)
987
            responses.append(scan)
988
        elif scan_id:
989
            text = "Failed to find scan '{0}'".format(scan_id)
990
            return simple_response_str('get_scans', 404, text)
991
        else:
992
            for scan_id in self.scan_collection.ids_iterator():
993
                self.check_scan_process(scan_id)
994
                scan = self.get_scan_xml(scan_id, details, pop_res)
995
                responses.append(scan)
996
        return simple_response_str('get_scans', 200, 'OK', responses)
997
998
    def handle_get_vts_command(self, vt_et):
999
        """ Handles <get_vts> command.
1000
1001
        @return: Response string for <get_vts> command.
1002
        """
1003
1004
        vt_id = vt_et.attrib.get('vt_id')
1005
1006
        if vt_id and vt_id not in self.vts:
1007
            text = "Failed to find vulnerability test '{0}'".format(vt_id)
1008
            return simple_response_str('get_vts', 404, text)
1009
1010
        responses = []
1011
1012
        if vt_id:
1013
            vts_xml = self.get_vts_xml(vt_id)
1014
        else:
1015
            vts_xml = self.get_vts_xml()
1016
1017
        responses.append(vts_xml)
1018
1019
        return simple_response_str('get_vts', 200, 'OK', responses)
1020
1021
    def handle_help_command(self, scan_et):
1022
        """ Handles <help> command.
1023
1024
        @return: Response string for <help> command.
1025
        """
1026
        help_format = scan_et.attrib.get('format')
1027
        if help_format is None or help_format == "text":
1028
            # Default help format is text.
1029
            return simple_response_str('help', 200, 'OK',
1030
                                       self.get_help_text())
1031
        elif help_format == "xml":
1032
            text = self.get_xml_str(self.commands)
1033
            return simple_response_str('help', 200, 'OK', text)
1034
        raise OSPDError('Bogus help format', 'help')
1035
1036
    def get_help_text(self):
1037
        """ Returns the help output in plain text format."""
1038
1039
        txt = str('\n')
1040
        for name, info in self.commands.items():
1041
            command_txt = "\t{0: <22} {1}\n".format(name, info['description'])
1042
            if info['attributes']:
1043
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
1044
                for attrname, attrdesc in info['attributes'].items():
1045
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
1046
                    command_txt = ''.join([command_txt, attr_txt])
1047
            if info['elements']:
1048
                command_txt = ''.join([command_txt, "\t Elements:\n",
1049
                                       self.elements_as_text(info['elements'])])
1050
            txt = ''.join([txt, command_txt])
1051
        return txt
1052
1053
    def elements_as_text(self, elems, indent=2):
1054
        """ Returns the elems dictionary as formatted plain text. """
1055
        assert elems
1056
        text = ""
1057
        for elename, eledesc in elems.items():
1058
            if isinstance(eledesc, dict):
1059
                desc_txt = self.elements_as_text(eledesc, indent + 2)
1060
                desc_txt = ''.join(['\n', desc_txt])
1061
            elif isinstance(eledesc, str):
1062
                desc_txt = ''.join([eledesc, '\n'])
1063
            else:
1064
                assert False, "Only string or dictionary"
1065
            ele_txt = "\t{0}{1: <22} {2}".format(' ' * indent, elename,
1066
                                                 desc_txt)
0 ignored issues
show
introduced by
The variable desc_txt does not seem to be defined for all execution paths.
Loading history...
1067
            text = ''.join([text, ele_txt])
1068
        return text
1069
1070
    def handle_delete_scan_command(self, scan_et):
1071
        """ Handles <delete_scan> command.
1072
1073
        @return: Response string for <delete_scan> command.
1074
        """
1075
        scan_id = scan_et.attrib.get('scan_id')
1076
        if scan_id is None:
1077
            return simple_response_str('delete_scan', 404,
1078
                                       'No scan_id attribute')
1079
1080
        if not self.scan_exists(scan_id):
1081
            text = "Failed to find scan '{0}'".format(scan_id)
1082
            return simple_response_str('delete_scan', 404, text)
1083
        self.check_scan_process(scan_id)
1084
        if self.delete_scan(scan_id):
1085
            return simple_response_str('delete_scan', 200, 'OK')
1086
        raise OSPDError('Scan in progress', 'delete_scan')
1087
1088
    def delete_scan(self, scan_id):
1089
        """ Deletes scan_id scan from collection.
1090
1091
        @return: 1 if scan deleted, 0 otherwise.
1092
        """
1093
        try:
1094
            del self.scan_processes[scan_id]
1095
        except KeyError:
1096
            logger.debug('Scan process for %s not found', scan_id)
1097
        return self.scan_collection.delete_scan(scan_id)
1098
1099
    def get_scan_results_xml(self, scan_id, pop_res):
1100
        """ Gets scan_id scan's results in XML format.
1101
1102
        @return: String of scan results in xml.
1103
        """
1104
        results = Element('results')
1105
        for result in self.scan_collection.results_iterator(scan_id, pop_res):
1106
            results.append(get_result_xml(result))
1107
1108
        logger.info('Returning %d results', len(results))
1109
        return results
1110
1111
    def get_xml_str(self, data):
1112
        """ Creates a string in XML Format using the provided data structure.
1113
1114
        @param: Dictionary of xml tags and their elements.
1115
1116
        @return: String of data in xml format.
1117
        """
1118
1119
        responses = []
1120
        for tag, value in data.items():
1121
            elem = Element(tag)
1122
            if isinstance(value, dict):
1123
                for value in self.get_xml_str(value):
0 ignored issues
show
Comprehensibility Bug introduced by
value is re-defining a name which is already available in the outer-scope (previously defined on line 1120).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
1124
                    elem.append(value)
1125
            elif isinstance(value, list):
1126
                value = ', '.join([m for m in value])
1127
                elem.text = value
1128
            else:
1129
                elem.text = value
1130
            responses.append(elem)
1131
        return responses
1132
1133
    def get_scan_xml(self, scan_id, detailed=True, pop_res=False):
1134
        """ Gets scan in XML format.
1135
1136
        @return: String of scan in XML format.
1137
        """
1138
        if not scan_id:
1139
            return Element('scan')
1140
1141
        target = self.get_scan_target(scan_id)
1142
        progress = self.get_scan_progress(scan_id)
1143
        start_time = self.get_scan_start_time(scan_id)
1144
        end_time = self.get_scan_end_time(scan_id)
1145
        response = Element('scan')
1146
        for name, value in [('id', scan_id),
1147
                            ('target', target),
1148
                            ('progress', progress),
1149
                            ('start_time', start_time),
1150
                            ('end_time', end_time)]:
1151
            response.set(name, str(value))
1152
        if detailed:
1153
            response.append(self.get_scan_results_xml(scan_id, pop_res))
1154
        return response
1155
1156
    @staticmethod
1157
    def get_custom_vt_as_xml_str(vt_id, custom):
0 ignored issues
show
Unused Code introduced by
The argument custom seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1158
        """ Create a string representation of the XML object from the
1159
        custom data object.
1160
        This needs to be implemented by each ospd wrapper, in case
1161
        custom elements for VTs are used.
1162
1163
        The custom XML object which is returned will be embedded
1164
        into a <custom></custom> element.
1165
1166
        @return: XML object as string for custom data.
1167
        """
1168
        return ''
1169
1170
    @staticmethod
1171
    def get_params_vt_as_xml_str(vt_id, vt_params):
0 ignored issues
show
Unused Code introduced by
The argument vt_params seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1172
        """ Create a string representation of the XML object from the
1173
        vt_params data object.
1174
        This needs to be implemented by each ospd wrapper, in case
1175
        vt_params elements for VTs are used.
1176
1177
        The vt_params XML object which is returned will be embedded
1178
        into a <vt_params></vt_params> element.
1179
1180
        @return: XML object as string for vt parameters data.
1181
        """
1182
        return ''
1183
1184
    @staticmethod
1185
    def get_refs_vt_as_xml_str(vt_id, vt_refs):
0 ignored issues
show
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_refs seems to be unused.
Loading history...
1186
        """ Create a string representation of the XML object from the
1187
        vt_refs data object.
1188
        This needs to be implemented by each ospd wrapper, in case
1189
        vt_refs elements for VTs are used.
1190
1191
        The vt_refs XML object which is returned will be embedded
1192
        into a <vt_refs></vt_refs> element.
1193
1194
        @return: XML object as string for vt references data.
1195
        """
1196
        return ''
1197
1198
    @staticmethod
1199
    def get_dependencies_vt_as_xml_str(vt_id, vt_dependencies):
0 ignored issues
show
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_dependencies seems to be unused.
Loading history...
1200
        """ Create a string representation of the XML object from the
1201
        vt_dependencies data object.
1202
        This needs to be implemented by each ospd wrapper, in case
1203
        vt_dependencies elements for VTs are used.
1204
1205
        The vt_dependencies XML object which is returned will be embedded
1206
        into a <dependencies></dependencies> element.
1207
1208
        @return: XML object as string for vt dependencies data.
1209
        """
1210
        return ''
1211
1212
    @staticmethod
1213
    def get_creation_time_vt_as_xml_str(vt_id, vt_creation_time):
0 ignored issues
show
Unused Code introduced by
The argument vt_creation_time seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1214
        """ Create a string representation of the XML object from the
1215
        vt_creation_time data object.
1216
        This needs to be implemented by each ospd wrapper, in case
1217
        vt_creation_time elements for VTs are used.
1218
1219
        The vt_creation_time XML object which is returned will be embedded
1220
        into a <vt_creation_time></vt_creation_time> element.
1221
1222
        @return: XML object as string for vt creation time data.
1223
        """
1224
        return ''
1225
1226
    @staticmethod
1227
    def get_modification_time_vt_as_xml_str(vt_id, vt_modification_time):
0 ignored issues
show
Coding Style Naming introduced by
The name get_modification_time_vt_as_xml_str does not conform to the method naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
Unused Code introduced by
The argument vt_modification_time seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1228
        """ Create a string representation of the XML object from the
1229
        vt_modification_time data object.
1230
        This needs to be implemented by each ospd wrapper, in case
1231
        vt_modification_time elements for VTs are used.
1232
1233
        The vt_modification_time XML object which is returned will be embedded
1234
        into a <vt_modification_time></vt_modification_time> element.
1235
1236
        @return: XML object as string for vt references data.
1237
        """
1238
        return ''
1239
1240
    @staticmethod
1241
    def get_summary_vt_as_xml_str(vt_id, summary):
0 ignored issues
show
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
Unused Code introduced by
The argument summary seems to be unused.
Loading history...
1242
        """ Create a string representation of the XML object from the
1243
        summary data object.
1244
        This needs to be implemented by each ospd wrapper, in case
1245
        summary elements for VTs are used.
1246
1247
        The summary XML object which is returned will be embedded
1248
        into a <summary></summary> element.
1249
1250
        @return: XML object as string for summary data.
1251
        """
1252
        return ''
1253
1254
    @staticmethod
1255
    def get_impact_vt_as_xml_str(vt_id, impact):
0 ignored issues
show
Unused Code introduced by
The argument impact seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1256
        """ Create a string representation of the XML object from the
1257
        impact data object.
1258
        This needs to be implemented by each ospd wrapper, in case
1259
        impact elements for VTs are used.
1260
1261
        The impact XML object which is returned will be embedded
1262
        into a <impact></impact> element.
1263
1264
        @return: XML object as string for impact data.
1265
        """
1266
        return ''
1267
1268
    @staticmethod
1269
    def get_affected_vt_as_xml_str(vt_id, affected):
0 ignored issues
show
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
Unused Code introduced by
The argument affected seems to be unused.
Loading history...
1270
        """ Create a string representation of the XML object from the
1271
        affected data object.
1272
        This needs to be implemented by each ospd wrapper, in case
1273
        affected elements for VTs are used.
1274
1275
        The affected XML object which is returned will be embedded
1276
        into a <affected></affected> element.
1277
1278
        @return: XML object as string for affected data.
1279
        """
1280
        return ''
1281
1282
    @staticmethod
1283
    def get_insight_vt_as_xml_str(vt_id, insight):
0 ignored issues
show
Unused Code introduced by
The argument insight seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1284
        """ Create a string representation of the XML object from the
1285
        insight data object.
1286
        This needs to be implemented by each ospd wrapper, in case
1287
        insight elements for VTs are used.
1288
1289
        The insight XML object which is returned will be embedded
1290
        into a <insight></insight> element.
1291
1292
        @return: XML object as string for insight data.
1293
        """
1294
        return ''
1295
1296
    @staticmethod
1297
    def get_solution_vt_as_xml_str(vt_id, solution, solution_type=None):
0 ignored issues
show
Unused Code introduced by
The argument solution seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
Unused Code introduced by
The argument solution_type seems to be unused.
Loading history...
1298
        """ Create a string representation of the XML object from the
1299
        solution data object.
1300
        This needs to be implemented by each ospd wrapper, in case
1301
        solution elements for VTs are used.
1302
1303
        The solution XML object which is returned will be embedded
1304
        into a <solution></solution> element.
1305
1306
        @return: XML object as string for solution data.
1307
        """
1308
        return ''
1309
1310
    @staticmethod
1311
    def get_detection_vt_as_xml_str(vt_id, detection=None, qod_type=None, qod=None):
0 ignored issues
show
Unused Code introduced by
The argument qod_type seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
Unused Code introduced by
The argument qod seems to be unused.
Loading history...
Unused Code introduced by
The argument detection seems to be unused.
Loading history...
1312
        """ Create a string representation of the XML object from the
1313
        detection data object.
1314
        This needs to be implemented by each ospd wrapper, in case
1315
        detection elements for VTs are used.
1316
1317
        The detection XML object which is returned is an element with
1318
        tag <detection></detection> element
1319
1320
        @return: XML object as string for detection data.
1321
        """
1322
        return ''
1323
1324
    @staticmethod
1325
    def get_severities_vt_as_xml_str(vt_id, severities):
0 ignored issues
show
Unused Code introduced by
The argument severities seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1326
        """ Create a string representation of the XML object from the
1327
        severities data object.
1328
        This needs to be implemented by each ospd wrapper, in case
1329
        severities elements for VTs are used.
1330
1331
        The severities XML objects which are returned will be embedded
1332
        into a <severities></severities> element.
1333
1334
        @return: XML object as string for severities data.
1335
        """
1336
        return ''
1337
1338
    def get_vt_xml(self, vt_id):
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (24/15).
Loading history...
1339
        """ Gets a single vulnerability test information in XML format.
1340
1341
        @return: String of single vulnerability test information in XML format.
1342
        """
1343
        if not vt_id:
1344
            return Element('vt')
1345
1346
        vt = self.vts.get(vt_id)
0 ignored issues
show
Coding Style Naming introduced by
The name vt does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
1347
1348
        name = vt.get('name')
1349
        vt_xml = Element('vt')
1350
        vt_xml.set('id', vt_id)
1351
1352
        for name, value in [('name', name)]:
1353
            elem = SubElement(vt_xml, name)
1354
            elem.text = str(value)
1355
1356
        if vt.get('vt_params'):
1357
            params_xml_str = (
1358
                '<vt_params>%s</vt_params>' % self.get_params_vt_as_xml_str(
1359
                    vt_id, vt.get('vt_params')))
1360
            vt_xml.append(secET.fromstring(params_xml_str))
1361
1362
        if vt.get('vt_refs'):
1363
            refs_xml_str = (
1364
                '<vt_refs>%s</vt_refs>' % self.get_refs_vt_as_xml_str(
1365
                    vt_id, vt.get('vt_refs')))
1366
            vt_xml.append(secET.fromstring(refs_xml_str))
1367
1368
        if vt.get('vt_dependencies'):
1369
            dependencies = self.get_dependencies_vt_as_xml_str(
1370
                vt_id, vt.get('vt_dependencies'))
1371
            deps_xml_str = (
1372
                '<dependencies>%s</dependencies>' % dependencies)
1373
            vt_xml.append(secET.fromstring(deps_xml_str))
1374
1375
        if vt.get('creation_time'):
1376
            vt_ctime = self.get_creation_time_vt_as_xml_str(
1377
                vt_id, vt.get('creation_time'))
1378
            creation_time_xml_str = (
1379
                '<creation_time>%s</creation_time>' % vt_ctime)
1380
            vt_xml.append(secET.fromstring(creation_time_xml_str))
1381
1382
        if vt.get('modification_time'):
1383
            vt_mtime = self.get_modification_time_vt_as_xml_str(
1384
                vt_id, vt.get('modification_time'))
1385
            modification_time_xml_str = (
1386
                '<modification_time>%s</modification_time>' % vt_mtime)
1387
            vt_xml.append(secET.fromstring(modification_time_xml_str))
1388
1389
        if vt.get('summary'):
1390
            summary_xml_str = self.get_summary_vt_as_xml_str(
1391
                vt_id, vt.get('summary'))
1392
            vt_xml.append(secET.fromstring(summary_xml_str))
1393
1394
        if vt.get('impact'):
1395
            impact_xml_str = self.get_impact_vt_as_xml_str(
1396
                vt_id, vt.get('impact'))
1397
            vt_xml.append(secET.fromstring(impact_xml_str))
1398
1399
        if vt.get('affected'):
1400
            affected_xml_str = self.get_affected_vt_as_xml_str(
1401
                vt_id, vt.get('affected'))
1402
            vt_xml.append(secET.fromstring(affected_xml_str))
1403
1404
        if vt.get('insight'):
1405
            insight_xml_str = self.get_insight_vt_as_xml_str(
1406
                vt_id, vt.get('insight'))
1407
            vt_xml.append(secET.fromstring(insight_xml_str))
1408
1409
        if vt.get('solution'):
1410
            solution_xml_str = self.get_solution_vt_as_xml_str(
1411
                vt_id, vt.get('solution'), vt.get('solution_type'))
1412
            vt_xml.append(secET.fromstring(solution_xml_str))
1413
1414
        if vt.get('detection') or vt.get('qod_type') or vt.get('qod'):
1415
            detection_xml_str = self.get_detection_vt_as_xml_str(
1416
                vt_id, vt.get('detection'), vt.get('qod_type'), vt.get('qod'))
1417
            vt_xml.append(secET.fromstring(detection_xml_str))
1418
1419
        if vt.get('severities'):
1420
            severities_xml_str = (
1421
                '<severities>%s</severities>' % self.get_severities_vt_as_xml_str(
1422
                    vt_id, vt.get('severities')))
1423
            vt_xml.append(secET.fromstring(severities_xml_str))
1424
1425
        if vt.get('custom'):
1426
            custom_xml_str = (
1427
                '<custom>%s</custom>' % self.get_custom_vt_as_xml_str(
1428
                    vt_id, vt.get('custom')))
1429
            vt_xml.append(secET.fromstring(custom_xml_str))
1430
1431
        return vt_xml
1432
1433
    def get_vts_xml(self, vt_id=''):
1434
        """ Gets collection of vulnerability test information in XML format.
1435
        If vt_id is specified, the collection will contain only this vt, of found.
1436
        If no vt_id is specified, the collection will contain all vts.
1437
1438
        @return: String of collection of vulnerability test information in XML format.
1439
        """
1440
1441
        vts_xml = Element('vts')
1442
1443
        if vt_id != '':
1444
            vts_xml.append(self.get_vt_xml(vt_id))
1445
        else:
1446
            for vt_id in self.vts:
0 ignored issues
show
unused-code introduced by
Redefining argument with the local name 'vt_id'
Loading history...
1447
                vts_xml.append(self.get_vt_xml(vt_id))
1448
1449
        return vts_xml
1450
1451
    def handle_get_scanner_details(self):
1452
        """ Handles <get_scanner_details> command.
1453
1454
        @return: Response string for <get_scanner_details> command.
1455
        """
1456
        desc_xml = Element('description')
1457
        desc_xml.text = self.get_scanner_description()
1458
        details = [
1459
            desc_xml,
1460
            self.get_scanner_params_xml()
1461
        ]
1462
        return simple_response_str('get_scanner_details', 200, 'OK', details)
1463
1464
    def handle_get_version_command(self):
1465
        """ Handles <get_version> command.
1466
1467
        @return: Response string for <get_version> command.
1468
        """
1469
        protocol = Element('protocol')
1470
        for name, value in [('name', 'OSP'), ('version', self.get_protocol_version())]:
1471
            elem = SubElement(protocol, name)
1472
            elem.text = value
1473
1474
        daemon = Element('daemon')
1475
        for name, value in [('name', self.get_daemon_name()), ('version', self.get_daemon_version())]:
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (102/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
1476
            elem = SubElement(daemon, name)
1477
            elem.text = value
1478
1479
        scanner = Element('scanner')
1480
        for name, value in [('name', self.get_scanner_name()), ('version', self.get_scanner_version())]:
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (104/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
1481
            elem = SubElement(scanner, name)
1482
            elem.text = value
1483
1484
        content = [protocol, daemon, scanner]
1485
1486
        if self.get_vts_version():
1487
            vts = Element('vts')
1488
            elem = SubElement(vts, 'version')
1489
            elem.text = self.get_vts_version()
1490
            content.append(vts)
1491
1492
        return simple_response_str('get_version', 200, 'OK', content)
1493
1494
    def handle_command(self, command):
0 ignored issues
show
best-practice introduced by
Too many return statements (8/6)
Loading history...
1495
        """ Handles an osp command in a string.
1496
1497
        @return: OSP Response to command.
1498
        """
1499
        try:
1500
            tree = secET.fromstring(command)
1501
        except secET.ParseError:
1502
            logger.debug("Erroneous client input: %s", command)
1503
            raise OSPDError('Invalid data')
1504
1505
        if not self.command_exists(tree.tag) and tree.tag != "authenticate":
1506
            raise OSPDError('Bogus command name')
1507
1508
        if tree.tag == "get_version":
1509
            return self.handle_get_version_command()
1510
        elif tree.tag == "start_scan":
1511
            return self.handle_start_scan_command(tree)
1512
        elif tree.tag == "stop_scan":
1513
            return self.handle_stop_scan_command(tree)
1514
        elif tree.tag == "get_scans":
1515
            return self.handle_get_scans_command(tree)
1516
        elif tree.tag == "get_vts":
1517
            return self.handle_get_vts_command(tree)
1518
        elif tree.tag == "delete_scan":
1519
            return self.handle_delete_scan_command(tree)
1520
        elif tree.tag == "help":
1521
            return self.handle_help_command(tree)
1522
        elif tree.tag == "get_scanner_details":
1523
            return self.handle_get_scanner_details()
1524
        else:
1525
            assert False, "Unhandled command: {0}".format(tree.tag)
1526
1527
    def check(self):
1528
        """ Asserts to False. Should be implemented by subclass. """
1529
        raise NotImplementedError
1530
1531
    def run(self, address, port, unix_path):
1532
        """ Starts the Daemon, handling commands until interrupted.
1533
1534
        @return False if error. Runs indefinitely otherwise.
1535
        """
1536
        assert address or unix_path
1537
        if unix_path:
1538
            sock = bind_unix_socket(unix_path)
1539
        else:
1540
            sock = bind_socket(address, port)
1541
        if sock is None:
1542
            return False
1543
1544
        try:
1545
            while True:
1546
                if unix_path:
1547
                    client_stream, _ = sock.accept()
1548
                    logger.debug("New connection from %s", unix_path)
1549
                    self.handle_client_stream(client_stream, True)
1550
                else:
1551
                    client_stream = self.new_client_stream(sock)
1552
                    if client_stream is None:
1553
                        continue
1554
                    self.handle_client_stream(client_stream, False)
1555
                close_client_stream(client_stream, unix_path)
1556
        except KeyboardInterrupt:
1557
            logger.info("Received Ctrl-C shutting-down ...")
1558
        finally:
1559
            sock.shutdown(socket.SHUT_RDWR)
1560
            sock.close()
1561
1562
    def create_scan(self, scan_id, targets, target_str, options, vts):
0 ignored issues
show
best-practice introduced by
Too many arguments (6/5)
Loading history...
1563
        """ Creates a new scan.
1564
1565
        @target: Target to scan.
1566
        @options: Miscellaneous scan options.
1567
1568
        @return: New scan's ID.
1569
        """
1570
        return self.scan_collection.create_scan(scan_id, targets, target_str, options, vts)
1571
1572
    def get_scan_options(self, scan_id):
1573
        """ Gives a scan's list of options. """
1574
        return self.scan_collection.get_options(scan_id)
1575
1576
    def set_scan_option(self, scan_id, name, value):
1577
        """ Sets a scan's option to a provided value. """
1578
        return self.scan_collection.set_option(scan_id, name, value)
1579
1580
    def check_scan_process(self, scan_id):
1581
        """ Check the scan's process, and terminate the scan if not alive. """
1582
        scan_process = self.scan_processes[scan_id]
1583
        progress = self.get_scan_progress(scan_id)
1584
        if progress < 100 and not scan_process.is_alive():
1585
            self.set_scan_progress(scan_id, 100)
1586
            self.add_scan_error(scan_id, name="", host="",
1587
                                value="Scan process failure.")
1588
            logger.info("%s: Scan terminated.", scan_id)
1589
        elif progress == 100:
1590
            scan_process.join()
1591
1592
    def get_scan_progress(self, scan_id):
1593
        """ Gives a scan's current progress value. """
1594
        return self.scan_collection.get_progress(scan_id)
1595
1596
    def get_scan_target_progress(self, scan_id):
1597
        """ Gives a list with scan's current progress value of each target. """
1598
        return self.scan_collection.get_target_progress(scan_id)
1599
1600
    def get_scan_target(self, scan_id):
1601
        """ Gives a scan's target. """
1602
        return self.scan_collection.get_target(scan_id)
1603
1604
    def get_scan_ports(self, scan_id, target=''):
1605
        """ Gives a scan's ports list. """
1606
        return self.scan_collection.get_ports(scan_id, target)
1607
1608
    def get_scan_credentials(self, scan_id, target=''):
1609
        """ Gives a scan's credential list. If a target is passed gives
1610
        the credential list for the given target. """
1611
        return self.scan_collection.get_credentials(scan_id, target)
1612
1613
    def get_scan_vts(self, scan_id):
1614
        """ Gives a scan's vts list. """
1615
        return self.scan_collection.get_vts(scan_id)
1616
1617
    def get_scan_start_time(self, scan_id):
1618
        """ Gives a scan's start time. """
1619
        return self.scan_collection.get_start_time(scan_id)
1620
1621
    def get_scan_end_time(self, scan_id):
1622
        """ Gives a scan's end time. """
1623
        return self.scan_collection.get_end_time(scan_id)
1624
1625
    def add_scan_log(self, scan_id, host='', name='', value='', port='',
0 ignored issues
show
best-practice introduced by
Too many arguments (8/5)
Loading history...
1626
                     test_id='', qod=''):
1627
        """ Adds a log result to scan_id scan. """
1628
        self.scan_collection.add_result(scan_id, ResultType.LOG, host, name,
1629
                                        value, port, test_id, 0.0, qod)
1630
1631
    def add_scan_error(self, scan_id, host='', name='', value='', port=''):
0 ignored issues
show
best-practice introduced by
Too many arguments (6/5)
Loading history...
1632
        """ Adds an error result to scan_id scan. """
1633
        self.scan_collection.add_result(scan_id, ResultType.ERROR, host, name,
1634
                                        value, port)
1635
1636
    def add_scan_host_detail(self, scan_id, host='', name='', value=''):
1637
        """ Adds a host detail result to scan_id scan. """
1638
        self.scan_collection.add_result(scan_id, ResultType.HOST_DETAIL, host,
1639
                                        name, value)
1640
1641
    def add_scan_alarm(self, scan_id, host='', name='', value='', port='',
0 ignored issues
show
best-practice introduced by
Too many arguments (9/5)
Loading history...
1642
                       test_id='', severity='', qod=''):
1643
        """ Adds an alarm result to scan_id scan. """
1644
        self.scan_collection.add_result(scan_id, ResultType.ALARM, host, name,
1645
                                        value, port, test_id, severity, qod)
1646