Completed
Push — master ( 144b8b...7404f6 )
by
unknown
15s
created

ospd.ospd.OSPDaemon.handle_get_scanner_details()   A

Complexity

Conditions 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 12
Ratio 100 %

Importance

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

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

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

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

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

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

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

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

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

Loading history...
286
287
    def set_command_attributes(self, name, attributes):
288
        """ Sets the xml attributes of a specified command. """
289
        if self.command_exists(name):
290
            command = self.commands.get(name)
291
            command['attributes'] = attributes
292
293
    def add_scanner_param(self, name, scanner_param):
294
        """ Add a scanner parameter. """
295
296
        assert name
297
        assert scanner_param
298
        self.scanner_params[name] = scanner_param
299
        command = self.commands.get('start_scan')
300
        command['elements'] = {
301
            'scanner_params':
302
                {k: v['name'] for k, v in self.scanner_params.items()}}
303
304
    def add_vt(self, vt_id, name='', vt_params=None, custom=None):
305
        """ Add a vulnerability test information.
306
307
        Returns: The new number of stored VTs.
308
        -1 in case the VT ID was already present and thus the
309
        new VT was not considered.
310
        -2 in case the vt_id was invalid.
311
        """
312
313
        if not vt_id:
314
            return -2  # no valid vt_id
315
316
        if self.vt_id_pattern.fullmatch(vt_id) is None:
317
            return -2  # no valid vt_id
318
319
        if vt_id in self.vts:
320
            return -1  # The VT was already in the list.
321
322
        self.vts[vt_id] = {'name': name}
323
        if custom is not None:
324
            self.vts[vt_id]["custom"] = custom
325
        if vt_params is not None:
326
            self.vts[vt_id]["vt_params"] = vt_params
327
328
        return len(self.vts)
329
330
    def command_exists(self, name):
331
        """ Checks if a commands exists. """
332
        return name in self.commands.keys()
333
334
    def get_scanner_name(self):
335
        """ Gives the wrapped scanner's name. """
336
        return self.scanner_info['name']
337
338
    def get_scanner_version(self):
339
        """ Gives the wrapped scanner's version. """
340
        return self.scanner_info['version']
341
342
    def get_scanner_description(self):
343
        """ Gives the wrapped scanner's description. """
344
        return self.scanner_info['description']
345
346
    def get_server_version(self):
347
        """ Gives the specific OSP server's version. """
348
        assert self.server_version
349
        return self.server_version
350
351
    def get_protocol_version(self):
352
        """ Gives the OSP's version. """
353
        return self.protocol_version
354
355
    def _preprocess_scan_params(self, xml_params):
356
        """ Processes the scan parameters. """
357
        params = {}
358
        for param in xml_params:
359
            params[param.tag] = param.text or ''
360
        # Set default values.
361
        for key in self.scanner_params:
362
            if key not in params:
363
                params[key] = self.get_scanner_param_default(key)
364
                if self.get_scanner_param_type(key) == 'selection':
365
                    params[key] = params[key].split('|')[0]
366
        # Validate values.
367
        for key in params:
368
            param_type = self.get_scanner_param_type(key)
369
            if not param_type:
370
                continue
371
            if param_type in ['integer', 'boolean']:
372
                try:
373
                    params[key] = int(params[key])
374
                except ValueError:
375
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
376
            if param_type == 'boolean':
377
                if params[key] not in [0, 1]:
378
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
379
            elif param_type == 'selection':
380
                selection = self.get_scanner_param_default(key).split('|')
381
                if params[key] not in selection:
382
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
383
            if self.get_scanner_param_mandatory(key) and params[key] == '':
384
                    raise OSPDError('Mandatory %s value is missing' % key,
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 16 spaces were expected, but 20 were found.
Loading history...
385
                                    'start_scan')
386
        return params
387
388
    def process_scan_params(self, params):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
389
        """ This method is to be overridden by the child classes if necessary
390
        """
391
        return params
392
393
    def process_vts_params(self, scanner_vts):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
394
        """ Receive an XML object with the Vulnerability Tests an their
395
        parameters to be use in a scan and return a dictionary.
396
397
        @param: XML element with vt subelements. Each vt has an
398
                id attribute. Optinal parameters can be included
399
                as vt child.
400
                Example form:
401
                <vts>
402
                  <vt id='vt1' />
403
                  <vt id='vt2'>
404
                    <vt_param name='param1' type='type'>value</vt_param>
405
                  </vt>
406
                  <vtgroup filter='family = debian'/>
407
                  <vtgroup filter='family = general'/>
408
                <vts>
409
410
        @return: Dictionary containing the vts attribute and subelements,
411
                 like the VT's id and VT's parameters.
412
                 Example form:
413
                 {'v1',
414
                  'vt2': {param1: {'type': type', 'value': value}},
415
                  'vtgroups': ['family = debian', 'family = general']}
416
        """
417
        vts = {}
418
        filters = list()
419
        for vt in scanner_vts:
0 ignored issues
show
Coding Style Naming introduced by
The name vt does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

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

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

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

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

Loading history...
420
            if vt.tag == 'vt':
421
                vt_id = vt.attrib.get('id')
422
                vts[vt_id] = {}
423
                for param in vt:
424
                    if not param.attrib.get('name'):
425
                        raise OSPDError('Invalid VT parameter. No parameter name',
426
                                        'start_scan')
427
                    ptype = param.attrib.get('type', 'entry')
428
                    pvalue = param.text if param.text else ''
429
                    pname = param.attrib.get('name')
430
                    vts[vt_id][pname] = {'type': ptype, 'value': pvalue}
431
            if vt.tag == 'vtgroup':
432
                vts_filter = vt.attrib.get('filter', None)
433
                if vts_filter is None:
434
                    raise OSPDError('Invalid VT group. No filter given.',
435
                                    'start_scan')
436
                filters.append(vts_filter)
437
        vts['vtgroups'] = filters
438
        return vts
439
440
    @staticmethod
441
    def process_credentials_elements(cred_tree):
442
        """ Receive an XML object with the credentials to run
443
        a scan against a given target.
444
445
        @param:
446
        <credentials>
447
          <credential type="up" service="ssh" port="22">
448
            <username>scanuser</username>
449
            <password>mypass</password>
450
          </credential>
451
          <credential type="up" service="smb">
452
            <username>smbuser</username>
453
            <password>mypass</password>
454
          </credential>
455
        </credentials>
456
457
        @return: Dictionary containing the credentials for a given target.
458
                 Example form:
459
                 {'ssh': {'type': type,
460
                          'port': port,
461
                          'username': username,
462
                          'password': pass,
463
                        },
464
                  'smb': {'type': type,
465
                          'username': username,
466
                          'password': pass,
467
                         },
468
                   }
469
        """
470
        credentials = {}
471
        for credential in cred_tree:
472
            service = credential.attrib.get('service')
473
            credentials[service] = {}
474
            credentials[service]['type'] = credential.attrib.get('type')
475
            if service == 'ssh':
476
                credentials[service]['port'] = credential.attrib.get('port')
477
            for param in credential:
478
                credentials[service][param.tag] = param.text
479
480
        return credentials
481
482
    @classmethod
483
    def process_targets_element(cls, scanner_target):
484
        """ Receive an XML object with the target, ports and credentials to run
485
        a scan against.
486
487
        @param: XML element with target subelements. Each target has <hosts>
488
        and <ports> subelements. Hosts can be a single host, a host range,
489
        a comma-separated host list or a network address.
490
        <ports> and  <credentials> are optional. Therefore each ospd-scanner
491
        should check for a valid ones if needed.
492
493
                Example form:
494
                <targets>
495
                  <target>
496
                    <hosts>localhosts</hosts>
497
                    <ports>80,443</ports>
498
                  </target>
499
                  <target>
500
                    <hosts>192.168.0.0/24</hosts>
501
                    <ports>22</ports>
502
                    <credentials>
503
                      <credential type="up" service="ssh" port="22">
504
                        <username>scanuser</username>
505
                        <password>mypass</password>
506
                      </credential>
507
                      <credential type="up" service="smb">
508
                        <username>smbuser</username>
509
                        <password>mypass</password>
510
                      </credential>
511
                    </credentials>
512
                  </target>
513
                </targets>
514
515
        @return: A list of (hosts, port) tuples.
516
                 Example form:
517
                 [['localhost', '80,43'],
518
                  ['192.168.0.0/24', '22', {'smb': {'type': type,
519
                                                    'port': port,
520
                                                    'username': username,
521
                                                    'password': pass,
522
                                                   }}]]
523
        """
524
525
        target_list = []
526
        for target in scanner_target:
527
            ports= ''
0 ignored issues
show
Coding Style introduced by
Exactly one space required before assignment
Loading history...
528
            credentials = {}
529
            for child in target:
530
                if child.tag == 'hosts':
531
                    hosts = child.text
532
                if child.tag == 'ports':
533
                    ports = child.text
534
                if child.tag == 'credentials':
535
                    credentials = cls.process_credentials_elements(child)
536
            if hosts:
0 ignored issues
show
introduced by
The variable hosts does not seem to be defined for all execution paths.
Loading history...
537
                target_list.append([hosts, ports, credentials])
538
            else:
539
                raise OSPDError('No target to scan', 'start_scan')
540
541
        return target_list
542
543
    def handle_start_scan_command(self, scan_et):
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (18/15).
Loading history...
544
        """ Handles <start_scan> command.
545
546
        @return: Response string for <start_scan> command.
547
        """
548
549
        target_str = scan_et.attrib.get('target')
550
        ports_str = scan_et.attrib.get('ports')
551
        # For backward compatibility, if target and ports attributes are set,
552
        # <targets> element is ignored.
553
        if target_str is None or ports_str is None:
554
            target_list = scan_et.find('targets')
555
            if target_list is None or not target_list:
556
                raise OSPDError('No targets or ports', 'start_scan')
557
            else:
558
                scan_targets = self.process_targets_element(target_list)
559
        else:
560
            scan_targets = []
561
            for single_target in target_str_to_list(target_str):
562
                scan_targets.append([single_target, ports_str, ''])
563
564
        scan_id = scan_et.attrib.get('scan_id')
565
        if scan_id is not None and scan_id != '' and not valid_uuid(scan_id):
566
            raise OSPDError('Invalid scan_id UUID', 'start_scan')
567
568
        try:
569
            parallel = int(scan_et.attrib.get('parallel', '1'))
570
            if parallel < 1 or parallel > 20:
571
                parallel = 1
572
        except ValueError:
573
            raise OSPDError('Invalid value for parallel scans. '
574
                            'It must be a number', 'start_scan')
575
576
        scanner_params = scan_et.find('scanner_params')
577
        if scanner_params is None:
578
            raise OSPDError('No scanner_params element', 'start_scan')
579
580
        params = self._preprocess_scan_params(scanner_params)
581
582
        # VTS is an optional element. If present should not be empty.
583
        vts = {}
584
        scanner_vts = scan_et.find('vts')
585
        if scanner_vts is not None:
586
            if not scanner_vts:
587
                raise OSPDError('VTs list is empty', 'start_scan')
588
            else:
589
                vts = self.process_vts_params(scanner_vts)
590
591
        # Dry run case.
592
        if 'dry_run' in params and int(params['dry_run']):
593
            scan_func = self.dry_run_scan
594
            scan_params = None
595
        else:
596
            scan_func = self.start_scan
597
            scan_params = self.process_scan_params(params)
598
599
        scan_id = self.create_scan(scan_id, scan_targets, target_str, scan_params, vts)
600
        scan_process = multiprocessing.Process(target=scan_func,
601
                                               args=(scan_id,
602
                                                     scan_targets,
603
                                                     parallel))
604
        self.scan_processes[scan_id] = scan_process
605
        scan_process.start()
606
        id_ = ET.Element('id')
607
        id_.text = scan_id
608
        return simple_response_str('start_scan', 200, 'OK', id_)
609
610
    def handle_stop_scan_command(self, scan_et):
611
        """ Handles <stop_scan> command.
612
613
        @return: Response string for <stop_scan> command.
614
        """
615
616
        scan_id = scan_et.attrib.get('scan_id')
617
        if scan_id is None or scan_id == '':
618
            raise OSPDError('No scan_id attribute', 'stop_scan')
619
        scan_process = self.scan_processes.get(scan_id)
620
        if not scan_process:
621
            raise OSPDError('Scan not found {0}.'.format(scan_id), 'stop_scan')
622
        if not scan_process.is_alive():
623
            raise OSPDError('Scan already stopped or finished.', 'stop_scan')
624
625
        logger.info('{0}: Scan stopping {1}.'.format(scan_id, scan_process.ident))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
626
        self.stop_scan(scan_id)
627
        scan_process.terminate()
628
        os.killpg(os.getpgid(scan_process.ident), 15)
629
        scan_process.join()
630
        self.set_scan_progress(scan_id, 100)
631
        self.add_scan_log(scan_id, name='', host='', value='Scan stopped.')
632
        logger.info('{0}: Scan stopped.'.format(scan_id))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
633
        return simple_response_str('stop_scan', 200, 'OK')
634
635
    def stop_scan(self, scan_id):
636
        """ Should be implemented by subclass in case of a clean up before
637
        terminating is needed. """
638
639
    def exec_scan(self, scan_id, target):
640
        """ Asserts to False. Should be implemented by subclass. """
641
        raise NotImplementedError
642
643
    def finish_scan(self, scan_id):
644
        """ Sets a scan as finished. """
645
        self.set_scan_progress(scan_id, 100)
646
        logger.info("{0}: Scan finished.".format(scan_id))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
647
648
    def get_daemon_name(self):
649
        """ Gives osp daemon's name. """
650
        return self.daemon_info['name']
651
652
    def get_daemon_version(self):
653
        """ Gives osp daemon's version. """
654
        return self.daemon_info['version']
655
656
    def get_scanner_param_type(self, param):
657
        """ Returns type of a scanner parameter. """
658
        assert isinstance(param, str)
659
        entry = self.scanner_params.get(param)
660
        if not entry:
661
            return None
662
        return entry.get('type')
663
664
    def get_scanner_param_mandatory(self, param):
665
        """ Returns if a scanner parameter is mandatory. """
666
        assert isinstance(param, str)
667
        entry = self.scanner_params.get(param)
668
        if not entry:
669
            return False
670
        return entry.get('mandatory')
671
672
    def get_scanner_param_default(self, param):
673
        """ Returns default value of a scanner parameter. """
674
        assert isinstance(param, str)
675
        entry = self.scanner_params.get(param)
676
        if not entry:
677
            return None
678
        return entry.get('default')
679
680
    def get_scanner_params_xml(self):
681
        """ Returns the OSP Daemon's scanner params in xml format. """
682
        scanner_params = ET.Element('scanner_params')
683
        for param_id, param in self.scanner_params.items():
684
            param_xml = ET.SubElement(scanner_params, 'scanner_param')
685
            for name, value in [('id', param_id),
686
                                ('type', param['type'])]:
687
                param_xml.set(name, value)
688
            for name, value in [('name', param['name']),
689
                                ('description', param['description']),
690
                                ('default', param['default']),
691
                                ('mandatory', param['mandatory'])]:
692
                elem = ET.SubElement(param_xml, name)
693
                elem.text = str(value)
694
        return scanner_params
695
696
    def new_client_stream(self, sock):
697
        """ Returns a new ssl client stream from bind_socket. """
698
699
        assert sock
700
        newsocket, fromaddr = sock.accept()
701
        logger.debug("New connection from"
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
702
                     " {0}:{1}".format(fromaddr[0], fromaddr[1]))
703
        # NB: Despite the name, ssl.PROTOCOL_SSLv23 selects the highest
704
        # protocol version that both the client and server support. In modern
705
        # Python versions (>= 3.4) it suppports TLS >= 1.0 with SSLv2 and SSLv3
706
        # being disabled. For Python >=3.5, PROTOCOL_SSLv23 is an alias for
707
        # PROTOCOL_TLS which should be used once compatibility with Python 3.4
708
        # is no longer desired.
709
        try:
710
            ssl_socket = ssl.wrap_socket(newsocket, cert_reqs=ssl.CERT_REQUIRED,
711
                                         server_side=True,
712
                                         certfile=self.certs['cert_file'],
713
                                         keyfile=self.certs['key_file'],
714
                                         ca_certs=self.certs['ca_file'],
715
                                         ssl_version=ssl.PROTOCOL_SSLv23)
716
        except (ssl.SSLError, socket.error) as message:
717
            logger.error(message)
718
            return None
719
        return ssl_socket
720
721
    @staticmethod
722
    def write_to_stream(stream, response, block_len=1024):
723
        """
724
        Send the response in blocks of the given len using the
725
        passed method dependending on the socket type.
726
        """
727
        try:
728
            i_start = 0
729
            i_end = block_len
730
            while True:
731
                if i_end > len(response):
732
                    stream(response[i_start:])
733
                    break
734
                stream(response[i_start:i_end])
735
                i_start = i_end
736
                i_end += block_len
737
        except (socket.timeout, socket.error) as exception:
738
            logger.debug('Error sending response to the client: {0}'.format(exception))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
739
740
    def handle_client_stream(self, stream, is_unix=False):
741
        """ Handles stream of data received from client. """
742
743
        assert stream
744
        data = []
745
        stream.settimeout(2)
746
        while True:
747
            try:
748
                if is_unix:
749
                    data.append(stream.recv(1024))
750
                else:
751
                    data.append(stream.read(1024))
752
                if len(data) == 0:
0 ignored issues
show
Unused Code introduced by
Do not use len(SEQUENCE) as condition value
Loading history...
753
                    logger.warning(
754
                        "Empty client stream (Connection unexpectedly closed)")
755
                    return
756
            except (AttributeError, ValueError) as message:
757
                logger.error(message)
758
                return
759
            except (ssl.SSLError) as exception:
760
                logger.debug('Error: {0}'.format(exception[0]))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
761
                break
762
            except (socket.timeout) as exception:
763
                logger.debug('Error: {0}'.format(exception))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
764
                break
765
        data = b''.join(data)
766
        if len(data) <= 0:
767
            logger.debug("Empty client stream")
768
            return
769
        try:
770
            response = self.handle_command(data)
771
        except OSPDError as exception:
772
            response = exception.as_xml()
773
            logger.debug('Command error: {0}'.format(exception.message))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
774
        except Exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
775
            logger.exception('While handling client command:')
776
            exception = OSPDError('Fatal error', 'error')
777
            response = exception.as_xml()
778
        if is_unix:
779
            send_method = stream.send
780
        else:
781
            send_method = stream.write
782
        self.write_to_stream(send_method, response)
783
784
    def parallel_scan(self,scan_id, target):
0 ignored issues
show
Coding Style introduced by
Exactly one space required after comma
Loading history...
785
        """ Starts the scan with scan_id. """
786
        try:
787
            ret = self.exec_scan(scan_id, target)
788
            if ret == 0:
789
                self.add_scan_host_detail(scan_id, name='host_status',
790
                                          host=target, value='0')
791
            elif ret == 1:
792
                self.add_scan_host_detail(scan_id, name='host_status',
793
                                          host=target, value='1')
794
            elif ret == 2:
795
                self.add_scan_host_detail(scan_id, name='host_status',
796
                                          host=target, value='2')
797
            else:
798
                logger.debug('{0}: No host status returned'.format(target))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
799
        except Exception as e:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
Coding Style Naming introduced by
The name e does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

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

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

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

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

Loading history...
800
            self.add_scan_error(scan_id, name='', host=target,
801
                                value='Host process failure (%s).' % e)
802
            logger.exception('While scanning {0}:'.format(target))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
803
        else:
804
            logger.info("{0}: Host scan finished.".format(target))
0 ignored issues
show
introduced by
Use formatting in logging functions and pass the parameters as arguments
Loading history...
805
806
    def check_pending_target(self, scan_id, multiscan_proc):
807
        """ Check if a scan process is still alive. In case the process
808
        finished, removes the process from the multiscan_process list.
809
        In case of the progress is not set from the wrapper for a single
810
        target, it will be automatically set if the target scan finished.
811
812
        @input scan_id        Scan_id of the whole scan.
813
        @input multiscan_proc A list with the scan process which
814
                              may still be alive.
815
816
        @return Actualized list with current runnging scan processes."""
817
818
        for running_target in multiscan_proc:
819
            if not running_target[0].is_alive():
820
                target_prog = self.get_scan_target_progress(scan_id)
821
                if target_prog[running_target[1]] < 100:
822
                    self.set_scan_target_progress(scan_id,
823
                                                  running_target[1],
824
                                                  100)
825
                multiscan_proc.remove(running_target)
826
        return multiscan_proc
827
828
    def calculate_progress(self, scan_id):
829
        """ Calculate the total scan progress from the
830
        partial target progress. """
831
832
        target_progress = self.get_scan_target_progress(scan_id)
833
        return sum(target_progress.values())/len(target_progress)
834
835
    def start_scan(self, scan_id, targets, parallel=1):
836
        """ Handle N parallel scans if 'parallel' is greater than 1. """
837
838
        os.setsid()
839
        multiscan_proc = []
840
        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...
841
        target_list = targets
842
        if target_list is None or not target_list:
843
            raise OSPDError('Erroneous targets list', 'start_scan')
844
845
        for index, target in enumerate(target_list):
0 ignored issues
show
Unused Code introduced by
The variable index seems to be unused.
Loading history...
846
            while len(multiscan_proc) >= parallel:
847
                multiscan_proc = self.check_pending_target(scan_id,
848
                                                           multiscan_proc)
849
                progress = self.calculate_progress(scan_id)
850
                self.set_scan_progress(scan_id, progress)
851
                time.sleep(1)
852
853
            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...
854
            scan_process = multiprocessing.Process(target=self.parallel_scan,
855
                                               args=(scan_id, target[0]))
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 4 spaces).
Loading history...
856
            multiscan_proc.append((scan_process, target[0]))
857
            scan_process.start()
858
859
        # Wait until all single target were scanned
860
        while multiscan_proc:
861
            multiscan_proc = self.check_pending_target(scan_id, multiscan_proc)
862
            if multiscan_proc:
863
                progress = self.calculate_progress(scan_id)
864
                self.set_scan_progress(scan_id, progress)
865
            time.sleep(1)
866
867
        self.finish_scan(scan_id)
868
869
    def dry_run_scan(self, scan_id, targets):
870
        """ Dry runs a scan. """
871
872
        os.setsid()
873
        #target_list = target_str_to_list(target_str)
874
        for _, target in enumerate(targets):
875
            host = resolve_hostname(target[0])
876
            if host is None:
877
                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...
878
                continue
879
            port = self.get_scan_ports(scan_id, target=target[0])
880
            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...
881
            self.add_scan_log(scan_id, name='', host=host,
882
                              value='Dry run result')
883
        self.finish_scan(scan_id)
884
885
    def handle_timeout(self, scan_id, host):
886
        """ Handles scanner reaching timeout error. """
887
        self.add_scan_error(scan_id, host=host, name="Timeout",
888
                            value="{0} exec timeout."
889
                            .format(self.get_scanner_name()))
890
891
    def set_scan_progress(self, scan_id, progress):
892
        """ Sets scan_id scan's progress which is a number between 0 and 100. """
893
        self.scan_collection.set_progress(scan_id, progress)
894
895
    def set_scan_target_progress(self, scan_id, target, progress):
896
        """ Sets target's progress. """
897
        self.scan_collection.set_target_progress(scan_id, target, progress)
898
899
    def scan_exists(self, scan_id):
900
        """ Checks if a scan with ID scan_id is in collection.
901
902
        @return: 1 if scan exists, 0 otherwise.
903
        """
904
        return self.scan_collection.id_exists(scan_id)
905
906
    def handle_get_scans_command(self, scan_et):
907
        """ Handles <get_scans> command.
908
909
        @return: Response string for <get_scans> command.
910
        """
911
912
        scan_id = scan_et.attrib.get('scan_id')
913
        details = scan_et.attrib.get('details')
914
        pop_res = scan_et.attrib.get('pop_results')
915
        if details and details == '0':
916
            details = False
917
        else:
918
            details = True
919
            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...
920
                pop_res = True
921
            else:
922
                pop_res = False
923
924
        responses = []
925
        if scan_id and scan_id in self.scan_collection.ids_iterator():
926
            self.check_scan_process(scan_id)
927
            scan = self.get_scan_xml(scan_id, details, pop_res)
928
            responses.append(scan)
929
        elif scan_id:
930
            text = "Failed to find scan '{0}'".format(scan_id)
931
            return simple_response_str('get_scans', 404, text)
932
        else:
933
            for scan_id in self.scan_collection.ids_iterator():
934
                self.check_scan_process(scan_id)
935
                scan = self.get_scan_xml(scan_id, details, pop_res)
936
                responses.append(scan)
937
        return simple_response_str('get_scans', 200, 'OK', responses)
938
939
    def handle_get_vts_command(self, vt_et):
940
        """ Handles <get_vts> command.
941
942
        @return: Response string for <get_vts> command.
943
        """
944
945
        vt_id = vt_et.attrib.get('vt_id')
946
947
        if vt_id and vt_id not in self.vts:
948
            text = "Failed to find vulnerability test '{0}'".format(vt_id)
949
            return simple_response_str('get_vts', 404, text)
950
951
        responses = []
952
953
        if vt_id:
954
            vts_xml = self.get_vts_xml(vt_id)
955
        else:
956
            vts_xml = self.get_vts_xml()
957
958
        responses.append(vts_xml)
959
960
        return simple_response_str('get_vts', 200, 'OK', responses)
961
962
    def handle_help_command(self, scan_et):
963
        """ Handles <help> command.
964
965
        @return: Response string for <help> command.
966
        """
967
        help_format = scan_et.attrib.get('format')
968
        if help_format is None or help_format == "text":
969
            # Default help format is text.
970
            return simple_response_str('help', 200, 'OK',
971
                                       self.get_help_text())
972
        elif help_format == "xml":
973
            text = self.get_xml_str(self.commands)
974
            return simple_response_str('help', 200, 'OK', text)
975
        raise OSPDError('Bogus help format', 'help')
976
977
    def get_help_text(self):
978
        """ Returns the help output in plain text format."""
979
980
        txt = str('\n')
981
        for name, info in self.commands.items():
982
            command_txt = "\t{0: <22} {1}\n".format(name, info['description'])
983
            if info['attributes']:
984
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
985
                for attrname, attrdesc in info['attributes'].items():
986
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
987
                    command_txt = ''.join([command_txt, attr_txt])
988
            if info['elements']:
989
                command_txt = ''.join([command_txt, "\t Elements:\n",
990
                                       self.elements_as_text(info['elements'])])
991
            txt = ''.join([txt, command_txt])
992
        return txt
993
994
    def elements_as_text(self, elems, indent=2):
995
        """ Returns the elems dictionary as formatted plain text. """
996
        assert elems
997
        text = ""
998
        for elename, eledesc in elems.items():
999
            if isinstance(eledesc, dict):
1000
                desc_txt = self.elements_as_text(eledesc, indent + 2)
1001
                desc_txt = ''.join(['\n', desc_txt])
1002
            elif isinstance(eledesc, str):
1003
                desc_txt = ''.join([eledesc, '\n'])
1004
            else:
1005
                assert False, "Only string or dictionary"
1006
            ele_txt = "\t{0}{1: <22} {2}".format(' ' * indent, elename,
1007
                                                 desc_txt)
0 ignored issues
show
introduced by
The variable desc_txt does not seem to be defined for all execution paths.
Loading history...
1008
            text = ''.join([text, ele_txt])
1009
        return text
1010
1011
    def handle_delete_scan_command(self, scan_et):
1012
        """ Handles <delete_scan> command.
1013
1014
        @return: Response string for <delete_scan> command.
1015
        """
1016
        scan_id = scan_et.attrib.get('scan_id')
1017
        if scan_id is None:
1018
            return simple_response_str('delete_scan', 404,
1019
                                       'No scan_id attribute')
1020
1021
        if not self.scan_exists(scan_id):
1022
            text = "Failed to find scan '{0}'".format(scan_id)
1023
            return simple_response_str('delete_scan', 404, text)
1024
        self.check_scan_process(scan_id)
1025
        if self.delete_scan(scan_id):
1026
            return simple_response_str('delete_scan', 200, 'OK')
1027
        raise OSPDError('Scan in progress', 'delete_scan')
1028
1029
    def delete_scan(self, scan_id):
1030
        """ Deletes scan_id scan from collection.
1031
1032
        @return: 1 if scan deleted, 0 otherwise.
1033
        """
1034
        try:
1035
            del self.scan_processes[scan_id]
1036
        except KeyError:
1037
            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...
1038
        return self.scan_collection.delete_scan(scan_id)
1039
1040
    def get_scan_results_xml(self, scan_id, pop_res):
1041
        """ Gets scan_id scan's results in XML format.
1042
1043
        @return: String of scan results in xml.
1044
        """
1045
        results = ET.Element('results')
1046
        for result in self.scan_collection.results_iterator(scan_id, pop_res):
1047
            results.append(get_result_xml(result))
1048
1049
        logger.info('Returning %d results', len(results))
1050
        return results
1051
1052
    def get_xml_str(self, data):
1053
        """ Creates a string in XML Format using the provided data structure.
1054
1055
        @param: Dictionary of xml tags and their elements.
1056
1057
        @return: String of data in xml format.
1058
        """
1059
1060
        responses = []
1061
        for tag, value in data.items():
1062
            elem = ET.Element(tag)
1063
            if isinstance(value, dict):
1064
                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 1061).

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