Completed
Push — master ( 9305a2...0d408b )
by Björn
19s queued 12s
created

ospd.ospd.OSPDaemon._get_scan_progress_raw()   A

Complexity

Conditions 1

Size

Total Lines 27
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 20
nop 2
dl 0
loc 27
rs 9.4
c 0
b 0
f 0
1
# Copyright (C) 2014-2021 Greenbone Networks GmbH
2
#
3
# SPDX-License-Identifier: AGPL-3.0-or-later
4
#
5
# This program is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Affero General Public License as
7
# published by the Free Software Foundation, either version 3 of the
8
# 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 Affero General Public License for more details.
14
#
15
# You should have received a copy of the GNU Affero General Public License
16
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18
# pylint: disable=too-many-lines
19
20
""" OSP Daemon core class.
21
"""
22
23
import logging
24
import socket
25
import ssl
26
import multiprocessing
27
import time
28
import os
29
30
from pprint import pformat
31
from typing import (
32
    List,
33
    Any,
34
    Iterator,
35
    Dict,
36
    Optional,
37
    Iterable,
38
    Tuple,
39
    Union,
40
)
41
from xml.etree.ElementTree import Element, SubElement
42
43
import defusedxml.ElementTree as secET
44
45
import psutil
46
47
from ospd import __version__
48
from ospd.command import get_commands
49
from ospd.errors import OspdCommandError
50
from ospd.misc import ResultType, create_process
51
from ospd.network import resolve_hostname, target_str_to_list
52
from ospd.protocol import RequestParser
53
from ospd.scan import ScanCollection, ScanStatus, ScanProgress
54
from ospd.server import BaseServer, Stream
55
from ospd.vtfilter import VtsFilter
56
from ospd.vts import Vts
57
from ospd.xml import (
58
    elements_as_text,
59
    get_result_xml,
60
    get_progress_xml,
61
)
62
63
logger = logging.getLogger(__name__)
64
65
PROTOCOL_VERSION = __version__
66
67
SCHEDULER_CHECK_PERIOD = 10  # in seconds
68
69
BASE_SCANNER_PARAMS = {
70
    'debug_mode': {
71
        'type': 'boolean',
72
        'name': 'Debug Mode',
73
        'default': 0,
74
        'mandatory': 0,
75
        'description': 'Whether to get extra scan debug information.',
76
    },
77
    'dry_run': {
78
        'type': 'boolean',
79
        'name': 'Dry Run',
80
        'default': 0,
81
        'mandatory': 0,
82
        'description': 'Whether to dry run scan.',
83
    },
84
}  # type: Dict
85
86
87
def _terminate_process_group(process: multiprocessing.Process) -> None:
88
    os.killpg(os.getpgid(process.pid), 15)
89
90
91
class OSPDaemon:
92
93
    """Daemon class for OSP traffic handling.
94
95
    Every scanner wrapper should subclass it and make necessary additions and
96
    changes.
97
98
    * Add any needed parameters in __init__.
99
    * Implement check() method which verifies scanner availability and other
100
      environment related conditions.
101
    * Implement process_scan_params and exec_scan methods which are
102
      specific to handling the <start_scan> command, executing the wrapped
103
      scanner and storing the results.
104
    * Implement other methods that assert to False such as get_scanner_name,
105
      get_scanner_version.
106
    * Use Call set_command_attributes at init time to add scanner command
107
      specific options eg. the w3af profile for w3af wrapper.
108
    """
109
110
    def __init__(
111
        self,
112
        *,
113
        customvtfilter=None,
114
        storage=None,
115
        max_scans=0,
116
        min_free_mem_scan_queue=0,
117
        file_storage_dir='/var/run/ospd',
118
        max_queued_scans=0,
119
        **kwargs,
120
    ):  # pylint: disable=unused-argument
121
        """ Initializes the daemon's internal data. """
122
        self.scan_collection = ScanCollection(file_storage_dir)
123
        self.scan_processes = dict()
124
125
        self.daemon_info = dict()
126
        self.daemon_info['name'] = "OSPd"
127
        self.daemon_info['version'] = __version__
128
        self.daemon_info['description'] = "No description"
129
130
        self.scanner_info = dict()
131
        self.scanner_info['name'] = 'No name'
132
        self.scanner_info['version'] = 'No version'
133
        self.scanner_info['description'] = 'No description'
134
135
        self.server_version = None  # Set by the subclass.
136
137
        self.initialized = None  # Set after initialization finished
138
139
        self.max_scans = max_scans
140
        self.min_free_mem_scan_queue = min_free_mem_scan_queue
141
        self.max_queued_scans = max_queued_scans
142
143
        self.scaninfo_store_time = kwargs.get('scaninfo_store_time')
144
145
        self.protocol_version = PROTOCOL_VERSION
146
147
        self.commands = {}
148
149
        for command_class in get_commands():
150
            command = command_class(self)
151
            self.commands[command.get_name()] = command
152
153
        self.scanner_params = dict()
154
155
        for name, params in BASE_SCANNER_PARAMS.items():
156
            self.set_scanner_param(name, params)
157
158
        self.vts = Vts(storage)
159
        self.vts_version = None
160
161
        if customvtfilter:
162
            self.vts_filter = customvtfilter
163
        else:
164
            self.vts_filter = VtsFilter()
165
166
    def init(self, server: BaseServer) -> None:
167
        """Should be overridden by a subclass if the initialization is costly.
168
169
        Will be called after check.
170
        """
171
        self.scan_collection.init()
172
        server.start(self.handle_client_stream)
173
        self.initialized = True
174
175
    def set_command_attributes(self, name: str, attributes: Dict) -> None:
176
        """ Sets the xml attributes of a specified command. """
177
        if self.command_exists(name):
178
            command = self.commands.get(name)
179
            command.attributes = attributes
180
181
    def set_scanner_param(self, name: str, scanner_params: Dict) -> None:
182
        """ Set a scanner parameter. """
183
184
        assert name
185
        assert scanner_params
186
187
        self.scanner_params[name] = scanner_params
188
189
    def get_scanner_params(self) -> Dict:
190
        return self.scanner_params
191
192
    def add_vt(
193
        self,
194
        vt_id: str,
195
        name: str = None,
196
        vt_params: str = None,
197
        vt_refs: str = None,
198
        custom: str = None,
199
        vt_creation_time: str = None,
200
        vt_modification_time: str = None,
201
        vt_dependencies: str = None,
202
        summary: str = None,
203
        impact: str = None,
204
        affected: str = None,
205
        insight: str = None,
206
        solution: str = None,
207
        solution_t: str = None,
208
        solution_m: str = None,
209
        detection: str = None,
210
        qod_t: str = None,
211
        qod_v: str = None,
212
        severities: str = None,
213
    ) -> None:
214
        """Add a vulnerability test information.
215
216
        IMPORTANT: The VT's Data Manager will store the vts collection.
217
        If the collection is considerably big and it will be consultated
218
        intensible during a routine, consider to do a deepcopy(), since
219
        accessing the shared memory in the data manager is very expensive.
220
        At the end of the routine, the temporal copy must be set to None
221
        and deleted.
222
        """
223
        self.vts.add(
224
            vt_id,
225
            name=name,
226
            vt_params=vt_params,
227
            vt_refs=vt_refs,
228
            custom=custom,
229
            vt_creation_time=vt_creation_time,
230
            vt_modification_time=vt_modification_time,
231
            vt_dependencies=vt_dependencies,
232
            summary=summary,
233
            impact=impact,
234
            affected=affected,
235
            insight=insight,
236
            solution=solution,
237
            solution_t=solution_t,
238
            solution_m=solution_m,
239
            detection=detection,
240
            qod_t=qod_t,
241
            qod_v=qod_v,
242
            severities=severities,
243
        )
244
245
    def set_vts_version(self, vts_version: str) -> None:
246
        """Add into the vts dictionary an entry to identify the
247
        vts version.
248
249
        Parameters:
250
            vts_version (str): Identifies a unique vts version.
251
        """
252
        if not vts_version:
253
            raise OspdCommandError(
254
                'A vts_version parameter is required', 'set_vts_version'
255
            )
256
        self.vts_version = vts_version
257
258
    def get_vts_version(self) -> Optional[str]:
259
        """Return the vts version."""
260
        return self.vts_version
261
262
    def command_exists(self, name: str) -> bool:
263
        """ Checks if a commands exists. """
264
        return name in self.commands
265
266
    def get_scanner_name(self) -> str:
267
        """ Gives the wrapped scanner's name. """
268
        return self.scanner_info['name']
269
270
    def get_scanner_version(self) -> str:
271
        """ Gives the wrapped scanner's version. """
272
        return self.scanner_info['version']
273
274
    def get_scanner_description(self) -> str:
275
        """ Gives the wrapped scanner's description. """
276
        return self.scanner_info['description']
277
278
    def get_server_version(self) -> str:
279
        """ Gives the specific OSP server's version. """
280
        assert self.server_version
281
        return self.server_version
282
283
    def get_protocol_version(self) -> str:
284
        """ Gives the OSP's version. """
285
        return self.protocol_version
286
287
    def preprocess_scan_params(self, xml_params):
288
        """ Processes the scan parameters. """
289
        params = {}
290
291
        for param in xml_params:
292
            params[param.tag] = param.text or ''
293
294
        # Validate values.
295
        for key in params:
296
            param_type = self.get_scanner_param_type(key)
297
            if not param_type:
298
                continue
299
300
            if param_type in ['integer', 'boolean']:
301
                try:
302
                    params[key] = int(params[key])
303
                except ValueError:
304
                    raise OspdCommandError(
305
                        'Invalid %s value' % key, 'start_scan'
306
                    ) from None
307
308
            if param_type == 'boolean':
309
                if params[key] not in [0, 1]:
310
                    raise OspdCommandError(
311
                        'Invalid %s value' % key, 'start_scan'
312
                    )
313
            elif param_type == 'selection':
314
                selection = self.get_scanner_param_default(key).split('|')
315
                if params[key] not in selection:
316
                    raise OspdCommandError(
317
                        'Invalid %s value' % key, 'start_scan'
318
                    )
319
            if self.get_scanner_param_mandatory(key) and params[key] == '':
320
                raise OspdCommandError(
321
                    'Mandatory %s value is missing' % key, 'start_scan'
322
                )
323
324
        return params
325
326
    def process_scan_params(self, params: Dict) -> Dict:
327
        """This method is to be overridden by the child classes if necessary"""
328
        return params
329
330
    def stop_scan(self, scan_id: str) -> None:
331
        if (
332
            scan_id in self.scan_collection.ids_iterator()
333
            and self.get_scan_status(scan_id) == ScanStatus.QUEUED
334
        ):
335
            logger.info('Scan %s has been removed from the queue.', scan_id)
336
            self.scan_collection.remove_file_pickled_scan_info(scan_id)
337
            self.set_scan_status(scan_id, ScanStatus.STOPPED)
338
339
            return
340
341
        scan_process = self.scan_processes.get(scan_id)
342
        if not scan_process:
343
            raise OspdCommandError(
344
                'Scan not found {0}.'.format(scan_id), 'stop_scan'
345
            )
346
        if not scan_process.is_alive():
347
            raise OspdCommandError(
348
                'Scan already stopped or finished.', 'stop_scan'
349
            )
350
351
        self.set_scan_status(scan_id, ScanStatus.STOPPED)
352
353
        logger.info(
354
            '%s: Stopping Scan with the PID %s.', scan_id, scan_process.ident
355
        )
356
357
        self.stop_scan_cleanup(scan_id)
358
359
        try:
360
            scan_process.terminate()
361
        except AttributeError:
362
            logger.debug('%s: The scanner task stopped unexpectedly.', scan_id)
363
364
        try:
365
            logger.debug(
366
                '%s: Terminating process group after stopping.', scan_id
367
            )
368
            _terminate_process_group(scan_process)
369
        except ProcessLookupError:
370
            logger.info(
371
                '%s: Scan with the PID %s is already stopped.',
372
                scan_id,
373
                scan_process.pid,
374
            )
375
376
        if scan_process.ident != os.getpid():
377
            scan_process.join(0)
378
379
        logger.info('%s: Scan stopped.', scan_id)
380
381
    @staticmethod
382
    def stop_scan_cleanup(scan_id: str):
383
        """Should be implemented by subclass in case of a clean up before
384
        terminating is needed."""
385
386
    @staticmethod
387
    def target_is_finished(scan_id: str):
388
        """Should be implemented by subclass in case of a check before
389
        stopping is needed."""
390
391
    def exec_scan(self, scan_id: str):
392
        """ Asserts to False. Should be implemented by subclass. """
393
        raise NotImplementedError
394
395
    def finish_scan(self, scan_id: str) -> None:
396
        """ Sets a scan as finished. """
397
        self.scan_collection.set_progress(scan_id, ScanProgress.FINISHED.value)
398
        self.set_scan_status(scan_id, ScanStatus.FINISHED)
399
        logger.info("%s: Scan finished.", scan_id)
400
401
    def interrupt_scan(self, scan_id: str) -> None:
402
        """ Set scan status as interrupted. """
403
        self.set_scan_status(scan_id, ScanStatus.INTERRUPTED)
404
        logger.info("%s: Scan interrupted.", scan_id)
405
406
    def daemon_exit_cleanup(self) -> None:
407
        """ Perform a cleanup before exiting """
408
        self.scan_collection.clean_up_pickled_scan_info()
409
410
        # Stop scans which are not already stopped.
411
        for scan_id in self.scan_collection.ids_iterator():
412
            status = self.get_scan_status(scan_id)
413
            if (
414
                status != ScanStatus.STOPPED
415
                and status != ScanStatus.FINISHED
416
                and status != ScanStatus.INTERRUPTED
417
            ):
418
                logger.debug("%s: Stopping scan before daemon exit.", scan_id)
419
                self.stop_scan(scan_id)
420
421
        # Wait for scans to be in some stopped state.
422
        while True:
423
            all_stopped = True
424
            for scan_id in self.scan_collection.ids_iterator():
425
                status = self.get_scan_status(scan_id)
426
                if (
427
                    status != ScanStatus.STOPPED
428
                    and status != ScanStatus.FINISHED
429
                    and status != ScanStatus.INTERRUPTED
430
                ):
431
                    all_stopped = False
432
433
            if all_stopped:
434
                logger.debug(
435
                    "All scans stopped and daemon clean and ready to exit"
436
                )
437
                return
438
439
            logger.debug("Waiting for running scans before daemon exit. ")
440
            time.sleep(1)
441
442
    def get_daemon_name(self) -> str:
443
        """ Gives osp daemon's name. """
444
        return self.daemon_info['name']
445
446
    def get_daemon_version(self) -> str:
447
        """ Gives osp daemon's version. """
448
        return self.daemon_info['version']
449
450
    def get_scanner_param_type(self, param: str):
451
        """ Returns type of a scanner parameter. """
452
        assert isinstance(param, str)
453
        entry = self.scanner_params.get(param)
454
        if not entry:
455
            return None
456
        return entry.get('type')
457
458
    def get_scanner_param_mandatory(self, param: str):
459
        """ Returns if a scanner parameter is mandatory. """
460
        assert isinstance(param, str)
461
        entry = self.scanner_params.get(param)
462
        if not entry:
463
            return False
464
        return entry.get('mandatory')
465
466
    def get_scanner_param_default(self, param: str):
467
        """ Returns default value of a scanner parameter. """
468
        assert isinstance(param, str)
469
        entry = self.scanner_params.get(param)
470
        if not entry:
471
            return None
472
        return entry.get('default')
473
474
    def handle_client_stream(self, stream: Stream) -> None:
475
        """ Handles stream of data received from client. """
476
        data = b''
477
478
        request_parser = RequestParser()
479
480
        while True:
481
            try:
482
                buf = stream.read()
483
                if not buf:
484
                    break
485
486
                data += buf
487
488
                if request_parser.has_ended(buf):
489
                    break
490
            except (AttributeError, ValueError) as message:
491
                logger.error(message)
492
                return
493
            except (ssl.SSLError) as exception:
494
                logger.debug('Error: %s', exception)
495
                break
496
            except (socket.timeout) as exception:
497
                logger.debug('Request timeout: %s', exception)
498
                break
499
500
        if len(data) <= 0:
501
            logger.debug("Empty client stream")
502
            return
503
504
        response = None
505
        try:
506
            self.handle_command(data, stream)
507
        except OspdCommandError as exception:
508
            response = exception.as_xml()
509
            logger.debug('Command error: %s', exception.message)
510
        except Exception:  # pylint: disable=broad-except
511
            logger.exception('While handling client command:')
512
            exception = OspdCommandError('Fatal error', 'error')
513
            response = exception.as_xml()
514
515
        if response:
516
            stream.write(response)
517
518
        stream.close()
519
520
    def process_finished_hosts(self, scan_id: str) -> None:
521
        """ Process the finished hosts before launching the scans."""
522
523
        finished_hosts = self.scan_collection.get_finished_hosts(scan_id)
524
        if not finished_hosts:
525
            return
526
527
        exc_finished_hosts_list = target_str_to_list(finished_hosts)
528
        self.scan_collection.set_host_finished(scan_id, exc_finished_hosts_list)
529
530
    def start_scan(self, scan_id: str) -> None:
531
        """ Starts the scan with scan_id. """
532
        os.setsid()
533
534
        self.process_finished_hosts(scan_id)
535
536
        try:
537
            self.set_scan_status(scan_id, ScanStatus.RUNNING)
538
            self.exec_scan(scan_id)
539
        except Exception as e:  # pylint: disable=broad-except
540
            self.add_scan_error(
541
                scan_id,
542
                name='',
543
                host=self.get_scan_host(scan_id),
544
                value='Host process failure (%s).' % e,
545
            )
546
            logger.exception('%s: Exception %s while scanning', scan_id, e)
547
        else:
548
            logger.info("%s: Host scan finished.", scan_id)
549
550
        status = self.get_scan_status(scan_id)
551
        is_stopped = status == ScanStatus.STOPPED
552
        self.set_scan_progress(scan_id)
553
        progress = self.get_scan_progress(scan_id)
554
        if not is_stopped and progress == ScanProgress.FINISHED:
555
            self.finish_scan(scan_id)
556
        elif not is_stopped:
557
            logger.info(
558
                "%s: Host scan finished. Progress: %d, Status: %s",
559
                scan_id,
560
                progress,
561
                status.name,
562
            )
563
            self.interrupt_scan(scan_id)
564
565
        # For debug purposes
566
        self._get_scan_progress_raw(scan_id)
567
568
    def dry_run_scan(self, scan_id: str, target: Dict) -> None:
569
        """ Dry runs a scan. """
570
571
        os.setsid()
572
573
        host = resolve_hostname(target.get('hosts'))
574
        if host is None:
575
            logger.info("Couldn't resolve %s.", self.get_scan_host(scan_id))
576
577
        port = self.get_scan_ports(scan_id)
578
579
        logger.info("%s:%s: Dry run mode.", host, port)
580
581
        self.add_scan_log(scan_id, name='', host=host, value='Dry run result')
582
583
        self.finish_scan(scan_id)
584
585
    def handle_timeout(self, scan_id: str, host: str) -> None:
586
        """ Handles scanner reaching timeout error. """
587
        self.add_scan_error(
588
            scan_id,
589
            host=host,
590
            name="Timeout",
591
            value="{0} exec timeout.".format(self.get_scanner_name()),
592
        )
593
594
    def sort_host_finished(
595
        self,
596
        scan_id: str,
597
        finished_hosts: Union[List[str], str],
598
    ) -> None:
599
        """Check if the finished host in the list was alive or dead
600
        and update the corresponding alive_count or dead_count."""
601
        if isinstance(finished_hosts, str):
602
            finished_hosts = [finished_hosts]
603
604
        alive_hosts = []
605
        dead_hosts = []
606
607
        current_hosts = self.scan_collection.get_current_target_progress(
608
            scan_id
609
        )
610
        for finished_host in finished_hosts:
611
            progress = current_hosts.get(finished_host)
612
            if progress == ScanProgress.FINISHED:
613
                alive_hosts.append(finished_host)
614
            if progress == ScanProgress.DEAD_HOST:
615
                dead_hosts.append(finished_host)
616
617
        self.scan_collection.set_host_dead(scan_id, dead_hosts)
618
619
        self.scan_collection.set_host_finished(scan_id, alive_hosts)
620
621
        self.scan_collection.remove_hosts_from_target_progress(
622
            scan_id, finished_hosts
623
        )
624
625
    def set_scan_progress(self, scan_id: str):
626
        """Calculate the target progress with the current host states
627
        and stores in the scan table."""
628
        scan_progress = self.scan_collection.calculate_target_progress(scan_id)
629
        self.scan_collection.set_progress(scan_id, scan_progress)
630
631
    def set_scan_progress_batch(
632
        self, scan_id: str, host_progress: Dict[str, int]
633
    ):
634
        self.scan_collection.set_host_progress(scan_id, host_progress)
635
        self.set_scan_progress(scan_id)
636
637
    def set_scan_host_progress(
638
        self,
639
        scan_id: str,
640
        host: str = None,
641
        progress: int = None,
642
    ) -> None:
643
        """Sets host's progress which is part of target.
644
        Each time a host progress is updated, the scan progress
645
        is updated too.
646
        """
647
        if host is None or progress is None:
648
            return
649
650
        if not isinstance(progress, int):
651
            try:
652
                progress = int(progress)
653
            except (TypeError, ValueError):
654
                return
655
656
        host_progress = {host: progress}
657
        self.set_scan_progress_batch(scan_id, host_progress)
658
659
    def set_scan_status(self, scan_id: str, status: ScanStatus) -> None:
660
        """ Set the scan's status."""
661
        logger.debug('%s: Set scan status %s,', scan_id, status.name)
662
        self.scan_collection.set_status(scan_id, status)
663
664
    def get_scan_status(self, scan_id: str) -> ScanStatus:
665
        """ Get scan_id scans's status."""
666
        status = self.scan_collection.get_status(scan_id)
667
        logger.debug('%s: Current scan status: %s,', scan_id, status.name)
668
        return status
669
670
    def scan_exists(self, scan_id: str) -> bool:
671
        """Checks if a scan with ID scan_id is in collection.
672
673
        Returns:
674
            1 if scan exists, 0 otherwise.
675
        """
676
        return self.scan_collection.id_exists(scan_id)
677
678
    def get_help_text(self) -> str:
679
        """ Returns the help output in plain text format."""
680
681
        txt = ''
682
        for name, info in self.commands.items():
683
            description = info.get_description()
684
            attributes = info.get_attributes()
685
            elements = info.get_elements()
686
687
            command_txt = "\t{0: <22} {1}\n".format(name, description)
688
689
            if attributes:
690
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
691
692
                for attrname, attrdesc in attributes.items():
693
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
694
                    command_txt = ''.join([command_txt, attr_txt])
695
696
            if elements:
697
                command_txt = ''.join(
698
                    [
699
                        command_txt,
700
                        "\t Elements:\n",
701
                        elements_as_text(elements),
702
                    ]
703
                )
704
705
            txt += command_txt
706
707
        return txt
708
709
    def delete_scan(self, scan_id: str) -> int:
710
        """Deletes scan_id scan from collection.
711
712
        Returns:
713
            1 if scan deleted, 0 otherwise.
714
        """
715
        if self.get_scan_status(scan_id) == ScanStatus.RUNNING:
716
            return 0
717
718
        # Don't delete the scan until the process stops
719
        exitcode = None
720
        try:
721
            self.scan_processes[scan_id].join()
722
            exitcode = self.scan_processes[scan_id].exitcode
723
        except KeyError:
724
            logger.debug('Scan process for %s never started,', scan_id)
725
726
        if exitcode or exitcode == 0:
727
            del self.scan_processes[scan_id]
728
729
        return self.scan_collection.delete_scan(scan_id)
730
731
    def get_scan_results_xml(
732
        self, scan_id: str, pop_res: bool, max_res: Optional[int]
733
    ):
734
        """Gets scan_id scan's results in XML format.
735
736
        Returns:
737
            String of scan results in xml.
738
        """
739
        results = Element('results')
740
        for result in self.scan_collection.results_iterator(
741
            scan_id, pop_res, max_res
742
        ):
743
            results.append(get_result_xml(result))
744
745
        logger.debug('Returning %d results', len(results))
746
        return results
747
748
    def _get_scan_progress_raw(self, scan_id: str) -> Dict:
749
        """Returns a dictionary with scan_id scan's progress information."""
750
        current_progress = dict()
751
752
        current_progress[
753
            'current_hosts'
754
        ] = self.scan_collection.get_current_target_progress(scan_id)
755
        current_progress['overall'] = self.get_scan_progress(scan_id)
756
        current_progress['count_alive'] = self.scan_collection.get_count_alive(
757
            scan_id
758
        )
759
        current_progress['count_dead'] = self.scan_collection.get_count_dead(
760
            scan_id
761
        )
762
        current_progress[
763
            'count_excluded'
764
        ] = self.scan_collection.simplify_exclude_host_count(scan_id)
765
        current_progress['count_total'] = self.scan_collection.get_count_total(
766
            scan_id
767
        )
768
769
        logging.debug(
770
            "%s: Current progress: \n%s",
771
            scan_id,
772
            pformat(current_progress),
773
        )
774
        return current_progress
775
776
    def _get_scan_progress_xml(self, scan_id: str):
777
        """Gets scan_id scan's progress in XML format.
778
779
        Returns:
780
            String of scan progress in xml.
781
        """
782
        current_progress = self._get_scan_progress_raw(scan_id)
783
        return get_progress_xml(current_progress)
784
785
    def get_scan_xml(
786
        self,
787
        scan_id: str,
788
        detailed: bool = True,
789
        pop_res: bool = False,
790
        max_res: int = 0,
791
        progress: bool = False,
792
    ):
793
        """Gets scan in XML format.
794
795
        Returns:
796
            String of scan in XML format.
797
        """
798
        if not scan_id:
799
            return Element('scan')
800
801
        if self.get_scan_status(scan_id) == ScanStatus.QUEUED:
802
            target = ''
803
            scan_progress = 0
804
            status = self.get_scan_status(scan_id)
805
            start_time = 0
806
            end_time = 0
807
            response = Element('scan')
808
            detailed = False
809
            progress = False
810
            response.append(Element('results'))
811
        else:
812
            target = self.get_scan_host(scan_id)
813
            scan_progress = self.get_scan_progress(scan_id)
814
            status = self.get_scan_status(scan_id)
815
            start_time = self.get_scan_start_time(scan_id)
816
            end_time = self.get_scan_end_time(scan_id)
817
            response = Element('scan')
818
819
        for name, value in [
820
            ('id', scan_id),
821
            ('target', target),
822
            ('progress', scan_progress),
823
            ('status', status.name.lower()),
824
            ('start_time', start_time),
825
            ('end_time', end_time),
826
        ]:
827
            response.set(name, str(value))
828
        if detailed:
829
            response.append(
830
                self.get_scan_results_xml(scan_id, pop_res, max_res)
831
            )
832
        if progress:
833
            response.append(self._get_scan_progress_xml(scan_id))
834
835
        return response
836
837
    @staticmethod
838
    def get_custom_vt_as_xml_str(  # pylint: disable=unused-argument
839
        vt_id: str, custom: Dict
840
    ) -> str:
841
        """Create a string representation of the XML object from the
842
        custom data object.
843
        This needs to be implemented by each ospd wrapper, in case
844
        custom elements for VTs are used.
845
846
        The custom XML object which is returned will be embedded
847
        into a <custom></custom> element.
848
849
        Returns:
850
            XML object as string for custom data.
851
        """
852
        return ''
853
854
    @staticmethod
855
    def get_params_vt_as_xml_str(  # pylint: disable=unused-argument
856
        vt_id: str, vt_params
857
    ) -> str:
858
        """Create a string representation of the XML object from the
859
        vt_params data object.
860
        This needs to be implemented by each ospd wrapper, in case
861
        vt_params elements for VTs are used.
862
863
        The params XML object which is returned will be embedded
864
        into a <params></params> element.
865
866
        Returns:
867
            XML object as string for vt parameters data.
868
        """
869
        return ''
870
871
    @staticmethod
872
    def get_refs_vt_as_xml_str(  # pylint: disable=unused-argument
873
        vt_id: str, vt_refs
874
    ) -> str:
875
        """Create a string representation of the XML object from the
876
        refs data object.
877
        This needs to be implemented by each ospd wrapper, in case
878
        refs elements for VTs are used.
879
880
        The refs XML object which is returned will be embedded
881
        into a <refs></refs> element.
882
883
        Returns:
884
            XML object as string for vt references data.
885
        """
886
        return ''
887
888
    @staticmethod
889
    def get_dependencies_vt_as_xml_str(  # pylint: disable=unused-argument
890
        vt_id: str, vt_dependencies
891
    ) -> str:
892
        """Create a string representation of the XML object from the
893
        vt_dependencies data object.
894
        This needs to be implemented by each ospd wrapper, in case
895
        vt_dependencies elements for VTs are used.
896
897
        The vt_dependencies XML object which is returned will be embedded
898
        into a <dependencies></dependencies> element.
899
900
        Returns:
901
            XML object as string for vt dependencies data.
902
        """
903
        return ''
904
905
    @staticmethod
906
    def get_creation_time_vt_as_xml_str(  # pylint: disable=unused-argument
907
        vt_id: str, vt_creation_time
908
    ) -> str:
909
        """Create a string representation of the XML object from the
910
        vt_creation_time data object.
911
        This needs to be implemented by each ospd wrapper, in case
912
        vt_creation_time elements for VTs are used.
913
914
        The vt_creation_time XML object which is returned will be embedded
915
        into a <vt_creation_time></vt_creation_time> element.
916
917
        Returns:
918
            XML object as string for vt creation time data.
919
        """
920
        return ''
921
922
    @staticmethod
923
    def get_modification_time_vt_as_xml_str(  # pylint: disable=unused-argument
924
        vt_id: str, vt_modification_time
925
    ) -> str:
926
        """Create a string representation of the XML object from the
927
        vt_modification_time data object.
928
        This needs to be implemented by each ospd wrapper, in case
929
        vt_modification_time elements for VTs are used.
930
931
        The vt_modification_time XML object which is returned will be embedded
932
        into a <vt_modification_time></vt_modification_time> element.
933
934
        Returns:
935
            XML object as string for vt references data.
936
        """
937
        return ''
938
939
    @staticmethod
940
    def get_summary_vt_as_xml_str(  # pylint: disable=unused-argument
941
        vt_id: str, summary
942
    ) -> str:
943
        """Create a string representation of the XML object from the
944
        summary data object.
945
        This needs to be implemented by each ospd wrapper, in case
946
        summary elements for VTs are used.
947
948
        The summary XML object which is returned will be embedded
949
        into a <summary></summary> element.
950
951
        Returns:
952
            XML object as string for summary data.
953
        """
954
        return ''
955
956
    @staticmethod
957
    def get_impact_vt_as_xml_str(  # pylint: disable=unused-argument
958
        vt_id: str, impact
959
    ) -> str:
960
        """Create a string representation of the XML object from the
961
        impact data object.
962
        This needs to be implemented by each ospd wrapper, in case
963
        impact elements for VTs are used.
964
965
        The impact XML object which is returned will be embedded
966
        into a <impact></impact> element.
967
968
        Returns:
969
            XML object as string for impact data.
970
        """
971
        return ''
972
973
    @staticmethod
974
    def get_affected_vt_as_xml_str(  # pylint: disable=unused-argument
975
        vt_id: str, affected
976
    ) -> str:
977
        """Create a string representation of the XML object from the
978
        affected data object.
979
        This needs to be implemented by each ospd wrapper, in case
980
        affected elements for VTs are used.
981
982
        The affected XML object which is returned will be embedded
983
        into a <affected></affected> element.
984
985
        Returns:
986
            XML object as string for affected data.
987
        """
988
        return ''
989
990
    @staticmethod
991
    def get_insight_vt_as_xml_str(  # pylint: disable=unused-argument
992
        vt_id: str, insight
993
    ) -> str:
994
        """Create a string representation of the XML object from the
995
        insight data object.
996
        This needs to be implemented by each ospd wrapper, in case
997
        insight elements for VTs are used.
998
999
        The insight XML object which is returned will be embedded
1000
        into a <insight></insight> element.
1001
1002
        Returns:
1003
            XML object as string for insight data.
1004
        """
1005
        return ''
1006
1007
    @staticmethod
1008
    def get_solution_vt_as_xml_str(  # pylint: disable=unused-argument
1009
        vt_id: str, solution, solution_type=None, solution_method=None
1010
    ) -> str:
1011
        """Create a string representation of the XML object from the
1012
        solution data object.
1013
        This needs to be implemented by each ospd wrapper, in case
1014
        solution elements for VTs are used.
1015
1016
        The solution XML object which is returned will be embedded
1017
        into a <solution></solution> element.
1018
1019
        Returns:
1020
            XML object as string for solution data.
1021
        """
1022
        return ''
1023
1024
    @staticmethod
1025
    def get_detection_vt_as_xml_str(  # pylint: disable=unused-argument
1026
        vt_id: str, detection=None, qod_type=None, qod=None
1027
    ) -> str:
1028
        """Create a string representation of the XML object from the
1029
        detection data object.
1030
        This needs to be implemented by each ospd wrapper, in case
1031
        detection elements for VTs are used.
1032
1033
        The detection XML object which is returned is an element with
1034
        tag <detection></detection> element
1035
1036
        Returns:
1037
            XML object as string for detection data.
1038
        """
1039
        return ''
1040
1041
    @staticmethod
1042
    def get_severities_vt_as_xml_str(  # pylint: disable=unused-argument
1043
        vt_id: str, severities
1044
    ) -> str:
1045
        """Create a string representation of the XML object from the
1046
        severities data object.
1047
        This needs to be implemented by each ospd wrapper, in case
1048
        severities elements for VTs are used.
1049
1050
        The severities XML objects which are returned will be embedded
1051
        into a <severities></severities> element.
1052
1053
        Returns:
1054
            XML object as string for severities data.
1055
        """
1056
        return ''
1057
1058
    def get_vt_iterator(  # pylint: disable=unused-argument
1059
        self, vt_selection: List[str] = None, details: bool = True
1060
    ) -> Iterator[Tuple[str, Dict]]:
1061
        """Return iterator object for getting elements
1062
        from the VTs dictionary."""
1063
        return self.vts.items()
1064
1065
    def get_vt_xml(self, single_vt: Tuple[str, Dict]) -> Element:
1066
        """Gets a single vulnerability test information in XML format.
1067
1068
        Returns:
1069
            String of single vulnerability test information in XML format.
1070
        """
1071
        if not single_vt or single_vt[1] is None:
1072
            return Element('vt')
1073
1074
        vt_id, vt = single_vt
1075
1076
        name = vt.get('name')
1077
        vt_xml = Element('vt')
1078
        vt_xml.set('id', vt_id)
1079
1080
        for name, value in [('name', name)]:
1081
            elem = SubElement(vt_xml, name)
1082
            elem.text = str(value)
1083
1084
        if vt.get('vt_params'):
1085
            params_xml_str = self.get_params_vt_as_xml_str(
1086
                vt_id, vt.get('vt_params')
1087
            )
1088
            vt_xml.append(secET.fromstring(params_xml_str))
1089
1090
        if vt.get('vt_refs'):
1091
            refs_xml_str = self.get_refs_vt_as_xml_str(vt_id, vt.get('vt_refs'))
1092
            vt_xml.append(secET.fromstring(refs_xml_str))
1093
1094
        if vt.get('vt_dependencies'):
1095
            dependencies = self.get_dependencies_vt_as_xml_str(
1096
                vt_id, vt.get('vt_dependencies')
1097
            )
1098
            vt_xml.append(secET.fromstring(dependencies))
1099
1100
        if vt.get('creation_time'):
1101
            vt_ctime = self.get_creation_time_vt_as_xml_str(
1102
                vt_id, vt.get('creation_time')
1103
            )
1104
            vt_xml.append(secET.fromstring(vt_ctime))
1105
1106
        if vt.get('modification_time'):
1107
            vt_mtime = self.get_modification_time_vt_as_xml_str(
1108
                vt_id, vt.get('modification_time')
1109
            )
1110
            vt_xml.append(secET.fromstring(vt_mtime))
1111
1112
        if vt.get('summary'):
1113
            summary_xml_str = self.get_summary_vt_as_xml_str(
1114
                vt_id, vt.get('summary')
1115
            )
1116
            vt_xml.append(secET.fromstring(summary_xml_str))
1117
1118
        if vt.get('impact'):
1119
            impact_xml_str = self.get_impact_vt_as_xml_str(
1120
                vt_id, vt.get('impact')
1121
            )
1122
            vt_xml.append(secET.fromstring(impact_xml_str))
1123
1124
        if vt.get('affected'):
1125
            affected_xml_str = self.get_affected_vt_as_xml_str(
1126
                vt_id, vt.get('affected')
1127
            )
1128
            vt_xml.append(secET.fromstring(affected_xml_str))
1129
1130
        if vt.get('insight'):
1131
            insight_xml_str = self.get_insight_vt_as_xml_str(
1132
                vt_id, vt.get('insight')
1133
            )
1134
            vt_xml.append(secET.fromstring(insight_xml_str))
1135
1136
        if vt.get('solution'):
1137
            solution_xml_str = self.get_solution_vt_as_xml_str(
1138
                vt_id,
1139
                vt.get('solution'),
1140
                vt.get('solution_type'),
1141
                vt.get('solution_method'),
1142
            )
1143
            vt_xml.append(secET.fromstring(solution_xml_str))
1144
1145
        if vt.get('detection') or vt.get('qod_type') or vt.get('qod'):
1146
            detection_xml_str = self.get_detection_vt_as_xml_str(
1147
                vt_id, vt.get('detection'), vt.get('qod_type'), vt.get('qod')
1148
            )
1149
            vt_xml.append(secET.fromstring(detection_xml_str))
1150
1151
        if vt.get('severities'):
1152
            severities_xml_str = self.get_severities_vt_as_xml_str(
1153
                vt_id, vt.get('severities')
1154
            )
1155
            vt_xml.append(secET.fromstring(severities_xml_str))
1156
1157
        if vt.get('custom'):
1158
            custom_xml_str = self.get_custom_vt_as_xml_str(
1159
                vt_id, vt.get('custom')
1160
            )
1161
            vt_xml.append(secET.fromstring(custom_xml_str))
1162
1163
        return vt_xml
1164
1165
    def get_vts_selection_list(
1166
        self, vt_id: str = None, filtered_vts: Dict = None
1167
    ) -> Iterable[str]:
1168
        """
1169
        Get list of VT's OID.
1170
        If vt_id is specified, the collection will contain only this vt, if
1171
        found.
1172
        If no vt_id is specified or filtered_vts is None (default), the
1173
        collection will contain all vts. Otherwise those vts passed
1174
        in filtered_vts or vt_id are returned. In case of both vt_id and
1175
        filtered_vts are given, filtered_vts has priority.
1176
1177
        Arguments:
1178
            vt_id (vt_id, optional): ID of the vt to get.
1179
            filtered_vts (list, optional): Filtered VTs collection.
1180
1181
        Returns:
1182
            List of selected VT's OID.
1183
        """
1184
        vts_xml = []
1185
1186
        # No match for the filter
1187
        if filtered_vts is not None and len(filtered_vts) == 0:
1188
            return vts_xml
1189
1190
        if filtered_vts:
1191
            vts_list = filtered_vts
1192
        elif vt_id:
1193
            vts_list = [vt_id]
1194
        else:
1195
            vts_list = self.vts.keys()
1196
1197
        return vts_list
1198
1199
    def handle_command(self, data: bytes, stream: Stream) -> None:
1200
        """Handles an osp command in a string."""
1201
        try:
1202
            tree = secET.fromstring(data)
1203
        except secET.ParseError as e:
1204
            logger.debug("Erroneous client input: %s", data)
1205
            raise OspdCommandError('Invalid data') from e
1206
1207
        command_name = tree.tag
1208
1209
        logger.debug('Handling %s command request.', command_name)
1210
1211
        command = self.commands.get(command_name, None)
1212
        if not command and command_name != "authenticate":
1213
            raise OspdCommandError('Bogus command name')
1214
1215
        if not self.initialized and command.must_be_initialized:
1216
            exception = OspdCommandError(
1217
                '%s is still starting' % self.daemon_info['name'], 'error'
1218
            )
1219
            response = exception.as_xml()
1220
            stream.write(response)
1221
            return
1222
1223
        response = command.handle_xml(tree)
1224
1225
        write_success = True
1226
        if isinstance(response, bytes):
1227
            write_success = stream.write(response)
1228
        else:
1229
            for data in response:
1230
                write_success = stream.write(data)
1231
                if not write_success:
1232
                    break
1233
1234
        scan_id = tree.get('scan_id')
1235
        if self.scan_exists(scan_id) and command_name == "get_scans":
1236
            if write_success:
1237
                logger.debug(
1238
                    '%s: Results sent successfully to the client. Cleaning '
1239
                    'temporary result list.',
1240
                    scan_id,
1241
                )
1242
                self.scan_collection.clean_temp_result_list(scan_id)
1243
            else:
1244
                logger.debug(
1245
                    '%s: Failed sending results to the client. Restoring '
1246
                    'result list into the cache.',
1247
                    scan_id,
1248
                )
1249
                self.scan_collection.restore_temp_result_list(scan_id)
1250
1251
    def check(self):
1252
        """ Asserts to False. Should be implemented by subclass. """
1253
        raise NotImplementedError
1254
1255
    def run(self) -> None:
1256
        """Starts the Daemon, handling commands until interrupted."""
1257
1258
        try:
1259
            while True:
1260
                time.sleep(SCHEDULER_CHECK_PERIOD)
1261
                self.scheduler()
1262
                self.clean_forgotten_scans()
1263
                self.start_queued_scans()
1264
                self.wait_for_children()
1265
        except KeyboardInterrupt:
1266
            logger.info("Received Ctrl-C shutting-down ...")
1267
1268
    def start_queued_scans(self) -> None:
1269
        """ Starts a queued scan if it is allowed """
1270
1271
        current_queued_scans = self.get_count_queued_scans()
1272
        if not current_queued_scans:
1273
            return
1274
1275
        if not self.initialized:
1276
            logger.info(
1277
                "Queued task can not be started because a feed "
1278
                "update is being performed."
1279
            )
1280
            return
1281
1282
        logger.info('Currently %d queued scans.', current_queued_scans)
1283
1284
        for scan_id in self.scan_collection.ids_iterator():
1285
            scan_allowed = (
1286
                self.is_new_scan_allowed() and self.is_enough_free_memory()
1287
            )
1288
            scan_is_queued = self.get_scan_status(scan_id) == ScanStatus.QUEUED
1289
1290
            if scan_is_queued and scan_allowed:
1291
                try:
1292
                    self.scan_collection.unpickle_scan_info(scan_id)
1293
                except OspdCommandError as e:
1294
                    logger.error("Start scan error %s", e)
1295
                    self.stop_scan(scan_id)
1296
                    continue
1297
1298
                scan_func = self.start_scan
1299
                scan_process = create_process(func=scan_func, args=(scan_id,))
1300
                self.scan_processes[scan_id] = scan_process
1301
                scan_process.start()
1302
                self.set_scan_status(scan_id, ScanStatus.INIT)
1303
1304
                current_queued_scans = current_queued_scans - 1
1305
                logger.info('Starting scan %s.', scan_id)
1306
            elif scan_is_queued and not scan_allowed:
1307
                return
1308
1309
    def is_new_scan_allowed(self) -> bool:
1310
        """Check if max_scans has been reached.
1311
1312
        Returns:
1313
            True if a new scan can be launch.
1314
        """
1315
        if (self.max_scans != 0) and (
1316
            len(self.scan_processes) >= self.max_scans
1317
        ):
1318
            logger.info(
1319
                'Not possible to run a new scan. Max scan limit set '
1320
                'to %d reached.',
1321
                self.max_scans,
1322
            )
1323
            return False
1324
1325
        return True
1326
1327
    def is_enough_free_memory(self) -> bool:
1328
        """Check if there is enough free memory in the system to run
1329
        a new scan. The necessary memory is a rough calculation and very
1330
        conservative.
1331
1332
        Returns:
1333
            True if there is enough memory for a new scan.
1334
        """
1335
        if not self.min_free_mem_scan_queue:
1336
            return True
1337
1338
        free_mem = psutil.virtual_memory().available / (1024 * 1024)
1339
1340
        if free_mem > self.min_free_mem_scan_queue:
1341
            return True
1342
1343
        logger.info(
1344
            'Not possible to run a new scan. Not enough free memory. '
1345
            'Only %d MB available but at least %d are required',
1346
            free_mem,
1347
            self.min_free_mem_scan_queue,
1348
        )
1349
1350
        return False
1351
1352
    def scheduler(self):
1353
        """Should be implemented by subclass in case of need
1354
        to run tasks periodically."""
1355
1356
    def wait_for_children(self):
1357
        """ Join the zombie process to releases resources."""
1358
        for scan_id in self.scan_processes:
1359
            self.scan_processes[scan_id].join(0)
1360
1361
    def create_scan(
1362
        self,
1363
        scan_id: str,
1364
        targets: Dict,
1365
        options: Optional[Dict],
1366
        vt_selection: Dict,
1367
    ) -> Optional[str]:
1368
        """Creates a new scan.
1369
1370
        Arguments:
1371
            target: Target to scan.
1372
            options: Miscellaneous scan options supplied via <scanner_params>
1373
                  XML element.
1374
1375
        Returns:
1376
            New scan's ID. None if the scan_id already exists.
1377
        """
1378
        status = None
1379
        scan_exists = self.scan_exists(scan_id)
1380
        if scan_id and scan_exists:
1381
            status = self.get_scan_status(scan_id)
1382
            logger.info(
1383
                "Scan %s exists with status %s.", scan_id, status.name.lower()
1384
            )
1385
            return
1386
1387
        return self.scan_collection.create_scan(
1388
            scan_id, targets, options, vt_selection
1389
        )
1390
1391
    def get_scan_options(self, scan_id: str) -> str:
1392
        """ Gives a scan's list of options. """
1393
        return self.scan_collection.get_options(scan_id)
1394
1395
    def set_scan_option(self, scan_id: str, name: str, value: Any) -> None:
1396
        """ Sets a scan's option to a provided value. """
1397
        return self.scan_collection.set_option(scan_id, name, value)
1398
1399
    def set_scan_total_hosts(self, scan_id: str, count_total: int) -> None:
1400
        """Sets a scan's total hosts. Allow the scanner to update
1401
        the total count of host to be scanned."""
1402
        self.scan_collection.update_count_total(scan_id, count_total)
1403
1404
    def clean_forgotten_scans(self) -> None:
1405
        """Check for old stopped or finished scans which have not been
1406
        deleted and delete them if the are older than the set value."""
1407
1408
        if not self.scaninfo_store_time:
1409
            return
1410
1411
        for scan_id in list(self.scan_collection.ids_iterator()):
1412
            end_time = int(self.get_scan_end_time(scan_id))
1413
            scan_status = self.get_scan_status(scan_id)
1414
1415
            if (
1416
                scan_status == ScanStatus.STOPPED
1417
                or scan_status == ScanStatus.FINISHED
1418
                or scan_status == ScanStatus.INTERRUPTED
1419
            ) and end_time:
1420
                stored_time = int(time.time()) - end_time
1421
                if stored_time > self.scaninfo_store_time * 3600:
1422
                    logger.debug(
1423
                        'Scan %s is older than %d hours and seems have been '
1424
                        'forgotten. Scan info will be deleted from the '
1425
                        'scan table',
1426
                        scan_id,
1427
                        self.scaninfo_store_time,
1428
                    )
1429
                    self.delete_scan(scan_id)
1430
1431
    def check_scan_process(self, scan_id: str) -> None:
1432
        """ Check the scan's process, and terminate the scan if not alive. """
1433
        status = self.get_scan_status(scan_id)
1434
        if status == ScanStatus.QUEUED:
1435
            return
1436
1437
        scan_process = self.scan_processes.get(scan_id)
1438
        progress = self.get_scan_progress(scan_id)
1439
1440
        if (
1441
            progress < ScanProgress.FINISHED
1442
            and scan_process
1443
            and not scan_process.is_alive()
1444
        ):
1445
            if not status == ScanStatus.STOPPED:
1446
                self.add_scan_error(
1447
                    scan_id, name="", host="", value="Scan process Failure"
1448
                )
1449
1450
                logger.info(
1451
                    "%s: Scan process is dead and its progress is %d",
1452
                    scan_id,
1453
                    progress,
1454
                )
1455
                self.interrupt_scan(scan_id)
1456
1457
        elif progress == ScanProgress.FINISHED:
1458
            scan_process.join(0)
1459
1460
        logger.debug(
1461
            "%s: Check scan process: \n\tProgress %d\n\t Status: %s",
1462
            scan_id,
1463
            progress,
1464
            status.name,
1465
        )
1466
1467
    def get_count_queued_scans(self) -> int:
1468
        """ Get the amount of scans with queued status """
1469
        count = 0
1470
        for scan_id in self.scan_collection.ids_iterator():
1471
            if self.get_scan_status(scan_id) == ScanStatus.QUEUED:
1472
                count += 1
1473
        return count
1474
1475
    def get_scan_progress(self, scan_id: str) -> int:
1476
        """ Gives a scan's current progress value. """
1477
        progress = self.scan_collection.get_progress(scan_id)
1478
        logger.debug('%s: Current scan progress: %s,', scan_id, progress)
1479
        return progress
1480
1481
    def get_scan_host(self, scan_id: str) -> str:
1482
        """ Gives a scan's target. """
1483
        return self.scan_collection.get_host_list(scan_id)
1484
1485
    def get_scan_ports(self, scan_id: str) -> str:
1486
        """ Gives a scan's ports list. """
1487
        return self.scan_collection.get_ports(scan_id)
1488
1489
    def get_scan_exclude_hosts(self, scan_id: str):
1490
        """Gives a scan's exclude host list. If a target is passed gives
1491
        the exclude host list for the given target."""
1492
        return self.scan_collection.get_exclude_hosts(scan_id)
1493
1494
    def get_scan_credentials(self, scan_id: str) -> Dict:
1495
        """Gives a scan's credential list. If a target is passed gives
1496
        the credential list for the given target."""
1497
        return self.scan_collection.get_credentials(scan_id)
1498
1499
    def get_scan_target_options(self, scan_id: str) -> Dict:
1500
        """Gives a scan's target option dict. If a target is passed gives
1501
        the credential list for the given target."""
1502
        return self.scan_collection.get_target_options(scan_id)
1503
1504
    def get_scan_vts(self, scan_id: str) -> Dict:
1505
        """ Gives a scan's vts. """
1506
        return self.scan_collection.get_vts(scan_id)
1507
1508
    def get_scan_start_time(self, scan_id: str) -> str:
1509
        """ Gives a scan's start time. """
1510
        return self.scan_collection.get_start_time(scan_id)
1511
1512
    def get_scan_end_time(self, scan_id: str) -> str:
1513
        """ Gives a scan's end time. """
1514
        return self.scan_collection.get_end_time(scan_id)
1515
1516
    def add_scan_log(
1517
        self,
1518
        scan_id: str,
1519
        host: str = '',
1520
        hostname: str = '',
1521
        name: str = '',
1522
        value: str = '',
1523
        port: str = '',
1524
        test_id: str = '',
1525
        qod: str = '',
1526
        uri: str = '',
1527
    ) -> None:
1528
        """ Adds a log result to scan_id scan. """
1529
1530
        self.scan_collection.add_result(
1531
            scan_id,
1532
            ResultType.LOG,
1533
            host,
1534
            hostname,
1535
            name,
1536
            value,
1537
            port,
1538
            test_id,
1539
            '0.0',
1540
            qod,
1541
            uri,
1542
        )
1543
1544
    def add_scan_error(
1545
        self,
1546
        scan_id: str,
1547
        host: str = '',
1548
        hostname: str = '',
1549
        name: str = '',
1550
        value: str = '',
1551
        port: str = '',
1552
        test_id='',
1553
        uri: str = '',
1554
    ) -> None:
1555
        """ Adds an error result to scan_id scan. """
1556
        self.scan_collection.add_result(
1557
            scan_id,
1558
            ResultType.ERROR,
1559
            host,
1560
            hostname,
1561
            name,
1562
            value,
1563
            port,
1564
            test_id,
1565
            uri,
1566
        )
1567
1568
    def add_scan_host_detail(
1569
        self,
1570
        scan_id: str,
1571
        host: str = '',
1572
        hostname: str = '',
1573
        name: str = '',
1574
        value: str = '',
1575
        uri: str = '',
1576
    ) -> None:
1577
        """ Adds a host detail result to scan_id scan. """
1578
        self.scan_collection.add_result(
1579
            scan_id, ResultType.HOST_DETAIL, host, hostname, name, value, uri
1580
        )
1581
1582
    def add_scan_alarm(
1583
        self,
1584
        scan_id: str,
1585
        host: str = '',
1586
        hostname: str = '',
1587
        name: str = '',
1588
        value: str = '',
1589
        port: str = '',
1590
        test_id: str = '',
1591
        severity: str = '',
1592
        qod: str = '',
1593
        uri: str = '',
1594
    ) -> None:
1595
        """ Adds an alarm result to scan_id scan. """
1596
        self.scan_collection.add_result(
1597
            scan_id,
1598
            ResultType.ALARM,
1599
            host,
1600
            hostname,
1601
            name,
1602
            value,
1603
            port,
1604
            test_id,
1605
            severity,
1606
            qod,
1607
            uri,
1608
        )
1609