Passed
Pull Request — master (#134)
by Juan José
01:22
created

ospd.ospd.OSPDaemon.set_scan_host_progress()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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