Passed
Pull Request — master (#133)
by Juan José
02:21
created

ospd.ospd.OSPDaemon.elements_as_text()   A

Complexity

Conditions 4

Size

Total Lines 17
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nop 3
dl 0
loc 17
rs 9.7
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
                    else:
832
                        self.set_scan_status(scan_id, ScanStatus.FINISHED)
833
834
                running_target = (running_target_proc, running_target_id)
835
                multiscan_proc.remove(running_target)
836
837
        return multiscan_proc
838
839
    def calculate_progress(self, scan_id):
840
        """ Calculate the total scan progress from the
841
        partial target progress. """
842
843
        t_prog = dict()
844
        for target in self.get_scan_target(scan_id):
845
            t_prog[target] = self.get_scan_target_progress(scan_id, target)
846
        return sum(t_prog.values()) / len(t_prog)
847
848
    def process_exclude_hosts(self, scan_id, target_list):
849
        """ Process the exclude hosts before launching the scans.
850
        Set exclude hosts as finished with 100% to calculate
851
        the scan progress."""
852
853
        for target, _, _, exclude_hosts in target_list:
854
            exc_hosts_list = ''
855
            if not exclude_hosts:
856
                continue
857
            exc_hosts_list = target_str_to_list(exclude_hosts)
858
            for host in exc_hosts_list:
859
                self.set_scan_host_finished(scan_id, target, host)
860
                self.set_scan_target_progress(scan_id, target, host, 100)
861
862
    def start_scan(self, scan_id, targets, parallel=1):
863
        """ Handle N parallel scans if 'parallel' is greater than 1. """
864
865
        os.setsid()
866
        multiscan_proc = []
867
        logger.info("%s: Scan started.", scan_id)
868
        target_list = targets
869
        if target_list is None or not target_list:
870
            raise OspdCommandError('Erroneous targets list', 'start_scan')
871
872
        self.process_exclude_hosts(scan_id, target_list)
873
874
        for _index, target in enumerate(target_list):
875
            while len(multiscan_proc) >= parallel:
876
                progress = self.calculate_progress(scan_id)
877
                self.set_scan_progress(scan_id, progress)
878
                multiscan_proc = self.check_pending_target(
879
                    scan_id, multiscan_proc
880
                )
881
                time.sleep(1)
882
883
            # If the scan status is stopped, does not launch anymore target
884
            # scans
885
            if self.get_scan_status(scan_id) == ScanStatus.STOPPED:
886
                return
887
888
            logger.info(
889
                "%s: Host scan started on ports %s.", target[0], target[1]
890
            )
891
            scan_process = multiprocessing.Process(
892
                target=self.parallel_scan, args=(scan_id, target[0])
893
            )
894
            multiscan_proc.append((scan_process, target[0]))
895
            scan_process.start()
896
            self.set_scan_status(scan_id, ScanStatus.RUNNING)
897
898
        # Wait until all single target were scanned
899
        while multiscan_proc:
900
            multiscan_proc = self.check_pending_target(scan_id, multiscan_proc)
901
            if multiscan_proc:
902
                progress = self.calculate_progress(scan_id)
903
                self.set_scan_progress(scan_id, progress)
904
            time.sleep(1)
905
906
        # Only set the scan as finished if the scan was not stopped.
907
        if self.get_scan_status(scan_id) != ScanStatus.STOPPED:
908
            self.finish_scan(scan_id)
909
910
    def dry_run_scan(self, scan_id, targets):
911
        """ Dry runs a scan. """
912
913
        os.setsid()
914
        for _, target in enumerate(targets):
915
            host = resolve_hostname(target[0])
916
            if host is None:
917
                logger.info("Couldn't resolve %s.", target[0])
918
                continue
919
            port = self.get_scan_ports(scan_id, target=target[0])
920
            logger.info("%s:%s: Dry run mode.", host, port)
921
            self.add_scan_log(
922
                scan_id, name='', host=host, value='Dry run result'
923
            )
924
        self.finish_scan(scan_id)
925
926
    def handle_timeout(self, scan_id, host):
927
        """ Handles scanner reaching timeout error. """
928
        self.add_scan_error(
929
            scan_id,
930
            host=host,
931
            name="Timeout",
932
            value="{0} exec timeout.".format(self.get_scanner_name()),
933
        )
934
935
    def set_scan_host_finished(self, scan_id, target, host):
936
        """ Add the host in a list of finished hosts """
937
        self.scan_collection.set_host_finished(scan_id, target, host)
938
939
    def set_scan_progress(self, scan_id, progress):
940
        """ Sets scan_id scan's progress which is a number
941
        between 0 and 100. """
942
        self.scan_collection.set_progress(scan_id, progress)
943
944
    def set_scan_target_progress(self, scan_id, target, host, progress):
945
        """ Sets host's progress which is part of target. """
946
        self.scan_collection.set_target_progress(
947
            scan_id, target, host, progress
948
        )
949
950
    def set_scan_status(self, scan_id, status):
951
        """ Set the scan's status."""
952
        self.scan_collection.set_status(scan_id, status)
953
954
    def get_scan_status(self, scan_id):
955
        """ Get scan_id scans's status."""
956
        return self.scan_collection.get_status(scan_id)
957
958
    def scan_exists(self, scan_id):
959
        """ Checks if a scan with ID scan_id is in collection.
960
961
        @return: 1 if scan exists, 0 otherwise.
962
        """
963
        return self.scan_collection.id_exists(scan_id)
964
965
    def handle_get_scans_command(self, scan_et):
966
        """ Handles <get_scans> command.
967
968
        @return: Response string for <get_scans> command.
969
        """
970
971
        scan_id = scan_et.attrib.get('scan_id')
972
        details = scan_et.attrib.get('details')
973
        pop_res = scan_et.attrib.get('pop_results')
974
        if details and details == '0':
975
            details = False
976
        else:
977
            details = True
978
            if pop_res and pop_res == '1':
979
                pop_res = True
980
            else:
981
                pop_res = False
982
983
        responses = []
984
        if scan_id and scan_id in self.scan_collection.ids_iterator():
985
            self.check_scan_process(scan_id)
986
            scan = self.get_scan_xml(scan_id, details, pop_res)
987
            responses.append(scan)
988
        elif scan_id:
989
            text = "Failed to find scan '{0}'".format(scan_id)
990
            return simple_response_str('get_scans', 404, text)
991
        else:
992
            for scan_id in self.scan_collection.ids_iterator():
993
                self.check_scan_process(scan_id)
994
                scan = self.get_scan_xml(scan_id, details, pop_res)
995
                responses.append(scan)
996
        return simple_response_str('get_scans', 200, 'OK', responses)
997
998
    def handle_get_vts_command(self, vt_et):
999
        """ Handles <get_vts> command.
1000
1001
        @return: Response string for <get_vts> command.
1002
        """
1003
1004
        vt_id = vt_et.attrib.get('vt_id')
1005
        vt_filter = vt_et.attrib.get('filter')
1006
1007
        if vt_id and vt_id not in self.vts:
1008
            text = "Failed to find vulnerability test '{0}'".format(vt_id)
1009
            return simple_response_str('get_vts', 404, text)
1010
1011
        filtered_vts = None
1012
        if vt_filter:
1013
            filtered_vts = self.vts_filter.get_filtered_vts_list(
1014
                self.vts, vt_filter
1015
            )
1016
1017
        responses = []
1018
1019
        vts_xml = self.get_vts_xml(vt_id, filtered_vts)
1020
1021
        responses.append(vts_xml)
1022
1023
        return simple_response_str('get_vts', 200, 'OK', responses)
1024
1025
    def handle_get_performance(self, scan_et):
1026
        """ Handles <get_performance> command.
1027
1028
        @return: Response string for <get_performance> command.
1029
        """
1030
        start = scan_et.attrib.get('start')
1031
        end = scan_et.attrib.get('end')
1032
        titles = scan_et.attrib.get('titles')
1033
1034
        cmd = ['gvmcg']
1035
        if start:
1036
            try:
1037
                int(start)
1038
            except ValueError:
1039
                raise OspdCommandError(
1040
                    'Start argument must be integer.',
1041
                    'get_performance'
1042
            )
1043
            cmd.append(start)
1044
1045
        if end:
1046
            try:
1047
                int(end)
1048
            except ValueError:
1049
                raise OspdCommandError(
1050
                    'End argument must be integer.',
1051
                    'get_performance'
1052
                )
1053
            cmd.append(end)
1054
1055
        if titles:
1056
            combined = "(" + ")|(".join(GVMCG_TITLES) + ")"
1057
            forbidden = "^[^|&;]+$"
1058
            if re.match(combined, titles) and re.match(forbidden, titles):
1059
                cmd.append(titles)
1060
            else:
1061
                raise OspdCommandError(
1062
                    'Arguments not allowed',
1063
                    'get_performance'
1064
                )
1065
1066
        try:
1067
            output = subprocess.check_output(cmd)
1068
        except (
1069
                subprocess.CalledProcessError,
1070
                PermissionError,
1071
                FileNotFoundError,
1072
        ) as e:
1073
            raise OspdCommandError(
1074
                'Bogus get_performance format. %s' % e,
1075
                'get_performance'
1076
            )
1077
1078
        return simple_response_str('get_performance', 200, 'OK', str(output))
1079
1080
    def handle_help_command(self, scan_et):
1081
        """ Handles <help> command.
1082
1083
        @return: Response string for <help> command.
1084
        """
1085
        help_format = scan_et.attrib.get('format')
1086
        if help_format is None or help_format == "text":
1087
            # Default help format is text.
1088
            return simple_response_str('help', 200, 'OK', self.get_help_text())
1089
        elif help_format == "xml":
1090
            text = self.get_xml_str(self.commands)
1091
            return simple_response_str('help', 200, 'OK', text)
1092
        raise OspdCommandError('Bogus help format', 'help')
1093
1094
    def get_help_text(self):
1095
        """ Returns the help output in plain text format."""
1096
1097
        txt = str('\n')
1098
        for name, info in self.commands.items():
1099
            command_txt = "\t{0: <22} {1}\n".format(name, info['description'])
1100
            if info['attributes']:
1101
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
1102
                for attrname, attrdesc in info['attributes'].items():
1103
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
1104
                    command_txt = ''.join([command_txt, attr_txt])
1105
            if info['elements']:
1106
                command_txt = ''.join(
1107
                    [
1108
                        command_txt,
1109
                        "\t Elements:\n",
1110
                        self.elements_as_text(info['elements']),
1111
                    ]
1112
                )
1113
            txt = ''.join([txt, command_txt])
1114
        return txt
1115
1116
    def elements_as_text(self, elems, indent=2):
1117
        """ Returns the elems dictionary as formatted plain text. """
1118
        assert elems
1119
        text = ""
1120
        for elename, eledesc in elems.items():
1121
            if isinstance(eledesc, dict):
1122
                desc_txt = self.elements_as_text(eledesc, indent + 2)
1123
                desc_txt = ''.join(['\n', desc_txt])
1124
            elif isinstance(eledesc, str):
1125
                desc_txt = ''.join([eledesc, '\n'])
1126
            else:
1127
                assert False, "Only string or dictionary"
1128
            ele_txt = "\t{0}{1: <22} {2}".format(
1129
                ' ' * 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...
1130
            )
1131
            text = ''.join([text, ele_txt])
1132
        return text
1133
1134
    def handle_delete_scan_command(self, scan_et):
1135
        """ Handles <delete_scan> command.
1136
1137
        @return: Response string for <delete_scan> command.
1138
        """
1139
        scan_id = scan_et.attrib.get('scan_id')
1140
        if scan_id is None:
1141
            return simple_response_str(
1142
                'delete_scan', 404, 'No scan_id attribute'
1143
            )
1144
1145
        if not self.scan_exists(scan_id):
1146
            text = "Failed to find scan '{0}'".format(scan_id)
1147
            return simple_response_str('delete_scan', 404, text)
1148
        self.check_scan_process(scan_id)
1149
        if self.delete_scan(scan_id):
1150
            return simple_response_str('delete_scan', 200, 'OK')
1151
        raise OspdCommandError('Scan in progress', 'delete_scan')
1152
1153
    def delete_scan(self, scan_id):
1154
        """ Deletes scan_id scan from collection.
1155
1156
        @return: 1 if scan deleted, 0 otherwise.
1157
        """
1158
        if self.get_scan_status(scan_id) == ScanStatus.RUNNING:
1159
            return 0
1160
1161
        try:
1162
            del self.scan_processes[scan_id]
1163
        except KeyError:
1164
            logger.debug('Scan process for %s not found', scan_id)
1165
        return self.scan_collection.delete_scan(scan_id)
1166
1167
    def get_scan_results_xml(self, scan_id, pop_res):
1168
        """ Gets scan_id scan's results in XML format.
1169
1170
        @return: String of scan results in xml.
1171
        """
1172
        results = Element('results')
1173
        for result in self.scan_collection.results_iterator(scan_id, pop_res):
1174
            results.append(get_result_xml(result))
1175
1176
        logger.info('Returning %d results', len(results))
1177
        return results
1178
1179
    def get_xml_str(self, data):
1180
        """ Creates a string in XML Format using the provided data structure.
1181
1182
        @param: Dictionary of xml tags and their elements.
1183
1184
        @return: String of data in xml format.
1185
        """
1186
1187
        responses = []
1188
        for tag, value in data.items():
1189
            elem = Element(tag)
1190
            if isinstance(value, dict):
1191
                for val in self.get_xml_str(value):
1192
                    elem.append(val)
1193
            elif isinstance(value, list):
1194
                elem.text = ', '.join(value)
1195
            else:
1196
                elem.text = value
1197
            responses.append(elem)
1198
        return responses
1199
1200
    def get_scan_xml(self, scan_id, detailed=True, pop_res=False):
1201
        """ Gets scan in XML format.
1202
1203
        @return: String of scan in XML format.
1204
        """
1205
        if not scan_id:
1206
            return Element('scan')
1207
1208
        target = ','.join(self.get_scan_target(scan_id))
1209
        progress = self.get_scan_progress(scan_id)
1210
        status = self.get_scan_status(scan_id)
1211
        start_time = self.get_scan_start_time(scan_id)
1212
        end_time = self.get_scan_end_time(scan_id)
1213
        response = Element('scan')
1214
        for name, value in [
1215
            ('id', scan_id),
1216
            ('target', target),
1217
            ('progress', progress),
1218
            ('status', status.name.lower()),
1219
            ('start_time', start_time),
1220
            ('end_time', end_time),
1221
        ]:
1222
            response.set(name, str(value))
1223
        if detailed:
1224
            response.append(self.get_scan_results_xml(scan_id, pop_res))
1225
        return response
1226
1227
    @staticmethod
1228
    def get_custom_vt_as_xml_str(
1229
        vt_id, custom
1230
    ):  # pylint: disable=unused-argument
1231
        """ Create a string representation of the XML object from the
1232
        custom data object.
1233
        This needs to be implemented by each ospd wrapper, in case
1234
        custom elements for VTs are used.
1235
1236
        The custom XML object which is returned will be embedded
1237
        into a <custom></custom> element.
1238
1239
        @return: XML object as string for custom data.
1240
        """
1241
        return ''
1242
1243
    @staticmethod
1244
    def get_params_vt_as_xml_str(
1245
        vt_id, vt_params
1246
    ):  # pylint: disable=unused-argument
1247
        """ Create a string representation of the XML object from the
1248
        vt_params data object.
1249
        This needs to be implemented by each ospd wrapper, in case
1250
        vt_params elements for VTs are used.
1251
1252
        The params XML object which is returned will be embedded
1253
        into a <params></params> element.
1254
1255
        @return: XML object as string for vt parameters data.
1256
        """
1257
        return ''
1258
1259
    @staticmethod
1260
    def get_refs_vt_as_xml_str(
1261
        vt_id, vt_refs
1262
    ):  # pylint: disable=unused-argument
1263
        """ Create a string representation of the XML object from the
1264
        refs data object.
1265
        This needs to be implemented by each ospd wrapper, in case
1266
        refs elements for VTs are used.
1267
1268
        The refs XML object which is returned will be embedded
1269
        into a <refs></refs> element.
1270
1271
        @return: XML object as string for vt references data.
1272
        """
1273
        return ''
1274
1275
    @staticmethod
1276
    def get_dependencies_vt_as_xml_str(
1277
        vt_id, vt_dependencies
1278
    ):  # pylint: disable=unused-argument
1279
        """ Create a string representation of the XML object from the
1280
        vt_dependencies data object.
1281
        This needs to be implemented by each ospd wrapper, in case
1282
        vt_dependencies elements for VTs are used.
1283
1284
        The vt_dependencies XML object which is returned will be embedded
1285
        into a <dependencies></dependencies> element.
1286
1287
        @return: XML object as string for vt dependencies data.
1288
        """
1289
        return ''
1290
1291
    @staticmethod
1292
    def get_creation_time_vt_as_xml_str(
1293
        vt_id, vt_creation_time
1294
    ):  # pylint: disable=unused-argument
1295
        """ Create a string representation of the XML object from the
1296
        vt_creation_time data object.
1297
        This needs to be implemented by each ospd wrapper, in case
1298
        vt_creation_time elements for VTs are used.
1299
1300
        The vt_creation_time XML object which is returned will be embedded
1301
        into a <vt_creation_time></vt_creation_time> element.
1302
1303
        @return: XML object as string for vt creation time data.
1304
        """
1305
        return ''
1306
1307
    @staticmethod
1308
    def get_modification_time_vt_as_xml_str(
1309
        vt_id, vt_modification_time
1310
    ):  # pylint: disable=unused-argument
1311
        """ Create a string representation of the XML object from the
1312
        vt_modification_time data object.
1313
        This needs to be implemented by each ospd wrapper, in case
1314
        vt_modification_time elements for VTs are used.
1315
1316
        The vt_modification_time XML object which is returned will be embedded
1317
        into a <vt_modification_time></vt_modification_time> element.
1318
1319
        @return: XML object as string for vt references data.
1320
        """
1321
        return ''
1322
1323
    @staticmethod
1324
    def get_summary_vt_as_xml_str(
1325
        vt_id, summary
1326
    ):  # pylint: disable=unused-argument
1327
        """ Create a string representation of the XML object from the
1328
        summary data object.
1329
        This needs to be implemented by each ospd wrapper, in case
1330
        summary elements for VTs are used.
1331
1332
        The summary XML object which is returned will be embedded
1333
        into a <summary></summary> element.
1334
1335
        @return: XML object as string for summary data.
1336
        """
1337
        return ''
1338
1339
    @staticmethod
1340
    def get_impact_vt_as_xml_str(
1341
        vt_id, impact
1342
    ):  # pylint: disable=unused-argument
1343
        """ Create a string representation of the XML object from the
1344
        impact data object.
1345
        This needs to be implemented by each ospd wrapper, in case
1346
        impact elements for VTs are used.
1347
1348
        The impact XML object which is returned will be embedded
1349
        into a <impact></impact> element.
1350
1351
        @return: XML object as string for impact data.
1352
        """
1353
        return ''
1354
1355
    @staticmethod
1356
    def get_affected_vt_as_xml_str(
1357
        vt_id, affected
1358
    ):  # pylint: disable=unused-argument
1359
        """ Create a string representation of the XML object from the
1360
        affected data object.
1361
        This needs to be implemented by each ospd wrapper, in case
1362
        affected elements for VTs are used.
1363
1364
        The affected XML object which is returned will be embedded
1365
        into a <affected></affected> element.
1366
1367
        @return: XML object as string for affected data.
1368
        """
1369
        return ''
1370
1371
    @staticmethod
1372
    def get_insight_vt_as_xml_str(
1373
        vt_id, insight
1374
    ):  # pylint: disable=unused-argument
1375
        """ Create a string representation of the XML object from the
1376
        insight data object.
1377
        This needs to be implemented by each ospd wrapper, in case
1378
        insight elements for VTs are used.
1379
1380
        The insight XML object which is returned will be embedded
1381
        into a <insight></insight> element.
1382
1383
        @return: XML object as string for insight data.
1384
        """
1385
        return ''
1386
1387
    @staticmethod
1388
    def get_solution_vt_as_xml_str(
1389
        vt_id, solution, solution_type=None
1390
    ):  # pylint: disable=unused-argument
1391
        """ Create a string representation of the XML object from the
1392
        solution data object.
1393
        This needs to be implemented by each ospd wrapper, in case
1394
        solution elements for VTs are used.
1395
1396
        The solution XML object which is returned will be embedded
1397
        into a <solution></solution> element.
1398
1399
        @return: XML object as string for solution data.
1400
        """
1401
        return ''
1402
1403
    @staticmethod
1404
    def get_detection_vt_as_xml_str(
1405
        vt_id, detection=None, qod_type=None, qod=None
1406
    ):  # pylint: disable=unused-argument
1407
        """ Create a string representation of the XML object from the
1408
        detection data object.
1409
        This needs to be implemented by each ospd wrapper, in case
1410
        detection elements for VTs are used.
1411
1412
        The detection XML object which is returned is an element with
1413
        tag <detection></detection> element
1414
1415
        @return: XML object as string for detection data.
1416
        """
1417
        return ''
1418
1419
    @staticmethod
1420
    def get_severities_vt_as_xml_str(
1421
        vt_id, severities
1422
    ):  # pylint: disable=unused-argument
1423
        """ Create a string representation of the XML object from the
1424
        severities data object.
1425
        This needs to be implemented by each ospd wrapper, in case
1426
        severities elements for VTs are used.
1427
1428
        The severities XML objects which are returned will be embedded
1429
        into a <severities></severities> element.
1430
1431
        @return: XML object as string for severities data.
1432
        """
1433
        return ''
1434
1435
    def get_vt_xml(self, vt_id):
1436
        """ Gets a single vulnerability test information in XML format.
1437
1438
        @return: String of single vulnerability test information in XML format.
1439
        """
1440
        if not vt_id:
1441
            return Element('vt')
1442
1443
        vt = self.vts.get(vt_id)
1444
1445
        name = vt.get('name')
1446
        vt_xml = Element('vt')
1447
        vt_xml.set('id', vt_id)
1448
1449
        for name, value in [('name', name)]:
1450
            elem = SubElement(vt_xml, name)
1451
            elem.text = str(value)
1452
1453
        if vt.get('vt_params'):
1454
            params_xml_str = self.get_params_vt_as_xml_str(
1455
                vt_id, vt.get('vt_params')
1456
            )
1457
            vt_xml.append(secET.fromstring(params_xml_str))
1458
1459
        if vt.get('vt_refs'):
1460
            refs_xml_str = self.get_refs_vt_as_xml_str(vt_id, vt.get('vt_refs'))
1461
            vt_xml.append(secET.fromstring(refs_xml_str))
1462
1463
        if vt.get('vt_dependencies'):
1464
            dependencies = self.get_dependencies_vt_as_xml_str(
1465
                vt_id, vt.get('vt_dependencies')
1466
            )
1467
            vt_xml.append(secET.fromstring(dependencies))
1468
1469
        if vt.get('creation_time'):
1470
            vt_ctime = self.get_creation_time_vt_as_xml_str(
1471
                vt_id, vt.get('creation_time')
1472
            )
1473
            vt_xml.append(secET.fromstring(vt_ctime))
1474
1475
        if vt.get('modification_time'):
1476
            vt_mtime = self.get_modification_time_vt_as_xml_str(
1477
                vt_id, vt.get('modification_time')
1478
            )
1479
            vt_xml.append(secET.fromstring(vt_mtime))
1480
1481
        if vt.get('summary'):
1482
            summary_xml_str = self.get_summary_vt_as_xml_str(
1483
                vt_id, vt.get('summary')
1484
            )
1485
            vt_xml.append(secET.fromstring(summary_xml_str))
1486
1487
        if vt.get('impact'):
1488
            impact_xml_str = self.get_impact_vt_as_xml_str(
1489
                vt_id, vt.get('impact')
1490
            )
1491
            vt_xml.append(secET.fromstring(impact_xml_str))
1492
1493
        if vt.get('affected'):
1494
            affected_xml_str = self.get_affected_vt_as_xml_str(
1495
                vt_id, vt.get('affected')
1496
            )
1497
            vt_xml.append(secET.fromstring(affected_xml_str))
1498
1499
        if vt.get('insight'):
1500
            insight_xml_str = self.get_insight_vt_as_xml_str(
1501
                vt_id, vt.get('insight')
1502
            )
1503
            vt_xml.append(secET.fromstring(insight_xml_str))
1504
1505
        if vt.get('solution'):
1506
            solution_xml_str = self.get_solution_vt_as_xml_str(
1507
                vt_id, vt.get('solution'), vt.get('solution_type')
1508
            )
1509
            vt_xml.append(secET.fromstring(solution_xml_str))
1510
1511
        if vt.get('detection') or vt.get('qod_type') or vt.get('qod'):
1512
            detection_xml_str = self.get_detection_vt_as_xml_str(
1513
                vt_id, vt.get('detection'), vt.get('qod_type'), vt.get('qod')
1514
            )
1515
            vt_xml.append(secET.fromstring(detection_xml_str))
1516
1517
        if vt.get('severities'):
1518
            severities_xml_str = self.get_severities_vt_as_xml_str(
1519
                vt_id, vt.get('severities')
1520
            )
1521
            vt_xml.append(secET.fromstring(severities_xml_str))
1522
1523
        if vt.get('custom'):
1524
            custom_xml_str = self.get_custom_vt_as_xml_str(
1525
                vt_id, vt.get('custom')
1526
            )
1527
            vt_xml.append(secET.fromstring(custom_xml_str))
1528
1529
        return vt_xml
1530
1531
    def get_vts_xml(self, vt_id=None, filtered_vts=None):
1532
        """ Gets collection of vulnerability test information in XML format.
1533
        If vt_id is specified, the collection will contain only this vt, if
1534
        found.
1535
        If no vt_id is specified, the collection will contain all vts or those
1536
        passed in filtered_vts.
1537
1538
        Arguments:
1539
            vt_id (vt_id, optional): ID of the vt to get.
1540
            filtered_vts (dict, optional): Filtered VTs collection.
1541
1542
        Return:
1543
            String of collection of vulnerability test information in
1544
            XML format.
1545
        """
1546
1547
        vts_xml = Element('vts')
1548
1549
        if vt_id:
1550
            vts_xml.append(self.get_vt_xml(vt_id))
1551
        elif filtered_vts:
1552
            for vt_id in filtered_vts:
1553
                vts_xml.append(self.get_vt_xml(vt_id))
1554
        else:
1555
            for vt_id in self.vts:
1556
                vts_xml.append(self.get_vt_xml(vt_id))
1557
1558
        return vts_xml
1559
1560
    def handle_get_scanner_details(self):
1561
        """ Handles <get_scanner_details> command.
1562
1563
        @return: Response string for <get_scanner_details> command.
1564
        """
1565
        desc_xml = Element('description')
1566
        desc_xml.text = self.get_scanner_description()
1567
        details = [desc_xml, self.get_scanner_params_xml()]
1568
        return simple_response_str('get_scanner_details', 200, 'OK', details)
1569
1570
    def handle_get_version_command(self):
1571
        """ Handles <get_version> command.
1572
1573
        @return: Response string for <get_version> command.
1574
        """
1575
        protocol = Element('protocol')
1576
        for name, value in [
1577
            ('name', 'OSP'),
1578
            ('version', self.get_protocol_version()),
1579
        ]:
1580
            elem = SubElement(protocol, name)
1581
            elem.text = value
1582
1583
        daemon = Element('daemon')
1584
        for name, value in [
1585
            ('name', self.get_daemon_name()),
1586
            ('version', self.get_daemon_version()),
1587
        ]:
1588
            elem = SubElement(daemon, name)
1589
            elem.text = value
1590
1591
        scanner = Element('scanner')
1592
        for name, value in [
1593
            ('name', self.get_scanner_name()),
1594
            ('version', self.get_scanner_version()),
1595
        ]:
1596
            elem = SubElement(scanner, name)
1597
            elem.text = value
1598
1599
        content = [protocol, daemon, scanner]
1600
1601
        if self.get_vts_version():
1602
            vts = Element('vts')
1603
            elem = SubElement(vts, 'version')
1604
            elem.text = self.get_vts_version()
1605
            content.append(vts)
1606
1607
        return simple_response_str('get_version', 200, 'OK', content)
1608
1609
    def handle_command(self, command):
1610
        """ Handles an osp command in a string.
1611
1612
        @return: OSP Response to command.
1613
        """
1614
        try:
1615
            tree = secET.fromstring(command)
1616
        except secET.ParseError:
1617
            logger.debug("Erroneous client input: %s", command)
1618
            raise OspdCommandError('Invalid data')
1619
1620
        logger.debug('Handling command %s', command)
1621
1622
        if not self.command_exists(tree.tag) and tree.tag != "authenticate":
1623
            raise OspdCommandError('Bogus command name')
1624
1625
        if tree.tag == "get_version":
1626
            return self.handle_get_version_command()
1627
        elif tree.tag == "start_scan":
1628
            return self.handle_start_scan_command(tree)
1629
        elif tree.tag == "stop_scan":
1630
            return self.handle_stop_scan_command(tree)
1631
        elif tree.tag == "get_scans":
1632
            return self.handle_get_scans_command(tree)
1633
        elif tree.tag == "get_vts":
1634
            return self.handle_get_vts_command(tree)
1635
        elif tree.tag == "delete_scan":
1636
            return self.handle_delete_scan_command(tree)
1637
        elif tree.tag == "help":
1638
            return self.handle_help_command(tree)
1639
        elif tree.tag == "get_scanner_details":
1640
            return self.handle_get_scanner_details()
1641
        elif tree.tag == "get_performance":
1642
            return self.handle_get_performance(tree)
1643
        else:
1644
            assert False, "Unhandled command: {0}".format(tree.tag)
1645
1646
    def check(self):
1647
        """ Asserts to False. Should be implemented by subclass. """
1648
        raise NotImplementedError
1649
1650
    def run(self, server: Server):
1651
        """ Starts the Daemon, handling commands until interrupted.
1652
        """
1653
1654
        server.bind()
1655
1656
        try:
1657
            while True:
1658
                server.select(
1659
                    self.handle_client_stream, timeout=SCHEDULER_CHECK_PERIOD
1660
                )
1661
                self.scheduler()
1662
        except KeyboardInterrupt:
1663
            logger.info("Received Ctrl-C shutting-down ...")
1664
        finally:
1665
            server.close()
1666
1667
    def scheduler(self):
1668
        """ Should be implemented by subclass in case of need
1669
        to run tasks periodically. """
1670
1671
    def create_scan(self, scan_id, targets, options, vts):
1672
        """ Creates a new scan.
1673
1674
        @target: Target to scan.
1675
        @options: Miscellaneous scan options.
1676
1677
        @return: New scan's ID.
1678
        """
1679
        if self.scan_exists(scan_id):
1680
            logger.info("Scan %s exists. Resuming scan.", scan_id)
1681
1682
        return self.scan_collection.create_scan(scan_id, targets, options, vts)
1683
1684
    def get_scan_options(self, scan_id):
1685
        """ Gives a scan's list of options. """
1686
        return self.scan_collection.get_options(scan_id)
1687
1688
    def set_scan_option(self, scan_id, name, value):
1689
        """ Sets a scan's option to a provided value. """
1690
        return self.scan_collection.set_option(scan_id, name, value)
1691
1692
    def check_scan_process(self, scan_id):
1693
        """ Check the scan's process, and terminate the scan if not alive. """
1694
        scan_process = self.scan_processes[scan_id]
1695
        progress = self.get_scan_progress(scan_id)
1696
        if progress < 100 and not scan_process.is_alive():
1697
            if not (self.get_scan_status(scan_id) == ScanStatus.STOPPED):
1698
                self.set_scan_status(scan_id, ScanStatus.STOPPED)
1699
                self.add_scan_error(
1700
                    scan_id, name="", host="", value="Scan process failure."
1701
                )
1702
                logger.info("%s: Scan stopped with errors.", scan_id)
1703
        elif progress == 100:
1704
            scan_process.join()
1705
1706
    def get_scan_progress(self, scan_id):
1707
        """ Gives a scan's current progress value. """
1708
        return self.scan_collection.get_progress(scan_id)
1709
1710
    def get_scan_target_progress(self, scan_id, target):
1711
        """ Gives a list with scan's current progress value of each target. """
1712
        return self.scan_collection.get_target_progress(scan_id, target)
1713
1714
    def get_scan_target(self, scan_id):
1715
        """ Gives a scan's target. """
1716
        return self.scan_collection.get_target_list(scan_id)
1717
1718
    def get_scan_ports(self, scan_id, target=''):
1719
        """ Gives a scan's ports list. """
1720
        return self.scan_collection.get_ports(scan_id, target)
1721
1722
    def get_scan_exclude_hosts(self, scan_id, target=''):
1723
        """ Gives a scan's exclude host list. If a target is passed gives
1724
        the exclude host list for the given target. """
1725
        return self.scan_collection.get_exclude_hosts(scan_id, target)
1726
1727
    def get_scan_credentials(self, scan_id, target=''):
1728
        """ Gives a scan's credential list. If a target is passed gives
1729
        the credential list for the given target. """
1730
        return self.scan_collection.get_credentials(scan_id, target)
1731
1732
    def get_scan_vts(self, scan_id):
1733
        """ Gives a scan's vts list. """
1734
        return self.scan_collection.get_vts(scan_id)
1735
1736
    def get_scan_unfinished_hosts(self, scan_id):
1737
        """ Get a list of unfinished hosts."""
1738
        return self.scan_collection.get_hosts_unfinished(scan_id)
1739
1740
    def get_scan_finished_hosts(self, scan_id):
1741
        """ Get a list of unfinished hosts."""
1742
        return self.scan_collection.get_hosts_finished(scan_id)
1743
1744
    def get_scan_start_time(self, scan_id):
1745
        """ Gives a scan's start time. """
1746
        return self.scan_collection.get_start_time(scan_id)
1747
1748
    def get_scan_end_time(self, scan_id):
1749
        """ Gives a scan's end time. """
1750
        return self.scan_collection.get_end_time(scan_id)
1751
1752
    def add_scan_log(
1753
        self,
1754
        scan_id,
1755
        host='',
1756
        hostname='',
1757
        name='',
1758
        value='',
1759
        port='',
1760
        test_id='',
1761
        qod='',
1762
    ):
1763
        """ Adds a log result to scan_id scan. """
1764
        self.scan_collection.add_result(
1765
            scan_id,
1766
            ResultType.LOG,
1767
            host,
1768
            hostname,
1769
            name,
1770
            value,
1771
            port,
1772
            test_id,
1773
            0.0,
1774
            qod,
1775
        )
1776
1777
    def add_scan_error(
1778
        self, scan_id, host='', hostname='', name='', value='', port=''
1779
    ):
1780
        """ Adds an error result to scan_id scan. """
1781
        self.scan_collection.add_result(
1782
            scan_id, ResultType.ERROR, host, hostname, name, value, port
1783
        )
1784
1785
    def add_scan_host_detail(
1786
        self, scan_id, host='', hostname='', name='', value=''
1787
    ):
1788
        """ Adds a host detail result to scan_id scan. """
1789
        self.scan_collection.add_result(
1790
            scan_id, ResultType.HOST_DETAIL, host, hostname, name, value
1791
        )
1792
1793
    def add_scan_alarm(
1794
        self,
1795
        scan_id,
1796
        host='',
1797
        hostname='',
1798
        name='',
1799
        value='',
1800
        port='',
1801
        test_id='',
1802
        severity='',
1803
        qod='',
1804
    ):
1805
        """ Adds an alarm result to scan_id scan. """
1806
        self.scan_collection.add_result(
1807
            scan_id,
1808
            ResultType.ALARM,
1809
            host,
1810
            hostname,
1811
            name,
1812
            value,
1813
            port,
1814
            test_id,
1815
            severity,
1816
            qod,
1817
        )
1818