Passed
Pull Request — master (#164)
by Juan José
02:08
created

ospd.ospd.OSPDaemon.get_vts_xml()   B

Complexity

Conditions 7

Size

Total Lines 33
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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