Passed
Pull Request — master (#178)
by Juan José
01:38
created

ospd.ospd.OSPDaemon.set_scan_progress()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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