Completed
Push — master ( a291c4...25336d )
by Juan José
14s queued 11s
created

ospd.ospd.OSPDaemon.get_scan_options()   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 2
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 (1440/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 (76/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 (8/5)
Loading history...
298
               custom=None, vt_creation_time=None, vt_modification_time=None):
299
        """ Add a vulnerability test information.
300
301
        Returns: The new number of stored VTs.
302
        -1 in case the VT ID was already present and thus the
303
        new VT was not considered.
304
        -2 in case the vt_id was invalid.
305
        """
306
307
        if not vt_id:
308
            return -2  # no valid vt_id
309
310
        if self.vt_id_pattern.fullmatch(vt_id) is None:
311
            return -2  # no valid vt_id
312
313
        if vt_id in self.vts:
314
            return -1  # The VT was already in the list.
315
316
        if name is None:
317
            name = ''
318
319
        self.vts[vt_id] = {'name': name}
320
        if custom is not None:
321
            self.vts[vt_id]["custom"] = custom
322
        if vt_params is not None:
323
            self.vts[vt_id]["vt_params"] = vt_params
324
        if vt_refs is not None:
325
            self.vts[vt_id]["vt_refs"] = vt_refs
326
        if vt_creation_time is not None:
327
            self.vts[vt_id]["creation_time"] = vt_creation_time
328
        if vt_modification_time is not None:
329
            self.vts[vt_id]["modification_time"] = vt_modification_time
330
        return len(self.vts)
331
332
    def command_exists(self, name):
333
        """ Checks if a commands exists. """
334
        return name in self.commands.keys()
335
336
    def get_scanner_name(self):
337
        """ Gives the wrapped scanner's name. """
338
        return self.scanner_info['name']
339
340
    def get_scanner_version(self):
341
        """ Gives the wrapped scanner's version. """
342
        return self.scanner_info['version']
343
344
    def get_scanner_description(self):
345
        """ Gives the wrapped scanner's description. """
346
        return self.scanner_info['description']
347
348
    def get_server_version(self):
349
        """ Gives the specific OSP server's version. """
350
        assert self.server_version
351
        return self.server_version
352
353
    def get_protocol_version(self):
354
        """ Gives the OSP's version. """
355
        return self.protocol_version
356
357
    def _preprocess_scan_params(self, xml_params):
358
        """ Processes the scan parameters. """
359
        params = {}
360
        for param in xml_params:
361
            params[param.tag] = param.text or ''
362
        # Set default values.
363
        for key in self.scanner_params:
364
            if key not in params:
365
                params[key] = self.get_scanner_param_default(key)
366
                if self.get_scanner_param_type(key) == 'selection':
367
                    params[key] = params[key].split('|')[0]
368
        # Validate values.
369
        for key in params:
370
            param_type = self.get_scanner_param_type(key)
371
            if not param_type:
372
                continue
373
            if param_type in ['integer', 'boolean']:
374
                try:
375
                    params[key] = int(params[key])
376
                except ValueError:
377
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
378
            if param_type == 'boolean':
379
                if params[key] not in [0, 1]:
380
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
381
            elif param_type == 'selection':
382
                selection = self.get_scanner_param_default(key).split('|')
383
                if params[key] not in selection:
384
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
385
            if self.get_scanner_param_mandatory(key) and params[key] == '':
386
                    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...
387
                                    'start_scan')
388
        return params
389
390
    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...
391
        """ This method is to be overridden by the child classes if necessary
392
        """
393
        return params
394
395
    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...
396
        """ Receive an XML object with the Vulnerability Tests an their
397
        parameters to be use in a scan and return a dictionary.
398
399
        @param: XML element with vt subelements. Each vt has an
400
                id attribute. Optional parameters can be included
401
                as vt child.
402
                Example form:
403
                <vt_selection>
404
                  <vt_single id='vt1' />
405
                  <vt_single id='vt2'>
406
                    <vt_value id='param1'>value</vt_value>
407
                  </vt_single>
408
                  <vt_group filter='family=debian'/>
409
                  <vt_group filter='family=general'/>
410
                </vt_selection>
411
412
        @return: Dictionary containing the vts attribute and subelements,
413
                 like the VT's id and VT's parameters.
414
                 Example form:
415
                 {'vt1': {},
416
                  'vt2': {'value_id': 'value'},
417
                  'vt_groups': ['family=debian', 'family=general']}
418
        """
419
        vt_selection = {}
420
        filters = list()
421
        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...
422
            if vt.tag == 'vt_single':
423
                vt_id = vt.attrib.get('id')
424
                vt_selection[vt_id] = {}
425
                for vt_value in vt:
426
                    if not vt_value.attrib.get('id'):
427
                        raise OSPDError('Invalid VT preference. No attribute id',
428
                                        'start_scan')
429
                    vt_value_id = vt_value.attrib.get('id')
430
                    vt_value_value = vt_value.text if vt_value.text else ''
431
                    vt_selection[vt_id][vt_value_id] = vt_value_value
432
            if vt.tag == 'vt_group':
433
                vts_filter = vt.attrib.get('filter', None)
434
                if vts_filter is None:
435
                    raise OSPDError('Invalid VT group. No filter given.',
436
                                    'start_scan')
437
                filters.append(vts_filter)
438
        vt_selection['vt_groups'] = filters
439
        return vt_selection
440
441
    @staticmethod
442
    def process_credentials_elements(cred_tree):
443
        """ Receive an XML object with the credentials to run
444
        a scan against a given target.
445
446
        @param:
447
        <credentials>
448
          <credential type="up" service="ssh" port="22">
449
            <username>scanuser</username>
450
            <password>mypass</password>
451
          </credential>
452
          <credential type="up" service="smb">
453
            <username>smbuser</username>
454
            <password>mypass</password>
455
          </credential>
456
        </credentials>
457
458
        @return: Dictionary containing the credentials for a given target.
459
                 Example form:
460
                 {'ssh': {'type': type,
461
                          'port': port,
462
                          'username': username,
463
                          'password': pass,
464
                        },
465
                  'smb': {'type': type,
466
                          'username': username,
467
                          'password': pass,
468
                         },
469
                   }
470
        """
471
        credentials = {}
472
        for credential in cred_tree:
473
            service = credential.attrib.get('service')
474
            credentials[service] = {}
475
            credentials[service]['type'] = credential.attrib.get('type')
476
            if service == 'ssh':
477
                credentials[service]['port'] = credential.attrib.get('port')
478
            for param in credential:
479
                credentials[service][param.tag] = param.text
480
481
        return credentials
482
483
    @classmethod
484
    def process_targets_element(cls, scanner_target):
485
        """ Receive an XML object with the target, ports and credentials to run
486
        a scan against.
487
488
        @param: XML element with target subelements. Each target has <hosts>
489
        and <ports> subelements. Hosts can be a single host, a host range,
490
        a comma-separated host list or a network address.
491
        <ports> and  <credentials> are optional. Therefore each ospd-scanner
492
        should check for a valid ones if needed.
493
494
                Example form:
495
                <targets>
496
                  <target>
497
                    <hosts>localhosts</hosts>
498
                    <ports>80,443</ports>
499
                  </target>
500
                  <target>
501
                    <hosts>192.168.0.0/24</hosts>
502
                    <ports>22</ports>
503
                    <credentials>
504
                      <credential type="up" service="ssh" port="22">
505
                        <username>scanuser</username>
506
                        <password>mypass</password>
507
                      </credential>
508
                      <credential type="up" service="smb">
509
                        <username>smbuser</username>
510
                        <password>mypass</password>
511
                      </credential>
512
                    </credentials>
513
                  </target>
514
                </targets>
515
516
        @return: A list of (hosts, port) tuples.
517
                 Example form:
518
                 [['localhost', '80,43'],
519
                  ['192.168.0.0/24', '22', {'smb': {'type': type,
520
                                                    'port': port,
521
                                                    'username': username,
522
                                                    'password': pass,
523
                                                   }}]]
524
        """
525
526
        target_list = []
527
        for target in scanner_target:
528
            ports= ''
0 ignored issues
show
Coding Style introduced by
Exactly one space required before assignment
Loading history...
529
            credentials = {}
530
            for child in target:
531
                if child.tag == 'hosts':
532
                    hosts = child.text
533
                if child.tag == 'ports':
534
                    ports = child.text
535
                if child.tag == 'credentials':
536
                    credentials = cls.process_credentials_elements(child)
537
            if hosts:
0 ignored issues
show
introduced by
The variable hosts does not seem to be defined for all execution paths.
Loading history...
538
                target_list.append([hosts, ports, credentials])
539
            else:
540
                raise OSPDError('No target to scan', 'start_scan')
541
542
        return target_list
543
544
    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...
545
        """ Handles <start_scan> command.
546
547
        @return: Response string for <start_scan> command.
548
        """
549
550
        target_str = scan_et.attrib.get('target')
551
        ports_str = scan_et.attrib.get('ports')
552
        # For backward compatibility, if target and ports attributes are set,
553
        # <targets> element is ignored.
554
        if target_str is None or ports_str is None:
555
            target_list = scan_et.find('targets')
556
            if target_list is None or not target_list:
557
                raise OSPDError('No targets or ports', 'start_scan')
558
            else:
559
                scan_targets = self.process_targets_element(target_list)
560
        else:
561
            scan_targets = []
562
            for single_target in target_str_to_list(target_str):
563
                scan_targets.append([single_target, ports_str, ''])
564
565
        scan_id = scan_et.attrib.get('scan_id')
566
        if scan_id is not None and scan_id != '' and not valid_uuid(scan_id):
567
            raise OSPDError('Invalid scan_id UUID', 'start_scan')
568
569
        try:
570
            parallel = int(scan_et.attrib.get('parallel', '1'))
571
            if parallel < 1 or parallel > 20:
572
                parallel = 1
573
        except ValueError:
574
            raise OSPDError('Invalid value for parallel scans. '
575
                            'It must be a number', 'start_scan')
576
577
        scanner_params = scan_et.find('scanner_params')
578
        if scanner_params is None:
579
            raise OSPDError('No scanner_params element', 'start_scan')
580
581
        params = self._preprocess_scan_params(scanner_params)
582
583
        # VTS is an optional element. If present should not be empty.
584
        vt_selection = {}
585
        scanner_vts = scan_et.find('vt_selection')
586
        if scanner_vts is not None:
587
            if not scanner_vts:
588
                raise OSPDError('VTs list is empty', 'start_scan')
589
            else:
590
                vt_selection = self.process_vts_params(scanner_vts)
591
592
        # Dry run case.
593
        if 'dry_run' in params and int(params['dry_run']):
594
            scan_func = self.dry_run_scan
595
            scan_params = None
596
        else:
597
            scan_func = self.start_scan
598
            scan_params = self.process_scan_params(params)
599
600
        scan_id = self.create_scan(scan_id, scan_targets,
601
                                   target_str, scan_params,
602
                                   vt_selection)
603
        scan_process = multiprocessing.Process(target=scan_func,
604
                                               args=(scan_id,
605
                                                     scan_targets,
606
                                                     parallel))
607
        self.scan_processes[scan_id] = scan_process
608
        scan_process.start()
609
        id_ = Element('id')
610
        id_.text = scan_id
611
        return simple_response_str('start_scan', 200, 'OK', id_)
612
613
    def handle_stop_scan_command(self, scan_et):
614
        """ Handles <stop_scan> command.
615
616
        @return: Response string for <stop_scan> command.
617
        """
618
619
        scan_id = scan_et.attrib.get('scan_id')
620
        if scan_id is None or scan_id == '':
621
            raise OSPDError('No scan_id attribute', 'stop_scan')
622
        scan_process = self.scan_processes.get(scan_id)
623
        if not scan_process:
624
            raise OSPDError('Scan not found {0}.'.format(scan_id), 'stop_scan')
625
        if not scan_process.is_alive():
626
            raise OSPDError('Scan already stopped or finished.', 'stop_scan')
627
628
        self.set_scan_status(scan_id, "stopped")
629
        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...
630
        self.stop_scan(scan_id)
631
        scan_process.terminate()
632
        os.killpg(os.getpgid(scan_process.ident), 15)
633
        scan_process.join()
634
        self.set_scan_progress(scan_id, 100)
635
        self.add_scan_log(scan_id, name='', host='', value='Scan stopped.')
636
        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...
637
        return simple_response_str('stop_scan', 200, 'OK')
638
639
    @staticmethod
640
    def stop_scan(scan_id):
641
        """ Should be implemented by subclass in case of a clean up before
642
        terminating is needed. """
643
644
    def exec_scan(self, scan_id, target):
645
        """ Asserts to False. Should be implemented by subclass. """
646
        raise NotImplementedError
647
648
    def finish_scan(self, scan_id):
649
        """ Sets a scan as finished. """
650
        self.set_scan_progress(scan_id, 100)
651
        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...
652
653
    def get_daemon_name(self):
654
        """ Gives osp daemon's name. """
655
        return self.daemon_info['name']
656
657
    def get_daemon_version(self):
658
        """ Gives osp daemon's version. """
659
        return self.daemon_info['version']
660
661
    def get_scanner_param_type(self, param):
662
        """ Returns type of a scanner parameter. """
663
        assert isinstance(param, str)
664
        entry = self.scanner_params.get(param)
665
        if not entry:
666
            return None
667
        return entry.get('type')
668
669
    def get_scanner_param_mandatory(self, param):
670
        """ Returns if a scanner parameter is mandatory. """
671
        assert isinstance(param, str)
672
        entry = self.scanner_params.get(param)
673
        if not entry:
674
            return False
675
        return entry.get('mandatory')
676
677
    def get_scanner_param_default(self, param):
678
        """ Returns default value of a scanner parameter. """
679
        assert isinstance(param, str)
680
        entry = self.scanner_params.get(param)
681
        if not entry:
682
            return None
683
        return entry.get('default')
684
685
    def get_scanner_params_xml(self):
686
        """ Returns the OSP Daemon's scanner params in xml format. """
687
        scanner_params = Element('scanner_params')
688
        for param_id, param in self.scanner_params.items():
689
            param_xml = SubElement(scanner_params, 'scanner_param')
690
            for name, value in [('id', param_id),
691
                                ('type', param['type'])]:
692
                param_xml.set(name, value)
693
            for name, value in [('name', param['name']),
694
                                ('description', param['description']),
695
                                ('default', param['default']),
696
                                ('mandatory', param['mandatory'])]:
697
                elem = SubElement(param_xml, name)
698
                elem.text = str(value)
699
        return scanner_params
700
701
    def new_client_stream(self, sock):
702
        """ Returns a new ssl client stream from bind_socket. """
703
704
        assert sock
705
        newsocket, fromaddr = sock.accept()
706
        logger.debug("New connection from"
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
707
                     " {0}:{1}".format(fromaddr[0], fromaddr[1]))
708
        # NB: Despite the name, ssl.PROTOCOL_SSLv23 selects the highest
709
        # protocol version that both the client and server support. In modern
710
        # Python versions (>= 3.4) it supports TLS >= 1.0 with SSLv2 and SSLv3
711
        # being disabled. For Python >=3.5, PROTOCOL_SSLv23 is an alias for
712
        # PROTOCOL_TLS which should be used once compatibility with Python 3.4
713
        # is no longer desired.
714
        try:
715
            ssl_socket = ssl.wrap_socket(newsocket, cert_reqs=ssl.CERT_REQUIRED,
716
                                         server_side=True,
717
                                         certfile=self.certs['cert_file'],
718
                                         keyfile=self.certs['key_file'],
719
                                         ca_certs=self.certs['ca_file'],
720
                                         ssl_version=ssl.PROTOCOL_SSLv23)
721
        except (ssl.SSLError, socket.error) as message:
722
            logger.error(message)
723
            return None
724
        return ssl_socket
725
726
    @staticmethod
727
    def write_to_stream(stream, response, block_len=1024):
728
        """
729
        Send the response in blocks of the given len using the
730
        passed method dependending on the socket type.
731
        """
732
        try:
733
            i_start = 0
734
            i_end = block_len
735
            while True:
736
                if i_end > len(response):
737
                    stream(response[i_start:])
738
                    break
739
                stream(response[i_start:i_end])
740
                i_start = i_end
741
                i_end += block_len
742
        except (socket.timeout, socket.error) as exception:
743
            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...
744
745
    def handle_client_stream(self, stream, is_unix=False):
746
        """ Handles stream of data received from client. """
747
748
        assert stream
749
        data = []
750
        stream.settimeout(2)
751
        while True:
752
            try:
753
                if is_unix:
754
                    data.append(stream.recv(1024))
755
                else:
756
                    data.append(stream.read(1024))
757
                if len(data) == 0:
0 ignored issues
show
Unused Code introduced by
Do not use len(SEQUENCE) as condition value
Loading history...
758
                    logger.warning(
759
                        "Empty client stream (Connection unexpectedly closed)")
760
                    return
761
            except (AttributeError, ValueError) as message:
762
                logger.error(message)
763
                return
764
            except (ssl.SSLError) as exception:
765
                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...
766
                break
767
            except (socket.timeout) as exception:
768
                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...
769
                break
770
        data = b''.join(data)
771
        if len(data) <= 0:
772
            logger.debug("Empty client stream")
773
            return
774
        try:
775
            response = self.handle_command(data)
776
        except OSPDError as exception:
777
            response = exception.as_xml()
778
            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...
779
        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...
780
            logger.exception('While handling client command:')
781
            exception = OSPDError('Fatal error', 'error')
782
            response = exception.as_xml()
783
        if is_unix:
784
            send_method = stream.send
785
        else:
786
            send_method = stream.write
787
        self.write_to_stream(send_method, response)
788
789
    def parallel_scan(self,scan_id, target):
0 ignored issues
show
Coding Style introduced by
Exactly one space required after comma
Loading history...
790
        """ Starts the scan with scan_id. """
791
        try:
792
            ret = self.exec_scan(scan_id, target)
793
            if ret == 0:
794
                self.add_scan_host_detail(scan_id, name='host_status',
795
                                          host=target, value='0')
796
            elif ret == 1:
797
                self.add_scan_host_detail(scan_id, name='host_status',
798
                                          host=target, value='1')
799
            elif ret == 2:
800
                self.add_scan_host_detail(scan_id, name='host_status',
801
                                          host=target, value='2')
802
            else:
803
                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...
804
        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...
805
            self.add_scan_error(scan_id, name='', host=target,
806
                                value='Host process failure (%s).' % e)
807
            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...
808
        else:
809
            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...
810
811
    def check_pending_target(self, scan_id, multiscan_proc):
812
        """ Check if a scan process is still alive. In case the process
813
        finished, removes the process from the multiscan_process list.
814
        In case of the progress is not set from the wrapper for a single
815
        target, it will be automatically set if the target scan finished.
816
817
        @input scan_id        Scan_id of the whole scan.
818
        @input multiscan_proc A list with the scan process which
819
                              may still be alive.
820
821
        @return Actualized list with current runnging scan processes."""
822
823
        for running_target in multiscan_proc:
824
            if not running_target[0].is_alive():
825
                target_prog = self.get_scan_target_progress(scan_id)
826
                if target_prog[running_target[1]] < 100:
827
                    self.set_scan_target_progress(scan_id,
828
                                                  running_target[1],
829
                                                  100)
830
                multiscan_proc.remove(running_target)
831
        return multiscan_proc
832
833
    def calculate_progress(self, scan_id):
834
        """ Calculate the total scan progress from the
835
        partial target progress. """
836
837
        target_progress = self.get_scan_target_progress(scan_id)
838
        return sum(target_progress.values())/len(target_progress)
839
840
    def start_scan(self, scan_id, targets, parallel=1):
841
        """ Handle N parallel scans if 'parallel' is greater than 1. """
842
843
        os.setsid()
844
        multiscan_proc = []
845
        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...
846
        target_list = targets
847
        if target_list is None or not target_list:
848
            raise OSPDError('Erroneous targets list', 'start_scan')
849
850
        for index, target in enumerate(target_list):
0 ignored issues
show
Unused Code introduced by
The variable index seems to be unused.
Loading history...
851
            while len(multiscan_proc) >= parallel:
852
                multiscan_proc = self.check_pending_target(scan_id,
853
                                                           multiscan_proc)
854
                progress = self.calculate_progress(scan_id)
855
                self.set_scan_progress(scan_id, progress)
856
                time.sleep(1)
857
858
            if self.get_scan_status(scan_id) == "stopped":
859
                self.finish_scan(scan_id)
860
                return
861
862
            logger.info("{0}: Host scan started on ports {1}.".format(target[0],target[1]))
0 ignored issues
show
Coding Style introduced by
Exactly one space required after comma
Loading history...
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
863
            scan_process = multiprocessing.Process(target=self.parallel_scan,
864
                                               args=(scan_id, target[0]))
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 4 spaces).
Loading history...
865
            multiscan_proc.append((scan_process, target[0]))
866
            scan_process.start()
867
868
        # Wait until all single target were scanned
869
        while multiscan_proc:
870
            multiscan_proc = self.check_pending_target(scan_id, multiscan_proc)
871
            if multiscan_proc:
872
                progress = self.calculate_progress(scan_id)
873
                self.set_scan_progress(scan_id, progress)
874
            time.sleep(1)
875
876
        self.finish_scan(scan_id)
877
878
    def dry_run_scan(self, scan_id, targets):
879
        """ Dry runs a scan. """
880
881
        os.setsid()
882
        #target_list = target_str_to_list(target_str)
883
        for _, target in enumerate(targets):
884
            host = resolve_hostname(target[0])
885
            if host is None:
886
                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...
887
                continue
888
            port = self.get_scan_ports(scan_id, target=target[0])
889
            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...
890
            self.add_scan_log(scan_id, name='', host=host,
891
                              value='Dry run result')
892
        self.finish_scan(scan_id)
893
894
    def handle_timeout(self, scan_id, host):
895
        """ Handles scanner reaching timeout error. """
896
        self.add_scan_error(scan_id, host=host, name="Timeout",
897
                            value="{0} exec timeout."
898
                            .format(self.get_scanner_name()))
899
900
    def set_scan_progress(self, scan_id, progress):
901
        """ Sets scan_id scan's progress which is a number between 0 and 100. """
902
        self.scan_collection.set_progress(scan_id, progress)
903
904
    def set_scan_target_progress(self, scan_id, target, progress):
905
        """ Sets target's progress. """
906
        self.scan_collection.set_target_progress(scan_id, target, progress)
907
908
    def set_scan_status(self, scan_id, status):
909
        """ Set the scan's status."""
910
        self.scan_collection.set_status(scan_id, status)
911
912
    def get_scan_status(self, scan_id):
913
        """ Get scan_id scans's status."""
914
        return self.scan_collection.get_status(scan_id)
915
916
    def scan_exists(self, scan_id):
917
        """ Checks if a scan with ID scan_id is in collection.
918
919
        @return: 1 if scan exists, 0 otherwise.
920
        """
921
        return self.scan_collection.id_exists(scan_id)
922
923
    def handle_get_scans_command(self, scan_et):
924
        """ Handles <get_scans> command.
925
926
        @return: Response string for <get_scans> command.
927
        """
928
929
        scan_id = scan_et.attrib.get('scan_id')
930
        details = scan_et.attrib.get('details')
931
        pop_res = scan_et.attrib.get('pop_results')
932
        if details and details == '0':
933
            details = False
934
        else:
935
            details = True
936
            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...
937
                pop_res = True
938
            else:
939
                pop_res = False
940
941
        responses = []
942
        if scan_id and scan_id in self.scan_collection.ids_iterator():
943
            self.check_scan_process(scan_id)
944
            scan = self.get_scan_xml(scan_id, details, pop_res)
945
            responses.append(scan)
946
        elif scan_id:
947
            text = "Failed to find scan '{0}'".format(scan_id)
948
            return simple_response_str('get_scans', 404, text)
949
        else:
950
            for scan_id in self.scan_collection.ids_iterator():
951
                self.check_scan_process(scan_id)
952
                scan = self.get_scan_xml(scan_id, details, pop_res)
953
                responses.append(scan)
954
        return simple_response_str('get_scans', 200, 'OK', responses)
955
956
    def handle_get_vts_command(self, vt_et):
957
        """ Handles <get_vts> command.
958
959
        @return: Response string for <get_vts> command.
960
        """
961
962
        vt_id = vt_et.attrib.get('vt_id')
963
964
        if vt_id and vt_id not in self.vts:
965
            text = "Failed to find vulnerability test '{0}'".format(vt_id)
966
            return simple_response_str('get_vts', 404, text)
967
968
        responses = []
969
970
        if vt_id:
971
            vts_xml = self.get_vts_xml(vt_id)
972
        else:
973
            vts_xml = self.get_vts_xml()
974
975
        responses.append(vts_xml)
976
977
        return simple_response_str('get_vts', 200, 'OK', responses)
978
979
    def handle_help_command(self, scan_et):
980
        """ Handles <help> command.
981
982
        @return: Response string for <help> command.
983
        """
984
        help_format = scan_et.attrib.get('format')
985
        if help_format is None or help_format == "text":
986
            # Default help format is text.
987
            return simple_response_str('help', 200, 'OK',
988
                                       self.get_help_text())
989
        elif help_format == "xml":
990
            text = self.get_xml_str(self.commands)
991
            return simple_response_str('help', 200, 'OK', text)
992
        raise OSPDError('Bogus help format', 'help')
993
994
    def get_help_text(self):
995
        """ Returns the help output in plain text format."""
996
997
        txt = str('\n')
998
        for name, info in self.commands.items():
999
            command_txt = "\t{0: <22} {1}\n".format(name, info['description'])
1000
            if info['attributes']:
1001
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
1002
                for attrname, attrdesc in info['attributes'].items():
1003
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
1004
                    command_txt = ''.join([command_txt, attr_txt])
1005
            if info['elements']:
1006
                command_txt = ''.join([command_txt, "\t Elements:\n",
1007
                                       self.elements_as_text(info['elements'])])
1008
            txt = ''.join([txt, command_txt])
1009
        return txt
1010
1011
    def elements_as_text(self, elems, indent=2):
1012
        """ Returns the elems dictionary as formatted plain text. """
1013
        assert elems
1014
        text = ""
1015
        for elename, eledesc in elems.items():
1016
            if isinstance(eledesc, dict):
1017
                desc_txt = self.elements_as_text(eledesc, indent + 2)
1018
                desc_txt = ''.join(['\n', desc_txt])
1019
            elif isinstance(eledesc, str):
1020
                desc_txt = ''.join([eledesc, '\n'])
1021
            else:
1022
                assert False, "Only string or dictionary"
1023
            ele_txt = "\t{0}{1: <22} {2}".format(' ' * indent, elename,
1024
                                                 desc_txt)
0 ignored issues
show
introduced by
The variable desc_txt does not seem to be defined for all execution paths.
Loading history...
1025
            text = ''.join([text, ele_txt])
1026
        return text
1027
1028
    def handle_delete_scan_command(self, scan_et):
1029
        """ Handles <delete_scan> command.
1030
1031
        @return: Response string for <delete_scan> command.
1032
        """
1033
        scan_id = scan_et.attrib.get('scan_id')
1034
        if scan_id is None:
1035
            return simple_response_str('delete_scan', 404,
1036
                                       'No scan_id attribute')
1037
1038
        if not self.scan_exists(scan_id):
1039
            text = "Failed to find scan '{0}'".format(scan_id)
1040
            return simple_response_str('delete_scan', 404, text)
1041
        self.check_scan_process(scan_id)
1042
        if self.delete_scan(scan_id):
1043
            return simple_response_str('delete_scan', 200, 'OK')
1044
        raise OSPDError('Scan in progress', 'delete_scan')
1045
1046
    def delete_scan(self, scan_id):
1047
        """ Deletes scan_id scan from collection.
1048
1049
        @return: 1 if scan deleted, 0 otherwise.
1050
        """
1051
        try:
1052
            del self.scan_processes[scan_id]
1053
        except KeyError:
1054
            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...
1055
        return self.scan_collection.delete_scan(scan_id)
1056
1057
    def get_scan_results_xml(self, scan_id, pop_res):
1058
        """ Gets scan_id scan's results in XML format.
1059
1060
        @return: String of scan results in xml.
1061
        """
1062
        results = Element('results')
1063
        for result in self.scan_collection.results_iterator(scan_id, pop_res):
1064
            results.append(get_result_xml(result))
1065
1066
        logger.info('Returning %d results', len(results))
1067
        return results
1068
1069
    def get_xml_str(self, data):
1070
        """ Creates a string in XML Format using the provided data structure.
1071
1072
        @param: Dictionary of xml tags and their elements.
1073
1074
        @return: String of data in xml format.
1075
        """
1076
1077
        responses = []
1078
        for tag, value in data.items():
1079
            elem = Element(tag)
1080
            if isinstance(value, dict):
1081
                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 1078).

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...
1082
                    elem.append(value)
1083
            elif isinstance(value, list):
1084
                value = ', '.join([m for m in value])
1085
                elem.text = value
1086
            else:
1087
                elem.text = value
1088
            responses.append(elem)
1089
        return responses
1090
1091
    def get_scan_xml(self, scan_id, detailed=True, pop_res=False):
1092
        """ Gets scan in XML format.
1093
1094
        @return: String of scan in XML format.
1095
        """
1096
        if not scan_id:
1097
            return Element('scan')
1098
1099
        target = self.get_scan_target(scan_id)
1100
        progress = self.get_scan_progress(scan_id)
1101
        start_time = self.get_scan_start_time(scan_id)
1102
        end_time = self.get_scan_end_time(scan_id)
1103
        response = Element('scan')
1104
        for name, value in [('id', scan_id),
1105
                            ('target', target),
1106
                            ('progress', progress),
1107
                            ('start_time', start_time),
1108
                            ('end_time', end_time)]:
1109
            response.set(name, str(value))
1110
        if detailed:
1111
            response.append(self.get_scan_results_xml(scan_id, pop_res))
1112
        return response
1113
1114
    @staticmethod
1115
    def get_custom_vt_as_xml_str(custom):
0 ignored issues
show
Unused Code introduced by
The argument custom seems to be unused.
Loading history...
1116
        """ Create a string representation of the XML object from the
1117
        custom data object.
1118
        This needs to be implemented by each ospd wrapper, in case
1119
        custom elements for VTs are used.
1120
1121
        The custom XML object which is returned will be embedded
1122
        into a <custom></custom> element.
1123
1124
        @return: XML object as string for custom data.
1125
        """
1126
        return ''
1127
1128
    @staticmethod
1129
    def get_params_vt_as_xml_str(vt_params):
0 ignored issues
show
Unused Code introduced by
The argument vt_params seems to be unused.
Loading history...
1130
        """ Create a string representation of the XML object from the
1131
        vt_params data object.
1132
        This needs to be implemented by each ospd wrapper, in case
1133
        vt_params elements for VTs are used.
1134
1135
        The vt_params XML object which is returned will be embedded
1136
        into a <vt_params></vt_params> element.
1137
1138
        @return: XML object as string for vt parameters data.
1139
        """
1140
        return ''
1141
1142
    @staticmethod
1143
    def get_refs_vt_as_xml_str(vt_refs):
0 ignored issues
show
Unused Code introduced by
The argument vt_refs seems to be unused.
Loading history...
1144
        """ Create a string representation of the XML object from the
1145
        vt_refs data object.
1146
        This needs to be implemented by each ospd wrapper, in case
1147
        vt_refs elements for VTs are used.
1148
1149
        The vt_refs XML object which is returned will be embedded
1150
        into a <vt_refs></vt_refs> element.
1151
1152
        @return: XML object as string for vt references data.
1153
        """
1154
        return ''
1155
1156
    @staticmethod
1157
    def get_creation_time_vt_as_xml_str(vt_creation_time):
0 ignored issues
show
Unused Code introduced by
The argument vt_creation_time seems to be unused.
Loading history...
1158
        """ Create a string representation of the XML object from the
1159
        vt_creation_time data object.
1160
        This needs to be implemented by each ospd wrapper, in case
1161
        vt_creation_time elements for VTs are used.
1162
1163
        The vt_creation_time XML object which is returned will be embedded
1164
        into a <vt_creation_time></vt_creation_time> element.
1165
1166
        @return: XML object as string for vt creation time data.
1167
        """
1168
        return ''
1169
1170
    @staticmethod
1171
    def get_modification_time_vt_as_xml_str(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...
1172
        """ Create a string representation of the XML object from the
1173
        vt_modification_time data object.
1174
        This needs to be implemented by each ospd wrapper, in case
1175
        vt_modification_time elements for VTs are used.
1176
1177
        The vt_modification_time XML object which is returned will be embedded
1178
        into a <vt_modification_time></vt_modification_time> element.
1179
1180
        @return: XML object as string for vt references data.
1181
        """
1182
        return ''
1183
1184
    def get_vt_xml(self, vt_id):
1185
        """ Gets a single vulnerability test information in XML format.
1186
1187
        @return: String of single vulnerability test information in XML format.
1188
        """
1189
        if not vt_id:
1190
            return Element('vt')
1191
1192
        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...
1193
1194
        name = vt.get('name')
1195
        vt_xml = Element('vt')
1196
        vt_xml.set('id', vt_id)
1197
1198
        for name, value in [('name', name)]:
1199
            elem = SubElement(vt_xml, name)
1200
            elem.text = str(value)
1201
1202
        if vt.get('custom'):
1203
            custom_xml_str = (
1204
                '<custom>%s</custom>' % self.get_custom_vt_as_xml_str(
1205
                    vt.get('custom')))
1206
            vt_xml.append(secET.fromstring(custom_xml_str))
1207
1208
        if vt.get('vt_params'):
1209
            params_xml_str = (
1210
                '<vt_params>%s</vt_params>' % self.get_params_vt_as_xml_str(
1211
                    vt.get('vt_params')))
1212
            vt_xml.append(secET.fromstring(params_xml_str))
1213
1214
        if vt.get('vt_refs'):
1215
            refs_xml_str = (
1216
                '<vt_refs>%s</vt_refs>' % self.get_refs_vt_as_xml_str(
1217
                    vt.get('vt_refs')))
1218
            vt_xml.append(secET.fromstring(refs_xml_str))
1219
1220
        if vt.get('creation_time'):
1221
            vt_ctime = self.get_creation_time_vt_as_xml_str(
1222
                vt.get('creation_time'))
1223
            creation_time_xml_str = (
1224
                '<creation_time>%s</creation_time>' % vt_ctime)
1225
            vt_xml.append(secET.fromstring(creation_time_xml_str))
1226
1227
        if vt.get('modification_time'):
1228
            vt_mtime = self.get_modification_time_vt_as_xml_str(
1229
                vt.get('modification_time'))
1230
            modification_time_xml_str = (
1231
                '<modification_time>%s</modification_time>' % vt_mtime)
1232
            vt_xml.append(secET.fromstring(modification_time_xml_str))
1233
1234
        return vt_xml
1235
1236
    def get_vts_xml(self, vt_id=''):
1237
        """ Gets collection of vulnerability test information in XML format.
1238
        If vt_id is specified, the collection will contain only this vt, of found.
1239
        If no vt_id is specified, the collection will contain all vts.
1240
1241
        @return: String of collection of vulnerability test information in XML format.
1242
        """
1243
1244
        vts_xml = Element('vts')
1245
1246
        if vt_id != '':
1247
            vts_xml.append(self.get_vt_xml(vt_id))
1248
        else:
1249
            for vt_id in self.vts:
0 ignored issues
show
unused-code introduced by
Redefining argument with the local name 'vt_id'
Loading history...
1250
                vts_xml.append(self.get_vt_xml(vt_id))
1251
1252
        return vts_xml
1253
1254
    def handle_get_scanner_details(self):
1255
        """ Handles <get_scanner_details> command.
1256
1257
        @return: Response string for <get_scanner_details> command.
1258
        """
1259
        desc_xml = Element('description')
1260
        desc_xml.text = self.get_scanner_description()
1261
        details = [
1262
            desc_xml,
1263
            self.get_scanner_params_xml()
1264
        ]
1265
        return simple_response_str('get_scanner_details', 200, 'OK', details)
1266
1267
    def handle_get_version_command(self):
1268
        """ Handles <get_version> command.
1269
1270
        @return: Response string for <get_version> command.
1271
        """
1272
        protocol = Element('protocol')
1273
        for name, value in [('name', 'OSP'), ('version', self.get_protocol_version())]:
1274
            elem = SubElement(protocol, name)
1275
            elem.text = value
1276
1277
        daemon = Element('daemon')
1278
        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...
1279
            elem = SubElement(daemon, name)
1280
            elem.text = value
1281
1282
        scanner = Element('scanner')
1283
        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...
1284
            elem = SubElement(scanner, name)
1285
            elem.text = value
1286
1287
        return simple_response_str('get_version', 200, 'OK', [protocol, daemon, scanner])
1288
1289
    def handle_command(self, command):
0 ignored issues
show
best-practice introduced by
Too many return statements (8/6)
Loading history...
1290
        """ Handles an osp command in a string.
1291
1292
        @return: OSP Response to command.
1293
        """
1294
        try:
1295
            tree = secET.fromstring(command)
1296
        except secET.ParseError:
1297
            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...
1298
            raise OSPDError('Invalid data')
1299
1300
        if not self.command_exists(tree.tag) and tree.tag != "authenticate":
1301
            raise OSPDError('Bogus command name')
1302
1303
        if tree.tag == "get_version":
1304
            return self.handle_get_version_command()
1305
        elif tree.tag == "start_scan":
1306
            return self.handle_start_scan_command(tree)
1307
        elif tree.tag == "stop_scan":
1308
            return self.handle_stop_scan_command(tree)
1309
        elif tree.tag == "get_scans":
1310
            return self.handle_get_scans_command(tree)
1311
        elif tree.tag == "get_vts":
1312
            return self.handle_get_vts_command(tree)
1313
        elif tree.tag == "delete_scan":
1314
            return self.handle_delete_scan_command(tree)
1315
        elif tree.tag == "help":
1316
            return self.handle_help_command(tree)
1317
        elif tree.tag == "get_scanner_details":
1318
            return self.handle_get_scanner_details()
1319
        else:
1320
            assert False, "Unhandled command: {0}".format(tree.tag)
1321
1322
    def check(self):
1323
        """ Asserts to False. Should be implemented by subclass. """
1324
        raise NotImplementedError
1325
1326
    def run(self, address, port, unix_path):
1327
        """ Starts the Daemon, handling commands until interrupted.
1328
1329
        @return False if error. Runs indefinitely otherwise.
1330
        """
1331
        assert address or unix_path
1332
        if unix_path:
1333
            sock = bind_unix_socket(unix_path)
1334
        else:
1335
            sock = bind_socket(address, port)
1336
        if sock is None:
1337
            return False
1338
1339
        try:
1340
            while True:
1341
                if unix_path:
1342
                    client_stream, _ = sock.accept()
1343
                    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...
1344
                    self.handle_client_stream(client_stream, True)
1345
                else:
1346
                    client_stream = self.new_client_stream(sock)
1347
                    if client_stream is None:
1348
                        continue
1349
                    self.handle_client_stream(client_stream, False)
1350
                close_client_stream(client_stream, unix_path)
1351
        except KeyboardInterrupt:
1352
            logger.info("Received Ctrl-C shutting-down ...")
1353
        finally:
1354
            sock.shutdown(socket.SHUT_RDWR)
1355
            sock.close()
1356
1357
    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...
1358
        """ Creates a new scan.
1359
1360
        @target: Target to scan.
1361
        @options: Miscellaneous scan options.
1362
1363
        @return: New scan's ID.
1364
        """
1365
        return self.scan_collection.create_scan(scan_id, targets, target_str, options, vts)
1366
1367
    def get_scan_options(self, scan_id):
1368
        """ Gives a scan's list of options. """
1369
        return self.scan_collection.get_options(scan_id)
1370
1371
    def set_scan_option(self, scan_id, name, value):
1372
        """ Sets a scan's option to a provided value. """
1373
        return self.scan_collection.set_option(scan_id, name, value)
1374
1375
    def check_scan_process(self, scan_id):
1376
        """ Check the scan's process, and terminate the scan if not alive. """
1377
        scan_process = self.scan_processes[scan_id]
1378
        progress = self.get_scan_progress(scan_id)
1379
        if progress < 100 and not scan_process.is_alive():
1380
            self.set_scan_progress(scan_id, 100)
1381
            self.add_scan_error(scan_id, name="", host="",
1382
                                value="Scan process failure.")
1383
            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...
1384
        elif progress == 100:
1385
            scan_process.join()
1386
1387
    def get_scan_progress(self, scan_id):
1388
        """ Gives a scan's current progress value. """
1389
        return self.scan_collection.get_progress(scan_id)
1390
1391
    def get_scan_target_progress(self, scan_id):
1392
        """ Gives a list with scan's current progress value of each target. """
1393
        return self.scan_collection.get_target_progress(scan_id)
1394
1395
    def get_scan_target(self, scan_id):
1396
        """ Gives a scan's target. """
1397
        return self.scan_collection.get_target(scan_id)
1398
1399
    def get_scan_ports(self, scan_id, target=''):
1400
        """ Gives a scan's ports list. """
1401
        return self.scan_collection.get_ports(scan_id, target)
1402
1403
    def get_scan_credentials(self, scan_id, target=''):
1404
        """ Gives a scan's credential list. If a target is passed gives
1405
        the credential list for the given target. """
1406
        return self.scan_collection.get_credentials(scan_id, target)
1407
1408
    def get_scan_vts(self, scan_id):
1409
        """ Gives a scan's vts list. """
1410
        return self.scan_collection.get_vts(scan_id)
1411
1412
    def get_scan_start_time(self, scan_id):
1413
        """ Gives a scan's start time. """
1414
        return self.scan_collection.get_start_time(scan_id)
1415
1416
    def get_scan_end_time(self, scan_id):
1417
        """ Gives a scan's end time. """
1418
        return self.scan_collection.get_end_time(scan_id)
1419
1420
    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...
1421
                     test_id='', qod=''):
1422
        """ Adds a log result to scan_id scan. """
1423
        self.scan_collection.add_result(scan_id, ResultType.LOG, host, name,
1424
                                        value, port, test_id, 0.0, qod)
1425
1426
    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...
1427
        """ Adds an error result to scan_id scan. """
1428
        self.scan_collection.add_result(scan_id, ResultType.ERROR, host, name,
1429
                                        value, port)
1430
1431
    def add_scan_host_detail(self, scan_id, host='', name='', value=''):
1432
        """ Adds a host detail result to scan_id scan. """
1433
        self.scan_collection.add_result(scan_id, ResultType.HOST_DETAIL, host,
1434
                                        name, value)
1435
1436
    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...
1437
                       test_id='', severity='', qod=''):
1438
        """ Adds an alarm result to scan_id scan. """
1439
        self.scan_collection.add_result(scan_id, ResultType.ALARM, host, name,
1440
                                        value, port, test_id, severity, qod)
1441