Passed
Pull Request — master (#46)
by
unknown
02:23
created

ospd.ospd.OSPDaemon.handle_get_scans_command()   C

Complexity

Conditions 9

Size

Total Lines 32
Code Lines 23

Duplication

Lines 32
Ratio 100 %

Importance

Changes 0
Metric Value
cc 9
eloc 23
nop 2
dl 32
loc 32
rs 6.6666
c 0
b 0
f 0
1
# -*- coding: utf-8 -*-
0 ignored issues
show
coding-style introduced by
Too many lines in module (1327/1000)
Loading history...
2
# $Id$
3
# Description:
4
# OSP Daemon core class.
5
#
6
# Authors:
7
# Hani Benhabiles <[email protected]>
8
# Benoît Allard <[email protected]>
9
# Jan-Oliver Wahmer <[email protected]>
10
#
11
# Copyright:
12
# Copyright (C) 2014, 2015, 2018 Greenbone Networks GmbH
13
#
14
# This program is free software; you can redistribute it and/or
15
# modify it under the terms of the GNU General Public License
16
# as published by the Free Software Foundation; either version 2
17
# of the License, or (at your option) any later version.
18
#
19
# This program is distributed in the hope that it will be useful,
20
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
# GNU General Public License for more details.
23
#
24
# You should have received a copy of the GNU General Public License
25
# along with this program; if not, write to the Free Software
26
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
27
28
""" OSP Daemon core class. """
29
30
# This is needed for older pythons as our current module is called the same
31
# as the package we are in ...
32
# Another solution would be to rename that file.
33
from __future__ import absolute_import
34
35
import logging
36
import socket
37
import ssl
38
import multiprocessing
39
import xml.etree.ElementTree as ET
40
import defusedxml.ElementTree as secET
41
import os
0 ignored issues
show
introduced by
standard import "import os" should be placed before "import defusedxml.ElementTree as secET"
Loading history...
42
import re
0 ignored issues
show
introduced by
standard import "import re" should be placed before "import defusedxml.ElementTree as secET"
Loading history...
43
import time
0 ignored issues
show
introduced by
standard import "import time" should be placed before "import defusedxml.ElementTree as secET"
Loading history...
44
45
from ospd import __version__
46
from ospd.misc import ScanCollection, ResultType, target_str_to_list
47
from ospd.misc import resolve_hostname, valid_uuid
48
49
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...
50
51
PROTOCOL_VERSION = "1.2"
52
53
BASE_SCANNER_PARAMS = {
54
    'debug_mode': {
55
        'type': 'boolean',
56
        'name': 'Debug Mode',
57
        'default': 0,
58
        'mandatory': 0,
59
        'description': 'Whether to get extra scan debug information.',
60
    },
61
    'dry_run': {
62
        'type': 'boolean',
63
        'name': 'Dry Run',
64
        'default': 0,
65
        'mandatory': 0,
66
        'description': 'Whether to dry run scan.',
67
    },
68
}
69
70
COMMANDS_TABLE = {
71
    'start_scan': {
72
        'description': 'Start a new scan.',
73
        'attributes': {
74
            'target': 'Target host to scan',
75
            'ports': 'Ports list to scan',
76
            'scan_id': 'Optional UUID value to use as scan ID',
77
            'parallel': 'Optional nummer of parallel target to scan',
78
        },
79
        'elements': None
80
    },
81
    'stop_scan': {
82
        'description': 'Stop a currently running scan.',
83
        'attributes': {
84
            'scan_id': 'ID of scan to stop.'
85
        },
86
        'elements': None
87
    },
88
    'help': {
89
        'description': 'Print the commands help.',
90
        'attributes': {
91
            'format': 'Help format. Could be text or xml.'
92
        },
93
        'elements': None
94
    },
95
    'get_scans': {
96
        'description': 'List the scans in buffer.',
97
        'attributes': {
98
            'scan_id': 'ID of a specific scan to get.',
99
            'details': 'Whether to return the full scan report.'
100
        },
101
        'elements': None
102
    },
103
    'get_vts': {
104
        'description': 'List of available vulnerability tests.',
105
        'attributes': {
106
            'vt_id': 'ID of a specific vulnerability test to get.'
107
        },
108
        'elements': None
109
    },
110
    'delete_scan': {
111
        'description': 'Delete a finished scan.',
112
        'attributes': {
113
            'scan_id': 'ID of scan to delete.'
114
        },
115
        'elements': None
116
    },
117
    'get_version': {
118
        'description': 'Return various versions.',
119
        'attributes': None,
120
        'elements': None
121
    },
122
    'get_scanner_details': {
123
        'description': 'Return scanner description and parameters',
124
        'attributes': None,
125
        'elements': None
126
    }
127
}
128
129
130 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...
131
    """ Formats a scan result to XML format. """
132
    result_xml = ET.Element('result')
133
    for name, value in [('name', result['name']),
134
                        ('type', ResultType.get_str(result['type'])),
135
                        ('severity', result['severity']),
136
                        ('host', result['host']),
137
                        ('test_id', result['test_id']),
138
                        ('port', result['port']),
139
                        ('qod', result['qod'])]:
140
        result_xml.set(name, str(value))
141
    result_xml.text = result['value']
142
    return result_xml
143
144
145 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...
146
    """ Creates an OSP response XML string.
147
148
    @param: OSP Command to respond to.
149
    @param: Status of the response.
150
    @param: Status text of the response.
151
    @param: Text part of the response XML element.
152
153
    @return: String of response in xml format.
154
    """
155
    response = ET.Element('%s_response' % command)
156
    for name, value in [('status', str(status)), ('status_text', status_text)]:
157
        response.set(name, str(value))
158
    if isinstance(content, list):
159
        for elem in content:
160
            response.append(elem)
161
    elif isinstance(content, ET.Element):
162
        response.append(content)
163
    else:
164
        response.text = content
165
    return ET.tostring(response)
166
167
168 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...
169
170
    """ This is an exception that will result in an error message to the
171
    client """
172
173
    def __init__(self, message, command='osp', status=400):
174
        super(OSPDError, self).__init__()
175
        self.message = message
176
        self.command = command
177
        self.status = status
178
179
    def as_xml(self):
180
        """ Return the error in xml format. """
181
        return simple_response_str(self.command, self.status, self.message)
182
183
184 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...
185
    """ Returns a socket bound on (address:port). """
186
187
    assert address
188
    assert port
189
    bindsocket = socket.socket()
190
    try:
191
        bindsocket.bind((address, port))
192
    except socket.error:
193
        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...
194
                     .format(address, port))
195
        return None
196
197
    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...
198
    bindsocket.listen(0)
199
    return bindsocket
200
201 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...
202
    """ Returns a unix file socket bound on (path). """
203
204
    assert path
205
    bindsocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
206
    try:
207
        os.unlink(path)
208
    except OSError:
209
        if os.path.exists(path):
210
            raise
211
    try:
212
        bindsocket.bind(path)
213
    except socket.error:
214
        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...
215
        return None
216
217
    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...
218
    bindsocket.listen(0)
219
    return bindsocket
220
221
222 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...
223
    """ Closes provided client stream """
224
    try:
225
        client_stream.shutdown(socket.SHUT_RDWR)
226
        if unix_path:
227
            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...
228
        else:
229
            peer = client_stream.getpeername()
230
            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...
231
    except (socket.error, OSError) as exception:
232
        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...
233
    client_stream.close()
234
235
236 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 (68/20)
Loading history...
237
238
    """ Daemon class for OSP traffic handling.
239
240
    Every scanner wrapper should subclass it and make necessary additions and
241
    changes.
242
    * Add any needed parameters in __init__.
243
    * Implement check() method which verifies scanner availability and other
244
      environment related conditions.
245
    * Implement process_scan_params and exec_scan methods which are
246
      specific to handling the <start_scan> command, executing the wrapped
247
      scanner and storing the results.
248
    * exec_scan() should return 0 if host is dead or not reached, 1 if host is
249
      alive and 2 if scan error or status is unknown.
250
    * Implement other methods that assert to False such as get_scanner_name,
251
      get_scanner_version.
252
    * Use Call set_command_attributes at init time to add scanner command
253
      specific options eg. the w3af profile for w3af wrapper.
254
    """
255
256
    def __init__(self, certfile, keyfile, cafile):
257
        """ Initializes the daemon's internal data. """
258
        # @todo: Actually it makes sense to move the certificate params to
259
        #        a separate function because it is not mandatory anymore to
260
        #        use a TLS setup (unix file socket is an alternative).
261
        #        However, changing this makes it mandatory for any ospd scanner
262
        #        to change the function calls as well. So this breaks the API
263
        #        and should only be done with a major release.
264
        self.certs = dict()
265
        self.certs['cert_file'] = certfile
266
        self.certs['key_file'] = keyfile
267
        self.certs['ca_file'] = cafile
268
        self.scan_collection = ScanCollection()
269
        self.scan_processes = dict()
270
        self.daemon_info = dict()
271
        self.daemon_info['name'] = "OSPd"
272
        self.daemon_info['version'] = __version__
273
        self.daemon_info['description'] = "No description"
274
        self.scanner_info = dict()
275
        self.scanner_info['name'] = 'No name'
276
        self.scanner_info['version'] = 'No version'
277
        self.scanner_info['description'] = 'No description'
278
        self.server_version = None  # Set by the subclass.
279
        self.protocol_version = PROTOCOL_VERSION
280
        self.commands = COMMANDS_TABLE
281
        self.scanner_params = dict()
282
        for name, param in BASE_SCANNER_PARAMS.items():
283
            self.add_scanner_param(name, param)
284
        self.vts = dict()
285
        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...
286
287
    def set_command_attributes(self, name, attributes):
288
        """ Sets the xml attributes of a specified command. """
289
        if self.command_exists(name):
290
            command = self.commands.get(name)
291
            command['attributes'] = attributes
292
293
    def add_scanner_param(self, name, scanner_param):
294
        """ Add a scanner parameter. """
295
296
        assert name
297
        assert scanner_param
298
        self.scanner_params[name] = scanner_param
299
        command = self.commands.get('start_scan')
300
        command['elements'] = {
301
            'scanner_params':
302
                {k: v['name'] for k, v in self.scanner_params.items()}}
303
304
    def add_vt(self, vt_id, name='', vt_params=None, custom=None):
305
        """ Add a vulnerability test information.
306
307
        Returns: The new number of stored VTs.
308
        -1 in case the VT ID was already present and thus the
309
        new VT was not considered.
310
        -2 in case the vt_id was invalid.
311
        """
312
313
        if not vt_id:
314
            return -2  # no valid vt_id
315
316
        if self.vt_id_pattern.fullmatch(vt_id) is None:
317
            return -2  # no valid vt_id
318
319
        if vt_id in self.vts:
320
            return -1  # The VT was already in the list.
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
328
        return len(self.vts)
329
330
    def command_exists(self, name):
331
        """ Checks if a commands exists. """
332
        return name in self.commands.keys()
333
334
    def get_scanner_name(self):
335
        """ Gives the wrapped scanner's name. """
336
        return self.scanner_info['name']
337
338
    def get_scanner_version(self):
339
        """ Gives the wrapped scanner's version. """
340
        return self.scanner_info['version']
341
342
    def get_scanner_description(self):
343
        """ Gives the wrapped scanner's description. """
344
        return self.scanner_info['description']
345
346
    def get_server_version(self):
347
        """ Gives the specific OSP server's version. """
348
        assert self.server_version
349
        return self.server_version
350
351
    def get_protocol_version(self):
352
        """ Gives the OSP's version. """
353
        return self.protocol_version
354
355
    def _preprocess_scan_params(self, xml_params):
356
        """ Processes the scan parameters. """
357
        params = {}
358
        for param in xml_params:
359
            params[param.tag] = param.text or ''
360
        # Set default values.
361
        for key in self.scanner_params:
362
            if key not in params:
363
                params[key] = self.get_scanner_param_default(key)
364
                if self.get_scanner_param_type(key) == 'selection':
365
                    params[key] = params[key].split('|')[0]
366
        # Validate values.
367
        for key in params:
368
            param_type = self.get_scanner_param_type(key)
369
            if not param_type:
370
                continue
371
            if param_type in ['integer', 'boolean']:
372
                try:
373
                    params[key] = int(params[key])
374
                except ValueError:
375
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
376
            if param_type == 'boolean':
377
                if params[key] not in [0, 1]:
378
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
379
            elif param_type == 'selection':
380
                selection = self.get_scanner_param_default(key).split('|')
381
                if params[key] not in selection:
382
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
383
            if self.get_scanner_param_mandatory(key) and params[key] == '':
384
                    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...
385
                                    'start_scan')
386
        return params
387
388
    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...
389
        """ This method is to be overridden by the child classes if necessary
390
        """
391
        return params
392
393
    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...
394
        """ Receive an XML object with the Vulnerability Tests an their
395
        parameters to be use in a scan and return a dictionary.
396
397
        @param: XML element with vt subelements. Each vt has an
398
                id attribute. Optinal parameters can be included
399
                as vt child.
400
                Example form:
401
                <vts>
402
                  <vt id='vt1' />
403
                  <vt id='vt2'>
404
                    <vt_param name='param1' type='type'>value</vt_param>
405
                  </vt>
406
                  <vtgroup filter='family = debian'/>
407
                  <vtgroup filter='family = general'/>
408
                <vts>
409
410
        @return: Dictionary containing the vts attribute and subelements,
411
                 like the VT's id and VT's parameters.
412
                 Example form:
413
                 {'v1',
414
                  'vt2': {param1: {'type': type', 'value': value}},
415
                  'vtgroups': ['family = debian', 'family = general']}
416
        """
417
        vts = {}
418
        filters = list()
419
        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...
420
            if vt.tag == 'vt':
421
                vt_id = vt.attrib.get('id')
422
                vts[vt_id] = {}
423
                for param in vt:
424
                    if not param.attrib.get('name'):
425
                        raise OSPDError('Invalid VT parameter. No parameter name',
426
                                        'start_scan')
427
                    ptype = param.attrib.get('type', 'entry')
428
                    pvalue = param.text if param.text else ''
429
                    pname = param.attrib.get('name')
430
                    vts[vt_id][pname] = {'type': ptype, 'value': pvalue}
431
            if vt.tag == 'vtgroup':
432
                vts_filter = vt.attrib.get('filter', None)
433
                if vts_filter is None:
434
                    raise OSPDError('Invalid VT group. No filter given.',
435
                                    'start_scan')
436
                filters.append(vts_filter)
437
        vts['vtgroups'] = filters
438
        return vts
439
440
    @staticmethod
441
    def process_credentials_elements(cred_tree):
442
        """ Receive an XML object with the credentials to run
443
        a scan against a given target.
444
445
        @param:
446
        <credentials>
447
          <credential type="up" service="ssh" port="22">
448
            <username>scanuser</username>
449
            <password>mypass</password>
450
          </credential>
451
          <credential type="up" service="smb">
452
            <username>smbuser</username>
453
            <password>mypass</password>
454
          </credential>
455
        </credentials>
456
457
        @return: Dictionary containing the credentials for a given target.
458
                 Example form:
459
                 {'ssh': {'type': type,
460
                          'port': port,
461
                          'username': username,
462
                          'password': pass,
463
                        },
464
                  'smb': {'type': type,
465
                          'username': username,
466
                          'password': pass,
467
                         },
468
                   }
469
        """
470
        credentials = {}
471
        for credential in cred_tree:
472
            service = credential.attrib.get('service')
473
            credentials[service] = {}
474
            credentials[service]['type'] = credential.attrib.get('type')
475
            if service == 'ssh':
476
                credentials[service]['port'] = credential.attrib.get('port')
477
            for param in credential:
478
                credentials[service][param.tag] = param.text
479
480
        return credentials
481
482
    @classmethod
483
    def process_targets_element(cls, scanner_target):
484
        """ Receive an XML object with the target, ports and credentials to run
485
        a scan against.
486
487
        @param: XML element with target subelements. Each target has <hosts>
488
        and <ports> subelements. Hosts can be a single host, a host range,
489
        a comma-separated host list or a network address.
490
        <ports> and  <credentials> are optional. Therefore each ospd-scanner
491
        should check for a valid ones if needed.
492
493
                Example form:
494
                <targets>
495
                  <target>
496
                    <hosts>localhosts</hosts>
497
                    <ports>80,443</ports>
498
                  </target>
499
                  <target>
500
                    <hosts>192.168.0.0/24</hosts>
501
                    <ports>22</ports>
502
                    <credentials>
503
                      <credential type="up" service="ssh" port="22">
504
                        <username>scanuser</username>
505
                        <password>mypass</password>
506
                      </credential>
507
                      <credential type="up" service="smb">
508
                        <username>smbuser</username>
509
                        <password>mypass</password>
510
                      </credential>
511
                    </credentials>
512
                  </target>
513
                </targets>
514
515
        @return: A list of (hosts, port) tuples.
516
                 Example form:
517
                 [['localhost', '80,43'],
518
                  ['192.168.0.0/24', '22', {'smb': {'type': type,
519
                                                    'port': port,
520
                                                    'username': username,
521
                                                    'password': pass,
522
                                                   }}]]
523
        """
524
525
        target_list = []
526
        for target in scanner_target:
527
            ports= ''
0 ignored issues
show
Coding Style introduced by
Exactly one space required before assignment
Loading history...
528
            credentials = {}
529
            for child in target:
530
                if child.tag == 'hosts':
531
                    hosts = child.text
532
                if child.tag == 'ports':
533
                    ports = child.text
534
                if child.tag == 'credentials':
535
                    credentials = cls.process_credentials_elements(child)
536
            if hosts:
0 ignored issues
show
introduced by
The variable hosts does not seem to be defined for all execution paths.
Loading history...
537
                target_list.append([hosts, ports, credentials])
538
            else:
539
                raise OSPDError('No target to scan', 'start_scan')
540
541
        return target_list
542
543
    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...
544
        """ Handles <start_scan> command.
545
546
        @return: Response string for <start_scan> command.
547
        """
548
549
        target_str = scan_et.attrib.get('target')
550
        ports_str = scan_et.attrib.get('ports')
551
        # For backward compatibility, if target and ports attributes are set,
552
        # <targets> element is ignored.
553
        if target_str is None or ports_str is None:
554
            target_list = scan_et.find('targets')
555
            if target_list is None or not target_list:
556
                raise OSPDError('No targets or ports', 'start_scan')
557
            else:
558
                scan_targets = self.process_targets_element(target_list)
559
        else:
560
            scan_targets = []
561
            for single_target in target_str_to_list(target_str):
562
                scan_targets.append([single_target, ports_str, ''])
563
564
        scan_id = scan_et.attrib.get('scan_id')
565
        if scan_id is not None and scan_id != '' and not valid_uuid(scan_id):
566
            raise OSPDError('Invalid scan_id UUID', 'start_scan')
567
568
        try:
569
            parallel = int(scan_et.attrib.get('parallel', '1'))
570
            if parallel < 1 or parallel > 20:
571
                parallel = 1
572
        except ValueError:
573
            raise OSPDError('Invalid value for parallel scans. '
574
                            'It must be a number', 'start_scan')
575
576
        scanner_params = scan_et.find('scanner_params')
577
        if scanner_params is None:
578
            raise OSPDError('No scanner_params element', 'start_scan')
579
580
        params = self._preprocess_scan_params(scanner_params)
581
582
        # VTS is an optional element. If present should not be empty.
583
        vts = {}
584
        scanner_vts = scan_et.find('vts')
585
        if scanner_vts is not None:
586
            if not scanner_vts:
587
                raise OSPDError('VTs list is empty', 'start_scan')
588
            else:
589
                vts = self.process_vts_params(scanner_vts)
590
591
        # Dry run case.
592
        if 'dry_run' in params and int(params['dry_run']):
593
            scan_func = self.dry_run_scan
594
            scan_params = None
595
        else:
596
            scan_func = self.start_scan
597
            scan_params = self.process_scan_params(params)
598
599
        scan_id = self.create_scan(scan_id, scan_targets, target_str, scan_params, vts)
600
        scan_process = multiprocessing.Process(target=scan_func,
601
                                               args=(scan_id,
602
                                                     scan_targets,
603
                                                     parallel))
604
        self.scan_processes[scan_id] = scan_process
605
        scan_process.start()
606
        id_ = ET.Element('id')
607
        id_.text = scan_id
608
        return simple_response_str('start_scan', 200, 'OK', id_)
609
610
    def handle_stop_scan_command(self, scan_et):
611
        """ Handles <stop_scan> command.
612
613
        @return: Response string for <stop_scan> command.
614
        """
615
616
        scan_id = scan_et.attrib.get('scan_id')
617
        if scan_id is None or scan_id == '':
618
            raise OSPDError('No scan_id attribute', 'stop_scan')
619
        scan_process = self.scan_processes.get(scan_id)
620
        if not scan_process:
621
            raise OSPDError('Scan not found {0}.'.format(scan_id), 'stop_scan')
622
        if not scan_process.is_alive():
623
            raise OSPDError('Scan already stopped or finished.', 'stop_scan')
624
625
        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...
626
        self.stop_scan(scan_id)
627
        scan_process.terminate()
628
        os.killpg(os.getpgid(scan_process.ident), 15)
629
        scan_process.join()
630
        self.set_scan_progress(scan_id, 100)
631
        self.add_scan_log(scan_id, name='', host='', value='Scan stopped.')
632
        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...
633
        return simple_response_str('stop_scan', 200, 'OK')
634
635
    def stop_scan(self, scan_id):
636
        """ Should be implemented by subclass in case of a clean up before
637
        terminating is needed. """
638
639
    def exec_scan(self, scan_id, target):
640
        """ Asserts to False. Should be implemented by subclass. """
641
        raise NotImplementedError
642
643
    def finish_scan(self, scan_id):
644
        """ Sets a scan as finished. """
645
        self.set_scan_progress(scan_id, 100)
646
        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...
647
648
    def get_daemon_name(self):
649
        """ Gives osp daemon's name. """
650
        return self.daemon_info['name']
651
652
    def get_daemon_version(self):
653
        """ Gives osp daemon's version. """
654
        return self.daemon_info['version']
655
656
    def get_scanner_param_type(self, param):
657
        """ Returns type of a scanner parameter. """
658
        assert isinstance(param, str)
659
        entry = self.scanner_params.get(param)
660
        if not entry:
661
            return None
662
        return entry.get('type')
663
664
    def get_scanner_param_mandatory(self, param):
665
        """ Returns if a scanner parameter is mandatory. """
666
        assert isinstance(param, str)
667
        entry = self.scanner_params.get(param)
668
        if not entry:
669
            return False
670
        return entry.get('mandatory')
671
672
    def get_scanner_param_default(self, param):
673
        """ Returns default value of a scanner parameter. """
674
        assert isinstance(param, str)
675
        entry = self.scanner_params.get(param)
676
        if not entry:
677
            return None
678
        return entry.get('default')
679
680
    def get_scanner_params_xml(self):
681
        """ Returns the OSP Daemon's scanner params in xml format. """
682
        scanner_params = ET.Element('scanner_params')
683
        for param_id, param in self.scanner_params.items():
684
            param_xml = ET.SubElement(scanner_params, 'scanner_param')
685
            for name, value in [('id', param_id),
686
                                ('type', param['type'])]:
687
                param_xml.set(name, value)
688
            for name, value in [('name', param['name']),
689
                                ('description', param['description']),
690
                                ('default', param['default']),
691
                                ('mandatory', param['mandatory'])]:
692
                elem = ET.SubElement(param_xml, name)
693
                elem.text = str(value)
694
        return scanner_params
695
696
    def new_client_stream(self, sock):
697
        """ Returns a new ssl client stream from bind_socket. """
698
699
        assert sock
700
        newsocket, fromaddr = sock.accept()
701
        logger.debug("New connection from"
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
702
                     " {0}:{1}".format(fromaddr[0], fromaddr[1]))
703
        # NB: Despite the name, ssl.PROTOCOL_SSLv23 selects the highest
704
        # protocol version that both the client and server support. In modern
705
        # Python versions (>= 3.4) it suppports TLS >= 1.0 with SSLv2 and SSLv3
706
        # being disabled. For Python >=3.5, PROTOCOL_SSLv23 is an alias for
707
        # PROTOCOL_TLS which should be used once compatibility with Python 3.4
708
        # is no longer desired.
709
        try:
710
            ssl_socket = ssl.wrap_socket(newsocket, cert_reqs=ssl.CERT_REQUIRED,
711
                                         server_side=True,
712
                                         certfile=self.certs['cert_file'],
713
                                         keyfile=self.certs['key_file'],
714
                                         ca_certs=self.certs['ca_file'],
715
                                         ssl_version=ssl.PROTOCOL_SSLv23)
716
        except (ssl.SSLError, socket.error) as message:
717
            logger.error(message)
718
            return None
719
        return ssl_socket
720
721
    @staticmethod
722
    def write_to_stream(stream, response, block_len=1024):
723
        """
724
        Send the response in blocks of the given len using the
725
        passed method dependending on the socket type.
726
        """
727
        try:
728
            i_start = 0
729
            i_end = block_len
730
            while True:
731
                if i_end > len(response):
732
                    stream(response[i_start:])
733
                    break
734
                stream(response[i_start:i_end])
735
                i_start = i_end
736
                i_end += block_len
737
        except (socket.timeout, socket.error) as exception:
738
            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...
739
740
    def handle_client_stream(self, stream, is_unix=False):
741
        """ Handles stream of data received from client. """
742
743
        assert stream
744
        data = []
745
        stream.settimeout(2)
746
        while True:
747
            try:
748
                if is_unix:
749
                    data.append(stream.recv(1024))
750
                else:
751
                    data.append(stream.read(1024))
752
                if len(data) == 0:
0 ignored issues
show
Unused Code introduced by
Do not use len(SEQUENCE) as condition value
Loading history...
753
                    logger.warning(
754
                        "Empty client stream (Connection unexpectedly closed)")
755
                    return
756
            except (AttributeError, ValueError) as message:
757
                logger.error(message)
758
                return
759
            except (ssl.SSLError) as exception:
760
                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...
761
                break
762
            except (socket.timeout) as exception:
763
                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...
764
                break
765
        data = b''.join(data)
766
        if len(data) <= 0:
767
            logger.debug("Empty client stream")
768
            return
769
        try:
770
            response = self.handle_command(data)
771
        except OSPDError as exception:
772
            response = exception.as_xml()
773
            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...
774
        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...
775
            logger.exception('While handling client command:')
776
            exception = OSPDError('Fatal error', 'error')
777
            response = exception.as_xml()
778
        if is_unix:
779
            send_method = stream.send
780
        else:
781
            send_method = stream.write
782
        self.write_to_stream(send_method, response)
783
784
    def parallel_scan(self,scan_id, target):
0 ignored issues
show
Coding Style introduced by
Exactly one space required after comma
Loading history...
785
        """ Starts the scan with scan_id. """
786
        try:
787
            ret = self.exec_scan(scan_id, target)
788
            if ret == 0:
789
                self.add_scan_host_detail(scan_id, name='host_status',
790
                                          host=target, value='0')
791
            elif ret == 1:
792
                self.add_scan_host_detail(scan_id, name='host_status',
793
                                          host=target, value='1')
794
            elif ret == 2:
795
                self.add_scan_host_detail(scan_id, name='host_status',
796
                                          host=target, value='2')
797
            else:
798
                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...
799
        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...
800
            self.add_scan_error(scan_id, name='', host=target,
801
                                value='Host process failure (%s).' % e)
802
            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...
803
        else:
804
            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...
805
806
    @staticmethod
807
    def check_pending_target(multiscan_proc):
808
        """ Check if a scan process is still alive. In case the process
809
        finished, removes the process from the multiscan_process list
810
811
        @input multiscan_proc A list with the scan process which
812
        may still be alive.
813
        @return Actualized list with current runnging scan processes
814
        """
815
        for run_target in multiscan_proc:
816
            if not run_target.is_alive():
817
                multiscan_proc.remove(run_target)
818
        return multiscan_proc
819
820
    def start_scan(self, scan_id, targets, parallel=1):
821
        """ Handle N parallel scans if 'parallel' is greater than 1. """
822
        os.setsid()
823
        multiscan_proc = []
824
        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...
825
        target_list = targets
826
        if target_list is None or not target_list:
827
            raise OSPDError('Erroneous targets list', 'start_scan')
828
829
        for index, target in enumerate(target_list):
830
            while len(multiscan_proc) >= parallel:
831
               multiscan_proc = self.check_pending_target(multiscan_proc)
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 16 spaces were expected, but 15 were found.
Loading history...
832
               time.sleep(1)
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 16 spaces were expected, but 15 were found.
Loading history...
833
834
            progress = float(index) * 100 / len(target_list)
835
            self.set_scan_progress(scan_id, int(progress))
836
            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...
837
838
            scan_process = multiprocessing.Process(target=self.parallel_scan,
839
                                               args=(scan_id, target[0]))
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 4 spaces).
Loading history...
840
            multiscan_proc.append(scan_process)
841
            scan_process.start()
842
843
        # Wait until all single target were scanned
844
        while multiscan_proc:
845
            multiscan_proc = self.check_pending_target(multiscan_proc)
846
            time.sleep(1)
847
        self.finish_scan(scan_id)
848
849
    def dry_run_scan(self, scan_id, targets):
850
        """ Dry runs a scan. """
851
852
        os.setsid()
853
        #target_list = target_str_to_list(target_str)
854
        for _, target in enumerate(targets):
855
            host = resolve_hostname(target[0])
856
            if host is None:
857
                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...
858
                continue
859
            port = self.get_scan_ports(scan_id, target=target[0])
860
            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...
861
            self.add_scan_log(scan_id, name='', host=host,
862
                              value='Dry run result')
863
        self.finish_scan(scan_id)
864
865
    def handle_timeout(self, scan_id, host):
866
        """ Handles scanner reaching timeout error. """
867
        self.add_scan_error(scan_id, host=host, name="Timeout",
868
                            value="{0} exec timeout."
869
                            .format(self.get_scanner_name()))
870
871
    def set_scan_progress(self, scan_id, progress):
872
        """ Sets scan_id scan's progress which is a number between 0 and 100. """
873
        self.scan_collection.set_progress(scan_id, progress)
874
875
    def scan_exists(self, scan_id):
876
        """ Checks if a scan with ID scan_id is in collection.
877
878
        @return: 1 if scan exists, 0 otherwise.
879
        """
880
        return self.scan_collection.id_exists(scan_id)
881
882
    def handle_get_scans_command(self, scan_et):
883
        """ Handles <get_scans> command.
884
885
        @return: Response string for <get_scans> command.
886
        """
887
888
        scan_id = scan_et.attrib.get('scan_id')
889
        details = scan_et.attrib.get('details')
890
        pop_res = scan_et.attrib.get('pop_results')
891
        if details and details == '0':
892
            details = False
893
        else:
894
            details = True
895
            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...
896
                pop_res = True
897
            else:
898
                pop_res = False
899
900
        responses = []
901
        if scan_id and scan_id in self.scan_collection.ids_iterator():
902
            self.check_scan_process(scan_id)
903
            scan = self.get_scan_xml(scan_id, details, pop_res)
904
            responses.append(scan)
905
        elif scan_id:
906
            text = "Failed to find scan '{0}'".format(scan_id)
907
            return simple_response_str('get_scans', 404, text)
908
        else:
909
            for scan_id in self.scan_collection.ids_iterator():
910
                self.check_scan_process(scan_id)
911
                scan = self.get_scan_xml(scan_id, details, pop_res)
912
                responses.append(scan)
913
        return simple_response_str('get_scans', 200, 'OK', responses)
914
915
    def handle_get_vts_command(self, vt_et):
916
        """ Handles <get_vts> command.
917
918
        @return: Response string for <get_vts> command.
919
        """
920
921
        vt_id = vt_et.attrib.get('vt_id')
922
923
        if vt_id and vt_id not in self.vts:
924
            text = "Failed to find vulnerability test '{0}'".format(vt_id)
925
            return simple_response_str('get_vts', 404, text)
926
927
        responses = []
928
929
        if vt_id:
930
            vts_xml = self.get_vts_xml(vt_id)
931
        else:
932
            vts_xml = self.get_vts_xml()
933
934
        responses.append(vts_xml)
935
936
        return simple_response_str('get_vts', 200, 'OK', responses)
937
938
    def handle_help_command(self, scan_et):
939
        """ Handles <help> command.
940
941
        @return: Response string for <help> command.
942
        """
943
        help_format = scan_et.attrib.get('format')
944
        if help_format is None or help_format == "text":
945
            # Default help format is text.
946
            return simple_response_str('help', 200, 'OK',
947
                                       self.get_help_text())
948
        elif help_format == "xml":
949
            text = self.get_xml_str(self.commands)
950
            return simple_response_str('help', 200, 'OK', text)
951
        raise OSPDError('Bogus help format', 'help')
952
953
    def get_help_text(self):
954
        """ Returns the help output in plain text format."""
955
956
        txt = str('\n')
957
        for name, info in self.commands.items():
958
            command_txt = "\t{0: <22} {1}\n".format(name, info['description'])
959
            if info['attributes']:
960
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
961
                for attrname, attrdesc in info['attributes'].items():
962
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
963
                    command_txt = ''.join([command_txt, attr_txt])
964
            if info['elements']:
965
                command_txt = ''.join([command_txt, "\t Elements:\n",
966
                                       self.elements_as_text(info['elements'])])
967
            txt = ''.join([txt, command_txt])
968
        return txt
969
970
    def elements_as_text(self, elems, indent=2):
971
        """ Returns the elems dictionary as formatted plain text. """
972
        assert elems
973
        text = ""
974
        for elename, eledesc in elems.items():
975
            if isinstance(eledesc, dict):
976
                desc_txt = self.elements_as_text(eledesc, indent + 2)
977
                desc_txt = ''.join(['\n', desc_txt])
978
            elif isinstance(eledesc, str):
979
                desc_txt = ''.join([eledesc, '\n'])
980
            else:
981
                assert False, "Only string or dictionary"
982
            ele_txt = "\t{0}{1: <22} {2}".format(' ' * indent, elename,
983
                                                 desc_txt)
0 ignored issues
show
introduced by
The variable desc_txt does not seem to be defined for all execution paths.
Loading history...
984
            text = ''.join([text, ele_txt])
985
        return text
986
987
    def handle_delete_scan_command(self, scan_et):
988
        """ Handles <delete_scan> command.
989
990
        @return: Response string for <delete_scan> command.
991
        """
992
        scan_id = scan_et.attrib.get('scan_id')
993
        if scan_id is None:
994
            return simple_response_str('delete_scan', 404,
995
                                       'No scan_id attribute')
996
997
        if not self.scan_exists(scan_id):
998
            text = "Failed to find scan '{0}'".format(scan_id)
999
            return simple_response_str('delete_scan', 404, text)
1000
        self.check_scan_process(scan_id)
1001
        if self.delete_scan(scan_id):
1002
            return simple_response_str('delete_scan', 200, 'OK')
1003
        raise OSPDError('Scan in progress', 'delete_scan')
1004
1005
    def delete_scan(self, scan_id):
1006
        """ Deletes scan_id scan from collection.
1007
1008
        @return: 1 if scan deleted, 0 otherwise.
1009
        """
1010
        try:
1011
            del self.scan_processes[scan_id]
1012
        except KeyError:
1013
            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...
1014
        return self.scan_collection.delete_scan(scan_id)
1015
1016
    def get_scan_results_xml(self, scan_id, pop_res):
1017
        """ Gets scan_id scan's results in XML format.
1018
1019
        @return: String of scan results in xml.
1020
        """
1021
        results = ET.Element('results')
1022
        for result in self.scan_collection.results_iterator(scan_id, pop_res):
1023
            results.append(get_result_xml(result))
1024
1025
        logger.info('Returning %d results', len(results))
1026
        return results
1027
1028
    def get_xml_str(self, data):
1029
        """ Creates a string in XML Format using the provided data structure.
1030
1031
        @param: Dictionary of xml tags and their elements.
1032
1033
        @return: String of data in xml format.
1034
        """
1035
1036
        responses = []
1037
        for tag, value in data.items():
1038
            elem = ET.Element(tag)
1039
            if isinstance(value, dict):
1040
                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 1037).

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...
1041
                    elem.append(value)
1042
            elif isinstance(value, list):
1043
                value = ', '.join([m for m in value])
1044
                elem.text = value
1045
            else:
1046
                elem.text = value
1047
            responses.append(elem)
1048
        return responses
1049
1050
    def get_scan_xml(self, scan_id, detailed=True, pop_res=False):
1051
        """ Gets scan in XML format.
1052
1053
        @return: String of scan in XML format.
1054
        """
1055
        if not scan_id:
1056
            return ET.Element('scan')
1057
1058
        target = self.get_scan_target(scan_id)
1059
        progress = self.get_scan_progress(scan_id)
1060
        start_time = self.get_scan_start_time(scan_id)
1061
        end_time = self.get_scan_end_time(scan_id)
1062
        response = ET.Element('scan')
1063
        for name, value in [('id', scan_id),
1064
                            ('target', target),
1065
                            ('progress', progress),
1066
                            ('start_time', start_time),
1067
                            ('end_time', end_time)]:
1068
            response.set(name, str(value))
1069
        if detailed:
1070
            response.append(self.get_scan_results_xml(scan_id, pop_res))
1071
        return response
1072
1073
    def get_custom_vt_as_xml_str(self, custom):
0 ignored issues
show
Unused Code introduced by
The argument custom seems to be unused.
Loading history...
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...
1074
        """ Create a string representation of the XML object from the
1075
        custom data object.
1076
        This needs to be implemented by each ospd wrapper, in case
1077
        custom elements for VTs are used.
1078
1079
        The custom XML object which is returned will be embedded
1080
        into a <custom></custom> element.
1081
1082
        @return: XML object as string for custom data.
1083
        """
1084
        return ''
1085
1086
    def get_params_vt_as_xml_str(self, vt_params):
0 ignored issues
show
Unused Code introduced by
The argument vt_params seems to be unused.
Loading history...
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...
1087
        """ Create a string representation of the XML object from the
1088
        vt_params data object.
1089
        This needs to be implemented by each ospd wrapper, in case
1090
        vt_params elements for VTs are used.
1091
1092
        The vt_params XML object which is returned will be embedded
1093
        into a <vt_params></vt_params> element.
1094
1095
        @return: XML object as string for vt parameters data.
1096
        """
1097
        return ''
1098
1099
    def get_vt_xml(self, vt_id):
1100
        """ Gets a single vulnerability test information in XML format.
1101
1102
        @return: String of single vulnerability test information in XML format.
1103
        """
1104
        if not vt_id:
1105
            return ET.Element('vt')
1106
1107
        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...
1108
1109
        name = vt.get('name')
1110
        vt_xml = ET.Element('vt')
1111
        vt_xml.set('id', vt_id)
1112
1113
        for name, value in [('name', name)]:
1114
            elem = ET.SubElement(vt_xml, name)
1115
            elem.text = str(value)
1116
1117
        if vt.get('custom'):
1118
            custom_xml_str = '<custom>%s</custom>' % self.get_custom_vt_as_xml_str(vt.get('custom'))
1119
            vt_xml.append(secET.fromstring(custom_xml_str))
1120
1121
        if vt.get('vt_params'):
1122
            params_xml_str = '<vt_params>%s</vt_params>' % self.get_params_vt_as_xml_str(vt.get('vt_params'))
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (109/100).

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

Loading history...
1123
            vt_xml.append(secET.fromstring(params_xml_str))
1124
1125
        return vt_xml
1126
1127
    def get_vts_xml(self, vt_id=''):
1128
        """ Gets collection of vulnerability test information in XML format.
1129
        If vt_id is specified, the collection will contain only this vt, of found.
1130
        If no vt_id is specified, the collection will contain all vts.
1131
1132
        @return: String of collection of vulnerability test information in XML format.
1133
        """
1134
1135
        vts_xml = ET.Element('vts')
1136
1137
        if vt_id != '':
1138
            vts_xml.append(self.get_vt_xml(vt_id))
1139
        else:
1140
            for vt_id in self.vts:
0 ignored issues
show
unused-code introduced by
Redefining argument with the local name 'vt_id'
Loading history...
1141
                vts_xml.append(self.get_vt_xml(vt_id))
1142
1143
        return vts_xml
1144
1145
    def handle_get_scanner_details(self):
1146
        """ Handles <get_scanner_details> command.
1147
1148
        @return: Response string for <get_scanner_details> command.
1149
        """
1150
        desc_xml = ET.Element('description')
1151
        desc_xml.text = self.get_scanner_description()
1152
        details = [
1153
            desc_xml,
1154
            self.get_scanner_params_xml()
1155
        ]
1156
        return simple_response_str('get_scanner_details', 200, 'OK', details)
1157
1158
    def handle_get_version_command(self):
1159
        """ Handles <get_version> command.
1160
1161
        @return: Response string for <get_version> command.
1162
        """
1163
        protocol = ET.Element('protocol')
1164
        for name, value in [('name', 'OSP'), ('version', self.get_protocol_version())]:
1165
            elem = ET.SubElement(protocol, name)
1166
            elem.text = value
1167
1168
        daemon = ET.Element('daemon')
1169
        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...
1170
            elem = ET.SubElement(daemon, name)
1171
            elem.text = value
1172
1173
        scanner = ET.Element('scanner')
1174
        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...
1175
            elem = ET.SubElement(scanner, name)
1176
            elem.text = value
1177
1178
        return simple_response_str('get_version', 200, 'OK', [protocol, daemon, scanner])
1179
1180
    def handle_command(self, command):
0 ignored issues
show
best-practice introduced by
Too many return statements (8/6)
Loading history...
1181
        """ Handles an osp command in a string.
1182
1183
        @return: OSP Response to command.
1184
        """
1185
        try:
1186
            tree = secET.fromstring(command)
1187
        except secET.ParseError:
1188
            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...
1189
            raise OSPDError('Invalid data')
1190
1191
        if not self.command_exists(tree.tag) and tree.tag != "authenticate":
1192
            raise OSPDError('Bogus command name')
1193
1194
        if tree.tag == "get_version":
1195
            return self.handle_get_version_command()
1196
        elif tree.tag == "start_scan":
1197
            return self.handle_start_scan_command(tree)
1198
        elif tree.tag == "stop_scan":
1199
            return self.handle_stop_scan_command(tree)
1200
        elif tree.tag == "get_scans":
1201
            return self.handle_get_scans_command(tree)
1202
        elif tree.tag == "get_vts":
1203
            return self.handle_get_vts_command(tree)
1204
        elif tree.tag == "delete_scan":
1205
            return self.handle_delete_scan_command(tree)
1206
        elif tree.tag == "help":
1207
            return self.handle_help_command(tree)
1208
        elif tree.tag == "get_scanner_details":
1209
            return self.handle_get_scanner_details()
1210
        else:
1211
            assert False, "Unhandled command: {0}".format(tree.tag)
1212
1213
    def check(self):
1214
        """ Asserts to False. Should be implemented by subclass. """
1215
        raise NotImplementedError
1216
1217
    def run(self, address, port, unix_path):
1218
        """ Starts the Daemon, handling commands until interrupted.
1219
1220
        @return False if error. Runs indefinitely otherwise.
1221
        """
1222
        assert address or unix_path
1223
        if unix_path:
1224
            sock = bind_unix_socket(unix_path)
1225
        else:
1226
            sock = bind_socket(address, port)
1227
        if sock is None:
1228
            return False
1229
1230
        try:
1231
            while True:
1232
                if unix_path:
1233
                    client_stream, _ = sock.accept()
1234
                    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...
1235
                    self.handle_client_stream(client_stream, True)
1236
                else:
1237
                    client_stream = self.new_client_stream(sock)
1238
                    if client_stream is None:
1239
                        continue
1240
                    self.handle_client_stream(client_stream, False)
1241
                close_client_stream(client_stream, unix_path)
1242
        except KeyboardInterrupt:
1243
            logger.info("Received Ctrl-C shutting-down ...")
1244
        finally:
1245
            sock.shutdown(socket.SHUT_RDWR)
1246
            sock.close()
1247
1248
    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...
1249
        """ Creates a new scan.
1250
1251
        @target: Target to scan.
1252
        @options: Miscellaneous scan options.
1253
1254
        @return: New scan's ID.
1255
        """
1256
        return self.scan_collection.create_scan(scan_id, targets, target_str, options, vts)
1257
1258
    def get_scan_options(self, scan_id):
1259
        """ Gives a scan's list of options. """
1260
        return self.scan_collection.get_options(scan_id)
1261
1262
    def set_scan_option(self, scan_id, name, value):
1263
        """ Sets a scan's option to a provided value. """
1264
        return self.scan_collection.set_option(scan_id, name, value)
1265
1266
    def check_scan_process(self, scan_id):
1267
        """ Check the scan's process, and terminate the scan if not alive. """
1268
        scan_process = self.scan_processes[scan_id]
1269
        progress = self.get_scan_progress(scan_id)
1270
        if progress < 100 and not scan_process.is_alive():
1271
            self.set_scan_progress(scan_id, 100)
1272
            self.add_scan_error(scan_id, name="", host="",
1273
                                value="Scan process failure.")
1274
            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...
1275
        elif progress == 100:
1276
            scan_process.join()
1277
1278
    def get_scan_progress(self, scan_id):
1279
        """ Gives a scan's current progress value. """
1280
        return self.scan_collection.get_progress(scan_id)
1281
1282
    def get_scan_target(self, scan_id):
1283
        """ Gives a scan's target. """
1284
        return self.scan_collection.get_target(scan_id)
1285
1286
    def get_scan_ports(self, scan_id, target=''):
1287
        """ Gives a scan's ports list. """
1288
        return self.scan_collection.get_ports(scan_id, target)
1289
1290
    def get_scan_credentials(self, scan_id, target=''):
1291
        """ Gives a scan's credential list. If a target is passed gives
1292
        the credential list for the given target. """
1293
        return self.scan_collection.get_credentials(scan_id, target)
1294
1295
    def get_scan_vts(self, scan_id):
1296
        """ Gives a scan's vts list. """
1297
        return self.scan_collection.get_vts(scan_id)
1298
1299
    def get_scan_start_time(self, scan_id):
1300
        """ Gives a scan's start time. """
1301
        return self.scan_collection.get_start_time(scan_id)
1302
1303
    def get_scan_end_time(self, scan_id):
1304
        """ Gives a scan's end time. """
1305
        return self.scan_collection.get_end_time(scan_id)
1306
1307
    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...
1308
                     test_id='', qod=''):
1309
        """ Adds a log result to scan_id scan. """
1310
        self.scan_collection.add_result(scan_id, ResultType.LOG, host, name,
1311
                                        value, port, test_id, 0.0, qod)
1312
1313
    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...
1314
        """ Adds an error result to scan_id scan. """
1315
        self.scan_collection.add_result(scan_id, ResultType.ERROR, host, name,
1316
                                        value, port)
1317
1318
    def add_scan_host_detail(self, scan_id, host='', name='', value=''):
1319
        """ Adds a host detail result to scan_id scan. """
1320
        self.scan_collection.add_result(scan_id, ResultType.HOST_DETAIL, host,
1321
                                        name, value)
1322
1323
    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...
1324
                       test_id='', severity='', qod=''):
1325
        """ Adds an alarm result to scan_id scan. """
1326
        self.scan_collection.add_result(scan_id, ResultType.ALARM, host, name,
1327
                                        value, port, test_id, severity, qod)
1328