Passed
Pull Request — master (#174)
by Juan José
01:27
created

ospd.ospd.OSPDaemon.process_targets_element()   C

Complexity

Conditions 9

Size

Total Lines 69
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 24
nop 2
dl 0
loc 69
rs 6.6666
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 = self.create_scan(
626
            scan_id, scan_targets, scan_params, vt_selection
627
        )
628
        scan_process = multiprocessing.Process(
629
            target=scan_func, args=(scan_id, scan_targets, parallel)
630
        )
631
        self.scan_processes[scan_id] = scan_process
632
        scan_process.start()
633
        id_ = Element('id')
634
        id_.text = scan_id
635
        return simple_response_str('start_scan', 200, 'OK', id_)
636
637
    def handle_stop_scan_command(self, scan_et):
638
        """ Handles <stop_scan> command.
639
640
        @return: Response string for <stop_scan> command.
641
        """
642
643
        scan_id = scan_et.attrib.get('scan_id')
644
        if scan_id is None or scan_id == '':
645
            raise OspdCommandError('No scan_id attribute', 'stop_scan')
646
        self.stop_scan(scan_id)
647
648
        return simple_response_str('stop_scan', 200, 'OK')
649
650
    def stop_scan(self, scan_id):
651
        scan_process = self.scan_processes.get(scan_id)
652
        if not scan_process:
653
            raise OspdCommandError(
654
                'Scan not found {0}.'.format(scan_id), 'stop_scan'
655
            )
656
        if not scan_process.is_alive():
657
            raise OspdCommandError(
658
                'Scan already stopped or finished.', 'stop_scan'
659
            )
660
661
        self.set_scan_status(scan_id, ScanStatus.STOPPED)
662
        logger.info('%s: Scan stopping %s.', scan_id, scan_process.ident)
663
        self.stop_scan_cleanup(scan_id)
664
        try:
665
            scan_process.terminate()
666
        except AttributeError:
667
            logger.debug('%s: The scanner task stopped unexpectedly.', scan_id)
668
669
        try:
670
            os.killpg(os.getpgid(scan_process.ident), 15)
671
        except ProcessLookupError as e:
672
            logger.info(
673
                '%s: Scan already stopped %s.', scan_id, scan_process.ident
674
            )
675
676
        if scan_process.ident != os.getpid():
677
            scan_process.join()
678
        logger.info('%s: Scan stopped.', scan_id)
679
680
    @staticmethod
681
    def stop_scan_cleanup(scan_id):
682
        """ Should be implemented by subclass in case of a clean up before
683
        terminating is needed. """
684
685
    @staticmethod
686
    def target_is_finished(scan_id):
687
        """ Should be implemented by subclass in case of a check before
688
        stopping is needed. """
689
690
    def exec_scan(self, scan_id, target):
691
        """ Asserts to False. Should be implemented by subclass. """
692
        raise NotImplementedError
693
694
    def finish_scan(self, scan_id):
695
        """ Sets a scan as finished. """
696
        self.set_scan_progress(scan_id, 100)
697
        self.set_scan_status(scan_id, ScanStatus.FINISHED)
698
        logger.info("%s: Scan finished.", scan_id)
699
700
    def get_daemon_name(self):
701
        """ Gives osp daemon's name. """
702
        return self.daemon_info['name']
703
704
    def get_daemon_version(self):
705
        """ Gives osp daemon's version. """
706
        return self.daemon_info['version']
707
708
    def get_scanner_param_type(self, param):
709
        """ Returns type of a scanner parameter. """
710
        assert isinstance(param, str)
711
        entry = self.scanner_params.get(param)
712
        if not entry:
713
            return None
714
        return entry.get('type')
715
716
    def get_scanner_param_mandatory(self, param):
717
        """ Returns if a scanner parameter is mandatory. """
718
        assert isinstance(param, str)
719
        entry = self.scanner_params.get(param)
720
        if not entry:
721
            return False
722
        return entry.get('mandatory')
723
724
    def get_scanner_param_default(self, param):
725
        """ Returns default value of a scanner parameter. """
726
        assert isinstance(param, str)
727
        entry = self.scanner_params.get(param)
728
        if not entry:
729
            return None
730
        return entry.get('default')
731
732
    def get_scanner_params_xml(self):
733
        """ Returns the OSP Daemon's scanner params in xml format. """
734
        scanner_params = Element('scanner_params')
735
        for param_id, param in self.scanner_params.items():
736
            param_xml = SubElement(scanner_params, 'scanner_param')
737
            for name, value in [('id', param_id), ('type', param['type'])]:
738
                param_xml.set(name, value)
739
            for name, value in [
740
                ('name', param['name']),
741
                ('description', param['description']),
742
                ('default', param['default']),
743
                ('mandatory', param['mandatory']),
744
            ]:
745
                elem = SubElement(param_xml, name)
746
                elem.text = str(value)
747
        return scanner_params
748
749
    def handle_client_stream(self, stream):
750
        """ Handles stream of data received from client. """
751
752
        data = b''
753
754
        while True:
755
            try:
756
                buf = stream.read()
757
                if not buf:
758
                    break
759
760
                data += buf
761
            except (AttributeError, ValueError) as message:
762
                logger.error(message)
763
                return
764
            except (ssl.SSLError) as exception:
765
                logger.debug('Error: %s', exception)
766
                break
767
            except (socket.timeout) as exception:
768
                break
769
770
        if len(data) <= 0:
771
            logger.debug("Empty client stream")
772
            return
773
774
        try:
775
            response = self.handle_command(data)
776
        except OspdCommandError as exception:
777
            response = exception.as_xml()
778
            logger.debug('Command error: %s', exception.message)
779
        except Exception:  # pylint: disable=broad-except
780
            logger.exception('While handling client command:')
781
            exception = OspdCommandError('Fatal error', 'error')
782
            response = exception.as_xml()
783
784
        stream.write(response)
785
        stream.close()
786
787
    def parallel_scan(self, scan_id, target):
788
        """ Starts the scan with scan_id. """
789
        try:
790
            ret = self.exec_scan(scan_id, target)
791
            if ret == 0:
792
                logger.info("%s: Host scan dead.", target)
793
            elif ret == 1:
794
                logger.info("%s: Host scan alived.", target)
795
            elif ret == 2:
796
                logger.info("%s: Scan error or status unknown.", target)
797
            else:
798
                logger.debug('%s: No host status returned', target)
799
        except Exception as e:  # pylint: disable=broad-except
800
            self.add_scan_error(
801
                scan_id,
802
                name='',
803
                host=target,
804
                value='Host process failure (%s).' % e,
805
            )
806
            logger.exception('While scanning %s:', target)
807
        else:
808
            logger.info("%s: Host scan finished.", target)
809
810
    def check_pending_target(self, scan_id, multiscan_proc):
811
        """ Check if a scan process is still alive. In case the process
812
        finished or is stopped, removes the process from the multiscan
813
        _process list.
814
        Processes dead and with progress < 100% are considered stopped
815
        or with failures. Then will try to stop the other runnings (target)
816
        scans owned by the same task.
817
818
        @input scan_id        Scan_id of the whole scan.
819
        @input multiscan_proc A list with the scan process which
820
                              may still be alive.
821
822
        @return Actualized list with current running scan processes.
823
        """
824
        for running_target_proc, running_target_id in multiscan_proc:
825
            if not running_target_proc.is_alive():
826
                target_prog = self.get_scan_target_progress(
827
                    scan_id, running_target_id
828
                )
829
830
                _not_finished_clean = target_prog < 100
831
                _not_stopped = (
832
                    self.get_scan_status(scan_id) != ScanStatus.STOPPED
833
                )
834
835
                if _not_finished_clean and _not_stopped:
836
                    if not self.target_is_finished(scan_id):
837
                        self.stop_scan(scan_id)
838
839
                running_target = (running_target_proc, running_target_id)
840
                multiscan_proc.remove(running_target)
841
842
        return multiscan_proc
843
844
    def calculate_progress(self, scan_id):
845
        """ Calculate the total scan progress from the
846
        partial target progress. """
847
848
        t_prog = dict()
849
        for target in self.get_scan_target(scan_id):
850
            t_prog[target] = self.get_scan_target_progress(scan_id, target)
851
        return sum(t_prog.values()) / len(t_prog)
852
853
    def process_exclude_hosts(self, scan_id, target_list):
854
        """ Process the exclude hosts before launching the scans."""
855
856
        for target, _, _, exclude_hosts, _ in target_list:
857
            exc_hosts_list = ''
858
            if not exclude_hosts:
859
                continue
860
            exc_hosts_list = target_str_to_list(exclude_hosts)
861
            self.remove_scan_hosts_from_target_progress(
862
                scan_id, target, exc_hosts_list
863
            )
864
865
    def process_finished_hosts(self, scan_id, target_list):
866
        """ Process the finished hosts before launching the scans.
867
        Set finished hosts as finished with 100% to calculate
868
        the scan progress."""
869
870
        for target, _, _, _, finished_hosts in target_list:
871
            exc_hosts_list = ''
872
            if not finished_hosts:
873
                continue
874
            exc_hosts_list = target_str_to_list(finished_hosts)
875
876
            for host in exc_hosts_list:
877
                self.set_scan_host_finished(scan_id, target, host)
878
                self.set_scan_host_progress(scan_id, target, host, 100)
879
880
    def start_scan(self, scan_id, targets, parallel=1):
881
        """ Handle N parallel scans if 'parallel' is greater than 1. """
882
883
        os.setsid()
884
        multiscan_proc = []
885
        logger.info("%s: Scan started.", scan_id)
886
        target_list = targets
887
        if target_list is None or not target_list:
888
            raise OspdCommandError('Erroneous targets list', 'start_scan')
889
890
        self.process_exclude_hosts(scan_id, target_list)
891
        self.process_finished_hosts(scan_id, target_list)
892
893
        for _index, target in enumerate(target_list):
894
            while len(multiscan_proc) >= parallel:
895
                progress = self.calculate_progress(scan_id)
896
                self.set_scan_progress(scan_id, progress)
897
                multiscan_proc = self.check_pending_target(
898
                    scan_id, multiscan_proc
899
                )
900
                time.sleep(1)
901
902
            # If the scan status is stopped, does not launch anymore target
903
            # scans
904
            if self.get_scan_status(scan_id) == ScanStatus.STOPPED:
905
                return
906
907
            logger.debug(
908
                "%s: Host scan started on ports %s.", target[0], target[1]
909
            )
910
            scan_process = multiprocessing.Process(
911
                target=self.parallel_scan, args=(scan_id, target[0])
912
            )
913
            multiscan_proc.append((scan_process, target[0]))
914
            scan_process.start()
915
            self.set_scan_status(scan_id, ScanStatus.RUNNING)
916
917
        # Wait until all single target were scanned
918
        while multiscan_proc:
919
            multiscan_proc = self.check_pending_target(scan_id, multiscan_proc)
920
            if multiscan_proc:
921
                progress = self.calculate_progress(scan_id)
922
                self.set_scan_progress(scan_id, progress)
923
            time.sleep(1)
924
925
        # Only set the scan as finished if the scan was not stopped.
926
        if self.get_scan_status(scan_id) != ScanStatus.STOPPED:
927
            self.finish_scan(scan_id)
928
929
    def dry_run_scan(self, scan_id, targets, parallel):
930
        """ Dry runs a scan. """
931
932
        os.setsid()
933
        for _, target in enumerate(targets):
934
            host = resolve_hostname(target[0])
935
            if host is None:
936
                logger.info("Couldn't resolve %s.", target[0])
937
                continue
938
            port = self.get_scan_ports(scan_id, target=target[0])
939
            logger.info("%s:%s: Dry run mode.", host, port)
940
            self.add_scan_log(
941
                scan_id, name='', host=host, value='Dry run result'
942
            )
943
        self.finish_scan(scan_id)
944
945
    def handle_timeout(self, scan_id, host):
946
        """ Handles scanner reaching timeout error. """
947
        self.add_scan_error(
948
            scan_id,
949
            host=host,
950
            name="Timeout",
951
            value="{0} exec timeout.".format(self.get_scanner_name()),
952
        )
953
954
    def remove_scan_hosts_from_target_progress(
955
        self, scan_id, target, exc_hosts_list
956
    ):
957
        """ Remove a list of hosts from the main scan progress table."""
958
        self.scan_collection.remove_hosts_from_target_progress(
959
            scan_id, target, exc_hosts_list
960
        )
961
962
    def set_scan_host_finished(self, scan_id, target, host):
963
        """ Add the host in a list of finished hosts """
964
        self.scan_collection.set_host_finished(scan_id, target, host)
965
966
    def set_scan_progress(self, scan_id, progress):
967
        """ Sets scan_id scan's progress which is a number
968
        between 0 and 100. """
969
        self.scan_collection.set_progress(scan_id, progress)
970
971
    def set_scan_host_progress(self, scan_id, target, host, progress):
972
        """ Sets host's progress which is part of target. """
973
        self.scan_collection.set_host_progress(scan_id, target, host, progress)
974
975
    def set_scan_status(self, scan_id, status):
976
        """ Set the scan's status."""
977
        self.scan_collection.set_status(scan_id, status)
978
979
    def get_scan_status(self, scan_id):
980
        """ Get scan_id scans's status."""
981
        return self.scan_collection.get_status(scan_id)
982
983
    def scan_exists(self, scan_id):
984
        """ Checks if a scan with ID scan_id is in collection.
985
986
        @return: 1 if scan exists, 0 otherwise.
987
        """
988
        return self.scan_collection.id_exists(scan_id)
989
990
    def handle_get_scans_command(self, scan_et):
991
        """ Handles <get_scans> command.
992
993
        @return: Response string for <get_scans> command.
994
        """
995
996
        scan_id = scan_et.attrib.get('scan_id')
997
        details = scan_et.attrib.get('details')
998
        pop_res = scan_et.attrib.get('pop_results')
999
        if details and details == '0':
1000
            details = False
1001
        else:
1002
            details = True
1003
            if pop_res and pop_res == '1':
1004
                pop_res = True
1005
            else:
1006
                pop_res = False
1007
1008
        responses = []
1009
        if scan_id and scan_id in self.scan_collection.ids_iterator():
1010
            self.check_scan_process(scan_id)
1011
            scan = self.get_scan_xml(scan_id, details, pop_res)
1012
            responses.append(scan)
1013
        elif scan_id:
1014
            text = "Failed to find scan '{0}'".format(scan_id)
1015
            return simple_response_str('get_scans', 404, text)
1016
        else:
1017
            for scan_id in self.scan_collection.ids_iterator():
1018
                self.check_scan_process(scan_id)
1019
                scan = self.get_scan_xml(scan_id, details, pop_res)
1020
                responses.append(scan)
1021
        return simple_response_str('get_scans', 200, 'OK', responses)
1022
1023
    def handle_get_vts_command(self, vt_et):
1024
        """ Handles <get_vts> command.
1025
        The <get_vts> element accept two optional arguments.
1026
        vt_id argument receives a single vt id.
1027
        filter argument receives a filter selecting a sub set of vts.
1028
        If both arguments are given, the vts which match with the filter
1029
        are return.
1030
1031
        @return: Response string for <get_vts> command.
1032
        """
1033
1034
        vt_id = vt_et.attrib.get('vt_id')
1035
        vt_filter = vt_et.attrib.get('filter')
1036
1037
        if vt_id and vt_id not in self.vts:
1038
            text = "Failed to find vulnerability test '{0}'".format(vt_id)
1039
            return simple_response_str('get_vts', 404, text)
1040
1041
        filtered_vts = None
1042
        if vt_filter:
1043
            filtered_vts = self.vts_filter.get_filtered_vts_list(
1044
                self.vts, vt_filter
1045
            )
1046
1047
        responses = []
1048
1049
        vts_xml = self.get_vts_xml(vt_id, filtered_vts)
1050
1051
        responses.append(vts_xml)
1052
1053
        return simple_response_str('get_vts', 200, 'OK', responses)
1054
1055
    def handle_get_performance(self, scan_et):
1056
        """ Handles <get_performance> command.
1057
1058
        @return: Response string for <get_performance> command.
1059
        """
1060
        start = scan_et.attrib.get('start')
1061
        end = scan_et.attrib.get('end')
1062
        titles = scan_et.attrib.get('titles')
1063
1064
        cmd = ['gvmcg']
1065
        if start:
1066
            try:
1067
                int(start)
1068
            except ValueError:
1069
                raise OspdCommandError(
1070
                    'Start argument must be integer.', 'get_performance'
1071
                )
1072
            cmd.append(start)
1073
1074
        if end:
1075
            try:
1076
                int(end)
1077
            except ValueError:
1078
                raise OspdCommandError(
1079
                    'End argument must be integer.', 'get_performance'
1080
                )
1081
            cmd.append(end)
1082
1083
        if titles:
1084
            combined = "(" + ")|(".join(GVMCG_TITLES) + ")"
1085
            forbidden = "^[^|&;]+$"
1086
            if re.match(combined, titles) and re.match(forbidden, titles):
1087
                cmd.append(titles)
1088
            else:
1089
                raise OspdCommandError(
1090
                    'Arguments not allowed', 'get_performance'
1091
                )
1092
1093
        try:
1094
            output = subprocess.check_output(cmd)
1095
        except (
1096
            subprocess.CalledProcessError,
1097
            PermissionError,
1098
            FileNotFoundError,
1099
        ) as e:
1100
            raise OspdCommandError(
1101
                'Bogus get_performance format. %s' % e, 'get_performance'
1102
            )
1103
1104
        return simple_response_str(
1105
            'get_performance', 200, 'OK', output.decode()
1106
        )
1107
1108
    def handle_help_command(self, scan_et):
1109
        """ Handles <help> command.
1110
1111
        @return: Response string for <help> command.
1112
        """
1113
        help_format = scan_et.attrib.get('format')
1114
        if help_format is None or help_format == "text":
1115
            # Default help format is text.
1116
            return simple_response_str('help', 200, 'OK', self.get_help_text())
1117
        elif help_format == "xml":
1118
            text = self.get_xml_str(self.commands)
1119
            return simple_response_str('help', 200, 'OK', text)
1120
        raise OspdCommandError('Bogus help format', 'help')
1121
1122
    def get_help_text(self):
1123
        """ Returns the help output in plain text format."""
1124
1125
        txt = str('\n')
1126
        for name, info in self.commands.items():
1127
            command_txt = "\t{0: <22} {1}\n".format(name, info['description'])
1128
            if info['attributes']:
1129
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
1130
                for attrname, attrdesc in info['attributes'].items():
1131
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
1132
                    command_txt = ''.join([command_txt, attr_txt])
1133
            if info['elements']:
1134
                command_txt = ''.join(
1135
                    [
1136
                        command_txt,
1137
                        "\t Elements:\n",
1138
                        self.elements_as_text(info['elements']),
1139
                    ]
1140
                )
1141
            txt = ''.join([txt, command_txt])
1142
        return txt
1143
1144
    def elements_as_text(self, elems, indent=2):
1145
        """ Returns the elems dictionary as formatted plain text. """
1146
        assert elems
1147
        text = ""
1148
        for elename, eledesc in elems.items():
1149
            if isinstance(eledesc, dict):
1150
                desc_txt = self.elements_as_text(eledesc, indent + 2)
1151
                desc_txt = ''.join(['\n', desc_txt])
1152
            elif isinstance(eledesc, str):
1153
                desc_txt = ''.join([eledesc, '\n'])
1154
            else:
1155
                assert False, "Only string or dictionary"
1156
            ele_txt = "\t{0}{1: <22} {2}".format(
1157
                ' ' * 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...
1158
            )
1159
            text = ''.join([text, ele_txt])
1160
        return text
1161
1162
    def handle_delete_scan_command(self, scan_et):
1163
        """ Handles <delete_scan> command.
1164
1165
        @return: Response string for <delete_scan> command.
1166
        """
1167
        scan_id = scan_et.attrib.get('scan_id')
1168
        if scan_id is None:
1169
            return simple_response_str(
1170
                'delete_scan', 404, 'No scan_id attribute'
1171
            )
1172
1173
        if not self.scan_exists(scan_id):
1174
            text = "Failed to find scan '{0}'".format(scan_id)
1175
            return simple_response_str('delete_scan', 404, text)
1176
        self.check_scan_process(scan_id)
1177
        if self.delete_scan(scan_id):
1178
            return simple_response_str('delete_scan', 200, 'OK')
1179
        raise OspdCommandError('Scan in progress', 'delete_scan')
1180
1181
    def delete_scan(self, scan_id):
1182
        """ Deletes scan_id scan from collection.
1183
1184
        @return: 1 if scan deleted, 0 otherwise.
1185
        """
1186
        if self.get_scan_status(scan_id) == ScanStatus.RUNNING:
1187
            return 0
1188
1189
        try:
1190
            del self.scan_processes[scan_id]
1191
        except KeyError:
1192
            logger.debug('Scan process for %s not found', scan_id)
1193
        return self.scan_collection.delete_scan(scan_id)
1194
1195
    def get_scan_results_xml(self, scan_id, pop_res):
1196
        """ Gets scan_id scan's results in XML format.
1197
1198
        @return: String of scan results in xml.
1199
        """
1200
        results = Element('results')
1201
        for result in self.scan_collection.results_iterator(scan_id, pop_res):
1202
            results.append(get_result_xml(result))
1203
1204
        logger.debug('Returning %d results', len(results))
1205
        return results
1206
1207
    def get_xml_str(self, data):
1208
        """ Creates a string in XML Format using the provided data structure.
1209
1210
        @param: Dictionary of xml tags and their elements.
1211
1212
        @return: String of data in xml format.
1213
        """
1214
1215
        responses = []
1216
        for tag, value in data.items():
1217
            elem = Element(tag)
1218
            if isinstance(value, dict):
1219
                for val in self.get_xml_str(value):
1220
                    elem.append(val)
1221
            elif isinstance(value, list):
1222
                elem.text = ', '.join(value)
1223
            else:
1224
                elem.text = value
1225
            responses.append(elem)
1226
        return responses
1227
1228
    def get_scan_xml(self, scan_id, detailed=True, pop_res=False):
1229
        """ Gets scan in XML format.
1230
1231
        @return: String of scan in XML format.
1232
        """
1233
        if not scan_id:
1234
            return Element('scan')
1235
1236
        target = ','.join(self.get_scan_target(scan_id))
1237
        progress = self.get_scan_progress(scan_id)
1238
        status = self.get_scan_status(scan_id)
1239
        start_time = self.get_scan_start_time(scan_id)
1240
        end_time = self.get_scan_end_time(scan_id)
1241
        response = Element('scan')
1242
        for name, value in [
1243
            ('id', scan_id),
1244
            ('target', target),
1245
            ('progress', progress),
1246
            ('status', status.name.lower()),
1247
            ('start_time', start_time),
1248
            ('end_time', end_time),
1249
        ]:
1250
            response.set(name, str(value))
1251
        if detailed:
1252
            response.append(self.get_scan_results_xml(scan_id, pop_res))
1253
        return response
1254
1255
    @staticmethod
1256
    def get_custom_vt_as_xml_str(
1257
        vt_id, custom
1258
    ):  # pylint: disable=unused-argument
1259
        """ Create a string representation of the XML object from the
1260
        custom data object.
1261
        This needs to be implemented by each ospd wrapper, in case
1262
        custom elements for VTs are used.
1263
1264
        The custom XML object which is returned will be embedded
1265
        into a <custom></custom> element.
1266
1267
        @return: XML object as string for custom data.
1268
        """
1269
        return ''
1270
1271
    @staticmethod
1272
    def get_params_vt_as_xml_str(
1273
        vt_id, vt_params
1274
    ):  # pylint: disable=unused-argument
1275
        """ Create a string representation of the XML object from the
1276
        vt_params data object.
1277
        This needs to be implemented by each ospd wrapper, in case
1278
        vt_params elements for VTs are used.
1279
1280
        The params XML object which is returned will be embedded
1281
        into a <params></params> element.
1282
1283
        @return: XML object as string for vt parameters data.
1284
        """
1285
        return ''
1286
1287
    @staticmethod
1288
    def get_refs_vt_as_xml_str(
1289
        vt_id, vt_refs
1290
    ):  # pylint: disable=unused-argument
1291
        """ Create a string representation of the XML object from the
1292
        refs data object.
1293
        This needs to be implemented by each ospd wrapper, in case
1294
        refs elements for VTs are used.
1295
1296
        The refs XML object which is returned will be embedded
1297
        into a <refs></refs> element.
1298
1299
        @return: XML object as string for vt references data.
1300
        """
1301
        return ''
1302
1303
    @staticmethod
1304
    def get_dependencies_vt_as_xml_str(
1305
        vt_id, vt_dependencies
1306
    ):  # pylint: disable=unused-argument
1307
        """ Create a string representation of the XML object from the
1308
        vt_dependencies data object.
1309
        This needs to be implemented by each ospd wrapper, in case
1310
        vt_dependencies elements for VTs are used.
1311
1312
        The vt_dependencies XML object which is returned will be embedded
1313
        into a <dependencies></dependencies> element.
1314
1315
        @return: XML object as string for vt dependencies data.
1316
        """
1317
        return ''
1318
1319
    @staticmethod
1320
    def get_creation_time_vt_as_xml_str(
1321
        vt_id, vt_creation_time
1322
    ):  # pylint: disable=unused-argument
1323
        """ Create a string representation of the XML object from the
1324
        vt_creation_time data object.
1325
        This needs to be implemented by each ospd wrapper, in case
1326
        vt_creation_time elements for VTs are used.
1327
1328
        The vt_creation_time XML object which is returned will be embedded
1329
        into a <vt_creation_time></vt_creation_time> element.
1330
1331
        @return: XML object as string for vt creation time data.
1332
        """
1333
        return ''
1334
1335
    @staticmethod
1336
    def get_modification_time_vt_as_xml_str(
1337
        vt_id, vt_modification_time
1338
    ):  # pylint: disable=unused-argument
1339
        """ Create a string representation of the XML object from the
1340
        vt_modification_time data object.
1341
        This needs to be implemented by each ospd wrapper, in case
1342
        vt_modification_time elements for VTs are used.
1343
1344
        The vt_modification_time XML object which is returned will be embedded
1345
        into a <vt_modification_time></vt_modification_time> element.
1346
1347
        @return: XML object as string for vt references data.
1348
        """
1349
        return ''
1350
1351
    @staticmethod
1352
    def get_summary_vt_as_xml_str(
1353
        vt_id, summary
1354
    ):  # pylint: disable=unused-argument
1355
        """ Create a string representation of the XML object from the
1356
        summary data object.
1357
        This needs to be implemented by each ospd wrapper, in case
1358
        summary elements for VTs are used.
1359
1360
        The summary XML object which is returned will be embedded
1361
        into a <summary></summary> element.
1362
1363
        @return: XML object as string for summary data.
1364
        """
1365
        return ''
1366
1367
    @staticmethod
1368
    def get_impact_vt_as_xml_str(
1369
        vt_id, impact
1370
    ):  # pylint: disable=unused-argument
1371
        """ Create a string representation of the XML object from the
1372
        impact data object.
1373
        This needs to be implemented by each ospd wrapper, in case
1374
        impact elements for VTs are used.
1375
1376
        The impact XML object which is returned will be embedded
1377
        into a <impact></impact> element.
1378
1379
        @return: XML object as string for impact data.
1380
        """
1381
        return ''
1382
1383
    @staticmethod
1384
    def get_affected_vt_as_xml_str(
1385
        vt_id, affected
1386
    ):  # pylint: disable=unused-argument
1387
        """ Create a string representation of the XML object from the
1388
        affected data object.
1389
        This needs to be implemented by each ospd wrapper, in case
1390
        affected elements for VTs are used.
1391
1392
        The affected XML object which is returned will be embedded
1393
        into a <affected></affected> element.
1394
1395
        @return: XML object as string for affected data.
1396
        """
1397
        return ''
1398
1399
    @staticmethod
1400
    def get_insight_vt_as_xml_str(
1401
        vt_id, insight
1402
    ):  # pylint: disable=unused-argument
1403
        """ Create a string representation of the XML object from the
1404
        insight data object.
1405
        This needs to be implemented by each ospd wrapper, in case
1406
        insight elements for VTs are used.
1407
1408
        The insight XML object which is returned will be embedded
1409
        into a <insight></insight> element.
1410
1411
        @return: XML object as string for insight data.
1412
        """
1413
        return ''
1414
1415
    @staticmethod
1416
    def get_solution_vt_as_xml_str(
1417
        vt_id, solution, solution_type=None
1418
    ):  # pylint: disable=unused-argument
1419
        """ Create a string representation of the XML object from the
1420
        solution data object.
1421
        This needs to be implemented by each ospd wrapper, in case
1422
        solution elements for VTs are used.
1423
1424
        The solution XML object which is returned will be embedded
1425
        into a <solution></solution> element.
1426
1427
        @return: XML object as string for solution data.
1428
        """
1429
        return ''
1430
1431
    @staticmethod
1432
    def get_detection_vt_as_xml_str(
1433
        vt_id, detection=None, qod_type=None, qod=None
1434
    ):  # pylint: disable=unused-argument
1435
        """ Create a string representation of the XML object from the
1436
        detection data object.
1437
        This needs to be implemented by each ospd wrapper, in case
1438
        detection elements for VTs are used.
1439
1440
        The detection XML object which is returned is an element with
1441
        tag <detection></detection> element
1442
1443
        @return: XML object as string for detection data.
1444
        """
1445
        return ''
1446
1447
    @staticmethod
1448
    def get_severities_vt_as_xml_str(
1449
        vt_id, severities
1450
    ):  # pylint: disable=unused-argument
1451
        """ Create a string representation of the XML object from the
1452
        severities data object.
1453
        This needs to be implemented by each ospd wrapper, in case
1454
        severities elements for VTs are used.
1455
1456
        The severities XML objects which are returned will be embedded
1457
        into a <severities></severities> element.
1458
1459
        @return: XML object as string for severities data.
1460
        """
1461
        return ''
1462
1463
    def get_vt_xml(self, vt_id):
1464
        """ Gets a single vulnerability test information in XML format.
1465
1466
        @return: String of single vulnerability test information in XML format.
1467
        """
1468
        if not vt_id:
1469
            return Element('vt')
1470
1471
        vt = self.vts.get(vt_id)
1472
1473
        name = vt.get('name')
1474
        vt_xml = Element('vt')
1475
        vt_xml.set('id', vt_id)
1476
1477
        for name, value in [('name', name)]:
1478
            elem = SubElement(vt_xml, name)
1479
            elem.text = str(value)
1480
1481
        if vt.get('vt_params'):
1482
            params_xml_str = self.get_params_vt_as_xml_str(
1483
                vt_id, vt.get('vt_params')
1484
            )
1485
            vt_xml.append(secET.fromstring(params_xml_str))
1486
1487
        if vt.get('vt_refs'):
1488
            refs_xml_str = self.get_refs_vt_as_xml_str(vt_id, vt.get('vt_refs'))
1489
            vt_xml.append(secET.fromstring(refs_xml_str))
1490
1491
        if vt.get('vt_dependencies'):
1492
            dependencies = self.get_dependencies_vt_as_xml_str(
1493
                vt_id, vt.get('vt_dependencies')
1494
            )
1495
            vt_xml.append(secET.fromstring(dependencies))
1496
1497
        if vt.get('creation_time'):
1498
            vt_ctime = self.get_creation_time_vt_as_xml_str(
1499
                vt_id, vt.get('creation_time')
1500
            )
1501
            vt_xml.append(secET.fromstring(vt_ctime))
1502
1503
        if vt.get('modification_time'):
1504
            vt_mtime = self.get_modification_time_vt_as_xml_str(
1505
                vt_id, vt.get('modification_time')
1506
            )
1507
            vt_xml.append(secET.fromstring(vt_mtime))
1508
1509
        if vt.get('summary'):
1510
            summary_xml_str = self.get_summary_vt_as_xml_str(
1511
                vt_id, vt.get('summary')
1512
            )
1513
            vt_xml.append(secET.fromstring(summary_xml_str))
1514
1515
        if vt.get('impact'):
1516
            impact_xml_str = self.get_impact_vt_as_xml_str(
1517
                vt_id, vt.get('impact')
1518
            )
1519
            vt_xml.append(secET.fromstring(impact_xml_str))
1520
1521
        if vt.get('affected'):
1522
            affected_xml_str = self.get_affected_vt_as_xml_str(
1523
                vt_id, vt.get('affected')
1524
            )
1525
            vt_xml.append(secET.fromstring(affected_xml_str))
1526
1527
        if vt.get('insight'):
1528
            insight_xml_str = self.get_insight_vt_as_xml_str(
1529
                vt_id, vt.get('insight')
1530
            )
1531
            vt_xml.append(secET.fromstring(insight_xml_str))
1532
1533
        if vt.get('solution'):
1534
            solution_xml_str = self.get_solution_vt_as_xml_str(
1535
                vt_id, vt.get('solution'), vt.get('solution_type')
1536
            )
1537
            vt_xml.append(secET.fromstring(solution_xml_str))
1538
1539
        if vt.get('detection') or vt.get('qod_type') or vt.get('qod'):
1540
            detection_xml_str = self.get_detection_vt_as_xml_str(
1541
                vt_id, vt.get('detection'), vt.get('qod_type'), vt.get('qod')
1542
            )
1543
            vt_xml.append(secET.fromstring(detection_xml_str))
1544
1545
        if vt.get('severities'):
1546
            severities_xml_str = self.get_severities_vt_as_xml_str(
1547
                vt_id, vt.get('severities')
1548
            )
1549
            vt_xml.append(secET.fromstring(severities_xml_str))
1550
1551
        if vt.get('custom'):
1552
            custom_xml_str = self.get_custom_vt_as_xml_str(
1553
                vt_id, vt.get('custom')
1554
            )
1555
            vt_xml.append(secET.fromstring(custom_xml_str))
1556
1557
        return vt_xml
1558
1559
    def get_vts_xml(self, vt_id=None, filtered_vts=None):
1560
        """ Gets collection of vulnerability test information in XML format.
1561
        If vt_id is specified, the collection will contain only this vt, if
1562
        found.
1563
        If no vt_id is specified or filtered_vts is None (default), the
1564
        collection will contain all vts. Otherwise those vts passed
1565
        in filtered_vts or vt_id are returned. In case of both vt_id and
1566
        filtered_vts are given, filtered_vts has priority.
1567
1568
        Arguments:
1569
            vt_id (vt_id, optional): ID of the vt to get.
1570
            filtered_vts (dict, optional): Filtered VTs collection.
1571
1572
        Return:
1573
            String of collection of vulnerability test information in
1574
            XML format.
1575
        """
1576
1577
        vts_xml = Element('vts')
1578
1579
        if filtered_vts is not None and len(filtered_vts) == 0:
1580
            return vts_xml
1581
1582
        if filtered_vts:
1583
            for vt_id in filtered_vts:
1584
                vts_xml.append(self.get_vt_xml(vt_id))
1585
        elif vt_id:
1586
            vts_xml.append(self.get_vt_xml(vt_id))
1587
        else:
1588
            for vt_id in self.vts:
1589
                vts_xml.append(self.get_vt_xml(vt_id))
1590
1591
        return vts_xml
1592
1593
    def handle_get_scanner_details(self):
1594
        """ Handles <get_scanner_details> command.
1595
1596
        @return: Response string for <get_scanner_details> command.
1597
        """
1598
        desc_xml = Element('description')
1599
        desc_xml.text = self.get_scanner_description()
1600
        details = [desc_xml, self.get_scanner_params_xml()]
1601
        return simple_response_str('get_scanner_details', 200, 'OK', details)
1602
1603
    def handle_get_version_command(self):
1604
        """ Handles <get_version> command.
1605
1606
        @return: Response string for <get_version> command.
1607
        """
1608
        protocol = Element('protocol')
1609
        for name, value in [
1610
            ('name', 'OSP'),
1611
            ('version', self.get_protocol_version()),
1612
        ]:
1613
            elem = SubElement(protocol, name)
1614
            elem.text = value
1615
1616
        daemon = Element('daemon')
1617
        for name, value in [
1618
            ('name', self.get_daemon_name()),
1619
            ('version', self.get_daemon_version()),
1620
        ]:
1621
            elem = SubElement(daemon, name)
1622
            elem.text = value
1623
1624
        scanner = Element('scanner')
1625
        for name, value in [
1626
            ('name', self.get_scanner_name()),
1627
            ('version', self.get_scanner_version()),
1628
        ]:
1629
            elem = SubElement(scanner, name)
1630
            elem.text = value
1631
1632
        content = [protocol, daemon, scanner]
1633
1634
        if self.get_vts_version():
1635
            vts = Element('vts')
1636
            elem = SubElement(vts, 'version')
1637
            elem.text = self.get_vts_version()
1638
            content.append(vts)
1639
1640
        return simple_response_str('get_version', 200, 'OK', content)
1641
1642
    def handle_command(self, command):
1643
        """ Handles an osp command in a string.
1644
1645
        @return: OSP Response to command.
1646
        """
1647
        try:
1648
            tree = secET.fromstring(command)
1649
        except secET.ParseError:
1650
            logger.debug("Erroneous client input: %s", command)
1651
            raise OspdCommandError('Invalid data')
1652
1653
        if not self.command_exists(tree.tag) and tree.tag != "authenticate":
1654
            raise OspdCommandError('Bogus command name')
1655
1656
        if tree.tag == "get_version":
1657
            return self.handle_get_version_command()
1658
        elif tree.tag == "start_scan":
1659
            return self.handle_start_scan_command(tree)
1660
        elif tree.tag == "stop_scan":
1661
            return self.handle_stop_scan_command(tree)
1662
        elif tree.tag == "get_scans":
1663
            return self.handle_get_scans_command(tree)
1664
        elif tree.tag == "get_vts":
1665
            return self.handle_get_vts_command(tree)
1666
        elif tree.tag == "delete_scan":
1667
            return self.handle_delete_scan_command(tree)
1668
        elif tree.tag == "help":
1669
            return self.handle_help_command(tree)
1670
        elif tree.tag == "get_scanner_details":
1671
            return self.handle_get_scanner_details()
1672
        elif tree.tag == "get_performance":
1673
            return self.handle_get_performance(tree)
1674
        else:
1675
            assert False, "Unhandled command: {0}".format(tree.tag)
1676
1677
    def check(self):
1678
        """ Asserts to False. Should be implemented by subclass. """
1679
        raise NotImplementedError
1680
1681
    def run(self, server: BaseServer):
1682
        """ Starts the Daemon, handling commands until interrupted.
1683
        """
1684
1685
        server.start(self.handle_client_stream)
1686
1687
        try:
1688
            while True:
1689
                time.sleep(10)
1690
                self.scheduler()
1691
                self.clean_forgotten_scans()
1692
        except KeyboardInterrupt:
1693
            logger.info("Received Ctrl-C shutting-down ...")
1694
        finally:
1695
            logger.info("Shutting-down server ...")
1696
            server.close()
1697
1698
    def scheduler(self):
1699
        """ Should be implemented by subclass in case of need
1700
        to run tasks periodically. """
1701
1702
    def create_scan(self, scan_id, targets, options, vts):
1703
        """ Creates a new scan.
1704
1705
        @target: Target to scan.
1706
        @options: Miscellaneous scan options.
1707
1708
        @return: New scan's ID.
1709
        """
1710
        if self.scan_exists(scan_id):
1711
            logger.info("Scan %s exists. Resuming scan.", scan_id)
1712
1713
        return self.scan_collection.create_scan(scan_id, targets, options, vts)
1714
1715
    def get_scan_options(self, scan_id):
1716
        """ Gives a scan's list of options. """
1717
        return self.scan_collection.get_options(scan_id)
1718
1719
    def set_scan_option(self, scan_id, name, value):
1720
        """ Sets a scan's option to a provided value. """
1721
        return self.scan_collection.set_option(scan_id, name, value)
1722
1723
    def clean_forgotten_scans(self):
1724
        """ Check for old stopped or finished scans which have not been
1725
        deleted and delete them if the are older than the set value."""
1726
1727
        if not self.scaninfo_store_time:
1728
            return
1729
1730
        for scan_id in list(self.scan_collection.ids_iterator()):
1731
            end_time = int(self.get_scan_end_time(scan_id))
1732
            scan_status = self.get_scan_status(scan_id)
1733
1734
            if (
1735
                scan_status == ScanStatus.STOPPED
1736
                or scan_status == ScanStatus.FINISHED
1737
            ) and end_time:
1738
                stored_time = int(time.time()) - end_time
1739
                if stored_time > self.scaninfo_store_time * 3600:
1740
                    logger.debug(
1741
                        'Scan %s is older than %d hours and seems have been '
1742
                        'forgotten. Scan info will be deleted from the '
1743
                        'scan table',
1744
                        scan_id,
1745
                        self.scaninfo_store_time,
1746
                    )
1747
                    self.delete_scan(scan_id)
1748
1749
    def check_scan_process(self, scan_id):
1750
        """ Check the scan's process, and terminate the scan if not alive. """
1751
        scan_process = self.scan_processes[scan_id]
1752
        progress = self.get_scan_progress(scan_id)
1753
        if progress < 100 and not scan_process.is_alive():
1754
            if not (self.get_scan_status(scan_id) == ScanStatus.STOPPED):
1755
                self.set_scan_status(scan_id, ScanStatus.STOPPED)
1756
                self.add_scan_error(
1757
                    scan_id, name="", host="", value="Scan process failure."
1758
                )
1759
                logger.info("%s: Scan stopped with errors.", scan_id)
1760
        elif progress == 100:
1761
            scan_process.join()
1762
1763
    def get_scan_progress(self, scan_id):
1764
        """ Gives a scan's current progress value. """
1765
        return self.scan_collection.get_progress(scan_id)
1766
1767
    def get_scan_target_progress(self, scan_id, target):
1768
        """ Gives a list with scan's current progress value of each target. """
1769
        return self.scan_collection.get_target_progress(scan_id, target)
1770
1771
    def get_scan_target(self, scan_id):
1772
        """ Gives a scan's target. """
1773
        return self.scan_collection.get_target_list(scan_id)
1774
1775
    def get_scan_ports(self, scan_id, target=''):
1776
        """ Gives a scan's ports list. """
1777
        return self.scan_collection.get_ports(scan_id, target)
1778
1779
    def get_scan_exclude_hosts(self, scan_id, target=''):
1780
        """ Gives a scan's exclude host list. If a target is passed gives
1781
        the exclude host list for the given target. """
1782
        return self.scan_collection.get_exclude_hosts(scan_id, target)
1783
1784
    def get_scan_credentials(self, scan_id, target=''):
1785
        """ Gives a scan's credential list. If a target is passed gives
1786
        the credential list for the given target. """
1787
        return self.scan_collection.get_credentials(scan_id, target)
1788
1789
    def get_scan_vts(self, scan_id):
1790
        """ Gives a scan's vts list. """
1791
        return self.scan_collection.get_vts(scan_id)
1792
1793
    def get_scan_unfinished_hosts(self, scan_id):
1794
        """ Get a list of unfinished hosts."""
1795
        return self.scan_collection.get_hosts_unfinished(scan_id)
1796
1797
    def get_scan_finished_hosts(self, scan_id):
1798
        """ Get a list of unfinished hosts."""
1799
        return self.scan_collection.get_hosts_finished(scan_id)
1800
1801
    def get_scan_start_time(self, scan_id):
1802
        """ Gives a scan's start time. """
1803
        return self.scan_collection.get_start_time(scan_id)
1804
1805
    def get_scan_end_time(self, scan_id):
1806
        """ Gives a scan's end time. """
1807
        return self.scan_collection.get_end_time(scan_id)
1808
1809
    def add_scan_log(
1810
        self,
1811
        scan_id,
1812
        host='',
1813
        hostname='',
1814
        name='',
1815
        value='',
1816
        port='',
1817
        test_id='',
1818
        qod='',
1819
    ):
1820
        """ Adds a log result to scan_id scan. """
1821
        self.scan_collection.add_result(
1822
            scan_id,
1823
            ResultType.LOG,
1824
            host,
1825
            hostname,
1826
            name,
1827
            value,
1828
            port,
1829
            test_id,
1830
            0.0,
1831
            qod,
1832
        )
1833
1834
    def add_scan_error(
1835
        self, scan_id, host='', hostname='', name='', value='', port=''
1836
    ):
1837
        """ Adds an error result to scan_id scan. """
1838
        self.scan_collection.add_result(
1839
            scan_id, ResultType.ERROR, host, hostname, name, value, port
1840
        )
1841
1842
    def add_scan_host_detail(
1843
        self, scan_id, host='', hostname='', name='', value=''
1844
    ):
1845
        """ Adds a host detail result to scan_id scan. """
1846
        self.scan_collection.add_result(
1847
            scan_id, ResultType.HOST_DETAIL, host, hostname, name, value
1848
        )
1849
1850
    def add_scan_alarm(
1851
        self,
1852
        scan_id,
1853
        host='',
1854
        hostname='',
1855
        name='',
1856
        value='',
1857
        port='',
1858
        test_id='',
1859
        severity='',
1860
        qod='',
1861
    ):
1862
        """ Adds an alarm result to scan_id scan. """
1863
        self.scan_collection.add_result(
1864
            scan_id,
1865
            ResultType.ALARM,
1866
            host,
1867
            hostname,
1868
            name,
1869
            value,
1870
            port,
1871
            test_id,
1872
            severity,
1873
            qod,
1874
        )
1875