Completed
Push — master ( 0fab9f...a1fb13 )
by Juan José
13s queued 10s
created

ospd.ospd.OSPDaemon.scheduler()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 2
Ratio 100 %

Importance

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

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

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

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

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

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

Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_modification_time seems to be unused.
Loading history...
1231
        """ Create a string representation of the XML object from the
1232
        vt_modification_time data object.
1233
        This needs to be implemented by each ospd wrapper, in case
1234
        vt_modification_time elements for VTs are used.
1235
1236
        The vt_modification_time XML object which is returned will be embedded
1237
        into a <vt_modification_time></vt_modification_time> element.
1238
1239
        @return: XML object as string for vt references data.
1240
        """
1241
        return ''
1242
1243
    @staticmethod
1244
    def get_summary_vt_as_xml_str(vt_id, summary):
0 ignored issues
show
Unused Code introduced by
The argument summary seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1245
        """ Create a string representation of the XML object from the
1246
        summary data object.
1247
        This needs to be implemented by each ospd wrapper, in case
1248
        summary elements for VTs are used.
1249
1250
        The summary XML object which is returned will be embedded
1251
        into a <summary></summary> element.
1252
1253
        @return: XML object as string for summary data.
1254
        """
1255
        return ''
1256
1257
    @staticmethod
1258
    def get_impact_vt_as_xml_str(vt_id, impact):
0 ignored issues
show
Unused Code introduced by
The argument impact seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1259
        """ Create a string representation of the XML object from the
1260
        impact data object.
1261
        This needs to be implemented by each ospd wrapper, in case
1262
        impact elements for VTs are used.
1263
1264
        The impact XML object which is returned will be embedded
1265
        into a <impact></impact> element.
1266
1267
        @return: XML object as string for impact data.
1268
        """
1269
        return ''
1270
1271
    @staticmethod
1272
    def get_affected_vt_as_xml_str(vt_id, affected):
0 ignored issues
show
Unused Code introduced by
The argument affected seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1273
        """ Create a string representation of the XML object from the
1274
        affected data object.
1275
        This needs to be implemented by each ospd wrapper, in case
1276
        affected elements for VTs are used.
1277
1278
        The affected XML object which is returned will be embedded
1279
        into a <affected></affected> element.
1280
1281
        @return: XML object as string for affected data.
1282
        """
1283
        return ''
1284
1285
    @staticmethod
1286
    def get_insight_vt_as_xml_str(vt_id, insight):
0 ignored issues
show
Unused Code introduced by
The argument insight seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1287
        """ Create a string representation of the XML object from the
1288
        insight data object.
1289
        This needs to be implemented by each ospd wrapper, in case
1290
        insight elements for VTs are used.
1291
1292
        The insight XML object which is returned will be embedded
1293
        into a <insight></insight> element.
1294
1295
        @return: XML object as string for insight data.
1296
        """
1297
        return ''
1298
1299
    @staticmethod
1300
    def get_solution_vt_as_xml_str(vt_id, solution, solution_type=None):
0 ignored issues
show
Unused Code introduced by
The argument solution_type seems to be unused.
Loading history...
Unused Code introduced by
The argument solution seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1301
        """ Create a string representation of the XML object from the
1302
        solution data object.
1303
        This needs to be implemented by each ospd wrapper, in case
1304
        solution elements for VTs are used.
1305
1306
        The solution XML object which is returned will be embedded
1307
        into a <solution></solution> element.
1308
1309
        @return: XML object as string for solution data.
1310
        """
1311
        return ''
1312
1313
    @staticmethod
1314
    def get_detection_vt_as_xml_str(vt_id, detection=None, qod_type=None, qod=None):
0 ignored issues
show
Unused Code introduced by
The argument qod seems to be unused.
Loading history...
Unused Code introduced by
The argument qod_type seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
Unused Code introduced by
The argument detection seems to be unused.
Loading history...
1315
        """ Create a string representation of the XML object from the
1316
        detection data object.
1317
        This needs to be implemented by each ospd wrapper, in case
1318
        detection elements for VTs are used.
1319
1320
        The detection XML object which is returned is an element with
1321
        tag <detection></detection> element
1322
1323
        @return: XML object as string for detection data.
1324
        """
1325
        return ''
1326
1327
    @staticmethod
1328
    def get_severities_vt_as_xml_str(vt_id, severities):
0 ignored issues
show
Unused Code introduced by
The argument severities seems to be unused.
Loading history...
Unused Code introduced by
The argument vt_id seems to be unused.
Loading history...
1329
        """ Create a string representation of the XML object from the
1330
        severities data object.
1331
        This needs to be implemented by each ospd wrapper, in case
1332
        severities elements for VTs are used.
1333
1334
        The severities XML objects which are returned will be embedded
1335
        into a <severities></severities> element.
1336
1337
        @return: XML object as string for severities data.
1338
        """
1339
        return ''
1340
1341
    def get_vt_xml(self, vt_id):
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (24/15).
Loading history...
1342
        """ Gets a single vulnerability test information in XML format.
1343
1344
        @return: String of single vulnerability test information in XML format.
1345
        """
1346
        if not vt_id:
1347
            return Element('vt')
1348
1349
        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...
1350
1351
        name = vt.get('name')
1352
        vt_xml = Element('vt')
1353
        vt_xml.set('id', vt_id)
1354
1355
        for name, value in [('name', name)]:
1356
            elem = SubElement(vt_xml, name)
1357
            elem.text = str(value)
1358
1359
        if vt.get('vt_params'):
1360
            params_xml_str = (
1361
                '<vt_params>%s</vt_params>' % self.get_params_vt_as_xml_str(
1362
                    vt_id, vt.get('vt_params')))
1363
            vt_xml.append(secET.fromstring(params_xml_str))
1364
1365
        if vt.get('vt_refs'):
1366
            refs_xml_str = (
1367
                '<vt_refs>%s</vt_refs>' % self.get_refs_vt_as_xml_str(
1368
                    vt_id, vt.get('vt_refs')))
1369
            vt_xml.append(secET.fromstring(refs_xml_str))
1370
1371
        if vt.get('vt_dependencies'):
1372
            dependencies = self.get_dependencies_vt_as_xml_str(
1373
                vt_id, vt.get('vt_dependencies'))
1374
            deps_xml_str = (
1375
                '<dependencies>%s</dependencies>' % dependencies)
1376
            vt_xml.append(secET.fromstring(deps_xml_str))
1377
1378
        if vt.get('creation_time'):
1379
            vt_ctime = self.get_creation_time_vt_as_xml_str(
1380
                vt_id, vt.get('creation_time'))
1381
            creation_time_xml_str = (
1382
                '<creation_time>%s</creation_time>' % vt_ctime)
1383
            vt_xml.append(secET.fromstring(creation_time_xml_str))
1384
1385
        if vt.get('modification_time'):
1386
            vt_mtime = self.get_modification_time_vt_as_xml_str(
1387
                vt_id, vt.get('modification_time'))
1388
            modification_time_xml_str = (
1389
                '<modification_time>%s</modification_time>' % vt_mtime)
1390
            vt_xml.append(secET.fromstring(modification_time_xml_str))
1391
1392
        if vt.get('summary'):
1393
            summary_xml_str = self.get_summary_vt_as_xml_str(
1394
                vt_id, vt.get('summary'))
1395
            vt_xml.append(secET.fromstring(summary_xml_str))
1396
1397
        if vt.get('impact'):
1398
            impact_xml_str = self.get_impact_vt_as_xml_str(
1399
                vt_id, vt.get('impact'))
1400
            vt_xml.append(secET.fromstring(impact_xml_str))
1401
1402
        if vt.get('affected'):
1403
            affected_xml_str = self.get_affected_vt_as_xml_str(
1404
                vt_id, vt.get('affected'))
1405
            vt_xml.append(secET.fromstring(affected_xml_str))
1406
1407
        if vt.get('insight'):
1408
            insight_xml_str = self.get_insight_vt_as_xml_str(
1409
                vt_id, vt.get('insight'))
1410
            vt_xml.append(secET.fromstring(insight_xml_str))
1411
1412
        if vt.get('solution'):
1413
            solution_xml_str = self.get_solution_vt_as_xml_str(
1414
                vt_id, vt.get('solution'), vt.get('solution_type'))
1415
            vt_xml.append(secET.fromstring(solution_xml_str))
1416
1417
        if vt.get('detection') or vt.get('qod_type') or vt.get('qod'):
1418
            detection_xml_str = self.get_detection_vt_as_xml_str(
1419
                vt_id, vt.get('detection'), vt.get('qod_type'), vt.get('qod'))
1420
            vt_xml.append(secET.fromstring(detection_xml_str))
1421
1422
        if vt.get('severities'):
1423
            severities_xml_str = (
1424
                '<severities>%s</severities>' % self.get_severities_vt_as_xml_str(
1425
                    vt_id, vt.get('severities')))
1426
            vt_xml.append(secET.fromstring(severities_xml_str))
1427
1428
        if vt.get('custom'):
1429
            custom_xml_str = (
1430
                '<custom>%s</custom>' % self.get_custom_vt_as_xml_str(
1431
                    vt_id, vt.get('custom')))
1432
            vt_xml.append(secET.fromstring(custom_xml_str))
1433
1434
        return vt_xml
1435
1436
    def get_vts_xml(self, vt_id=''):
1437
        """ Gets collection of vulnerability test information in XML format.
1438
        If vt_id is specified, the collection will contain only this vt, of found.
1439
        If no vt_id is specified, the collection will contain all vts.
1440
1441
        @return: String of collection of vulnerability test information in XML format.
1442
        """
1443
1444
        vts_xml = Element('vts')
1445
1446
        if vt_id != '':
1447
            vts_xml.append(self.get_vt_xml(vt_id))
1448
        else:
1449
            for vt_id in self.vts:
0 ignored issues
show
unused-code introduced by
Redefining argument with the local name 'vt_id'
Loading history...
1450
                vts_xml.append(self.get_vt_xml(vt_id))
1451
1452
        return vts_xml
1453
1454
    def handle_get_scanner_details(self):
1455
        """ Handles <get_scanner_details> command.
1456
1457
        @return: Response string for <get_scanner_details> command.
1458
        """
1459
        desc_xml = Element('description')
1460
        desc_xml.text = self.get_scanner_description()
1461
        details = [
1462
            desc_xml,
1463
            self.get_scanner_params_xml()
1464
        ]
1465
        return simple_response_str('get_scanner_details', 200, 'OK', details)
1466
1467
    def handle_get_version_command(self):
1468
        """ Handles <get_version> command.
1469
1470
        @return: Response string for <get_version> command.
1471
        """
1472
        protocol = Element('protocol')
1473
        for name, value in [('name', 'OSP'), ('version', self.get_protocol_version())]:
1474
            elem = SubElement(protocol, name)
1475
            elem.text = value
1476
1477
        daemon = Element('daemon')
1478
        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...
1479
            elem = SubElement(daemon, name)
1480
            elem.text = value
1481
1482
        scanner = Element('scanner')
1483
        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...
1484
            elem = SubElement(scanner, name)
1485
            elem.text = value
1486
1487
        content = [protocol, daemon, scanner]
1488
1489
        if self.get_vts_version():
1490
            vts = Element('vts')
1491
            elem = SubElement(vts, 'version')
1492
            elem.text = self.get_vts_version()
1493
            content.append(vts)
1494
1495
        return simple_response_str('get_version', 200, 'OK', content)
1496
1497
    def handle_command(self, command):
0 ignored issues
show
best-practice introduced by
Too many return statements (8/6)
Loading history...
1498
        """ Handles an osp command in a string.
1499
1500
        @return: OSP Response to command.
1501
        """
1502
        try:
1503
            tree = secET.fromstring(command)
1504
        except secET.ParseError:
1505
            logger.debug("Erroneous client input: %s", command)
1506
            raise OSPDError('Invalid data')
1507
1508
        if not self.command_exists(tree.tag) and tree.tag != "authenticate":
1509
            raise OSPDError('Bogus command name')
1510
1511
        if tree.tag == "get_version":
1512
            return self.handle_get_version_command()
1513
        elif tree.tag == "start_scan":
1514
            return self.handle_start_scan_command(tree)
1515
        elif tree.tag == "stop_scan":
1516
            return self.handle_stop_scan_command(tree)
1517
        elif tree.tag == "get_scans":
1518
            return self.handle_get_scans_command(tree)
1519
        elif tree.tag == "get_vts":
1520
            return self.handle_get_vts_command(tree)
1521
        elif tree.tag == "delete_scan":
1522
            return self.handle_delete_scan_command(tree)
1523
        elif tree.tag == "help":
1524
            return self.handle_help_command(tree)
1525
        elif tree.tag == "get_scanner_details":
1526
            return self.handle_get_scanner_details()
1527
        else:
1528
            assert False, "Unhandled command: {0}".format(tree.tag)
1529
1530
    def check(self):
1531
        """ Asserts to False. Should be implemented by subclass. """
1532
        raise NotImplementedError
1533
1534
    def run(self, address, port, unix_path):
1535
        """ Starts the Daemon, handling commands until interrupted.
1536
1537
        @return False if error. Runs indefinitely otherwise.
1538
        """
1539
        assert address or unix_path
1540
        if unix_path:
1541
            sock = bind_unix_socket(unix_path)
1542
        else:
1543
            sock = bind_socket(address, port)
1544
        if sock is None:
1545
            return False
1546
1547
        sock.setblocking(False)
1548
        inputs = [sock]
1549
        outputs = []
1550
        try:
0 ignored issues
show
unused-code introduced by
Too many nested blocks (6/5)
Loading history...
1551
            while True:
1552
                readable, _, _ = select.select(
1553
                    inputs, outputs, inputs, SCHEDULER_CHECK_PERIOD)
1554
                for r_socket in readable:
1555
                    if unix_path and r_socket is sock:
1556
                        client_stream, _ = sock.accept()
1557
                        logger.debug("New connection from %s", unix_path)
1558
                        self.handle_client_stream(client_stream, True)
1559
                    else:
1560
                        client_stream = self.new_client_stream(sock)
1561
                        if client_stream is None:
1562
                            continue
1563
                        self.handle_client_stream(client_stream, False)
1564
                    close_client_stream(client_stream, unix_path)
1565
                self.scheduler()
1566
        except KeyboardInterrupt:
1567
            logger.info("Received Ctrl-C shutting-down ...")
1568
        finally:
1569
            sock.shutdown(socket.SHUT_RDWR)
1570
            sock.close()
1571
1572
    def scheduler(self):
1573
        """ Should be implemented by subclass in case of need
1574
        to run tasks periodically. """
1575
1576
    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...
1577
        """ Creates a new scan.
1578
1579
        @target: Target to scan.
1580
        @options: Miscellaneous scan options.
1581
1582
        @return: New scan's ID.
1583
        """
1584
        return self.scan_collection.create_scan(scan_id, targets, target_str, options, vts)
1585
1586
    def get_scan_options(self, scan_id):
1587
        """ Gives a scan's list of options. """
1588
        return self.scan_collection.get_options(scan_id)
1589
1590
    def set_scan_option(self, scan_id, name, value):
1591
        """ Sets a scan's option to a provided value. """
1592
        return self.scan_collection.set_option(scan_id, name, value)
1593
1594
    def check_scan_process(self, scan_id):
1595
        """ Check the scan's process, and terminate the scan if not alive. """
1596
        scan_process = self.scan_processes[scan_id]
1597
        progress = self.get_scan_progress(scan_id)
1598
        if progress < 100 and not scan_process.is_alive():
1599
            self.set_scan_progress(scan_id, 100)
1600
            self.add_scan_error(scan_id, name="", host="",
1601
                                value="Scan process failure.")
1602
            logger.info("%s: Scan terminated.", scan_id)
1603
        elif progress == 100:
1604
            scan_process.join()
1605
1606
    def get_scan_progress(self, scan_id):
1607
        """ Gives a scan's current progress value. """
1608
        return self.scan_collection.get_progress(scan_id)
1609
1610
    def get_scan_target_progress(self, scan_id):
1611
        """ Gives a list with scan's current progress value of each target. """
1612
        return self.scan_collection.get_target_progress(scan_id)
1613
1614
    def get_scan_target(self, scan_id):
1615
        """ Gives a scan's target. """
1616
        return self.scan_collection.get_target(scan_id)
1617
1618
    def get_scan_ports(self, scan_id, target=''):
1619
        """ Gives a scan's ports list. """
1620
        return self.scan_collection.get_ports(scan_id, target)
1621
1622
    def get_scan_credentials(self, scan_id, target=''):
1623
        """ Gives a scan's credential list. If a target is passed gives
1624
        the credential list for the given target. """
1625
        return self.scan_collection.get_credentials(scan_id, target)
1626
1627
    def get_scan_vts(self, scan_id):
1628
        """ Gives a scan's vts list. """
1629
        return self.scan_collection.get_vts(scan_id)
1630
1631
    def get_scan_start_time(self, scan_id):
1632
        """ Gives a scan's start time. """
1633
        return self.scan_collection.get_start_time(scan_id)
1634
1635
    def get_scan_end_time(self, scan_id):
1636
        """ Gives a scan's end time. """
1637
        return self.scan_collection.get_end_time(scan_id)
1638
1639
    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...
1640
                     test_id='', qod=''):
1641
        """ Adds a log result to scan_id scan. """
1642
        self.scan_collection.add_result(scan_id, ResultType.LOG, host, name,
1643
                                        value, port, test_id, 0.0, qod)
1644
1645
    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...
1646
        """ Adds an error result to scan_id scan. """
1647
        self.scan_collection.add_result(scan_id, ResultType.ERROR, host, name,
1648
                                        value, port)
1649
1650
    def add_scan_host_detail(self, scan_id, host='', name='', value=''):
1651
        """ Adds a host detail result to scan_id scan. """
1652
        self.scan_collection.add_result(scan_id, ResultType.HOST_DETAIL, host,
1653
                                        name, value)
1654
1655
    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...
1656
                       test_id='', severity='', qod=''):
1657
        """ Adds an alarm result to scan_id scan. """
1658
        self.scan_collection.add_result(scan_id, ResultType.ALARM, host, name,
1659
                                        value, port, test_id, severity, qod)
1660