Passed
Pull Request — master (#127)
by Juan José
02:12
created

ospd.ospd.OSPDaemon.stop_scan()   B

Complexity

Conditions 5

Size

Total Lines 25
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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