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

ospd.ospd.OSPDaemon.get_scan_target_options()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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