Passed
Pull Request — master (#187)
by Juan José
01:54 queued 40s
created

ospd.ospd.OSPDaemon.wait_for_children()   A

Complexity

Conditions 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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