Passed
Pull Request — master (#60)
by
unknown
02:00
created

ospd.ospd.OSPDaemon.handle_help_command()   A

Complexity

Conditions 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 14
Ratio 100 %

Importance

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

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

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

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

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

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

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

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

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

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

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