Completed
Push — master ( d1b721...16ca13 )
by Juan José
22s queued 12s
created

ospd.ospd   F

Complexity

Total Complexity 305

Size/Duplication

Total Lines 1687
Duplicated Lines 91.88 %

Importance

Changes 0
Metric Value
eloc 983
dl 1550
loc 1687
rs 1.617
c 0
b 0
f 0
wmc 305

95 Methods

Rating   Name   Duplication   Size   Complexity  
A OSPDaemon.get_scanner_name() 3 3 1
F OSPDaemon._preprocess_scan_params() 32 32 15
A OSPDaemon.get_vts_version() 4 4 1
B OSPDaemon.process_vts_params() 45 45 8
A OSPDaemon.get_protocol_version() 3 3 1
A OSPDaemon.get_scanner_description() 3 3 1
A OSPDaemon.command_exists() 3 3 1
A OSPDaemon.process_credentials_elements() 41 41 4
A OSPDaemon.set_vts_version() 11 11 2
A OSPDaemon.__init__() 39 39 4
A OSPDaemon.add_scanner_param() 10 10 1
A OSPDaemon.set_command_attributes() 5 5 2
A OSPDaemon.get_server_version() 4 4 1
F OSPDaemon.add_vt() 60 60 21
A OSPDaemon.get_scanner_version() 3 3 1
A OSPDaemon.process_scan_params() 4 4 1
A OSPDaemon.new_client_stream() 24 24 2
A OSPDaemon.get_scan_finished_hosts() 3 3 1
A OSPDaemon.get_scan_options() 3 3 1
A OSPDaemon.create_scan() 12 12 2
A OSPDaemon.get_xml_str() 21 21 5
A OSPDaemon.process_exclude_hosts() 14 14 4
B OSPDaemon.handle_get_version_command() 29 29 5
A OSPDaemon.elements_as_text() 16 16 4
A OSPDaemon.calculate_progress() 8 8 2
A OSPDaemon.finish_scan() 5 5 1
A OSPDaemon.set_scan_target_progress() 5 5 1
A OSPDaemon.get_modification_time_vt_as_xml_str() 13 13 1
A OSPDaemon.scan_exists() 6 6 1
A OSPDaemon.get_custom_vt_as_xml_str() 13 13 1
A OSPDaemon.add_scan_error() 4 4 1
A OSPDaemon.get_scan_start_time() 3 3 1
B OSPDaemon.process_targets_element() 64 64 8
A OSPDaemon.get_params_vt_as_xml_str() 13 13 1
A OSPDaemon.add_scan_alarm() 5 5 1
A OSPDaemon.handle_delete_scan_command() 17 17 4
A OSPDaemon.dry_run_scan() 14 14 3
A OSPDaemon.get_scan_vts() 3 3 1
A OSPDaemon.get_scan_progress() 3 3 1
B OSPDaemon.parallel_scan() 21 21 6
A OSPDaemon.get_severities_vt_as_xml_str() 13 13 1
A OSPDaemon.get_affected_vt_as_xml_str() 13 13 1
A OSPDaemon.get_vts_xml() 28 28 5
A OSPDaemon.get_scanner_param_mandatory() 7 7 2
A OSPDaemon.get_scan_xml() 24 24 4
A OSPDaemon.get_detection_vt_as_xml_str() 13 13 1
C OSPDaemon.handle_get_scans_command() 32 32 9
A OSPDaemon.check() 3 3 1
A OSPDaemon.get_scan_unfinished_hosts() 3 3 1
A OSPDaemon.set_scan_option() 3 3 1
A OSPDaemon.get_insight_vt_as_xml_str() 13 13 1
A OSPDaemon.set_scan_host_finished() 3 3 1
A OSPDaemon.get_scanner_param_default() 7 7 2
A OSPDaemon.get_scan_status() 3 3 1
A OSPDaemon.scheduler() 2 2 1
A OSPDaemon.get_summary_vt_as_xml_str() 13 13 1
A OSPDaemon.get_dependencies_vt_as_xml_str() 13 13 1
A OSPDaemon.get_daemon_name() 3 3 1
A OSPDaemon.get_impact_vt_as_xml_str() 13 13 1
A OSPDaemon.handle_help_command() 14 14 4
F OSPDaemon.handle_start_scan_command() 67 67 17
A OSPDaemon.get_scanner_param_type() 7 7 2
A OSPDaemon.add_scan_log() 5 5 1
A OSPDaemon.get_refs_vt_as_xml_str() 13 13 1
C OSPDaemon.start_scan() 42 42 9
A OSPDaemon.get_scan_target() 3 3 1
A OSPDaemon.handle_stop_scan_command() 12 12 3
A OSPDaemon.get_scan_ports() 3 3 1
A OSPDaemon.exec_scan() 3 3 1
A OSPDaemon.check_scan_process() 11 11 4
F OSPDaemon.get_vt_xml() 84 84 18
A OSPDaemon.get_solution_vt_as_xml_str() 13 13 1
A OSPDaemon.delete_scan() 13 13 3
A OSPDaemon.get_scan_exclude_hosts() 4 4 1
A OSPDaemon.get_scan_end_time() 3 3 1
A OSPDaemon.add_scan_host_detail() 4 4 1
A OSPDaemon.get_scan_credentials() 4 4 1
A OSPDaemon.handle_get_vts_command() 25 25 4
A OSPDaemon.get_help_text() 16 16 5
A OSPDaemon.stop_scan_cleanup() 3 3 1
A OSPDaemon.set_scan_progress() 4 4 1
A OSPDaemon.get_creation_time_vt_as_xml_str() 13 13 1
A OSPDaemon.stop_scan() 20 20 4
A OSPDaemon.get_scanner_params_xml() 15 15 4
C OSPDaemon.handle_client_stream() 42 42 11
A OSPDaemon.handle_timeout() 5 5 1
A OSPDaemon.get_scan_target_progress() 3 3 1
A OSPDaemon.get_scan_results_xml() 11 11 2
A OSPDaemon.write_to_stream() 18 18 4
D OSPDaemon.handle_command() 32 32 12
A OSPDaemon.check_pending_target() 23 23 4
A OSPDaemon.set_scan_status() 3 3 1
A OSPDaemon.get_daemon_version() 3 3 1
C OSPDaemon.run() 37 37 9
A OSPDaemon.handle_get_scanner_details() 12 12 1

3 Functions

Rating   Name   Duplication   Size   Complexity  
A bind_socket() 15 15 2
A close_client_stream() 12 12 3
A bind_unix_socket() 19 19 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ospd.ospd often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# Copyright (C) 2014-2018 Greenbone Networks GmbH
2
#
3
# SPDX-License-Identifier: GPL-2.0-or-later
4
#
5
# This program is free software; you can redistribute it and/or
6
# modify it under the terms of the GNU General Public License
7
# as published by the Free Software Foundation; either version 2
8
# of the License, or (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18
19
""" OSP Daemon core class.
20
"""
21
22
23
# This is needed for older pythons as our current module is called the same
24
# as the package we are in ...
25
# Another solution would be to rename that file.
26
from __future__ import absolute_import
27
28
import logging
29
import select
30
import socket
31
import ssl
32
import multiprocessing
33
import re
34
import time
35
from xml.etree.ElementTree import tostring, Element, SubElement
36
import defusedxml.ElementTree as secET
37
import os
38
39
from ospd import __version__
40
from ospd.vtfilter import VtsFilter
41
from ospd.misc import ScanCollection, ResultType, target_str_to_list
42
from ospd.misc import resolve_hostname, valid_uuid
43
from ospd.misc import ScanStatus
44
from ospd.xml import simple_response_str, get_result_xml
45
from ospd.error import OSPDError
46
47
logger = logging.getLogger(__name__)
48
49
PROTOCOL_VERSION = "1.2"
50
51
SCHEDULER_CHECK_PERIOD = 5  #in seconds
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
            'filter': 'Optional filter to get an specific vt collection.'
108
        },
109
        'elements': None
110
    },
111
    'delete_scan': {
112
        'description': 'Delete a finished scan.',
113
        'attributes': {
114
            'scan_id': 'ID of scan to delete.'
115
        },
116
        'elements': None
117
    },
118
    'get_version': {
119
        'description': 'Return various versions.',
120
        'attributes': None,
121
        'elements': None
122
    },
123
    'get_scanner_details': {
124
        'description': 'Return scanner description and parameters',
125
        'attributes': None,
126
        'elements': None
127
    }
128
}
129
130
131
132 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...
133
    """ Returns a socket bound on (address:port). """
134
135
    assert address
136
    assert port
137
    bindsocket = socket.socket()
138
    try:
139
        bindsocket.bind((address, port))
140
    except socket.error:
141
        logger.error("Couldn't bind socket on %s:%s", address, port)
142
        return None
143
144
    logger.info('Listening on %s:%s', address, port)
145
    bindsocket.listen(0)
146
    return bindsocket
147
148 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...
149
    """ Returns a unix file socket bound on (path). """
150
151
    assert path
152
    bindsocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
153
    try:
154
        os.unlink(path)
155
    except OSError:
156
        if os.path.exists(path):
157
            raise
158
    try:
159
        bindsocket.bind(path)
160
    except socket.error:
161
        logger.error("Couldn't bind socket on %s", path)
162
        return None
163
164
    logger.info('Listening on %s', path)
165
    bindsocket.listen(0)
166
    return bindsocket
167
168
169 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...
170
    """ Closes provided client stream """
171
    try:
172
        client_stream.shutdown(socket.SHUT_RDWR)
173
        if unix_path:
174
            logger.debug('%s: Connection closed', unix_path)
175
        else:
176
            peer = client_stream.getpeername()
177
            logger.debug('%s:%s: Connection closed', peer[0], peer[1])
178
    except (socket.error, OSError) as exception:
179
        logger.debug('Connection closing error: %s', exception)
180
    client_stream.close()
181
182
183 View Code Duplication
class OSPDaemon(object):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
184
185
    """ Daemon class for OSP traffic handling.
186
187
    Every scanner wrapper should subclass it and make necessary additions and
188
    changes.
189
    * Add any needed parameters in __init__.
190
    * Implement check() method which verifies scanner availability and other
191
      environment related conditions.
192
    * Implement process_scan_params and exec_scan methods which are
193
      specific to handling the <start_scan> command, executing the wrapped
194
      scanner and storing the results.
195
    * exec_scan() should return 0 if host is dead or not reached, 1 if host is
196
      alive and 2 if scan error or status is unknown.
197
    * Implement other methods that assert to False such as get_scanner_name,
198
      get_scanner_version.
199
    * Use Call set_command_attributes at init time to add scanner command
200
      specific options eg. the w3af profile for w3af wrapper.
201
    """
202
203
    def __init__(self, certfile, keyfile, cafile,
204
                 customvtfilter=None, wrapper_logger=None):
205
        """ Initializes the daemon's internal data. """
206
        # @todo: Actually it makes sense to move the certificate params to
207
        #        a separate function because it is not mandatory anymore to
208
        #        use a TLS setup (unix file socket is an alternative).
209
        #        However, changing this makes it mandatory for any ospd scanner
210
        #        to change the function calls as well. So this breaks the API
211
        #        and should only be done with a major release.
212
        self.certs = dict()
213
        self.certs['cert_file'] = certfile
214
        self.certs['key_file'] = keyfile
215
        self.certs['ca_file'] = cafile
216
        self.scan_collection = ScanCollection()
217
        self.scan_processes = dict()
218
        self.daemon_info = dict()
219
        self.daemon_info['name'] = "OSPd"
220
        self.daemon_info['version'] = __version__
221
        self.daemon_info['description'] = "No description"
222
        self.scanner_info = dict()
223
        self.scanner_info['name'] = 'No name'
224
        self.scanner_info['version'] = 'No version'
225
        self.scanner_info['description'] = 'No description'
226
        self.server_version = None  # Set by the subclass.
227
        self.protocol_version = PROTOCOL_VERSION
228
        self.commands = COMMANDS_TABLE
229
        self.scanner_params = dict()
230
        for name, param in BASE_SCANNER_PARAMS.items():
231
            self.add_scanner_param(name, param)
232
        self.vts = dict()
233
        self.vt_id_pattern = re.compile("[0-9a-zA-Z_\-:.]{1,80}")
234
        self.vts_version = None
235
        if customvtfilter:
236
            self.vts_filter = customvtfilter
237
        else:
238
            self.vts_filter = VtsFilter()
239
        if wrapper_logger:
240
            global logger
241
            logger = wrapper_logger
242
243
    def set_command_attributes(self, name, attributes):
244
        """ Sets the xml attributes of a specified command. """
245
        if self.command_exists(name):
246
            command = self.commands.get(name)
247
            command['attributes'] = attributes
248
249
    def add_scanner_param(self, name, scanner_param):
250
        """ Add a scanner parameter. """
251
252
        assert name
253
        assert scanner_param
254
        self.scanner_params[name] = scanner_param
255
        command = self.commands.get('start_scan')
256
        command['elements'] = {
257
            'scanner_params':
258
                {k: v['name'] for k, v in self.scanner_params.items()}}
259
260
    def add_vt(self, vt_id, name=None, vt_params=None, vt_refs=None,
261
               custom=None, vt_creation_time=None, vt_modification_time=None,
262
               vt_dependencies=None, summary=None, impact=None, affected=None,
263
               insight=None, solution=None, solution_t=None, detection=None,
264
               qod_t=None, qod_v=None, severities=None):
265
        """ Add a vulnerability test information.
266
267
        Returns: The new number of stored VTs.
268
        -1 in case the VT ID was already present and thus the
269
        new VT was not considered.
270
        -2 in case the vt_id was invalid.
271
        """
272
273
        if not vt_id:
274
            return -2  # no valid vt_id
275
276
        if self.vt_id_pattern.fullmatch(vt_id) is None:
277
            return -2  # no valid vt_id
278
279
        if vt_id in self.vts:
280
            return -1  # The VT was already in the list.
281
282
        if name is None:
283
            name = ''
284
285
        self.vts[vt_id] = {'name': name}
286
        if custom is not None:
287
            self.vts[vt_id]["custom"] = custom
288
        if vt_params is not None:
289
            self.vts[vt_id]["vt_params"] = vt_params
290
        if vt_refs is not None:
291
            self.vts[vt_id]["vt_refs"] = vt_refs
292
        if vt_dependencies is not None:
293
            self.vts[vt_id]["vt_dependencies"] = vt_dependencies
294
        if vt_creation_time is not None:
295
            self.vts[vt_id]["creation_time"] = vt_creation_time
296
        if vt_modification_time is not None:
297
            self.vts[vt_id]["modification_time"] = vt_modification_time
298
        if summary is not None:
299
            self.vts[vt_id]["summary"] = summary
300
        if impact is not None:
301
            self.vts[vt_id]["impact"] = impact
302
        if affected is not None:
303
            self.vts[vt_id]["affected"] = affected
304
        if insight is not None:
305
            self.vts[vt_id]["insight"] = insight
306
        if solution is not None:
307
            self.vts[vt_id]["solution"] = solution
308
            if solution_t is not None:
309
                self.vts[vt_id]["solution_type"] = solution_t
310
        if detection is not None:
311
            self.vts[vt_id]["detection"] = detection
312
        if qod_t is not None:
313
            self.vts[vt_id]["qod_type"] = qod_t
314
        elif qod_v is not None:
315
            self.vts[vt_id]["qod"] = qod_v
316
        if severities is not None:
317
            self.vts[vt_id]["severities"] = severities
318
319
        return len(self.vts)
320
321
    def set_vts_version(self, vts_version):
322
        """ Add into the vts dictionary an entry to identify the
323
        vts version.
324
325
        Parameters:
326
            vts_version (str): Identifies a unique vts version.
327
        """
328
        if not vts_version:
329
            raise OSPDError('A vts_version parameter is required',
330
                            'set_vts_version')
331
        self.vts_version = vts_version
332
333
    def get_vts_version(self):
334
        """Return the vts version.
335
        """
336
        return self.vts_version
337
338
    def command_exists(self, name):
339
        """ Checks if a commands exists. """
340
        return name in self.commands.keys()
341
342
    def get_scanner_name(self):
343
        """ Gives the wrapped scanner's name. """
344
        return self.scanner_info['name']
345
346
    def get_scanner_version(self):
347
        """ Gives the wrapped scanner's version. """
348
        return self.scanner_info['version']
349
350
    def get_scanner_description(self):
351
        """ Gives the wrapped scanner's description. """
352
        return self.scanner_info['description']
353
354
    def get_server_version(self):
355
        """ Gives the specific OSP server's version. """
356
        assert self.server_version
357
        return self.server_version
358
359
    def get_protocol_version(self):
360
        """ Gives the OSP's version. """
361
        return self.protocol_version
362
363
    def _preprocess_scan_params(self, xml_params):
364
        """ Processes the scan parameters. """
365
        params = {}
366
        for param in xml_params:
367
            params[param.tag] = param.text or ''
368
        # Set default values.
369
        for key in self.scanner_params:
370
            if key not in params:
371
                params[key] = self.get_scanner_param_default(key)
372
                if self.get_scanner_param_type(key) == 'selection':
373
                    params[key] = params[key].split('|')[0]
374
        # Validate values.
375
        for key in params:
376
            param_type = self.get_scanner_param_type(key)
377
            if not param_type:
378
                continue
379
            if param_type in ['integer', 'boolean']:
380
                try:
381
                    params[key] = int(params[key])
382
                except ValueError:
383
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
384
            if param_type == 'boolean':
385
                if params[key] not in [0, 1]:
386
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
387
            elif param_type == 'selection':
388
                selection = self.get_scanner_param_default(key).split('|')
389
                if params[key] not in selection:
390
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
391
            if self.get_scanner_param_mandatory(key) and params[key] == '':
392
                    raise OSPDError('Mandatory %s value is missing' % key,
393
                                    'start_scan')
394
        return params
395
396
    def process_scan_params(self, params):
397
        """ This method is to be overridden by the child classes if necessary
398
        """
399
        return params
400
401
    def process_vts_params(self, scanner_vts):
402
        """ Receive an XML object with the Vulnerability Tests an their
403
        parameters to be use in a scan and return a dictionary.
404
405
        @param: XML element with vt subelements. Each vt has an
406
                id attribute. Optional parameters can be included
407
                as vt child.
408
                Example form:
409
                <vt_selection>
410
                  <vt_single id='vt1' />
411
                  <vt_single id='vt2'>
412
                    <vt_value id='param1'>value</vt_value>
413
                  </vt_single>
414
                  <vt_group filter='family=debian'/>
415
                  <vt_group filter='family=general'/>
416
                </vt_selection>
417
418
        @return: Dictionary containing the vts attribute and subelements,
419
                 like the VT's id and VT's parameters.
420
                 Example form:
421
                 {'vt1': {},
422
                  'vt2': {'value_id': 'value'},
423
                  'vt_groups': ['family=debian', 'family=general']}
424
        """
425
        vt_selection = {}
426
        filters = list()
427
        for vt in scanner_vts:
428
            if vt.tag == 'vt_single':
429
                vt_id = vt.attrib.get('id')
430
                vt_selection[vt_id] = {}
431
                for vt_value in vt:
432
                    if not vt_value.attrib.get('id'):
433
                        raise OSPDError('Invalid VT preference. No attribute id',
434
                                        'start_scan')
435
                    vt_value_id = vt_value.attrib.get('id')
436
                    vt_value_value = vt_value.text if vt_value.text else ''
437
                    vt_selection[vt_id][vt_value_id] = vt_value_value
438
            if vt.tag == 'vt_group':
439
                vts_filter = vt.attrib.get('filter', None)
440
                if vts_filter is None:
441
                    raise OSPDError('Invalid VT group. No filter given.',
442
                                    'start_scan')
443
                filters.append(vts_filter)
444
        vt_selection['vt_groups'] = filters
445
        return vt_selection
446
447
    @staticmethod
448
    def process_credentials_elements(cred_tree):
449
        """ Receive an XML object with the credentials to run
450
        a scan against a given target.
451
452
        @param:
453
        <credentials>
454
          <credential type="up" service="ssh" port="22">
455
            <username>scanuser</username>
456
            <password>mypass</password>
457
          </credential>
458
          <credential type="up" service="smb">
459
            <username>smbuser</username>
460
            <password>mypass</password>
461
          </credential>
462
        </credentials>
463
464
        @return: Dictionary containing the credentials for a given target.
465
                 Example form:
466
                 {'ssh': {'type': type,
467
                          'port': port,
468
                          'username': username,
469
                          'password': pass,
470
                        },
471
                  'smb': {'type': type,
472
                          'username': username,
473
                          'password': pass,
474
                         },
475
                   }
476
        """
477
        credentials = {}
478
        for credential in cred_tree:
479
            service = credential.attrib.get('service')
480
            credentials[service] = {}
481
            credentials[service]['type'] = credential.attrib.get('type')
482
            if service == 'ssh':
483
                credentials[service]['port'] = credential.attrib.get('port')
484
            for param in credential:
485
                credentials[service][param.tag] = param.text
486
487
        return credentials
488
489
    @classmethod
490
    def process_targets_element(cls, scanner_target):
491
        """ Receive an XML object with the target, ports and credentials to run
492
        a scan against.
493
494
        @param: XML element with target subelements. Each target has <hosts>
495
        and <ports> subelements. Hosts can be a single host, a host range,
496
        a comma-separated host list or a network address.
497
        <ports> and  <credentials> are optional. Therefore each ospd-scanner
498
        should check for a valid ones if needed.
499
500
                Example form:
501
                <targets>
502
                  <target>
503
                    <hosts>localhosts</hosts>
504
                    <exclude_hosts>localhost1</exclude_hosts>
505
                    <ports>80,443</ports>
506
                  </target>
507
                  <target>
508
                    <hosts>192.168.0.0/24</hosts>
509
                    <ports>22</ports>
510
                    <credentials>
511
                      <credential type="up" service="ssh" port="22">
512
                        <username>scanuser</username>
513
                        <password>mypass</password>
514
                      </credential>
515
                      <credential type="up" service="smb">
516
                        <username>smbuser</username>
517
                        <password>mypass</password>
518
                      </credential>
519
                    </credentials>
520
                  </target>
521
                </targets>
522
523
        @return: A list of [hosts, port, {credentials}, exclude_hosts] list.
524
                 Example form:
525
                 [['localhosts', '80,43', '', 'localhosts1'],
526
                  ['192.168.0.0/24', '22', {'smb': {'type': type,
527
                                                    'port': port,
528
                                                    'username': username,
529
                                                    'password': pass,
530
                                                   }}], '']
531
        """
532
533
        target_list = []
534
        for target in scanner_target:
535
            exclude_hosts = ''
536
            ports = ''
537
            credentials = {}
538
            for child in target:
539
                if child.tag == 'hosts':
540
                    hosts = child.text
541
                if child.tag == 'exclude_hosts':
542
                    exclude_hosts = child.text
543
                if child.tag == 'ports':
544
                    ports = child.text
545
                if child.tag == 'credentials':
546
                    credentials = cls.process_credentials_elements(child)
547
            if hosts:
0 ignored issues
show
introduced by
The variable hosts does not seem to be defined for all execution paths.
Loading history...
548
                target_list.append([hosts, ports, credentials, exclude_hosts])
549
            else:
550
                raise OSPDError('No target to scan', 'start_scan')
551
552
        return target_list
553
554
    def handle_start_scan_command(self, scan_et):
555
        """ Handles <start_scan> command.
556
557
        @return: Response string for <start_scan> command.
558
        """
559
560
        target_str = scan_et.attrib.get('target')
561
        ports_str = scan_et.attrib.get('ports')
562
        # For backward compatibility, if target and ports attributes are set,
563
        # <targets> element is ignored.
564
        if target_str is None or ports_str is None:
565
            target_list = scan_et.find('targets')
566
            if target_list is None or not target_list:
567
                raise OSPDError('No targets or ports', 'start_scan')
568
            else:
569
                scan_targets = self.process_targets_element(target_list)
570
        else:
571
            scan_targets = []
572
            for single_target in target_str_to_list(target_str):
573
                scan_targets.append([single_target, ports_str, '', ''])
574
575
        scan_id = scan_et.attrib.get('scan_id')
576
        if scan_id is not None and scan_id != '' and not valid_uuid(scan_id):
577
            raise OSPDError('Invalid scan_id UUID', 'start_scan')
578
579
        try:
580
            parallel = int(scan_et.attrib.get('parallel', '1'))
581
            if parallel < 1 or parallel > 20:
582
                parallel = 1
583
        except ValueError:
584
            raise OSPDError('Invalid value for parallel scans. '
585
                            'It must be a number', 'start_scan')
586
587
        scanner_params = scan_et.find('scanner_params')
588
        if scanner_params is None:
589
            raise OSPDError('No scanner_params element', 'start_scan')
590
591
        params = self._preprocess_scan_params(scanner_params)
592
593
        # VTS is an optional element. If present should not be empty.
594
        vt_selection = {}
595
        scanner_vts = scan_et.find('vt_selection')
596
        if scanner_vts is not None:
597
            if not scanner_vts:
598
                raise OSPDError('VTs list is empty', 'start_scan')
599
            else:
600
                vt_selection = self.process_vts_params(scanner_vts)
601
602
        # Dry run case.
603
        if 'dry_run' in params and int(params['dry_run']):
604
            scan_func = self.dry_run_scan
605
            scan_params = None
606
        else:
607
            scan_func = self.start_scan
608
            scan_params = self.process_scan_params(params)
609
610
        scan_id = self.create_scan(scan_id, scan_targets,
611
                                   scan_params, vt_selection)
612
        scan_process = multiprocessing.Process(target=scan_func,
613
                                               args=(scan_id,
614
                                                     scan_targets,
615
                                                     parallel))
616
        self.scan_processes[scan_id] = scan_process
617
        scan_process.start()
618
        id_ = Element('id')
619
        id_.text = scan_id
620
        return simple_response_str('start_scan', 200, 'OK', id_)
621
622
    def handle_stop_scan_command(self, scan_et):
623
        """ Handles <stop_scan> command.
624
625
        @return: Response string for <stop_scan> command.
626
        """
627
628
        scan_id = scan_et.attrib.get('scan_id')
629
        if scan_id is None or scan_id == '':
630
            raise OSPDError('No scan_id attribute', 'stop_scan')
631
        self.stop_scan(scan_id)
632
633
        return simple_response_str('stop_scan', 200, 'OK')
634
635
    def stop_scan(self, scan_id):
636
        scan_process = self.scan_processes.get(scan_id)
637
        if not scan_process:
638
            raise OSPDError('Scan not found {0}.'.format(scan_id), 'stop_scan')
639
        if not scan_process.is_alive():
640
            raise OSPDError('Scan already stopped or finished.', 'stop_scan')
641
642
        self.set_scan_status(scan_id, ScanStatus.STOPPED)
643
        logger.info('%s: Scan stopping %s.', scan_id, scan_process.ident)
644
        self.stop_scan_cleanup(scan_id)
645
        try:
646
            scan_process.terminate()
647
        except AttributeError:
648
            logger.debug('%s: The scanner task stopped unexpectedly.', scan_id)
649
            self.add_scan_log(scan_id, name='', host='', value='Scan stopped.')
650
651
        os.killpg(os.getpgid(scan_process.ident), 15)
652
        scan_process.join()
653
        self.add_scan_log(scan_id, name='', host='', value='Scan stopped.')
654
        logger.info('%s: Scan stopped.', scan_id)
655
656
    @staticmethod
657
    def stop_scan_cleanup(scan_id):
658
        """ Should be implemented by subclass in case of a clean up before
659
        terminating is needed. """
660
661
    def exec_scan(self, scan_id, target):
662
        """ Asserts to False. Should be implemented by subclass. """
663
        raise NotImplementedError
664
665
    def finish_scan(self, scan_id):
666
        """ Sets a scan as finished. """
667
        self.set_scan_progress(scan_id, 100)
668
        self.set_scan_status(scan_id, ScanStatus.FINISHED)
669
        logger.info("%s: Scan finished.", scan_id)
670
671
    def get_daemon_name(self):
672
        """ Gives osp daemon's name. """
673
        return self.daemon_info['name']
674
675
    def get_daemon_version(self):
676
        """ Gives osp daemon's version. """
677
        return self.daemon_info['version']
678
679
    def get_scanner_param_type(self, param):
680
        """ Returns type of a scanner parameter. """
681
        assert isinstance(param, str)
682
        entry = self.scanner_params.get(param)
683
        if not entry:
684
            return None
685
        return entry.get('type')
686
687
    def get_scanner_param_mandatory(self, param):
688
        """ Returns if a scanner parameter is mandatory. """
689
        assert isinstance(param, str)
690
        entry = self.scanner_params.get(param)
691
        if not entry:
692
            return False
693
        return entry.get('mandatory')
694
695
    def get_scanner_param_default(self, param):
696
        """ Returns default value of a scanner parameter. """
697
        assert isinstance(param, str)
698
        entry = self.scanner_params.get(param)
699
        if not entry:
700
            return None
701
        return entry.get('default')
702
703
    def get_scanner_params_xml(self):
704
        """ Returns the OSP Daemon's scanner params in xml format. """
705
        scanner_params = Element('scanner_params')
706
        for param_id, param in self.scanner_params.items():
707
            param_xml = SubElement(scanner_params, 'scanner_param')
708
            for name, value in [('id', param_id),
709
                                ('type', param['type'])]:
710
                param_xml.set(name, value)
711
            for name, value in [('name', param['name']),
712
                                ('description', param['description']),
713
                                ('default', param['default']),
714
                                ('mandatory', param['mandatory'])]:
715
                elem = SubElement(param_xml, name)
716
                elem.text = str(value)
717
        return scanner_params
718
719
    def new_client_stream(self, sock):
720
        """ Returns a new ssl client stream from bind_socket. """
721
722
        assert sock
723
        newsocket, fromaddr = sock.accept()
724
        logger.debug("New connection from"
725
                     " %s:%s", fromaddr[0], fromaddr[1])
726
        # NB: Despite the name, ssl.PROTOCOL_SSLv23 selects the highest
727
        # protocol version that both the client and server support. In modern
728
        # Python versions (>= 3.4) it supports TLS >= 1.0 with SSLv2 and SSLv3
729
        # being disabled. For Python >=3.5, PROTOCOL_SSLv23 is an alias for
730
        # PROTOCOL_TLS which should be used once compatibility with Python 3.4
731
        # is no longer desired.
732
        try:
733
            ssl_socket = ssl.wrap_socket(newsocket, cert_reqs=ssl.CERT_REQUIRED,
734
                                         server_side=True,
735
                                         certfile=self.certs['cert_file'],
736
                                         keyfile=self.certs['key_file'],
737
                                         ca_certs=self.certs['ca_file'],
738
                                         ssl_version=ssl.PROTOCOL_SSLv23)
739
        except (ssl.SSLError, socket.error) as message:
740
            logger.error(message)
741
            return None
742
        return ssl_socket
743
744
    @staticmethod
745
    def write_to_stream(stream, response, block_len=1024):
746
        """
747
        Send the response in blocks of the given len using the
748
        passed method dependending on the socket type.
749
        """
750
        try:
751
            i_start = 0
752
            i_end = block_len
753
            while True:
754
                if i_end > len(response):
755
                    stream(response[i_start:])
756
                    break
757
                stream(response[i_start:i_end])
758
                i_start = i_end
759
                i_end += block_len
760
        except (socket.timeout, socket.error) as exception:
761
            logger.debug('Error sending response to the client: %s', exception)
762
763
    def handle_client_stream(self, stream, is_unix=False):
764
        """ Handles stream of data received from client. """
765
766
        assert stream
767
        data = []
768
        stream.settimeout(2)
769
        while True:
770
            try:
771
                if is_unix:
772
                    buf = stream.recv(1024)
773
                else:
774
                    buf = stream.read(1024)
775
                if not buf:
776
                    break
777
                data.append(buf)
778
            except (AttributeError, ValueError) as message:
779
                logger.error(message)
780
                return
781
            except (ssl.SSLError) as exception:
782
                logger.debug('Error: %s', exception[0])
783
                break
784
            except (socket.timeout) as exception:
785
                logger.debug('Error: %s', exception)
786
                break
787
        data = b''.join(data)
788
        if len(data) <= 0:
789
            logger.debug("Empty client stream")
790
            return
791
        try:
792
            response = self.handle_command(data)
793
        except OSPDError as exception:
794
            response = exception.as_xml()
795
            logger.debug('Command error: %s', exception.message)
796
        except Exception:
797
            logger.exception('While handling client command:')
798
            exception = OSPDError('Fatal error', 'error')
799
            response = exception.as_xml()
800
        if is_unix:
801
            send_method = stream.send
802
        else:
803
            send_method = stream.write
804
        self.write_to_stream(send_method, response)
805
806
    def parallel_scan(self, scan_id, target):
807
        """ Starts the scan with scan_id. """
808
        try:
809
            ret = self.exec_scan(scan_id, target)
810
            if ret == 0:
811
                self.add_scan_host_detail(scan_id, name='host_status',
812
                                          host=target, value='0')
813
            elif ret == 1:
814
                self.add_scan_host_detail(scan_id, name='host_status',
815
                                          host=target, value='1')
816
            elif ret == 2:
817
                self.add_scan_host_detail(scan_id, name='host_status',
818
                                          host=target, value='2')
819
            else:
820
                logger.debug('%s: No host status returned', target)
821
        except Exception as e:
822
            self.add_scan_error(scan_id, name='', host=target,
823
                                value='Host process failure (%s).' % e)
824
            logger.exception('While scanning %s:', target)
825
        else:
826
            logger.info("%s: Host scan finished.", target)
827
828
    def check_pending_target(self, scan_id, multiscan_proc):
829
        """ Check if a scan process is still alive. In case the process
830
        finished or is stopped, removes the process from the multiscan
831
        _process list.
832
        Processes dead and with progress < 100% are considered stopped
833
        or with failures. Then will try to stop the other runnings (target)
834
        scans owned by the same task.
835
836
        @input scan_id        Scan_id of the whole scan.
837
        @input multiscan_proc A list with the scan process which
838
                              may still be alive.
839
840
        @return Actualized list with current running scan processes.
841
        """
842
        for running_target_proc, running_target_id in multiscan_proc:
843
            if not running_target_proc.is_alive():
844
                target_prog = self.get_scan_target_progress(
845
                    scan_id, running_target_id)
846
                if target_prog < 100:
847
                    self.stop_scan(scan_id)
848
                running_target = (running_target_proc, running_target_id)
849
                multiscan_proc.remove(running_target)
850
        return multiscan_proc
851
852
    def calculate_progress(self, scan_id):
853
        """ Calculate the total scan progress from the
854
        partial target progress. """
855
856
        t_prog = dict()
857
        for target in self.get_scan_target(scan_id):
858
            t_prog[target] = self.get_scan_target_progress(scan_id, target)
859
        return sum(t_prog.values())/len(t_prog)
860
861
    def process_exclude_hosts(self, scan_id, target_list):
862
        """ Process the exclude hosts before launching the scans.
863
        Set exclude hosts as finished with 100% to calculate
864
        the scan progress."""
865
866
        for target, _, _, exclude_hosts in target_list:
867
            exc_hosts_list = ''
868
            if not exclude_hosts:
869
                continue
870
            exc_hosts_list = target_str_to_list(exclude_hosts)
871
            for host in exc_hosts_list:
872
                self.set_scan_host_finished(scan_id, target, host)
873
                self.set_scan_target_progress(
874
                    scan_id, target, host, 100)
875
876
    def start_scan(self, scan_id, targets, parallel=1):
877
        """ Handle N parallel scans if 'parallel' is greater than 1. """
878
879
        os.setsid()
880
        multiscan_proc = []
881
        logger.info("%s: Scan started.", scan_id)
882
        target_list = targets
883
        if target_list is None or not target_list:
884
            raise OSPDError('Erroneous targets list', 'start_scan')
885
886
        self.process_exclude_hosts(scan_id, target_list)
887
888
        for index, target in enumerate(target_list):
889
            while len(multiscan_proc) >= parallel:
890
                progress = self.calculate_progress(scan_id)
891
                self.set_scan_progress(scan_id, progress)
892
                multiscan_proc = self.check_pending_target(scan_id,
893
                                                           multiscan_proc)
894
                time.sleep(1)
895
896
            #If the scan status is stopped, does not launch anymore target scans
897
            if self.get_scan_status(scan_id) == ScanStatus.STOPPED:
898
                return
899
900
            logger.info("%s: Host scan started on ports %s.", target[0], target[1])
901
            scan_process = multiprocessing.Process(target=self.parallel_scan,
902
                                                   args=(scan_id, target[0]))
903
            multiscan_proc.append((scan_process, target[0]))
904
            scan_process.start()
905
            self.set_scan_status(scan_id, ScanStatus.RUNNING)
906
907
        # Wait until all single target were scanned
908
        while multiscan_proc:
909
            multiscan_proc = self.check_pending_target(scan_id, multiscan_proc)
910
            if multiscan_proc:
911
                progress = self.calculate_progress(scan_id)
912
                self.set_scan_progress(scan_id, progress)
913
            time.sleep(1)
914
915
        # Only set the scan as finished if the scan was not stopped.
916
        if self.get_scan_status(scan_id) != ScanStatus.STOPPED:
917
            self.finish_scan(scan_id)
918
919
    def dry_run_scan(self, scan_id, targets):
920
        """ Dry runs a scan. """
921
922
        os.setsid()
923
        for _, target in enumerate(targets):
924
            host = resolve_hostname(target[0])
925
            if host is None:
926
                logger.info("Couldn't resolve %s.", target[0])
927
                continue
928
            port = self.get_scan_ports(scan_id, target=target[0])
929
            logger.info("%s:%s: Dry run mode.", host, port)
930
            self.add_scan_log(scan_id, name='', host=host,
931
                              value='Dry run result')
932
        self.finish_scan(scan_id)
933
934
    def handle_timeout(self, scan_id, host):
935
        """ Handles scanner reaching timeout error. """
936
        self.add_scan_error(scan_id, host=host, name="Timeout",
937
                            value="{0} exec timeout."
938
                            .format(self.get_scanner_name()))
939
940
    def set_scan_host_finished(self, scan_id, target, host):
941
        """ Add the host in a list of finished hosts """
942
        self.scan_collection.set_host_finished(scan_id, target, host)
943
944
    def set_scan_progress(self, scan_id, progress):
945
        """ Sets scan_id scan's progress which is a number
946
        between 0 and 100. """
947
        self.scan_collection.set_progress(scan_id, progress)
948
949
    def set_scan_target_progress(
950
            self, scan_id, target, host, progress):
951
        """ Sets host's progress which is part of target. """
952
        self.scan_collection.set_target_progress(
953
            scan_id, target, host, progress)
954
955
    def set_scan_status(self, scan_id, status):
956
        """ Set the scan's status."""
957
        self.scan_collection.set_status(scan_id, status)
958
959
    def get_scan_status(self, scan_id):
960
        """ Get scan_id scans's status."""
961
        return self.scan_collection.get_status(scan_id)
962
963
    def scan_exists(self, scan_id):
964
        """ Checks if a scan with ID scan_id is in collection.
965
966
        @return: 1 if scan exists, 0 otherwise.
967
        """
968
        return self.scan_collection.id_exists(scan_id)
969
970
    def handle_get_scans_command(self, scan_et):
971
        """ Handles <get_scans> command.
972
973
        @return: Response string for <get_scans> command.
974
        """
975
976
        scan_id = scan_et.attrib.get('scan_id')
977
        details = scan_et.attrib.get('details')
978
        pop_res = scan_et.attrib.get('pop_results')
979
        if details and details == '0':
980
            details = False
981
        else:
982
            details = True
983
            if pop_res and pop_res == '1':
984
                pop_res = True
985
            else:
986
                pop_res = False
987
988
        responses = []
989
        if scan_id and scan_id in self.scan_collection.ids_iterator():
990
            self.check_scan_process(scan_id)
991
            scan = self.get_scan_xml(scan_id, details, pop_res)
992
            responses.append(scan)
993
        elif scan_id:
994
            text = "Failed to find scan '{0}'".format(scan_id)
995
            return simple_response_str('get_scans', 404, text)
996
        else:
997
            for scan_id in self.scan_collection.ids_iterator():
998
                self.check_scan_process(scan_id)
999
                scan = self.get_scan_xml(scan_id, details, pop_res)
1000
                responses.append(scan)
1001
        return simple_response_str('get_scans', 200, 'OK', responses)
1002
1003
    def handle_get_vts_command(self, vt_et):
1004
        """ Handles <get_vts> command.
1005
1006
        @return: Response string for <get_vts> command.
1007
        """
1008
1009
        vt_id = vt_et.attrib.get('vt_id')
1010
        vt_filter = vt_et.attrib.get('filter')
1011
1012
        if vt_id and vt_id not in self.vts:
1013
            text = "Failed to find vulnerability test '{0}'".format(vt_id)
1014
            return simple_response_str('get_vts', 404, text)
1015
1016
        filtered_vts = None
1017
        if vt_filter:
1018
            filtered_vts = self.vts_filter.get_filtered_vts_list(
1019
                self.vts, vt_filter)
1020
1021
        responses = []
1022
1023
        vts_xml = self.get_vts_xml(vt_id, filtered_vts)
1024
1025
        responses.append(vts_xml)
1026
1027
        return simple_response_str('get_vts', 200, 'OK', responses)
1028
1029
    def handle_help_command(self, scan_et):
1030
        """ Handles <help> command.
1031
1032
        @return: Response string for <help> command.
1033
        """
1034
        help_format = scan_et.attrib.get('format')
1035
        if help_format is None or help_format == "text":
1036
            # Default help format is text.
1037
            return simple_response_str('help', 200, 'OK',
1038
                                       self.get_help_text())
1039
        elif help_format == "xml":
1040
            text = self.get_xml_str(self.commands)
1041
            return simple_response_str('help', 200, 'OK', text)
1042
        raise OSPDError('Bogus help format', 'help')
1043
1044
    def get_help_text(self):
1045
        """ Returns the help output in plain text format."""
1046
1047
        txt = str('\n')
1048
        for name, info in self.commands.items():
1049
            command_txt = "\t{0: <22} {1}\n".format(name, info['description'])
1050
            if info['attributes']:
1051
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
1052
                for attrname, attrdesc in info['attributes'].items():
1053
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
1054
                    command_txt = ''.join([command_txt, attr_txt])
1055
            if info['elements']:
1056
                command_txt = ''.join([command_txt, "\t Elements:\n",
1057
                                       self.elements_as_text(info['elements'])])
1058
            txt = ''.join([txt, command_txt])
1059
        return txt
1060
1061
    def elements_as_text(self, elems, indent=2):
1062
        """ Returns the elems dictionary as formatted plain text. """
1063
        assert elems
1064
        text = ""
1065
        for elename, eledesc in elems.items():
1066
            if isinstance(eledesc, dict):
1067
                desc_txt = self.elements_as_text(eledesc, indent + 2)
1068
                desc_txt = ''.join(['\n', desc_txt])
1069
            elif isinstance(eledesc, str):
1070
                desc_txt = ''.join([eledesc, '\n'])
1071
            else:
1072
                assert False, "Only string or dictionary"
1073
            ele_txt = "\t{0}{1: <22} {2}".format(' ' * indent, elename,
1074
                                                 desc_txt)
0 ignored issues
show
introduced by
The variable desc_txt does not seem to be defined for all execution paths.
Loading history...
1075
            text = ''.join([text, ele_txt])
1076
        return text
1077
1078
    def handle_delete_scan_command(self, scan_et):
1079
        """ Handles <delete_scan> command.
1080
1081
        @return: Response string for <delete_scan> command.
1082
        """
1083
        scan_id = scan_et.attrib.get('scan_id')
1084
        if scan_id is None:
1085
            return simple_response_str('delete_scan', 404,
1086
                                       'No scan_id attribute')
1087
1088
        if not self.scan_exists(scan_id):
1089
            text = "Failed to find scan '{0}'".format(scan_id)
1090
            return simple_response_str('delete_scan', 404, text)
1091
        self.check_scan_process(scan_id)
1092
        if self.delete_scan(scan_id):
1093
            return simple_response_str('delete_scan', 200, 'OK')
1094
        raise OSPDError('Scan in progress', 'delete_scan')
1095
1096
    def delete_scan(self, scan_id):
1097
        """ Deletes scan_id scan from collection.
1098
1099
        @return: 1 if scan deleted, 0 otherwise.
1100
        """
1101
        if self.get_scan_status(scan_id) == ScanStatus.RUNNING:
1102
            return 0
1103
1104
        try:
1105
            del self.scan_processes[scan_id]
1106
        except KeyError:
1107
            logger.debug('Scan process for %s not found', scan_id)
1108
        return self.scan_collection.delete_scan(scan_id)
1109
1110
    def get_scan_results_xml(self, scan_id, pop_res):
1111
        """ Gets scan_id scan's results in XML format.
1112
1113
        @return: String of scan results in xml.
1114
        """
1115
        results = Element('results')
1116
        for result in self.scan_collection.results_iterator(scan_id, pop_res):
1117
            results.append(get_result_xml(result))
1118
1119
        logger.info('Returning %d results', len(results))
1120
        return results
1121
1122
    def get_xml_str(self, data):
1123
        """ Creates a string in XML Format using the provided data structure.
1124
1125
        @param: Dictionary of xml tags and their elements.
1126
1127
        @return: String of data in xml format.
1128
        """
1129
1130
        responses = []
1131
        for tag, value in data.items():
1132
            elem = Element(tag)
1133
            if isinstance(value, dict):
1134
                for value in self.get_xml_str(value):
1135
                    elem.append(value)
1136
            elif isinstance(value, list):
1137
                value = ', '.join([m for m in value])
1138
                elem.text = value
1139
            else:
1140
                elem.text = value
1141
            responses.append(elem)
1142
        return responses
1143
1144
    def get_scan_xml(self, scan_id, detailed=True, pop_res=False):
1145
        """ Gets scan in XML format.
1146
1147
        @return: String of scan in XML format.
1148
        """
1149
        if not scan_id:
1150
            return Element('scan')
1151
1152
        target = ','.join(self.get_scan_target(scan_id))
1153
        progress = self.get_scan_progress(scan_id)
1154
        status = self.get_scan_status(scan_id)
1155
        start_time = self.get_scan_start_time(scan_id)
1156
        end_time = self.get_scan_end_time(scan_id)
1157
        response = Element('scan')
1158
        for name, value in [('id', scan_id),
1159
                            ('target', target),
1160
                            ('progress', progress),
1161
                            ('status', status.name.lower()),
1162
                            ('start_time', start_time),
1163
                            ('end_time', end_time)]:
1164
            response.set(name, str(value))
1165
        if detailed:
1166
            response.append(self.get_scan_results_xml(scan_id, pop_res))
1167
        return response
1168
1169
    @staticmethod
1170
    def get_custom_vt_as_xml_str(vt_id, custom):
1171
        """ Create a string representation of the XML object from the
1172
        custom data object.
1173
        This needs to be implemented by each ospd wrapper, in case
1174
        custom elements for VTs are used.
1175
1176
        The custom XML object which is returned will be embedded
1177
        into a <custom></custom> element.
1178
1179
        @return: XML object as string for custom data.
1180
        """
1181
        return ''
1182
1183
    @staticmethod
1184
    def get_params_vt_as_xml_str(vt_id, vt_params):
1185
        """ Create a string representation of the XML object from the
1186
        vt_params data object.
1187
        This needs to be implemented by each ospd wrapper, in case
1188
        vt_params elements for VTs are used.
1189
1190
        The params XML object which is returned will be embedded
1191
        into a <params></params> element.
1192
1193
        @return: XML object as string for vt parameters data.
1194
        """
1195
        return ''
1196
1197
    @staticmethod
1198
    def get_refs_vt_as_xml_str(vt_id, vt_refs):
1199
        """ Create a string representation of the XML object from the
1200
        refs data object.
1201
        This needs to be implemented by each ospd wrapper, in case
1202
        refs elements for VTs are used.
1203
1204
        The refs XML object which is returned will be embedded
1205
        into a <refs></refs> element.
1206
1207
        @return: XML object as string for vt references data.
1208
        """
1209
        return ''
1210
1211
    @staticmethod
1212
    def get_dependencies_vt_as_xml_str(vt_id, vt_dependencies):
1213
        """ Create a string representation of the XML object from the
1214
        vt_dependencies data object.
1215
        This needs to be implemented by each ospd wrapper, in case
1216
        vt_dependencies elements for VTs are used.
1217
1218
        The vt_dependencies XML object which is returned will be embedded
1219
        into a <dependencies></dependencies> element.
1220
1221
        @return: XML object as string for vt dependencies data.
1222
        """
1223
        return ''
1224
1225
    @staticmethod
1226
    def get_creation_time_vt_as_xml_str(vt_id, vt_creation_time):
1227
        """ Create a string representation of the XML object from the
1228
        vt_creation_time data object.
1229
        This needs to be implemented by each ospd wrapper, in case
1230
        vt_creation_time elements for VTs are used.
1231
1232
        The vt_creation_time XML object which is returned will be embedded
1233
        into a <vt_creation_time></vt_creation_time> element.
1234
1235
        @return: XML object as string for vt creation time data.
1236
        """
1237
        return ''
1238
1239
    @staticmethod
1240
    def get_modification_time_vt_as_xml_str(vt_id, vt_modification_time):
1241
        """ Create a string representation of the XML object from the
1242
        vt_modification_time data object.
1243
        This needs to be implemented by each ospd wrapper, in case
1244
        vt_modification_time elements for VTs are used.
1245
1246
        The vt_modification_time XML object which is returned will be embedded
1247
        into a <vt_modification_time></vt_modification_time> element.
1248
1249
        @return: XML object as string for vt references data.
1250
        """
1251
        return ''
1252
1253
    @staticmethod
1254
    def get_summary_vt_as_xml_str(vt_id, summary):
1255
        """ Create a string representation of the XML object from the
1256
        summary data object.
1257
        This needs to be implemented by each ospd wrapper, in case
1258
        summary elements for VTs are used.
1259
1260
        The summary XML object which is returned will be embedded
1261
        into a <summary></summary> element.
1262
1263
        @return: XML object as string for summary data.
1264
        """
1265
        return ''
1266
1267
    @staticmethod
1268
    def get_impact_vt_as_xml_str(vt_id, impact):
1269
        """ Create a string representation of the XML object from the
1270
        impact data object.
1271
        This needs to be implemented by each ospd wrapper, in case
1272
        impact elements for VTs are used.
1273
1274
        The impact XML object which is returned will be embedded
1275
        into a <impact></impact> element.
1276
1277
        @return: XML object as string for impact data.
1278
        """
1279
        return ''
1280
1281
    @staticmethod
1282
    def get_affected_vt_as_xml_str(vt_id, affected):
1283
        """ Create a string representation of the XML object from the
1284
        affected data object.
1285
        This needs to be implemented by each ospd wrapper, in case
1286
        affected elements for VTs are used.
1287
1288
        The affected XML object which is returned will be embedded
1289
        into a <affected></affected> element.
1290
1291
        @return: XML object as string for affected data.
1292
        """
1293
        return ''
1294
1295
    @staticmethod
1296
    def get_insight_vt_as_xml_str(vt_id, insight):
1297
        """ Create a string representation of the XML object from the
1298
        insight data object.
1299
        This needs to be implemented by each ospd wrapper, in case
1300
        insight elements for VTs are used.
1301
1302
        The insight XML object which is returned will be embedded
1303
        into a <insight></insight> element.
1304
1305
        @return: XML object as string for insight data.
1306
        """
1307
        return ''
1308
1309
    @staticmethod
1310
    def get_solution_vt_as_xml_str(vt_id, solution, solution_type=None):
1311
        """ Create a string representation of the XML object from the
1312
        solution data object.
1313
        This needs to be implemented by each ospd wrapper, in case
1314
        solution elements for VTs are used.
1315
1316
        The solution XML object which is returned will be embedded
1317
        into a <solution></solution> element.
1318
1319
        @return: XML object as string for solution data.
1320
        """
1321
        return ''
1322
1323
    @staticmethod
1324
    def get_detection_vt_as_xml_str(vt_id, detection=None, qod_type=None, qod=None):
1325
        """ Create a string representation of the XML object from the
1326
        detection data object.
1327
        This needs to be implemented by each ospd wrapper, in case
1328
        detection elements for VTs are used.
1329
1330
        The detection XML object which is returned is an element with
1331
        tag <detection></detection> element
1332
1333
        @return: XML object as string for detection data.
1334
        """
1335
        return ''
1336
1337
    @staticmethod
1338
    def get_severities_vt_as_xml_str(vt_id, severities):
1339
        """ Create a string representation of the XML object from the
1340
        severities data object.
1341
        This needs to be implemented by each ospd wrapper, in case
1342
        severities elements for VTs are used.
1343
1344
        The severities XML objects which are returned will be embedded
1345
        into a <severities></severities> element.
1346
1347
        @return: XML object as string for severities data.
1348
        """
1349
        return ''
1350
1351
    def get_vt_xml(self, vt_id):
1352
        """ Gets a single vulnerability test information in XML format.
1353
1354
        @return: String of single vulnerability test information in XML format.
1355
        """
1356
        if not vt_id:
1357
            return Element('vt')
1358
1359
        vt = self.vts.get(vt_id)
1360
1361
        name = vt.get('name')
1362
        vt_xml = Element('vt')
1363
        vt_xml.set('id', vt_id)
1364
1365
        for name, value in [('name', name)]:
1366
            elem = SubElement(vt_xml, name)
1367
            elem.text = str(value)
1368
1369
        if vt.get('vt_params'):
1370
            params_xml_str = self.get_params_vt_as_xml_str(
1371
                vt_id, vt.get('vt_params'))
1372
            vt_xml.append(secET.fromstring(params_xml_str))
1373
1374
        if vt.get('vt_refs'):
1375
            refs_xml_str = self.get_refs_vt_as_xml_str(
1376
                vt_id, vt.get('vt_refs'))
1377
            vt_xml.append(secET.fromstring(refs_xml_str))
1378
1379
        if vt.get('vt_dependencies'):
1380
            dependencies = self.get_dependencies_vt_as_xml_str(
1381
                vt_id, vt.get('vt_dependencies'))
1382
            vt_xml.append(secET.fromstring(dependencies))
1383
1384
        if vt.get('creation_time'):
1385
            vt_ctime = self.get_creation_time_vt_as_xml_str(
1386
                vt_id, vt.get('creation_time'))
1387
            vt_xml.append(secET.fromstring(vt_ctime))
1388
1389
        if vt.get('modification_time'):
1390
            vt_mtime = self.get_modification_time_vt_as_xml_str(
1391
                vt_id, vt.get('modification_time'))
1392
            vt_xml.append(secET.fromstring(vt_mtime))
1393
1394
        if vt.get('summary'):
1395
            summary_xml_str = self.get_summary_vt_as_xml_str(
1396
                vt_id, vt.get('summary'))
1397
            vt_xml.append(secET.fromstring(summary_xml_str))
1398
1399
        if vt.get('impact'):
1400
            impact_xml_str = self.get_impact_vt_as_xml_str(
1401
                vt_id, vt.get('impact'))
1402
            vt_xml.append(secET.fromstring(impact_xml_str))
1403
1404
        if vt.get('affected'):
1405
            affected_xml_str = self.get_affected_vt_as_xml_str(
1406
                vt_id, vt.get('affected'))
1407
            vt_xml.append(secET.fromstring(affected_xml_str))
1408
1409
        if vt.get('insight'):
1410
            insight_xml_str = self.get_insight_vt_as_xml_str(
1411
                vt_id, vt.get('insight'))
1412
            vt_xml.append(secET.fromstring(insight_xml_str))
1413
1414
        if vt.get('solution'):
1415
            solution_xml_str = self.get_solution_vt_as_xml_str(
1416
                vt_id, vt.get('solution'), vt.get('solution_type'))
1417
            vt_xml.append(secET.fromstring(solution_xml_str))
1418
1419
        if vt.get('detection') or vt.get('qod_type') or vt.get('qod'):
1420
            detection_xml_str = self.get_detection_vt_as_xml_str(
1421
                vt_id, vt.get('detection'), vt.get('qod_type'), vt.get('qod'))
1422
            vt_xml.append(secET.fromstring(detection_xml_str))
1423
1424
        if vt.get('severities'):
1425
            severities_xml_str = self.get_severities_vt_as_xml_str(
1426
                    vt_id, vt.get('severities'))
1427
            vt_xml.append(secET.fromstring(severities_xml_str))
1428
1429
        if vt.get('custom'):
1430
            custom_xml_str = self.get_custom_vt_as_xml_str(
1431
                vt_id, vt.get('custom'))
1432
            vt_xml.append(secET.fromstring(custom_xml_str))
1433
1434
        return vt_xml
1435
1436
    def get_vts_xml(self, vt_id=None, filtered_vts=None):
1437
        """ Gets collection of vulnerability test information in XML format.
1438
        If vt_id is specified, the collection will contain only this vt, if
1439
        found.
1440
        If no vt_id is specified, the collection will contain all vts or those
1441
        passed in filtered_vts.
1442
1443
        Arguments:
1444
            vt_id (vt_id, optional): ID of the vt to get.
1445
            filtered_vts (dict, optional): Filtered VTs collection.
1446
1447
        Return:
1448
            String of collection of vulnerability test information in
1449
            XML format.
1450
        """
1451
1452
        vts_xml = Element('vts')
1453
1454
        if vt_id:
1455
            vts_xml.append(self.get_vt_xml(vt_id))
1456
        elif filtered_vts:
1457
            for vt_id in filtered_vts:
1458
                vts_xml.append(self.get_vt_xml(vt_id))
1459
        else:
1460
            for vt_id in self.vts:
1461
                vts_xml.append(self.get_vt_xml(vt_id))
1462
1463
        return vts_xml
1464
1465
    def handle_get_scanner_details(self):
1466
        """ Handles <get_scanner_details> command.
1467
1468
        @return: Response string for <get_scanner_details> command.
1469
        """
1470
        desc_xml = Element('description')
1471
        desc_xml.text = self.get_scanner_description()
1472
        details = [
1473
            desc_xml,
1474
            self.get_scanner_params_xml()
1475
        ]
1476
        return simple_response_str('get_scanner_details', 200, 'OK', details)
1477
1478
    def handle_get_version_command(self):
1479
        """ Handles <get_version> command.
1480
1481
        @return: Response string for <get_version> command.
1482
        """
1483
        protocol = Element('protocol')
1484
        for name, value in [('name', 'OSP'), ('version', self.get_protocol_version())]:
1485
            elem = SubElement(protocol, name)
1486
            elem.text = value
1487
1488
        daemon = Element('daemon')
1489
        for name, value in [('name', self.get_daemon_name()), ('version', self.get_daemon_version())]:
1490
            elem = SubElement(daemon, name)
1491
            elem.text = value
1492
1493
        scanner = Element('scanner')
1494
        for name, value in [('name', self.get_scanner_name()), ('version', self.get_scanner_version())]:
1495
            elem = SubElement(scanner, name)
1496
            elem.text = value
1497
1498
        content = [protocol, daemon, scanner]
1499
1500
        if self.get_vts_version():
1501
            vts = Element('vts')
1502
            elem = SubElement(vts, 'version')
1503
            elem.text = self.get_vts_version()
1504
            content.append(vts)
1505
1506
        return simple_response_str('get_version', 200, 'OK', content)
1507
1508
    def handle_command(self, command):
1509
        """ Handles an osp command in a string.
1510
1511
        @return: OSP Response to command.
1512
        """
1513
        try:
1514
            tree = secET.fromstring(command)
1515
        except secET.ParseError:
1516
            logger.debug("Erroneous client input: %s", command)
1517
            raise OSPDError('Invalid data')
1518
1519
        if not self.command_exists(tree.tag) and tree.tag != "authenticate":
1520
            raise OSPDError('Bogus command name')
1521
1522
        if tree.tag == "get_version":
1523
            return self.handle_get_version_command()
1524
        elif tree.tag == "start_scan":
1525
            return self.handle_start_scan_command(tree)
1526
        elif tree.tag == "stop_scan":
1527
            return self.handle_stop_scan_command(tree)
1528
        elif tree.tag == "get_scans":
1529
            return self.handle_get_scans_command(tree)
1530
        elif tree.tag == "get_vts":
1531
            return self.handle_get_vts_command(tree)
1532
        elif tree.tag == "delete_scan":
1533
            return self.handle_delete_scan_command(tree)
1534
        elif tree.tag == "help":
1535
            return self.handle_help_command(tree)
1536
        elif tree.tag == "get_scanner_details":
1537
            return self.handle_get_scanner_details()
1538
        else:
1539
            assert False, "Unhandled command: {0}".format(tree.tag)
1540
1541
    def check(self):
1542
        """ Asserts to False. Should be implemented by subclass. """
1543
        raise NotImplementedError
1544
1545
    def run(self, address, port, unix_path):
1546
        """ Starts the Daemon, handling commands until interrupted.
1547
1548
        @return False if error. Runs indefinitely otherwise.
1549
        """
1550
        assert address or unix_path
1551
        if unix_path:
1552
            sock = bind_unix_socket(unix_path)
1553
        else:
1554
            sock = bind_socket(address, port)
1555
        if sock is None:
1556
            return False
1557
1558
        sock.setblocking(False)
1559
        inputs = [sock]
1560
        outputs = []
1561
        try:
1562
            while True:
1563
                readable, _, _ = select.select(
1564
                    inputs, outputs, inputs, SCHEDULER_CHECK_PERIOD)
1565
                for r_socket in readable:
1566
                    if unix_path and r_socket is sock:
1567
                        client_stream, _ = sock.accept()
1568
                        logger.debug("New connection from %s", unix_path)
1569
                        self.handle_client_stream(client_stream, True)
1570
                    else:
1571
                        client_stream = self.new_client_stream(sock)
1572
                        if client_stream is None:
1573
                            continue
1574
                        self.handle_client_stream(client_stream, False)
1575
                    close_client_stream(client_stream, unix_path)
1576
                self.scheduler()
1577
        except KeyboardInterrupt:
1578
            logger.info("Received Ctrl-C shutting-down ...")
1579
        finally:
1580
            sock.shutdown(socket.SHUT_RDWR)
1581
            sock.close()
1582
1583
    def scheduler(self):
1584
        """ Should be implemented by subclass in case of need
1585
        to run tasks periodically. """
1586
1587
    def create_scan(self, scan_id, targets, options, vts):
1588
        """ Creates a new scan.
1589
1590
        @target: Target to scan.
1591
        @options: Miscellaneous scan options.
1592
1593
        @return: New scan's ID.
1594
        """
1595
        if self.scan_exists(scan_id):
1596
            logger.info("Scan %s exists. Resuming scan.", scan_id)
1597
1598
        return self.scan_collection.create_scan(scan_id, targets, options, vts)
1599
1600
    def get_scan_options(self, scan_id):
1601
        """ Gives a scan's list of options. """
1602
        return self.scan_collection.get_options(scan_id)
1603
1604
    def set_scan_option(self, scan_id, name, value):
1605
        """ Sets a scan's option to a provided value. """
1606
        return self.scan_collection.set_option(scan_id, name, value)
1607
1608
    def check_scan_process(self, scan_id):
1609
        """ Check the scan's process, and terminate the scan if not alive. """
1610
        scan_process = self.scan_processes[scan_id]
1611
        progress = self.get_scan_progress(scan_id)
1612
        if progress < 100 and not scan_process.is_alive():
1613
            self.set_scan_status(scan_id, ScanStatus.STOPPED)
1614
            self.add_scan_error(scan_id, name="", host="",
1615
                                value="Scan process failure.")
1616
            logger.info("%s: Scan stopped with errors.", scan_id)
1617
        elif progress == 100:
1618
            scan_process.join()
1619
1620
    def get_scan_progress(self, scan_id):
1621
        """ Gives a scan's current progress value. """
1622
        return self.scan_collection.get_progress(scan_id)
1623
1624
    def get_scan_target_progress(self, scan_id, target):
1625
        """ Gives a list with scan's current progress value of each target. """
1626
        return self.scan_collection.get_target_progress(scan_id, target)
1627
1628
    def get_scan_target(self, scan_id):
1629
        """ Gives a scan's target. """
1630
        return self.scan_collection.get_target_list(scan_id)
1631
1632
    def get_scan_ports(self, scan_id, target=''):
1633
        """ Gives a scan's ports list. """
1634
        return self.scan_collection.get_ports(scan_id, target)
1635
1636
    def get_scan_exclude_hosts(self, scan_id, target=''):
1637
        """ Gives a scan's exclude host list. If a target is passed gives
1638
        the exclude host list for the given target. """
1639
        return self.scan_collection.get_exclude_hosts(scan_id, target)
1640
1641
    def get_scan_credentials(self, scan_id, target=''):
1642
        """ Gives a scan's credential list. If a target is passed gives
1643
        the credential list for the given target. """
1644
        return self.scan_collection.get_credentials(scan_id, target)
1645
1646
    def get_scan_vts(self, scan_id):
1647
        """ Gives a scan's vts list. """
1648
        return self.scan_collection.get_vts(scan_id)
1649
1650
    def get_scan_unfinished_hosts(self, scan_id):
1651
        """ Get a list of unfinished hosts."""
1652
        return self.scan_collection.get_hosts_unfinished(scan_id)
1653
1654
    def get_scan_finished_hosts(self, scan_id):
1655
        """ Get a list of unfinished hosts."""
1656
        return self.scan_collection.get_hosts_finished(scan_id)
1657
1658
    def get_scan_start_time(self, scan_id):
1659
        """ Gives a scan's start time. """
1660
        return self.scan_collection.get_start_time(scan_id)
1661
1662
    def get_scan_end_time(self, scan_id):
1663
        """ Gives a scan's end time. """
1664
        return self.scan_collection.get_end_time(scan_id)
1665
1666
    def add_scan_log(self, scan_id, host='', name='', value='', port='',
1667
                     test_id='', qod=''):
1668
        """ Adds a log result to scan_id scan. """
1669
        self.scan_collection.add_result(scan_id, ResultType.LOG, host, name,
1670
                                        value, port, test_id, 0.0, qod)
1671
1672
    def add_scan_error(self, scan_id, host='', name='', value='', port=''):
1673
        """ Adds an error result to scan_id scan. """
1674
        self.scan_collection.add_result(scan_id, ResultType.ERROR, host, name,
1675
                                        value, port)
1676
1677
    def add_scan_host_detail(self, scan_id, host='', name='', value=''):
1678
        """ Adds a host detail result to scan_id scan. """
1679
        self.scan_collection.add_result(scan_id, ResultType.HOST_DETAIL, host,
1680
                                        name, value)
1681
1682
    def add_scan_alarm(self, scan_id, host='', name='', value='', port='',
1683
                       test_id='', severity='', qod=''):
1684
        """ Adds an alarm result to scan_id scan. """
1685
        self.scan_collection.add_result(scan_id, ResultType.ALARM, host, name,
1686
                                        value, port, test_id, severity, qod)
1687