Passed
Pull Request — master (#95)
by Juan José
01:17
created

ospd.ospd.OSPDaemon.get_insight_vt_as_xml_str()   A

Complexity

Conditions 1

Size

Total Lines 13
Code Lines 3

Duplication

Lines 13
Ratio 100 %

Importance

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