Completed
Push — master ( 585251...3d2da1 )
by Juan José
16s queued 11s
created

ospd.ospd.OSPDaemon.handle_stop_scan_command()   B

Complexity

Conditions 5

Size

Total Lines 25
Code Lines 19

Duplication

Lines 25
Ratio 100 %

Importance

Changes 0
Metric Value
cc 5
eloc 19
nop 2
dl 25
loc 25
rs 8.9833
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 (1621/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 {0}:{1}"
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
187
                     .format(address, port))
188
        return None
189
190
    logger.info('Listening on {0}:{1}'.format(address, port))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
191
    bindsocket.listen(0)
192
    return bindsocket
193
194 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...
195
    """ Returns a unix file socket bound on (path). """
196
197
    assert path
198
    bindsocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
199
    try:
200
        os.unlink(path)
201
    except OSError:
202
        if os.path.exists(path):
203
            raise
204
    try:
205
        bindsocket.bind(path)
206
    except socket.error:
207
        logger.error("Couldn't bind socket on {0}".format(path))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
208
        return None
209
210
    logger.info('Listening on {0}'.format(path))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
211
    bindsocket.listen(0)
212
    return bindsocket
213
214
215 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...
216
    """ Closes provided client stream """
217
    try:
218
        client_stream.shutdown(socket.SHUT_RDWR)
219
        if unix_path:
220
            logger.debug('{0}: Connection closed'.format(unix_path))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
221
        else:
222
            peer = client_stream.getpeername()
223
            logger.debug('{0}:{1}: Connection closed'.format(peer[0], peer[1]))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
224
    except (socket.error, OSError) as exception:
225
        logger.debug('Connection closing error: {0}'.format(exception))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
226
    client_stream.close()
227
228
229 View Code Duplication
class OSPDaemon(object):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
best-practice introduced by
Too many instance attributes (11/7)
Loading history...
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
best-practice introduced by
Too many public methods (84/20)
Loading history...
230
231
    """ Daemon class for OSP traffic handling.
232
233
    Every scanner wrapper should subclass it and make necessary additions and
234
    changes.
235
    * Add any needed parameters in __init__.
236
    * Implement check() method which verifies scanner availability and other
237
      environment related conditions.
238
    * Implement process_scan_params and exec_scan methods which are
239
      specific to handling the <start_scan> command, executing the wrapped
240
      scanner and storing the results.
241
    * exec_scan() should return 0 if host is dead or not reached, 1 if host is
242
      alive and 2 if scan error or status is unknown.
243
    * Implement other methods that assert to False such as get_scanner_name,
244
      get_scanner_version.
245
    * Use Call set_command_attributes at init time to add scanner command
246
      specific options eg. the w3af profile for w3af wrapper.
247
    """
248
249
    def __init__(self, certfile, keyfile, cafile):
250
        """ Initializes the daemon's internal data. """
251
        # @todo: Actually it makes sense to move the certificate params to
252
        #        a separate function because it is not mandatory anymore to
253
        #        use a TLS setup (unix file socket is an alternative).
254
        #        However, changing this makes it mandatory for any ospd scanner
255
        #        to change the function calls as well. So this breaks the API
256
        #        and should only be done with a major release.
257
        self.certs = dict()
258
        self.certs['cert_file'] = certfile
259
        self.certs['key_file'] = keyfile
260
        self.certs['ca_file'] = cafile
261
        self.scan_collection = ScanCollection()
262
        self.scan_processes = dict()
263
        self.daemon_info = dict()
264
        self.daemon_info['name'] = "OSPd"
265
        self.daemon_info['version'] = __version__
266
        self.daemon_info['description'] = "No description"
267
        self.scanner_info = dict()
268
        self.scanner_info['name'] = 'No name'
269
        self.scanner_info['version'] = 'No version'
270
        self.scanner_info['description'] = 'No description'
271
        self.server_version = None  # Set by the subclass.
272
        self.protocol_version = PROTOCOL_VERSION
273
        self.commands = COMMANDS_TABLE
274
        self.scanner_params = dict()
275
        for name, param in BASE_SCANNER_PARAMS.items():
276
            self.add_scanner_param(name, param)
277
        self.vts = dict()
278
        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...
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 command_exists(self, name):
359
        """ Checks if a commands exists. """
360
        return name in self.commands.keys()
361
362
    def get_scanner_name(self):
363
        """ Gives the wrapped scanner's name. """
364
        return self.scanner_info['name']
365
366
    def get_scanner_version(self):
367
        """ Gives the wrapped scanner's version. """
368
        return self.scanner_info['version']
369
370
    def get_scanner_description(self):
371
        """ Gives the wrapped scanner's description. """
372
        return self.scanner_info['description']
373
374
    def get_server_version(self):
375
        """ Gives the specific OSP server's version. """
376
        assert self.server_version
377
        return self.server_version
378
379
    def get_protocol_version(self):
380
        """ Gives the OSP's version. """
381
        return self.protocol_version
382
383
    def _preprocess_scan_params(self, xml_params):
384
        """ Processes the scan parameters. """
385
        params = {}
386
        for param in xml_params:
387
            params[param.tag] = param.text or ''
388
        # Set default values.
389
        for key in self.scanner_params:
390
            if key not in params:
391
                params[key] = self.get_scanner_param_default(key)
392
                if self.get_scanner_param_type(key) == 'selection':
393
                    params[key] = params[key].split('|')[0]
394
        # Validate values.
395
        for key in params:
396
            param_type = self.get_scanner_param_type(key)
397
            if not param_type:
398
                continue
399
            if param_type in ['integer', 'boolean']:
400
                try:
401
                    params[key] = int(params[key])
402
                except ValueError:
403
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
404
            if param_type == 'boolean':
405
                if params[key] not in [0, 1]:
406
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
407
            elif param_type == 'selection':
408
                selection = self.get_scanner_param_default(key).split('|')
409
                if params[key] not in selection:
410
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
411
            if self.get_scanner_param_mandatory(key) and params[key] == '':
412
                    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...
413
                                    'start_scan')
414
        return params
415
416
    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...
417
        """ This method is to be overridden by the child classes if necessary
418
        """
419
        return params
420
421
    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...
422
        """ Receive an XML object with the Vulnerability Tests an their
423
        parameters to be use in a scan and return a dictionary.
424
425
        @param: XML element with vt subelements. Each vt has an
426
                id attribute. Optional parameters can be included
427
                as vt child.
428
                Example form:
429
                <vt_selection>
430
                  <vt_single id='vt1' />
431
                  <vt_single id='vt2'>
432
                    <vt_value id='param1'>value</vt_value>
433
                  </vt_single>
434
                  <vt_group filter='family=debian'/>
435
                  <vt_group filter='family=general'/>
436
                </vt_selection>
437
438
        @return: Dictionary containing the vts attribute and subelements,
439
                 like the VT's id and VT's parameters.
440
                 Example form:
441
                 {'vt1': {},
442
                  'vt2': {'value_id': 'value'},
443
                  'vt_groups': ['family=debian', 'family=general']}
444
        """
445
        vt_selection = {}
446
        filters = list()
447
        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...
448
            if vt.tag == 'vt_single':
449
                vt_id = vt.attrib.get('id')
450
                vt_selection[vt_id] = {}
451
                for vt_value in vt:
452
                    if not vt_value.attrib.get('id'):
453
                        raise OSPDError('Invalid VT preference. No attribute id',
454
                                        'start_scan')
455
                    vt_value_id = vt_value.attrib.get('id')
456
                    vt_value_value = vt_value.text if vt_value.text else ''
457
                    vt_selection[vt_id][vt_value_id] = vt_value_value
458
            if vt.tag == 'vt_group':
459
                vts_filter = vt.attrib.get('filter', None)
460
                if vts_filter is None:
461
                    raise OSPDError('Invalid VT group. No filter given.',
462
                                    'start_scan')
463
                filters.append(vts_filter)
464
        vt_selection['vt_groups'] = filters
465
        return vt_selection
466
467
    @staticmethod
468
    def process_credentials_elements(cred_tree):
469
        """ Receive an XML object with the credentials to run
470
        a scan against a given target.
471
472
        @param:
473
        <credentials>
474
          <credential type="up" service="ssh" port="22">
475
            <username>scanuser</username>
476
            <password>mypass</password>
477
          </credential>
478
          <credential type="up" service="smb">
479
            <username>smbuser</username>
480
            <password>mypass</password>
481
          </credential>
482
        </credentials>
483
484
        @return: Dictionary containing the credentials for a given target.
485
                 Example form:
486
                 {'ssh': {'type': type,
487
                          'port': port,
488
                          'username': username,
489
                          'password': pass,
490
                        },
491
                  'smb': {'type': type,
492
                          'username': username,
493
                          'password': pass,
494
                         },
495
                   }
496
        """
497
        credentials = {}
498
        for credential in cred_tree:
499
            service = credential.attrib.get('service')
500
            credentials[service] = {}
501
            credentials[service]['type'] = credential.attrib.get('type')
502
            if service == 'ssh':
503
                credentials[service]['port'] = credential.attrib.get('port')
504
            for param in credential:
505
                credentials[service][param.tag] = param.text
506
507
        return credentials
508
509
    @classmethod
510
    def process_targets_element(cls, scanner_target):
511
        """ Receive an XML object with the target, ports and credentials to run
512
        a scan against.
513
514
        @param: XML element with target subelements. Each target has <hosts>
515
        and <ports> subelements. Hosts can be a single host, a host range,
516
        a comma-separated host list or a network address.
517
        <ports> and  <credentials> are optional. Therefore each ospd-scanner
518
        should check for a valid ones if needed.
519
520
                Example form:
521
                <targets>
522
                  <target>
523
                    <hosts>localhosts</hosts>
524
                    <ports>80,443</ports>
525
                  </target>
526
                  <target>
527
                    <hosts>192.168.0.0/24</hosts>
528
                    <ports>22</ports>
529
                    <credentials>
530
                      <credential type="up" service="ssh" port="22">
531
                        <username>scanuser</username>
532
                        <password>mypass</password>
533
                      </credential>
534
                      <credential type="up" service="smb">
535
                        <username>smbuser</username>
536
                        <password>mypass</password>
537
                      </credential>
538
                    </credentials>
539
                  </target>
540
                </targets>
541
542
        @return: A list of (hosts, port) tuples.
543
                 Example form:
544
                 [['localhost', '80,43'],
545
                  ['192.168.0.0/24', '22', {'smb': {'type': type,
546
                                                    'port': port,
547
                                                    'username': username,
548
                                                    'password': pass,
549
                                                   }}]]
550
        """
551
552
        target_list = []
553
        for target in scanner_target:
554
            ports = ''
555
            credentials = {}
556
            for child in target:
557
                if child.tag == 'hosts':
558
                    hosts = child.text
559
                if child.tag == 'ports':
560
                    ports = child.text
561
                if child.tag == 'credentials':
562
                    credentials = cls.process_credentials_elements(child)
563
            if hosts:
0 ignored issues
show
introduced by
The variable hosts does not seem to be defined for all execution paths.
Loading history...
564
                target_list.append([hosts, ports, credentials])
565
            else:
566
                raise OSPDError('No target to scan', 'start_scan')
567
568
        return target_list
569
570
    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...
571
        """ Handles <start_scan> command.
572
573
        @return: Response string for <start_scan> command.
574
        """
575
576
        target_str = scan_et.attrib.get('target')
577
        ports_str = scan_et.attrib.get('ports')
578
        # For backward compatibility, if target and ports attributes are set,
579
        # <targets> element is ignored.
580
        if target_str is None or ports_str is None:
581
            target_list = scan_et.find('targets')
582
            if target_list is None or not target_list:
583
                raise OSPDError('No targets or ports', 'start_scan')
584
            else:
585
                scan_targets = self.process_targets_element(target_list)
586
        else:
587
            scan_targets = []
588
            for single_target in target_str_to_list(target_str):
589
                scan_targets.append([single_target, ports_str, ''])
590
591
        scan_id = scan_et.attrib.get('scan_id')
592
        if scan_id is not None and scan_id != '' and not valid_uuid(scan_id):
593
            raise OSPDError('Invalid scan_id UUID', 'start_scan')
594
595
        try:
596
            parallel = int(scan_et.attrib.get('parallel', '1'))
597
            if parallel < 1 or parallel > 20:
598
                parallel = 1
599
        except ValueError:
600
            raise OSPDError('Invalid value for parallel scans. '
601
                            'It must be a number', 'start_scan')
602
603
        scanner_params = scan_et.find('scanner_params')
604
        if scanner_params is None:
605
            raise OSPDError('No scanner_params element', 'start_scan')
606
607
        params = self._preprocess_scan_params(scanner_params)
608
609
        # VTS is an optional element. If present should not be empty.
610
        vt_selection = {}
611
        scanner_vts = scan_et.find('vt_selection')
612
        if scanner_vts is not None:
613
            if not scanner_vts:
614
                raise OSPDError('VTs list is empty', 'start_scan')
615
            else:
616
                vt_selection = self.process_vts_params(scanner_vts)
617
618
        # Dry run case.
619
        if 'dry_run' in params and int(params['dry_run']):
620
            scan_func = self.dry_run_scan
621
            scan_params = None
622
        else:
623
            scan_func = self.start_scan
624
            scan_params = self.process_scan_params(params)
625
626
        scan_id = self.create_scan(scan_id, scan_targets,
627
                                   target_str, scan_params,
628
                                   vt_selection)
629
        scan_process = multiprocessing.Process(target=scan_func,
630
                                               args=(scan_id,
631
                                                     scan_targets,
632
                                                     parallel))
633
        self.scan_processes[scan_id] = scan_process
634
        scan_process.start()
635
        id_ = Element('id')
636
        id_.text = scan_id
637
        return simple_response_str('start_scan', 200, 'OK', id_)
638
639
    def handle_stop_scan_command(self, scan_et):
640
        """ Handles <stop_scan> command.
641
642
        @return: Response string for <stop_scan> command.
643
        """
644
645
        scan_id = scan_et.attrib.get('scan_id')
646
        if scan_id is None or scan_id == '':
647
            raise OSPDError('No scan_id attribute', 'stop_scan')
648
        scan_process = self.scan_processes.get(scan_id)
649
        if not scan_process:
650
            raise OSPDError('Scan not found {0}.'.format(scan_id), 'stop_scan')
651
        if not scan_process.is_alive():
652
            raise OSPDError('Scan already stopped or finished.', 'stop_scan')
653
654
        self.set_scan_status(scan_id, "stopped")
655
        logger.info('{0}: Scan stopping {1}.'.format(scan_id, scan_process.ident))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
656
        self.stop_scan(scan_id)
657
        scan_process.terminate()
658
        os.killpg(os.getpgid(scan_process.ident), 15)
659
        scan_process.join()
660
        self.set_scan_progress(scan_id, 100)
661
        self.add_scan_log(scan_id, name='', host='', value='Scan stopped.')
662
        logger.info('{0}: Scan stopped.'.format(scan_id))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
663
        return simple_response_str('stop_scan', 200, 'OK')
664
665
    @staticmethod
666
    def stop_scan(scan_id):
667
        """ Should be implemented by subclass in case of a clean up before
668
        terminating is needed. """
669
670
    def exec_scan(self, scan_id, target):
671
        """ Asserts to False. Should be implemented by subclass. """
672
        raise NotImplementedError
673
674
    def finish_scan(self, scan_id):
675
        """ Sets a scan as finished. """
676
        self.set_scan_progress(scan_id, 100)
677
        logger.info("{0}: Scan finished.".format(scan_id))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
678
679
    def get_daemon_name(self):
680
        """ Gives osp daemon's name. """
681
        return self.daemon_info['name']
682
683
    def get_daemon_version(self):
684
        """ Gives osp daemon's version. """
685
        return self.daemon_info['version']
686
687
    def get_scanner_param_type(self, param):
688
        """ Returns type of a scanner parameter. """
689
        assert isinstance(param, str)
690
        entry = self.scanner_params.get(param)
691
        if not entry:
692
            return None
693
        return entry.get('type')
694
695
    def get_scanner_param_mandatory(self, param):
696
        """ Returns if a scanner parameter is mandatory. """
697
        assert isinstance(param, str)
698
        entry = self.scanner_params.get(param)
699
        if not entry:
700
            return False
701
        return entry.get('mandatory')
702
703
    def get_scanner_param_default(self, param):
704
        """ Returns default value of a scanner parameter. """
705
        assert isinstance(param, str)
706
        entry = self.scanner_params.get(param)
707
        if not entry:
708
            return None
709
        return entry.get('default')
710
711
    def get_scanner_params_xml(self):
712
        """ Returns the OSP Daemon's scanner params in xml format. """
713
        scanner_params = Element('scanner_params')
714
        for param_id, param in self.scanner_params.items():
715
            param_xml = SubElement(scanner_params, 'scanner_param')
716
            for name, value in [('id', param_id),
717
                                ('type', param['type'])]:
718
                param_xml.set(name, value)
719
            for name, value in [('name', param['name']),
720
                                ('description', param['description']),
721
                                ('default', param['default']),
722
                                ('mandatory', param['mandatory'])]:
723
                elem = SubElement(param_xml, name)
724
                elem.text = str(value)
725
        return scanner_params
726
727
    def new_client_stream(self, sock):
728
        """ Returns a new ssl client stream from bind_socket. """
729
730
        assert sock
731
        newsocket, fromaddr = sock.accept()
732
        logger.debug("New connection from"
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
733
                     " {0}:{1}".format(fromaddr[0], fromaddr[1]))
734
        # NB: Despite the name, ssl.PROTOCOL_SSLv23 selects the highest
735
        # protocol version that both the client and server support. In modern
736
        # Python versions (>= 3.4) it supports TLS >= 1.0 with SSLv2 and SSLv3
737
        # being disabled. For Python >=3.5, PROTOCOL_SSLv23 is an alias for
738
        # PROTOCOL_TLS which should be used once compatibility with Python 3.4
739
        # is no longer desired.
740
        try:
741
            ssl_socket = ssl.wrap_socket(newsocket, cert_reqs=ssl.CERT_REQUIRED,
742
                                         server_side=True,
743
                                         certfile=self.certs['cert_file'],
744
                                         keyfile=self.certs['key_file'],
745
                                         ca_certs=self.certs['ca_file'],
746
                                         ssl_version=ssl.PROTOCOL_SSLv23)
747
        except (ssl.SSLError, socket.error) as message:
748
            logger.error(message)
749
            return None
750
        return ssl_socket
751
752
    @staticmethod
753
    def write_to_stream(stream, response, block_len=1024):
754
        """
755
        Send the response in blocks of the given len using the
756
        passed method dependending on the socket type.
757
        """
758
        try:
759
            i_start = 0
760
            i_end = block_len
761
            while True:
762
                if i_end > len(response):
763
                    stream(response[i_start:])
764
                    break
765
                stream(response[i_start:i_end])
766
                i_start = i_end
767
                i_end += block_len
768
        except (socket.timeout, socket.error) as exception:
769
            logger.debug('Error sending response to the client: {0}'.format(exception))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
770
771
    def handle_client_stream(self, stream, is_unix=False):
772
        """ Handles stream of data received from client. """
773
774
        assert stream
775
        data = []
776
        stream.settimeout(2)
777
        while True:
778
            try:
779
                if is_unix:
780
                    data.append(stream.recv(1024))
781
                else:
782
                    data.append(stream.read(1024))
783
                if len(data) == 0:
0 ignored issues
show
Unused Code introduced by
Do not use len(SEQUENCE) as condition value
Loading history...
784
                    logger.warning(
785
                        "Empty client stream (Connection unexpectedly closed)")
786
                    return
787
            except (AttributeError, ValueError) as message:
788
                logger.error(message)
789
                return
790
            except (ssl.SSLError) as exception:
791
                logger.debug('Error: {0}'.format(exception[0]))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
792
                break
793
            except (socket.timeout) as exception:
794
                logger.debug('Error: {0}'.format(exception))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
795
                break
796
        data = b''.join(data)
797
        if len(data) <= 0:
798
            logger.debug("Empty client stream")
799
            return
800
        try:
801
            response = self.handle_command(data)
802
        except OSPDError as exception:
803
            response = exception.as_xml()
804
            logger.debug('Command error: {0}'.format(exception.message))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
805
        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...
806
            logger.exception('While handling client command:')
807
            exception = OSPDError('Fatal error', 'error')
808
            response = exception.as_xml()
809
        if is_unix:
810
            send_method = stream.send
811
        else:
812
            send_method = stream.write
813
        self.write_to_stream(send_method, response)
814
815
    def parallel_scan(self, scan_id, target):
816
        """ Starts the scan with scan_id. """
817
        try:
818
            ret = self.exec_scan(scan_id, target)
819
            if ret == 0:
820
                self.add_scan_host_detail(scan_id, name='host_status',
821
                                          host=target, value='0')
822
            elif ret == 1:
823
                self.add_scan_host_detail(scan_id, name='host_status',
824
                                          host=target, value='1')
825
            elif ret == 2:
826
                self.add_scan_host_detail(scan_id, name='host_status',
827
                                          host=target, value='2')
828
            else:
829
                logger.debug('{0}: No host status returned'.format(target))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
830
        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...
831
            self.add_scan_error(scan_id, name='', host=target,
832
                                value='Host process failure (%s).' % e)
833
            logger.exception('While scanning {0}:'.format(target))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
834
        else:
835
            logger.info("{0}: Host scan finished.".format(target))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
836
837
    def check_pending_target(self, scan_id, multiscan_proc):
838
        """ Check if a scan process is still alive. In case the process
839
        finished, removes the process from the multiscan_process list.
840
        In case of the progress is not set from the wrapper for a single
841
        target, it will be automatically set if the target scan finished.
842
843
        @input scan_id        Scan_id of the whole scan.
844
        @input multiscan_proc A list with the scan process which
845
                              may still be alive.
846
847
        @return Actualized list with current runnging scan processes."""
848
849
        for running_target in multiscan_proc:
850
            if not running_target[0].is_alive():
851
                target_prog = self.get_scan_target_progress(scan_id)
852
                if target_prog[running_target[1]] < 100:
853
                    self.set_scan_target_progress(scan_id,
854
                                                  running_target[1],
855
                                                  100)
856
                multiscan_proc.remove(running_target)
857
        return multiscan_proc
858
859
    def calculate_progress(self, scan_id):
860
        """ Calculate the total scan progress from the
861
        partial target progress. """
862
863
        target_progress = self.get_scan_target_progress(scan_id)
864
        return sum(target_progress.values())/len(target_progress)
865
866
    def start_scan(self, scan_id, targets, parallel=1):
867
        """ Handle N parallel scans if 'parallel' is greater than 1. """
868
869
        os.setsid()
870
        multiscan_proc = []
871
        logger.info("{0}: Scan started.".format(scan_id))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
872
        target_list = targets
873
        if target_list is None or not target_list:
874
            raise OSPDError('Erroneous targets list', 'start_scan')
875
876
        for index, target in enumerate(target_list):
0 ignored issues
show
Unused Code introduced by
The variable index seems to be unused.
Loading history...
877
            while len(multiscan_proc) >= parallel:
878
                multiscan_proc = self.check_pending_target(scan_id,
879
                                                           multiscan_proc)
880
                progress = self.calculate_progress(scan_id)
881
                self.set_scan_progress(scan_id, progress)
882
                time.sleep(1)
883
884
            if self.get_scan_status(scan_id) == "stopped":
885
                self.finish_scan(scan_id)
886
                return
887
888
            logger.info("{0}: Host scan started on ports {1}.".format(target[0], target[1]))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
889
            scan_process = multiprocessing.Process(target=self.parallel_scan,
890
                                                   args=(scan_id, target[0]))
891
            multiscan_proc.append((scan_process, target[0]))
892
            scan_process.start()
893
894
        # Wait until all single target were scanned
895
        while multiscan_proc:
896
            multiscan_proc = self.check_pending_target(scan_id, multiscan_proc)
897
            if multiscan_proc:
898
                progress = self.calculate_progress(scan_id)
899
                self.set_scan_progress(scan_id, progress)
900
            time.sleep(1)
901
902
        self.finish_scan(scan_id)
903
904
    def dry_run_scan(self, scan_id, targets):
905
        """ Dry runs a scan. """
906
907
        os.setsid()
908
        #target_list = target_str_to_list(target_str)
909
        for _, target in enumerate(targets):
910
            host = resolve_hostname(target[0])
911
            if host is None:
912
                logger.info("Couldn't resolve {0}.".format(target[0]))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
913
                continue
914
            port = self.get_scan_ports(scan_id, target=target[0])
915
            logger.info("{0}:{1}: Dry run mode.".format(host, port))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
916
            self.add_scan_log(scan_id, name='', host=host,
917
                              value='Dry run result')
918
        self.finish_scan(scan_id)
919
920
    def handle_timeout(self, scan_id, host):
921
        """ Handles scanner reaching timeout error. """
922
        self.add_scan_error(scan_id, host=host, name="Timeout",
923
                            value="{0} exec timeout."
924
                            .format(self.get_scanner_name()))
925
926
    def set_scan_progress(self, scan_id, progress):
927
        """ Sets scan_id scan's progress which is a number between 0 and 100. """
928
        self.scan_collection.set_progress(scan_id, progress)
929
930
    def set_scan_target_progress(self, scan_id, target, progress):
931
        """ Sets target's progress. """
932
        self.scan_collection.set_target_progress(scan_id, target, progress)
933
934
    def set_scan_status(self, scan_id, status):
935
        """ Set the scan's status."""
936
        self.scan_collection.set_status(scan_id, status)
937
938
    def get_scan_status(self, scan_id):
939
        """ Get scan_id scans's status."""
940
        return self.scan_collection.get_status(scan_id)
941
942
    def scan_exists(self, scan_id):
943
        """ Checks if a scan with ID scan_id is in collection.
944
945
        @return: 1 if scan exists, 0 otherwise.
946
        """
947
        return self.scan_collection.id_exists(scan_id)
948
949
    def handle_get_scans_command(self, scan_et):
950
        """ Handles <get_scans> command.
951
952
        @return: Response string for <get_scans> command.
953
        """
954
955
        scan_id = scan_et.attrib.get('scan_id')
956
        details = scan_et.attrib.get('details')
957
        pop_res = scan_et.attrib.get('pop_results')
958
        if details and details == '0':
959
            details = False
960
        else:
961
            details = True
962
            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...
963
                pop_res = True
964
            else:
965
                pop_res = False
966
967
        responses = []
968
        if scan_id and scan_id in self.scan_collection.ids_iterator():
969
            self.check_scan_process(scan_id)
970
            scan = self.get_scan_xml(scan_id, details, pop_res)
971
            responses.append(scan)
972
        elif scan_id:
973
            text = "Failed to find scan '{0}'".format(scan_id)
974
            return simple_response_str('get_scans', 404, text)
975
        else:
976
            for scan_id in self.scan_collection.ids_iterator():
977
                self.check_scan_process(scan_id)
978
                scan = self.get_scan_xml(scan_id, details, pop_res)
979
                responses.append(scan)
980
        return simple_response_str('get_scans', 200, 'OK', responses)
981
982
    def handle_get_vts_command(self, vt_et):
983
        """ Handles <get_vts> command.
984
985
        @return: Response string for <get_vts> command.
986
        """
987
988
        vt_id = vt_et.attrib.get('vt_id')
989
990
        if vt_id and vt_id not in self.vts:
991
            text = "Failed to find vulnerability test '{0}'".format(vt_id)
992
            return simple_response_str('get_vts', 404, text)
993
994
        responses = []
995
996
        if vt_id:
997
            vts_xml = self.get_vts_xml(vt_id)
998
        else:
999
            vts_xml = self.get_vts_xml()
1000
1001
        responses.append(vts_xml)
1002
1003
        return simple_response_str('get_vts', 200, 'OK', responses)
1004
1005
    def handle_help_command(self, scan_et):
1006
        """ Handles <help> command.
1007
1008
        @return: Response string for <help> command.
1009
        """
1010
        help_format = scan_et.attrib.get('format')
1011
        if help_format is None or help_format == "text":
1012
            # Default help format is text.
1013
            return simple_response_str('help', 200, 'OK',
1014
                                       self.get_help_text())
1015
        elif help_format == "xml":
1016
            text = self.get_xml_str(self.commands)
1017
            return simple_response_str('help', 200, 'OK', text)
1018
        raise OSPDError('Bogus help format', 'help')
1019
1020
    def get_help_text(self):
1021
        """ Returns the help output in plain text format."""
1022
1023
        txt = str('\n')
1024
        for name, info in self.commands.items():
1025
            command_txt = "\t{0: <22} {1}\n".format(name, info['description'])
1026
            if info['attributes']:
1027
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
1028
                for attrname, attrdesc in info['attributes'].items():
1029
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
1030
                    command_txt = ''.join([command_txt, attr_txt])
1031
            if info['elements']:
1032
                command_txt = ''.join([command_txt, "\t Elements:\n",
1033
                                       self.elements_as_text(info['elements'])])
1034
            txt = ''.join([txt, command_txt])
1035
        return txt
1036
1037
    def elements_as_text(self, elems, indent=2):
1038
        """ Returns the elems dictionary as formatted plain text. """
1039
        assert elems
1040
        text = ""
1041
        for elename, eledesc in elems.items():
1042
            if isinstance(eledesc, dict):
1043
                desc_txt = self.elements_as_text(eledesc, indent + 2)
1044
                desc_txt = ''.join(['\n', desc_txt])
1045
            elif isinstance(eledesc, str):
1046
                desc_txt = ''.join([eledesc, '\n'])
1047
            else:
1048
                assert False, "Only string or dictionary"
1049
            ele_txt = "\t{0}{1: <22} {2}".format(' ' * indent, elename,
1050
                                                 desc_txt)
0 ignored issues
show
introduced by
The variable desc_txt does not seem to be defined for all execution paths.
Loading history...
1051
            text = ''.join([text, ele_txt])
1052
        return text
1053
1054
    def handle_delete_scan_command(self, scan_et):
1055
        """ Handles <delete_scan> command.
1056
1057
        @return: Response string for <delete_scan> command.
1058
        """
1059
        scan_id = scan_et.attrib.get('scan_id')
1060
        if scan_id is None:
1061
            return simple_response_str('delete_scan', 404,
1062
                                       'No scan_id attribute')
1063
1064
        if not self.scan_exists(scan_id):
1065
            text = "Failed to find scan '{0}'".format(scan_id)
1066
            return simple_response_str('delete_scan', 404, text)
1067
        self.check_scan_process(scan_id)
1068
        if self.delete_scan(scan_id):
1069
            return simple_response_str('delete_scan', 200, 'OK')
1070
        raise OSPDError('Scan in progress', 'delete_scan')
1071
1072
    def delete_scan(self, scan_id):
1073
        """ Deletes scan_id scan from collection.
1074
1075
        @return: 1 if scan deleted, 0 otherwise.
1076
        """
1077
        try:
1078
            del self.scan_processes[scan_id]
1079
        except KeyError:
1080
            logger.debug('Scan process for {0} not found'.format(scan_id))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
1081
        return self.scan_collection.delete_scan(scan_id)
1082
1083
    def get_scan_results_xml(self, scan_id, pop_res):
1084
        """ Gets scan_id scan's results in XML format.
1085
1086
        @return: String of scan results in xml.
1087
        """
1088
        results = Element('results')
1089
        for result in self.scan_collection.results_iterator(scan_id, pop_res):
1090
            results.append(get_result_xml(result))
1091
1092
        logger.info('Returning %d results', len(results))
1093
        return results
1094
1095
    def get_xml_str(self, data):
1096
        """ Creates a string in XML Format using the provided data structure.
1097
1098
        @param: Dictionary of xml tags and their elements.
1099
1100
        @return: String of data in xml format.
1101
        """
1102
1103
        responses = []
1104
        for tag, value in data.items():
1105
            elem = Element(tag)
1106
            if isinstance(value, dict):
1107
                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 1104).

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...
1108
                    elem.append(value)
1109
            elif isinstance(value, list):
1110
                value = ', '.join([m for m in value])
1111
                elem.text = value
1112
            else:
1113
                elem.text = value
1114
            responses.append(elem)
1115
        return responses
1116
1117
    def get_scan_xml(self, scan_id, detailed=True, pop_res=False):
1118
        """ Gets scan in XML format.
1119
1120
        @return: String of scan in XML format.
1121
        """
1122
        if not scan_id:
1123
            return Element('scan')
1124
1125
        target = self.get_scan_target(scan_id)
1126
        progress = self.get_scan_progress(scan_id)
1127
        start_time = self.get_scan_start_time(scan_id)
1128
        end_time = self.get_scan_end_time(scan_id)
1129
        response = Element('scan')
1130
        for name, value in [('id', scan_id),
1131
                            ('target', target),
1132
                            ('progress', progress),
1133
                            ('start_time', start_time),
1134
                            ('end_time', end_time)]:
1135
            response.set(name, str(value))
1136
        if detailed:
1137
            response.append(self.get_scan_results_xml(scan_id, pop_res))
1138
        return response
1139
1140
    @staticmethod
1141
    def get_custom_vt_as_xml_str(vt_id, custom):
0 ignored issues
show
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
Unused Code introduced by
The argument custom seems to be unused.
Loading history...
1142
        """ Create a string representation of the XML object from the
1143
        custom data object.
1144
        This needs to be implemented by each ospd wrapper, in case
1145
        custom elements for VTs are used.
1146
1147
        The custom XML object which is returned will be embedded
1148
        into a <custom></custom> element.
1149
1150
        @return: XML object as string for custom data.
1151
        """
1152
        return ''
1153
1154
    @staticmethod
1155
    def get_params_vt_as_xml_str(vt_id, vt_params):
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_params seems to be unused.
Loading history...
1156
        """ Create a string representation of the XML object from the
1157
        vt_params data object.
1158
        This needs to be implemented by each ospd wrapper, in case
1159
        vt_params elements for VTs are used.
1160
1161
        The vt_params XML object which is returned will be embedded
1162
        into a <vt_params></vt_params> element.
1163
1164
        @return: XML object as string for vt parameters data.
1165
        """
1166
        return ''
1167
1168
    @staticmethod
1169
    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...
1170
        """ Create a string representation of the XML object from the
1171
        vt_refs data object.
1172
        This needs to be implemented by each ospd wrapper, in case
1173
        vt_refs elements for VTs are used.
1174
1175
        The vt_refs XML object which is returned will be embedded
1176
        into a <vt_refs></vt_refs> element.
1177
1178
        @return: XML object as string for vt references data.
1179
        """
1180
        return ''
1181
1182
    @staticmethod
1183
    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...
1184
        """ Create a string representation of the XML object from the
1185
        vt_dependencies data object.
1186
        This needs to be implemented by each ospd wrapper, in case
1187
        vt_dependencies elements for VTs are used.
1188
1189
        The vt_dependencies XML object which is returned will be embedded
1190
        into a <dependencies></dependencies> element.
1191
1192
        @return: XML object as string for vt dependencies data.
1193
        """
1194
        return ''
1195
1196
    @staticmethod
1197
    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...
1198
        """ Create a string representation of the XML object from the
1199
        vt_creation_time data object.
1200
        This needs to be implemented by each ospd wrapper, in case
1201
        vt_creation_time elements for VTs are used.
1202
1203
        The vt_creation_time XML object which is returned will be embedded
1204
        into a <vt_creation_time></vt_creation_time> element.
1205
1206
        @return: XML object as string for vt creation time data.
1207
        """
1208
        return ''
1209
1210
    @staticmethod
1211
    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...
1212
        """ Create a string representation of the XML object from the
1213
        vt_modification_time data object.
1214
        This needs to be implemented by each ospd wrapper, in case
1215
        vt_modification_time elements for VTs are used.
1216
1217
        The vt_modification_time XML object which is returned will be embedded
1218
        into a <vt_modification_time></vt_modification_time> element.
1219
1220
        @return: XML object as string for vt references data.
1221
        """
1222
        return ''
1223
1224
    @staticmethod
1225
    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...
1226
        """ Create a string representation of the XML object from the
1227
        summary data object.
1228
        This needs to be implemented by each ospd wrapper, in case
1229
        summary elements for VTs are used.
1230
1231
        The summary XML object which is returned will be embedded
1232
        into a <summary></summary> element.
1233
1234
        @return: XML object as string for summary data.
1235
        """
1236
        return ''
1237
1238
    @staticmethod
1239
    def get_impact_vt_as_xml_str(vt_id, impact):
0 ignored issues
show
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
Unused Code introduced by
The argument impact seems to be unused.
Loading history...
1240
        """ Create a string representation of the XML object from the
1241
        impact data object.
1242
        This needs to be implemented by each ospd wrapper, in case
1243
        impact elements for VTs are used.
1244
1245
        The impact XML object which is returned will be embedded
1246
        into a <impact></impact> element.
1247
1248
        @return: XML object as string for impact data.
1249
        """
1250
        return ''
1251
1252
    @staticmethod
1253
    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...
1254
        """ Create a string representation of the XML object from the
1255
        affected data object.
1256
        This needs to be implemented by each ospd wrapper, in case
1257
        affected elements for VTs are used.
1258
1259
        The affected XML object which is returned will be embedded
1260
        into a <affected></affected> element.
1261
1262
        @return: XML object as string for affected data.
1263
        """
1264
        return ''
1265
1266
    @staticmethod
1267
    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...
1268
        """ Create a string representation of the XML object from the
1269
        insight data object.
1270
        This needs to be implemented by each ospd wrapper, in case
1271
        insight elements for VTs are used.
1272
1273
        The insight XML object which is returned will be embedded
1274
        into a <insight></insight> element.
1275
1276
        @return: XML object as string for insight data.
1277
        """
1278
        return ''
1279
1280
    @staticmethod
1281
    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 solution_type seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1282
        """ Create a string representation of the XML object from the
1283
        solution data object.
1284
        This needs to be implemented by each ospd wrapper, in case
1285
        solution elements for VTs are used.
1286
1287
        The solution XML object which is returned will be embedded
1288
        into a <solution></solution> element.
1289
1290
        @return: XML object as string for solution data.
1291
        """
1292
        return ''
1293
1294
    @staticmethod
1295
    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 detection 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...
1296
        """ Create a string representation of the XML object from the
1297
        detection data object.
1298
        This needs to be implemented by each ospd wrapper, in case
1299
        detection elements for VTs are used.
1300
1301
        The detection XML object which is returned is an element with
1302
        tag <detection></detection> element
1303
1304
        @return: XML object as string for detection data.
1305
        """
1306
        return ''
1307
1308
    @staticmethod
1309
    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...
1310
        """ Create a string representation of the XML object from the
1311
        severities data object.
1312
        This needs to be implemented by each ospd wrapper, in case
1313
        severities elements for VTs are used.
1314
1315
        The severities XML objects which are returned will be embedded
1316
        into a <severities></severities> element.
1317
1318
        @return: XML object as string for severities data.
1319
        """
1320
        return ''
1321
1322
    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...
1323
        """ Gets a single vulnerability test information in XML format.
1324
1325
        @return: String of single vulnerability test information in XML format.
1326
        """
1327
        if not vt_id:
1328
            return Element('vt')
1329
1330
        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...
1331
1332
        name = vt.get('name')
1333
        vt_xml = Element('vt')
1334
        vt_xml.set('id', vt_id)
1335
1336
        for name, value in [('name', name)]:
1337
            elem = SubElement(vt_xml, name)
1338
            elem.text = str(value)
1339
1340
        if vt.get('vt_params'):
1341
            params_xml_str = (
1342
                '<vt_params>%s</vt_params>' % self.get_params_vt_as_xml_str(
1343
                    vt_id, vt.get('vt_params')))
1344
            vt_xml.append(secET.fromstring(params_xml_str))
1345
1346
        if vt.get('vt_refs'):
1347
            refs_xml_str = (
1348
                '<vt_refs>%s</vt_refs>' % self.get_refs_vt_as_xml_str(
1349
                    vt_id, vt.get('vt_refs')))
1350
            vt_xml.append(secET.fromstring(refs_xml_str))
1351
1352
        if vt.get('vt_dependencies'):
1353
            dependencies = self.get_dependencies_vt_as_xml_str(
1354
                vt_id, vt.get('vt_dependencies'))
1355
            deps_xml_str = (
1356
                '<dependencies>%s</dependencies>' % dependencies)
1357
            vt_xml.append(secET.fromstring(deps_xml_str))
1358
1359
        if vt.get('creation_time'):
1360
            vt_ctime = self.get_creation_time_vt_as_xml_str(
1361
                vt_id, vt.get('creation_time'))
1362
            creation_time_xml_str = (
1363
                '<creation_time>%s</creation_time>' % vt_ctime)
1364
            vt_xml.append(secET.fromstring(creation_time_xml_str))
1365
1366
        if vt.get('modification_time'):
1367
            vt_mtime = self.get_modification_time_vt_as_xml_str(
1368
                vt_id, vt.get('modification_time'))
1369
            modification_time_xml_str = (
1370
                '<modification_time>%s</modification_time>' % vt_mtime)
1371
            vt_xml.append(secET.fromstring(modification_time_xml_str))
1372
1373
        if vt.get('summary'):
1374
            summary_xml_str = self.get_summary_vt_as_xml_str(
1375
                vt_id, vt.get('summary'))
1376
            vt_xml.append(secET.fromstring(summary_xml_str))
1377
1378
        if vt.get('impact'):
1379
            impact_xml_str = self.get_impact_vt_as_xml_str(
1380
                vt_id, vt.get('impact'))
1381
            vt_xml.append(secET.fromstring(impact_xml_str))
1382
1383
        if vt.get('affected'):
1384
            affected_xml_str = self.get_affected_vt_as_xml_str(
1385
                vt_id, vt.get('affected'))
1386
            vt_xml.append(secET.fromstring(affected_xml_str))
1387
1388
        if vt.get('insight'):
1389
            insight_xml_str = self.get_insight_vt_as_xml_str(
1390
                vt_id, vt.get('insight'))
1391
            vt_xml.append(secET.fromstring(insight_xml_str))
1392
1393
        if vt.get('solution'):
1394
            solution_xml_str = self.get_solution_vt_as_xml_str(
1395
                vt_id, vt.get('solution'), vt.get('solution_type'))
1396
            vt_xml.append(secET.fromstring(solution_xml_str))
1397
1398
        if vt.get('detection') or vt.get('qod_type') or vt.get('qod'):
1399
            detection_xml_str = self.get_detection_vt_as_xml_str(
1400
                vt_id, vt.get('detection'), vt.get('qod_type'), vt.get('qod'))
1401
            vt_xml.append(secET.fromstring(detection_xml_str))
1402
1403
        if vt.get('severities'):
1404
            severities_xml_str = (
1405
                '<severities>%s</severities>' % self.get_severities_vt_as_xml_str(
1406
                    vt_id, vt.get('severities')))
1407
            vt_xml.append(secET.fromstring(severities_xml_str))
1408
1409
        if vt.get('custom'):
1410
            custom_xml_str = (
1411
                '<custom>%s</custom>' % self.get_custom_vt_as_xml_str(
1412
                    vt_id, vt.get('custom')))
1413
            vt_xml.append(secET.fromstring(custom_xml_str))
1414
1415
        return vt_xml
1416
1417
    def get_vts_xml(self, vt_id=''):
1418
        """ Gets collection of vulnerability test information in XML format.
1419
        If vt_id is specified, the collection will contain only this vt, of found.
1420
        If no vt_id is specified, the collection will contain all vts.
1421
1422
        @return: String of collection of vulnerability test information in XML format.
1423
        """
1424
1425
        vts_xml = Element('vts')
1426
1427
        if vt_id != '':
1428
            vts_xml.append(self.get_vt_xml(vt_id))
1429
        else:
1430
            for vt_id in self.vts:
0 ignored issues
show
unused-code introduced by
Redefining argument with the local name 'vt_id'
Loading history...
1431
                vts_xml.append(self.get_vt_xml(vt_id))
1432
1433
        return vts_xml
1434
1435
    def handle_get_scanner_details(self):
1436
        """ Handles <get_scanner_details> command.
1437
1438
        @return: Response string for <get_scanner_details> command.
1439
        """
1440
        desc_xml = Element('description')
1441
        desc_xml.text = self.get_scanner_description()
1442
        details = [
1443
            desc_xml,
1444
            self.get_scanner_params_xml()
1445
        ]
1446
        return simple_response_str('get_scanner_details', 200, 'OK', details)
1447
1448
    def handle_get_version_command(self):
1449
        """ Handles <get_version> command.
1450
1451
        @return: Response string for <get_version> command.
1452
        """
1453
        protocol = Element('protocol')
1454
        for name, value in [('name', 'OSP'), ('version', self.get_protocol_version())]:
1455
            elem = SubElement(protocol, name)
1456
            elem.text = value
1457
1458
        daemon = Element('daemon')
1459
        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...
1460
            elem = SubElement(daemon, name)
1461
            elem.text = value
1462
1463
        scanner = Element('scanner')
1464
        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...
1465
            elem = SubElement(scanner, name)
1466
            elem.text = value
1467
1468
        return simple_response_str('get_version', 200, 'OK', [protocol, daemon, scanner])
1469
1470
    def handle_command(self, command):
0 ignored issues
show
best-practice introduced by
Too many return statements (8/6)
Loading history...
1471
        """ Handles an osp command in a string.
1472
1473
        @return: OSP Response to command.
1474
        """
1475
        try:
1476
            tree = secET.fromstring(command)
1477
        except secET.ParseError:
1478
            logger.debug("Erroneous client input: {0}".format(command))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
1479
            raise OSPDError('Invalid data')
1480
1481
        if not self.command_exists(tree.tag) and tree.tag != "authenticate":
1482
            raise OSPDError('Bogus command name')
1483
1484
        if tree.tag == "get_version":
1485
            return self.handle_get_version_command()
1486
        elif tree.tag == "start_scan":
1487
            return self.handle_start_scan_command(tree)
1488
        elif tree.tag == "stop_scan":
1489
            return self.handle_stop_scan_command(tree)
1490
        elif tree.tag == "get_scans":
1491
            return self.handle_get_scans_command(tree)
1492
        elif tree.tag == "get_vts":
1493
            return self.handle_get_vts_command(tree)
1494
        elif tree.tag == "delete_scan":
1495
            return self.handle_delete_scan_command(tree)
1496
        elif tree.tag == "help":
1497
            return self.handle_help_command(tree)
1498
        elif tree.tag == "get_scanner_details":
1499
            return self.handle_get_scanner_details()
1500
        else:
1501
            assert False, "Unhandled command: {0}".format(tree.tag)
1502
1503
    def check(self):
1504
        """ Asserts to False. Should be implemented by subclass. """
1505
        raise NotImplementedError
1506
1507
    def run(self, address, port, unix_path):
1508
        """ Starts the Daemon, handling commands until interrupted.
1509
1510
        @return False if error. Runs indefinitely otherwise.
1511
        """
1512
        assert address or unix_path
1513
        if unix_path:
1514
            sock = bind_unix_socket(unix_path)
1515
        else:
1516
            sock = bind_socket(address, port)
1517
        if sock is None:
1518
            return False
1519
1520
        try:
1521
            while True:
1522
                if unix_path:
1523
                    client_stream, _ = sock.accept()
1524
                    logger.debug("New connection from {0}".format(unix_path))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
1525
                    self.handle_client_stream(client_stream, True)
1526
                else:
1527
                    client_stream = self.new_client_stream(sock)
1528
                    if client_stream is None:
1529
                        continue
1530
                    self.handle_client_stream(client_stream, False)
1531
                close_client_stream(client_stream, unix_path)
1532
        except KeyboardInterrupt:
1533
            logger.info("Received Ctrl-C shutting-down ...")
1534
        finally:
1535
            sock.shutdown(socket.SHUT_RDWR)
1536
            sock.close()
1537
1538
    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...
1539
        """ Creates a new scan.
1540
1541
        @target: Target to scan.
1542
        @options: Miscellaneous scan options.
1543
1544
        @return: New scan's ID.
1545
        """
1546
        return self.scan_collection.create_scan(scan_id, targets, target_str, options, vts)
1547
1548
    def get_scan_options(self, scan_id):
1549
        """ Gives a scan's list of options. """
1550
        return self.scan_collection.get_options(scan_id)
1551
1552
    def set_scan_option(self, scan_id, name, value):
1553
        """ Sets a scan's option to a provided value. """
1554
        return self.scan_collection.set_option(scan_id, name, value)
1555
1556
    def check_scan_process(self, scan_id):
1557
        """ Check the scan's process, and terminate the scan if not alive. """
1558
        scan_process = self.scan_processes[scan_id]
1559
        progress = self.get_scan_progress(scan_id)
1560
        if progress < 100 and not scan_process.is_alive():
1561
            self.set_scan_progress(scan_id, 100)
1562
            self.add_scan_error(scan_id, name="", host="",
1563
                                value="Scan process failure.")
1564
            logger.info("{0}: Scan terminated.".format(scan_id))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
1565
        elif progress == 100:
1566
            scan_process.join()
1567
1568
    def get_scan_progress(self, scan_id):
1569
        """ Gives a scan's current progress value. """
1570
        return self.scan_collection.get_progress(scan_id)
1571
1572
    def get_scan_target_progress(self, scan_id):
1573
        """ Gives a list with scan's current progress value of each target. """
1574
        return self.scan_collection.get_target_progress(scan_id)
1575
1576
    def get_scan_target(self, scan_id):
1577
        """ Gives a scan's target. """
1578
        return self.scan_collection.get_target(scan_id)
1579
1580
    def get_scan_ports(self, scan_id, target=''):
1581
        """ Gives a scan's ports list. """
1582
        return self.scan_collection.get_ports(scan_id, target)
1583
1584
    def get_scan_credentials(self, scan_id, target=''):
1585
        """ Gives a scan's credential list. If a target is passed gives
1586
        the credential list for the given target. """
1587
        return self.scan_collection.get_credentials(scan_id, target)
1588
1589
    def get_scan_vts(self, scan_id):
1590
        """ Gives a scan's vts list. """
1591
        return self.scan_collection.get_vts(scan_id)
1592
1593
    def get_scan_start_time(self, scan_id):
1594
        """ Gives a scan's start time. """
1595
        return self.scan_collection.get_start_time(scan_id)
1596
1597
    def get_scan_end_time(self, scan_id):
1598
        """ Gives a scan's end time. """
1599
        return self.scan_collection.get_end_time(scan_id)
1600
1601
    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...
1602
                     test_id='', qod=''):
1603
        """ Adds a log result to scan_id scan. """
1604
        self.scan_collection.add_result(scan_id, ResultType.LOG, host, name,
1605
                                        value, port, test_id, 0.0, qod)
1606
1607
    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...
1608
        """ Adds an error result to scan_id scan. """
1609
        self.scan_collection.add_result(scan_id, ResultType.ERROR, host, name,
1610
                                        value, port)
1611
1612
    def add_scan_host_detail(self, scan_id, host='', name='', value=''):
1613
        """ Adds a host detail result to scan_id scan. """
1614
        self.scan_collection.add_result(scan_id, ResultType.HOST_DETAIL, host,
1615
                                        name, value)
1616
1617
    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...
1618
                       test_id='', severity='', qod=''):
1619
        """ Adds an alarm result to scan_id scan. """
1620
        self.scan_collection.add_result(scan_id, ResultType.ALARM, host, name,
1621
                                        value, port, test_id, severity, qod)
1622