Test Failed
Push — master ( f9354e...ea6093 )
by
unknown
40s
created

ospd.ospd.OSPDaemon.get_scanner_params_xml()   A

Complexity

Conditions 4

Size

Total Lines 15
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nop 1
dl 0
loc 15
rs 9.2
c 0
b 0
f 0
1
# -*- coding: utf-8 -*-
2
# $Id$
3
# Description:
4
# OSP Daemon core class.
5
#
6
# Authors:
7
# Hani Benhabiles <[email protected]>
8
# Benoît Allard <[email protected]>
9
# Jan-Oliver Wahmer <[email protected]>
10
#
11
# Copyright:
12
# Copyright (C) 2014, 2015, 2018 Greenbone Networks GmbH
13
#
14
# This program is free software; you can redistribute it and/or
15
# modify it under the terms of the GNU General Public License
16
# as published by the Free Software Foundation; either version 2
17
# of the License, or (at your option) any later version.
18
#
19
# This program is distributed in the hope that it will be useful,
20
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
# GNU General Public License for more details.
23
#
24
# You should have received a copy of the GNU General Public License
25
# along with this program; if not, write to the Free Software
26
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
27
28
""" OSP Daemon core class. """
29
30
# This is needed for older pythons as our current module is called the same
31
# as the package we are in ...
32
# Another solution would be to rename that file.
33
from __future__ import absolute_import
34
35
import logging
36
import socket
37
import ssl
38
import multiprocessing
39
import xml.etree.ElementTree as ET
40
import os
41
42
from ospd import __version__
43
from ospd.misc import ScanCollection, ResultType, target_str_to_list
44
from ospd.misc import resolve_hostname, valid_uuid
45
46
logger = logging.getLogger(__name__)
47
48
PROTOCOL_VERSION = "1.2"
49
50
BASE_SCANNER_PARAMS = {
51
    'debug_mode': {
52
        'type': 'boolean',
53
        'name': 'Debug Mode',
54
        'default': 0,
55
        'mandatory': 0,
56
        'description': 'Whether to get extra scan debug information.',
57
    },
58
    'dry_run': {
59
        'type': 'boolean',
60
        'name': 'Dry Run',
61
        'default': 0,
62
        'mandatory': 0,
63
        'description': 'Whether to dry run scan.',
64
    },
65
}
66
67
COMMANDS_TABLE = {
68
    'start_scan': {
69
        'description': 'Start a new scan.',
70
        'attributes': {
71
            'target': 'Target host to scan',
72
            'ports': 'Ports list to scan',
73
            'scan_id': 'Optional UUID value to use as scan ID',
74
        },
75
        'elements': None
76
    },
77
    'stop_scan': {
78
        'description': 'Stop a currently running scan.',
79
        'attributes': {
80
            'scan_id': 'ID of scan to stop.'
81
        },
82
        'elements': None
83
    },
84
    'help': {
85
        'description': 'Print the commands help.',
86
        'attributes': {
87
            'format': 'Help format. Could be text or xml.'
88
        },
89
        'elements': None
90
    },
91
    'get_scans': {
92
        'description': 'List the scans in buffer.',
93
        'attributes': {
94
            'scan_id': 'ID of a specific scan to get.',
95
            'details': 'Whether to return the full scan report.'
96
        },
97
        'elements': None
98
    },
99
    'get_vts': {
100
        'description': 'List of available vulnerability tests.',
101
        'attributes': {
102
            'vt_id': 'ID of a specific vulnerability test to get.'
103
        },
104
        'elements': None
105
    },
106
    'delete_scan': {
107
        'description': 'Delete a finished scan.',
108
        'attributes': {
109
            'scan_id': 'ID of scan to delete.'
110
        },
111
        'elements': None
112
    },
113
    'get_version': {
114
        'description': 'Return various versions.',
115
        'attributes': None,
116
        'elements': None
117
    },
118
    'get_scanner_details': {
119
        'description': 'Return scanner description and parameters',
120
        'attributes': None,
121
        'elements': None
122
    }
123
}
124
125
126
def get_result_xml(result):
127
    """ Formats a scan result to XML format. """
128
    result_xml = ET.Element('result')
129
    for name, value in [('name', result['name']),
130
                        ('type', ResultType.get_str(result['type'])),
131
                        ('severity', result['severity']),
132
                        ('host', result['host']),
133
                        ('test_id', result['test_id']),
134
                        ('port', result['port']),
135
                        ('qod', result['qod'])]:
136
        result_xml.set(name, str(value))
137
    result_xml.text = result['value']
138
    return result_xml
139
140
141
def simple_response_str(command, status, status_text, content=""):
142
    """ Creates an OSP response XML string.
143
144
    @param: OSP Command to respond to.
145
    @param: Status of the response.
146
    @param: Status text of the response.
147
    @param: Text part of the response XML element.
148
149
    @return: String of response in xml format.
150
    """
151
    response = ET.Element('%s_response' % command)
152
    for name, value in [('status', str(status)), ('status_text', status_text)]:
153
        response.set(name, str(value))
154
    if isinstance(content, list):
155
        for elem in content:
156
            response.append(elem)
157
    elif isinstance(content, ET.Element):
158
        response.append(content)
159
    else:
160
        response.text = content
161
    return ET.tostring(response)
162
163
164
class OSPDError(Exception):
165
166
    """ This is an exception that will result in an error message to the
167
    client """
168
169
    def __init__(self, message, command='osp', status=400):
170
        super(OSPDError, self).__init__()
171
        self.message = message
172
        self.command = command
173
        self.status = status
174
175
    def as_xml(self):
176
        """ Return the error in xml format. """
177
        return simple_response_str(self.command, self.status, self.message)
178
179
180
def bind_socket(address, port):
181
    """ Returns a socket bound on (address:port). """
182
183
    assert address
184
    assert port
185
    bindsocket = socket.socket()
186
    try:
187
        bindsocket.bind((address, port))
188
    except socket.error:
189
        logger.error("Couldn't bind socket on {0}:{1}"
190
                     .format(address, port))
191
        return None
192
193
    logger.info('Listening on {0}:{1}'.format(address, port))
194
    bindsocket.listen(0)
195
    return bindsocket
196
197
def bind_unix_socket(path):
198
    """ Returns a unix file socket bound on (path). """
199
200
    assert path
201
    bindsocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
202
    try:
203
        os.unlink(path)
204
    except OSError:
205
        if os.path.exists(path):
206
            raise
207
    try:
208
        bindsocket.bind(path)
209
    except socket.error:
210
        logger.error("Couldn't bind socket on {0}".format(path))
211
        return None
212
213
    logger.info('Listening on {0}'.format(path))
214
    bindsocket.listen(0)
215
    return bindsocket
216
217
218
def close_client_stream(client_stream, unix_path):
219
    """ Closes provided client stream """
220
    try:
221
        client_stream.shutdown(socket.SHUT_RDWR)
222
        if unix_path:
223
            logger.debug('{0}: Connection closed'.format(unix_path))
224
        else:
225
            peer = client_stream.getpeername()
226
            logger.debug('{0}:{1}: Connection closed'.format(peer[0], peer[1]))
227
    except (socket.error, OSError) as exception:
228
        logger.debug('Connection closing error: {0}'.format(exception))
229
    client_stream.close()
230
231
232
class OSPDaemon(object):
233
234
    """ Daemon class for OSP traffic handling.
235
236
    Every scanner wrapper should subclass it and make necessary additions and
237
    changes.
238
    * Add any needed parameters in __init__.
239
    * Implement check() method which verifies scanner availability and other
240
      environment related conditions.
241
    * Implement process_scan_params and exec_scan methods which are
242
      specific to handling the <start_scan> command, executing the wrapped
243
      scanner and storing the results.
244
    * exec_scan() should return 0 if host is dead or not reached, 1 if host is
245
      alive and 2 if scan error or status is unknown.
246
    * Implement other methods that assert to False such as get_scanner_name,
247
      get_scanner_version.
248
    * Use Call set_command_attributes at init time to add scanner command
249
      specific options eg. the w3af profile for w3af wrapper.
250
    """
251
252
    def __init__(self, certfile, keyfile, cafile):
253
        """ Initializes the daemon's internal data. """
254
        # @todo: Actually it makes sense to move the certificate params to
255
        #        a separate function because it is not mandatory anymore to
256
        #        use a TLS setup (unix file socket is an alternative).
257
        #        However, changing this makes it mandatory for any ospd scanner
258
        #        to change the function calls as well. So this breaks the API
259
        #        and should only be done with a major release.
260
        self.certs = dict()
261
        self.certs['cert_file'] = certfile
262
        self.certs['key_file'] = keyfile
263
        self.certs['ca_file'] = cafile
264
        self.scan_collection = ScanCollection()
265
        self.scan_processes = dict()
266
        self.daemon_info = dict()
267
        self.daemon_info['name'] = "OSPd"
268
        self.daemon_info['version'] = __version__
269
        self.daemon_info['description'] = "No description"
270
        self.scanner_info = dict()
271
        self.scanner_info['name'] = 'No name'
272
        self.scanner_info['version'] = 'No version'
273
        self.scanner_info['description'] = 'No description'
274
        self.server_version = None  # Set by the subclass.
275
        self.protocol_version = PROTOCOL_VERSION
276
        self.commands = COMMANDS_TABLE
277
        self.scanner_params = dict()
278
        self.vts = dict()
279
        for name, param in BASE_SCANNER_PARAMS.items():
280
            self.add_scanner_param(name, param)
281
282
    def set_command_attributes(self, name, attributes):
283
        """ Sets the xml attributes of a specified command. """
284
        if self.command_exists(name):
285
            command = self.commands.get(name)
286
            command['attributes'] = attributes
287
288
    def add_scanner_param(self, name, scanner_param):
289
        """ Add a scanner parameter. """
290
291
        assert name
292
        assert scanner_param
293
        self.scanner_params[name] = scanner_param
294
        command = self.commands.get('start_scan')
295
        command['elements'] = {
296
            'scanner_params':
297
                {k: v['name'] for k, v in self.scanner_params.items()}}
298
299
    def add_vt(self, vt_id, name='', custom=None):
300
        """ Add a vulnerability test information.
301
302
        Returns: The new number of stored VTs.
303
        -1 in case the VT ID was already present and thus the
304
        new VT was not considered.
305
        -2 in case the vt_id was invalid.
306
        """
307
308
        if not vt_id:
309
            return -2 # no valid vt_id
310
311
        if vt_id in self.vts:
312
            return -1 # The VT was already in the list.
313
314
        if vt_id and custom is not None:
315
            self.vts[vt_id] = { 'name': name, 'custom': custom }
316
        else:
317
            self.vts[vt_id] = { 'name': name }
318
319
        return len(self.vts)
320
321
    def command_exists(self, name):
322
        """ Checks if a commands exists. """
323
        return name in self.commands.keys()
324
325
    def get_scanner_name(self):
326
        """ Gives the wrapped scanner's name. """
327
        return self.scanner_info['name']
328
329
    def get_scanner_version(self):
330
        """ Gives the wrapped scanner's version. """
331
        return self.scanner_info['version']
332
333
    def get_scanner_description(self):
334
        """ Gives the wrapped scanner's description. """
335
        return self.scanner_info['description']
336
337
    def get_server_version(self):
338
        """ Gives the specific OSP server's version. """
339
        assert self.server_version
340
        return self.server_version
341
342
    def get_protocol_version(self):
343
        """ Gives the OSP's version. """
344
        return self.protocol_version
345
346
    def _preprocess_scan_params(self, xml_params):
347
        """ Processes the scan parameters. """
348
        params = {}
349
        for param in xml_params:
350
            params[param.tag] = param.text or ''
351
        # Set default values.
352
        for key in self.scanner_params:
353
            if key not in params:
354
                params[key] = self.get_scanner_param_default(key)
355
                if self.get_scanner_param_type(key) == 'selection':
356
                    params[key] = params[key].split('|')[0]
357
        # Validate values.
358
        for key in params:
359
            param_type = self.get_scanner_param_type(key)
360
            if not param_type:
361
                continue
362
            if param_type in ['integer', 'boolean']:
363
                try:
364
                    params[key] = int(params[key])
365
                except ValueError:
366
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
367
            if param_type == 'boolean':
368
                if params[key] not in [0, 1]:
369
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
370
            elif param_type == 'selection':
371
                selection = self.get_scanner_param_default(key).split('|')
372
                if params[key] not in selection:
373
                    raise OSPDError('Invalid %s value' % key, 'start_scan')
374
            if self.get_scanner_param_mandatory(key) and params[key] == '':
375
                    raise OSPDError('Mandatory %s value is missing' % key,
376
                                    'start_scan')
377
        return params
378
379
    def process_scan_params(self, params):
380
        """ This method is to be overridden by the child classes if necessary
381
        """
382
        return params
383
384
    def handle_start_scan_command(self, scan_et):
385
        """ Handles <start_scan> command.
386
387
        @return: Response string for <start_scan> command.
388
        """
389
390
        target_str = scan_et.attrib.get('target')
391
        if target_str is None:
392
            raise OSPDError('No target attribute', 'start_scan')
393
        ports_str = scan_et.attrib.get('ports')
394
        if ports_str is None:
395
            raise OSPDError('No ports attribute', 'start_scan')
396
        scan_id = scan_et.attrib.get('scan_id')
397
        if scan_id is not None and scan_id != '' and not valid_uuid(scan_id):
398
            raise OSPDError('Invalid scan_id UUID', 'start_scan')
399
400
        scanner_params = scan_et.find('scanner_params')
401
        if scanner_params is None:
402
            raise OSPDError('No scanner_params element', 'start_scan')
403
404
        params = self._preprocess_scan_params(scanner_params)
405
406
        # Dry run case.
407
        if 'dry_run' in params and int(params['dry_run']):
408
            scan_func = self.dry_run_scan
409
            scan_params = None
410
        else:
411
            scan_func = self.start_scan
412
            scan_params = self.process_scan_params(params)
413
414
        scan_id = self.create_scan(scan_id, target_str, ports_str, scan_params)
415
        scan_process = multiprocessing.Process(target=scan_func,
416
                                               args=(scan_id, target_str))
417
        self.scan_processes[scan_id] = scan_process
418
        scan_process.start()
419
        id_ = ET.Element('id')
420
        id_.text = scan_id
421
        return simple_response_str('start_scan', 200, 'OK', id_)
422
423
    def handle_stop_scan_command(self, scan_et):
424
        """ Handles <stop_scan> command.
425
426
        @return: Response string for <stop_scan> command.
427
        """
428
429
        scan_id = scan_et.attrib.get('scan_id')
430
        if scan_id is None or scan_id == '':
431
            raise OSPDError('No scan_id attribute', 'stop_scan')
432
        scan_process = self.scan_processes.get(scan_id)
433
        if not scan_process:
434
            raise OSPDError('Scan not found {0}.'.format(scan_id), 'stop_scan')
435
        if not scan_process.is_alive():
436
            raise OSPDError('Scan already stopped or finished.', 'stop_scan')
437
438
        logger.info('{0}: Scan stopping {1}.'.format(scan_id, scan_process.ident))
439
        scan_process.terminate()
440
        os.killpg(os.getpgid(scan_process.ident), 15)
441
        scan_process.join()
442
        self.set_scan_progress(scan_id, 100)
443
        self.add_scan_log(scan_id, name='', host='', value='Scan stopped.')
444
        logger.info('{0}: Scan stopped.'.format(scan_id))
445
        return simple_response_str('stop_scan', 200, 'OK')
446
447
    def exec_scan(self, scan_id, target):
448
        """ Asserts to False. Should be implemented by subclass. """
449
        raise NotImplementedError
450
451
    def finish_scan(self, scan_id):
452
        """ Sets a scan as finished. """
453
        self.set_scan_progress(scan_id, 100)
454
        logger.info("{0}: Scan finished.".format(scan_id))
455
456
    def get_daemon_name(self):
457
        """ Gives osp daemon's name. """
458
        return self.daemon_info['name']
459
460
    def get_daemon_version(self):
461
        """ Gives osp daemon's version. """
462
        return self.daemon_info['version']
463
464
    def get_scanner_param_type(self, param):
465
        """ Returns type of a scanner parameter. """
466
        assert isinstance(param, str)
467
        entry = self.scanner_params.get(param)
468
        if not entry:
469
            return None
470
        return entry.get('type')
471
472
    def get_scanner_param_mandatory(self, param):
473
        """ Returns if a scanner parameter is mandatory. """
474
        assert isinstance(param, str)
475
        entry = self.scanner_params.get(param)
476
        if not entry:
477
            return False
478
        return entry.get('mandatory')
479
480
    def get_scanner_param_default(self, param):
481
        """ Returns default value of a scanner parameter. """
482
        assert isinstance(param, str)
483
        entry = self.scanner_params.get(param)
484
        if not entry:
485
            return None
486
        return entry.get('default')
487
488
    def get_scanner_params_xml(self):
489
        """ Returns the OSP Daemon's scanner params in xml format. """
490
        scanner_params = ET.Element('scanner_params')
491
        for param_id, param in self.scanner_params.items():
492
            param_xml = ET.SubElement(scanner_params, 'scanner_param')
493
            for name, value in [('id', param_id),
494
                                ('type', param['type'])]:
495
                param_xml.set(name, value)
496
            for name, value in [('name', param['name']),
497
                                ('description', param['description']),
498
                                ('default', param['default']),
499
                                ('mandatory', param['mandatory'])]:
500
                elem = ET.SubElement(param_xml, name)
501
                elem.text = str(value)
502
        return scanner_params
503
504
    def new_client_stream(self, sock):
505
        """ Returns a new ssl client stream from bind_socket. """
506
507
        assert sock
508
        newsocket, fromaddr = sock.accept()
509
        logger.debug("New connection from"
510
                     " {0}:{1}".format(fromaddr[0], fromaddr[1]))
511
        # NB: Despite the name, ssl.PROTOCOL_SSLv23 selects the highest
512
        # protocol version that both the client and server support. In modern
513
        # Python versions (>= 3.4) it suppports TLS >= 1.0 with SSLv2 and SSLv3
514
        # being disabled. For Python >=3.5, PROTOCOL_SSLv23 is an alias for
515
        # PROTOCOL_TLS which should be used once compatibility with Python 3.4
516
        # is no longer desired.
517
        try:
518
            ssl_socket = ssl.wrap_socket(newsocket, cert_reqs=ssl.CERT_REQUIRED,
519
                                         server_side=True,
520
                                         certfile=self.certs['cert_file'],
521
                                         keyfile=self.certs['key_file'],
522
                                         ca_certs=self.certs['ca_file'],
523
                                         ssl_version=ssl.PROTOCOL_SSLv23)
524
        except (ssl.SSLError, socket.error) as message:
525
            logger.error(message)
526
            return None
527
        return ssl_socket
528
529
    def handle_client_stream(self, stream, is_unix=False):
530
        """ Handles stream of data received from client. """
531
532
        assert stream
533
        data = []
534
        stream.settimeout(2)
535
        while True:
536
            try:
537
                if is_unix:
538
                    data.append(stream.recv(1024))
539
                else:
540
                    data.append(stream.read(1024))
541
                if len(data) == 0:
542
                    logger.warning(
543
                        "Empty client stream (Connection unexpectedly closed)")
544
                    return
545
            except (AttributeError, ValueError) as message:
546
                logger.error(message)
547
                return
548
            except (ssl.SSLError) as exception:
549
                logger.debug('Error: {0}'.format(exception[0]))
550
                break
551
            except (socket.timeout) as exception:
552
                logger.debug('Error: {0}'.format(exception))
553
                break
554
        data = b''.join(data)
555
        if len(data) <= 0:
556
            logger.debug("Empty client stream")
557
            return
558
        try:
559
            response = self.handle_command(data)
560
        except OSPDError as exception:
561
            response = exception.as_xml()
562
            logger.debug('Command error: {0}'.format(exception.message))
563
        except Exception:
564
            logger.exception('While handling client command:')
565
            exception = OSPDError('Fatal error', 'error')
566
            response = exception.as_xml()
567
        if is_unix:
568
            stream.sendall(response)
569
        else:
570
            stream.write(response)
571
572
    def start_scan(self, scan_id, target_str):
573
        """ Starts the scan with scan_id. """
574
575
        os.setsid()
576
        logger.info("{0}: Scan started.".format(scan_id))
577
        target_list = target_str_to_list(target_str)
578
        if target_list is None:
579
            raise OSPDError('Erroneous targets list', 'start_scan')
580
        for index, target in enumerate(target_list):
581
            progress = float(index) * 100 / len(target_list)
582
            self.set_scan_progress(scan_id, int(progress))
583
            logger.info("{0}: Host scan started.".format(target))
584
            try:
585
                ret = self.exec_scan(scan_id, target)
586
                if ret == 0:
587
                    self.add_scan_host_detail(scan_id, name='host_status',
588
                                              host=target, value='0')
589
                elif ret == 1:
590
                    self.add_scan_host_detail(scan_id, name='host_status',
591
                                              host=target, value='1')
592
                elif ret == 2:
593
                    self.add_scan_host_detail(scan_id, name='host_status',
594
                                              host=target, value='2')
595
                else:
596
                    logger.debug('{0}: No host status returned'.format(target))
597
            except Exception as e:
598
                self.add_scan_error(scan_id, name='', host=target,
599
                                    value='Host process failure (%s).' % e)
600
                logger.exception('While scanning {0}:'.format(target))
601
            else:
602
                logger.info("{0}: Host scan finished.".format(target))
603
604
        self.finish_scan(scan_id)
605
606
    def dry_run_scan(self, scan_id, target_str):
607
        """ Dry runs a scan. """
608
609
        os.setsid()
610
        target_list = target_str_to_list(target_str)
611
        for _, target in enumerate(target_list):
612
            host = resolve_hostname(target)
613
            if host is None:
614
                logger.info("Couldn't resolve {0}.".format(target))
615
                continue
616
            logger.info("{0}: Dry run mode.".format(host))
617
            self.add_scan_log(scan_id, name='', host=host,
618
                              value='Dry run result')
619
        self.finish_scan(scan_id)
620
621
    def handle_timeout(self, scan_id, host):
622
        """ Handles scanner reaching timeout error. """
623
        self.add_scan_error(scan_id, host=host, name="Timeout",
624
                            value="{0} exec timeout."
625
                            .format(self.get_scanner_name()))
626
627
    def set_scan_progress(self, scan_id, progress):
628
        """ Sets scan_id scan's progress which is a number between 0 and 100. """
629
        self.scan_collection.set_progress(scan_id, progress)
630
631
    def scan_exists(self, scan_id):
632
        """ Checks if a scan with ID scan_id is in collection.
633
634
        @return: 1 if scan exists, 0 otherwise.
635
        """
636
        return self.scan_collection.id_exists(scan_id)
637
638
    def handle_get_scans_command(self, scan_et):
639
        """ Handles <get_scans> command.
640
641
        @return: Response string for <get_scans> command.
642
        """
643
644
        scan_id = scan_et.attrib.get('scan_id')
645
        details = scan_et.attrib.get('details')
646
        if details and details == '0':
647
            details = False
648
        else:
649
            details = True
650
651
        responses = []
652
        if scan_id and scan_id in self.scan_collection.ids_iterator():
653
            self.check_scan_process(scan_id)
654
            scan = self.get_scan_xml(scan_id, details)
655
            responses.append(scan)
656
        elif scan_id:
657
            text = "Failed to find scan '{0}'".format(scan_id)
658
            return simple_response_str('get_scans', 404, text)
659
        else:
660
            for scan_id in self.scan_collection.ids_iterator():
661
                self.check_scan_process(scan_id)
662
                scan = self.get_scan_xml(scan_id, details)
663
                responses.append(scan)
664
        return simple_response_str('get_scans', 200, 'OK', responses)
665
666
    def handle_get_vts_command(self, vt_et):
667
        """ Handles <get_vts> command.
668
669
        @return: Response string for <get_vts> command.
670
        """
671
672
        vt_id = vt_et.attrib.get('vt_id')
673
674
        if vt_id and vt_id not in self.vts:
675
            text = "Failed to find vulnerability test '{0}'".format(vt_id)
676
            return simple_response_str('get_vts', 404, text)
677
678
        responses = []
679
680
        if vt_id:
681
            vts_xml = self.get_vts_xml(vt_id)
682
        else:
683
            vts_xml = self.get_vts_xml()
684
685
        responses.append(vts_xml)
686
687
        return simple_response_str('get_vts', 200, 'OK', responses)
688
689
    def handle_help_command(self, scan_et):
690
        """ Handles <help> command.
691
692
        @return: Response string for <help> command.
693
        """
694
        help_format = scan_et.attrib.get('format')
695
        if help_format is None or help_format == "text":
696
            # Default help format is text.
697
            return simple_response_str('help', 200, 'OK',
698
                                       self.get_help_text())
699
        elif help_format == "xml":
700
            text = self.get_xml_str(self.commands)
701
            return simple_response_str('help', 200, 'OK', text)
702
        raise OSPDError('Bogus help format', 'help')
703
704
    def get_help_text(self):
705
        """ Returns the help output in plain text format."""
706
707
        txt = str('\n')
708
        for name, info in self.commands.items():
709
            command_txt = "\t{0: <22} {1}\n".format(name, info['description'])
710
            if info['attributes']:
711
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
712
                for attrname, attrdesc in info['attributes'].items():
713
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
714
                    command_txt = ''.join([command_txt, attr_txt])
715
            if info['elements']:
716
                command_txt = ''.join([command_txt, "\t Elements:\n",
717
                                       self.elements_as_text(info['elements'])])
718
            txt = ''.join([txt, command_txt])
719
        return txt
720
721
    def elements_as_text(self, elems, indent=2):
722
        """ Returns the elems dictionary as formatted plain text. """
723
        assert elems
724
        text = ""
725
        for elename, eledesc in elems.items():
726
            if isinstance(eledesc, dict):
727
                desc_txt = self.elements_as_text(eledesc, indent + 2)
728
                desc_txt = ''.join(['\n', desc_txt])
729
            elif isinstance(eledesc, str):
730
                desc_txt = ''.join([eledesc, '\n'])
731
            else:
732
                assert False, "Only string or dictionary"
733
            ele_txt = "\t{0}{1: <22} {2}".format(' ' * indent, elename,
734
                                                 desc_txt)
0 ignored issues
show
introduced by
The variable desc_txt does not seem to be defined for all execution paths.
Loading history...
735
            text = ''.join([text, ele_txt])
736
        return text
737
738
    def handle_delete_scan_command(self, scan_et):
739
        """ Handles <delete_scan> command.
740
741
        @return: Response string for <delete_scan> command.
742
        """
743
        scan_id = scan_et.attrib.get('scan_id')
744
        if scan_id is None:
745
            return simple_response_str('delete_scan', 404,
746
                                       'No scan_id attribute')
747
748
        if not self.scan_exists(scan_id):
749
            text = "Failed to find scan '{0}'".format(scan_id)
750
            return simple_response_str('delete_scan', 404, text)
751
        self.check_scan_process(scan_id)
752
        if self.delete_scan(scan_id):
753
            return simple_response_str('delete_scan', 200, 'OK')
754
        raise OSPDError('Scan in progress', 'delete_scan')
755
756
    def delete_scan(self, scan_id):
757
        """ Deletes scan_id scan from collection.
758
759
        @return: 1 if scan deleted, 0 otherwise.
760
        """
761
        try:
762
            del self.scan_processes[scan_id]
763
        except KeyError:
764
            logger.debug('Scan process for {0} not found'.format(scan_id))
765
        return self.scan_collection.delete_scan(scan_id)
766
767
    def get_scan_results_xml(self, scan_id):
768
        """ Gets scan_id scan's results in XML format.
769
770
        @return: String of scan results in xml.
771
        """
772
        results = ET.Element('results')
773
        for result in self.scan_collection.results_iterator(scan_id):
774
            results.append(get_result_xml(result))
775
776
        logger.info('Returning %d results', len(results))
777
        return results
778
779
    def get_xml_str(self, data):
780
        """ Creates a string in XML Format using the provided data structure.
781
782
        @param: Dictionary of xml tags and their elements.
783
784
        @return: String of data in xml format.
785
        """
786
787
        responses = []
788
        for tag, value in data.items():
789
            elem = ET.Element(tag)
790
            if isinstance(value, dict):
791
                for value in self.get_xml_str(value):
792
                    elem.append(value)
793
            elif isinstance(value, list):
794
                value = ', '.join([m for m in value])
795
                elem.text = value
796
            else:
797
                elem.text = value
798
            responses.append(elem)
799
        return responses
800
801
    def get_scan_xml(self, scan_id, detailed=True):
802
        """ Gets scan in XML format.
803
804
        @return: String of scan in XML format.
805
        """
806
        if not scan_id:
807
            return ET.Element('scan')
808
809
        target = self.get_scan_target(scan_id)
810
        progress = self.get_scan_progress(scan_id)
811
        start_time = self.get_scan_start_time(scan_id)
812
        end_time = self.get_scan_end_time(scan_id)
813
        response = ET.Element('scan')
814
        for name, value in [('id', scan_id),
815
                            ('target', target),
816
                            ('progress', progress),
817
                            ('start_time', start_time),
818
                            ('end_time', end_time)]:
819
            response.set(name, str(value))
820
        if detailed:
821
            response.append(self.get_scan_results_xml(scan_id))
822
        return response
823
824
    def get_custom_vt_as_xml_str(self, custom):
825
        """ Create a string representation of the XML object from the
826
        custom data object.
827
        This needs to be implemented by each ospd wrapper, in case
828
        custom elements for VTs are used.
829
830
        The custom XML object which is returned will be embedded
831
        into a <custom></custom> element.
832
833
        @return: XML object as string for custom data.
834
        """
835
        return ''
836
837
    def get_vt_xml(self, vt_id):
838
        """ Gets a single vulnerability test information in XML format.
839
840
        @return: String of single vulnerability test information in XML format.
841
        """
842
        if not vt_id:
843
            return ET.Element('vt')
844
845
        vt = self.vts.get(vt_id)
846
847
        name = vt.get('name')
848
        vt_xml = ET.Element('vt')
849
        vt_xml.set('id', vt_id)
850
851
        for name, value in [('name', name)]:
852
            elem = ET.SubElement(vt_xml, name)
853
            elem.text = str(value)
854
855
        if vt.get('custom'):
856
            custom_xml_str = '<custom>%s</custom>' % self.get_custom_vt_as_xml_str(vt.get('custom'))
857
            vt_xml.append(ET.fromstring(custom_xml_str))
858
859
        return vt_xml
860
861
    def get_vts_xml(self, vt_id=''):
862
        """ Gets collection of vulnerability test information in XML format.
863
        If vt_id is specified, the collection will contain only this vt, of found.
864
        If no vt_id is specified, the collection will contain all vts.
865
866
        @return: String of collection of vulnerability test information in XML format.
867
        """
868
869
        vts_xml = ET.Element('vts')
870
871
        if vt_id != '':
872
            vts_xml.append(self.get_vt_xml(vt_id))
873
        else:
874
            for vt_id in self.vts:
875
                vts_xml.append(self.get_vt_xml(vt_id))
876
877
        return vts_xml
878
879
    def handle_get_scanner_details(self):
880
        """ Handles <get_scanner_details> command.
881
882
        @return: Response string for <get_scanner_details> command.
883
        """
884
        desc_xml = ET.Element('description')
885
        desc_xml.text = self.get_scanner_description()
886
        details = [
887
            desc_xml,
888
            self.get_scanner_params_xml()
889
        ]
890
        return simple_response_str('get_scanner_details', 200, 'OK', details)
891
892
    def handle_get_version_command(self):
893
        """ Handles <get_version> command.
894
895
        @return: Response string for <get_version> command.
896
        """
897
        protocol = ET.Element('protocol')
898
        for name, value in [('name', 'OSP'), ('version', self.get_protocol_version())]:
899
            elem = ET.SubElement(protocol, name)
900
            elem.text = value
901
902
        daemon = ET.Element('daemon')
903
        for name, value in [('name', self.get_daemon_name()), ('version', self.get_daemon_version())]:
904
            elem = ET.SubElement(daemon, name)
905
            elem.text = value
906
907
        scanner = ET.Element('scanner')
908
        for name, value in [('name', self.get_scanner_name()), ('version', self.get_scanner_version())]:
909
            elem = ET.SubElement(scanner, name)
910
            elem.text = value
911
912
        return simple_response_str('get_version', 200, 'OK', [protocol, daemon, scanner])
913
914
    def handle_command(self, command):
915
        """ Handles an osp command in a string.
916
917
        @return: OSP Response to command.
918
        """
919
        try:
920
            tree = ET.fromstring(command)
921
        except ET.ParseError:
922
            logger.debug("Erroneous client input: {0}".format(command))
923
            raise OSPDError('Invalid data')
924
925
        if not self.command_exists(tree.tag) and tree.tag != "authenticate":
926
            raise OSPDError('Bogus command name')
927
928
        if tree.tag == "get_version":
929
            return self.handle_get_version_command()
930
        elif tree.tag == "start_scan":
931
            return self.handle_start_scan_command(tree)
932
        elif tree.tag == "stop_scan":
933
            return self.handle_stop_scan_command(tree)
934
        elif tree.tag == "get_scans":
935
            return self.handle_get_scans_command(tree)
936
        elif tree.tag == "get_vts":
937
            return self.handle_get_vts_command(tree)
938
        elif tree.tag == "delete_scan":
939
            return self.handle_delete_scan_command(tree)
940
        elif tree.tag == "help":
941
            return self.handle_help_command(tree)
942
        elif tree.tag == "get_scanner_details":
943
            return self.handle_get_scanner_details()
944
        else:
945
            assert False, "Unhandled command: {0}".format(tree.tag)
946
947
    def check(self):
948
        """ Asserts to False. Should be implemented by subclass. """
949
        raise NotImplementedError
950
951
    def run(self, address, port, unix_path):
952
        """ Starts the Daemon, handling commands until interrupted.
953
954
        @return False if error. Runs indefinitely otherwise.
955
        """
956
        assert address or unix_path
957
        if unix_path:
958
            sock = bind_unix_socket(unix_path)
959
        else:
960
            sock = bind_socket(address, port)
961
        if sock is None:
962
            return False
963
964
        try:
965
            while True:
966
                if unix_path:
967
                    client_stream, _ = sock.accept()
968
                    logger.debug("New connection from {0}".format(unix_path))
969
                    self.handle_client_stream(client_stream, True)
970
                else:
971
                    client_stream = self.new_client_stream(sock)
972
                    if client_stream is None:
973
                        continue
974
                    self.handle_client_stream(client_stream, False)
975
                close_client_stream(client_stream, unix_path)
976
        except KeyboardInterrupt:
977
            logger.info("Received Ctrl-C shutting-down ...")
978
        finally:
979
            sock.shutdown(socket.SHUT_RDWR)
980
            sock.close()
981
982
    def create_scan(self, scan_id, target, ports, options):
983
        """ Creates a new scan.
984
985
        @target: Target to scan.
986
        @options: Miscellaneous scan options.
987
988
        @return: New scan's ID.
989
        """
990
        return self.scan_collection.create_scan(scan_id, target, ports, options)
991
992
    def get_scan_options(self, scan_id):
993
        """ Gives a scan's list of options. """
994
        return self.scan_collection.get_options(scan_id)
995
996
    def set_scan_option(self, scan_id, name, value):
997
        """ Sets a scan's option to a provided value. """
998
        return self.scan_collection.set_option(scan_id, name, value)
999
1000
    def check_scan_process(self, scan_id):
1001
        """ Check the scan's process, and terminate the scan if not alive. """
1002
        scan_process = self.scan_processes[scan_id]
1003
        progress = self.get_scan_progress(scan_id)
1004
        if progress < 100 and not scan_process.is_alive():
1005
            self.set_scan_progress(scan_id, 100)
1006
            self.add_scan_error(scan_id, name="", host="",
1007
                                value="Scan process failure.")
1008
            logger.info("{0}: Scan terminated.".format(scan_id))
1009
        elif progress == 100:
1010
            scan_process.join()
1011
1012
    def get_scan_progress(self, scan_id):
1013
        """ Gives a scan's current progress value. """
1014
        return self.scan_collection.get_progress(scan_id)
1015
1016
    def get_scan_target(self, scan_id):
1017
        """ Gives a scan's target. """
1018
        return self.scan_collection.get_target(scan_id)
1019
1020
    def get_scan_ports(self, scan_id):
1021
        """ Gives a scan's ports list. """
1022
        return self.scan_collection.get_ports(scan_id)
1023
1024
    def get_scan_start_time(self, scan_id):
1025
        """ Gives a scan's start time. """
1026
        return self.scan_collection.get_start_time(scan_id)
1027
1028
    def get_scan_end_time(self, scan_id):
1029
        """ Gives a scan's end time. """
1030
        return self.scan_collection.get_end_time(scan_id)
1031
1032
    def add_scan_log(self, scan_id, host='', name='', value='', port='',
1033
                     test_id='', qod=''):
1034
        """ Adds a log result to scan_id scan. """
1035
        self.scan_collection.add_result(scan_id, ResultType.LOG, host, name,
1036
                                        value, port, test_id, 0.0, qod)
1037
1038
    def add_scan_error(self, scan_id, host='', name='', value='', port=''):
1039
        """ Adds an error result to scan_id scan. """
1040
        self.scan_collection.add_result(scan_id, ResultType.ERROR, host, name,
1041
                                        value, port)
1042
1043
    def add_scan_host_detail(self, scan_id, host='', name='', value=''):
1044
        """ Adds a host detail result to scan_id scan. """
1045
        self.scan_collection.add_result(scan_id, ResultType.HOST_DETAIL, host,
1046
                                        name, value)
1047
1048
    def add_scan_alarm(self, scan_id, host='', name='', value='', port='',
1049
                       test_id='', severity='', qod=''):
1050
        """ Adds an alarm result to scan_id scan. """
1051
        self.scan_collection.add_result(scan_id, ResultType.ALARM, host, name,
1052
                                        value, port, test_id, severity, qod)
1053