Completed
Push — master ( f710af...8bfb49 )
by Juan José
18s queued 13s
created

ospd.ospd.OSPDaemon.process_targets_element()   D

Complexity

Conditions 12

Size

Total Lines 91
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 37
nop 2
dl 0
loc 91
rs 4.8
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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:

Complexity

Complex classes like ospd.ospd.OSPDaemon.process_targets_element() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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