Completed
Push — master ( dd3ea0...651e0a )
by Björn
20s queued 12s
created

ospd.ospd.OSPDaemon.get_scan_host_progress()   A

Complexity

Conditions 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nop 3
dl 0
loc 10
rs 10
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
            elif progress == ScanProgress.DEAD_HOST:
615
                dead_hosts.append(finished_host)
616
            else:
617
                logger.debug(
618
                    'The host %s is considered dead or finished, but '
619
                    'its progress is still %d. This can lead to '
620
                    'interrupted scan.',
621
                    finished_host,
622
                    progress,
623
                )
624
625
        self.scan_collection.set_host_dead(scan_id, dead_hosts)
626
627
        self.scan_collection.set_host_finished(scan_id, alive_hosts)
628
629
        self.scan_collection.remove_hosts_from_target_progress(
630
            scan_id, finished_hosts
631
        )
632
633
    def set_scan_progress(self, scan_id: str):
634
        """Calculate the target progress with the current host states
635
        and stores in the scan table."""
636
        # Get current scan progress for debugging purposes
637
        logger.debug("Calculating scan progress with the following data:")
638
        self._get_scan_progress_raw(scan_id)
639
640
        scan_progress = self.scan_collection.calculate_target_progress(scan_id)
641
        self.scan_collection.set_progress(scan_id, scan_progress)
642
643
    def set_scan_progress_batch(
644
        self, scan_id: str, host_progress: Dict[str, int]
645
    ):
646
        self.scan_collection.set_host_progress(scan_id, host_progress)
647
        self.set_scan_progress(scan_id)
648
649
    def set_scan_host_progress(
650
        self,
651
        scan_id: str,
652
        host: str = None,
653
        progress: int = None,
654
    ) -> None:
655
        """Sets host's progress which is part of target.
656
        Each time a host progress is updated, the scan progress
657
        is updated too.
658
        """
659
        if host is None or progress is None:
660
            return
661
662
        if not isinstance(progress, int):
663
            try:
664
                progress = int(progress)
665
            except (TypeError, ValueError):
666
                return
667
668
        host_progress = {host: progress}
669
        self.set_scan_progress_batch(scan_id, host_progress)
670
671
    def get_scan_host_progress(
672
        self,
673
        scan_id: str,
674
        host: str = None,
675
    ) -> int:
676
        """ Get host's progress which is part of target."""
677
        current_progress = self.scan_collection.get_current_target_progress(
678
            scan_id
679
        )
680
        return current_progress.get(host)
681
682
    def set_scan_status(self, scan_id: str, status: ScanStatus) -> None:
683
        """ Set the scan's status."""
684
        logger.debug('%s: Set scan status %s,', scan_id, status.name)
685
        self.scan_collection.set_status(scan_id, status)
686
687
    def get_scan_status(self, scan_id: str) -> ScanStatus:
688
        """ Get scan_id scans's status."""
689
        status = self.scan_collection.get_status(scan_id)
690
        logger.debug('%s: Current scan status: %s,', scan_id, status.name)
691
        return status
692
693
    def scan_exists(self, scan_id: str) -> bool:
694
        """Checks if a scan with ID scan_id is in collection.
695
696
        Returns:
697
            1 if scan exists, 0 otherwise.
698
        """
699
        return self.scan_collection.id_exists(scan_id)
700
701
    def get_help_text(self) -> str:
702
        """ Returns the help output in plain text format."""
703
704
        txt = ''
705
        for name, info in self.commands.items():
706
            description = info.get_description()
707
            attributes = info.get_attributes()
708
            elements = info.get_elements()
709
710
            command_txt = "\t{0: <22} {1}\n".format(name, description)
711
712
            if attributes:
713
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
714
715
                for attrname, attrdesc in attributes.items():
716
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
717
                    command_txt = ''.join([command_txt, attr_txt])
718
719
            if elements:
720
                command_txt = ''.join(
721
                    [
722
                        command_txt,
723
                        "\t Elements:\n",
724
                        elements_as_text(elements),
725
                    ]
726
                )
727
728
            txt += command_txt
729
730
        return txt
731
732
    def delete_scan(self, scan_id: str) -> int:
733
        """Deletes scan_id scan from collection.
734
735
        Returns:
736
            1 if scan deleted, 0 otherwise.
737
        """
738
        if self.get_scan_status(scan_id) == ScanStatus.RUNNING:
739
            return 0
740
741
        # Don't delete the scan until the process stops
742
        exitcode = None
743
        try:
744
            self.scan_processes[scan_id].join()
745
            exitcode = self.scan_processes[scan_id].exitcode
746
        except KeyError:
747
            logger.debug('Scan process for %s never started,', scan_id)
748
749
        if exitcode or exitcode == 0:
750
            del self.scan_processes[scan_id]
751
752
        return self.scan_collection.delete_scan(scan_id)
753
754
    def get_scan_results_xml(
755
        self, scan_id: str, pop_res: bool, max_res: Optional[int]
756
    ):
757
        """Gets scan_id scan's results in XML format.
758
759
        Returns:
760
            String of scan results in xml.
761
        """
762
        results = Element('results')
763
        for result in self.scan_collection.results_iterator(
764
            scan_id, pop_res, max_res
765
        ):
766
            results.append(get_result_xml(result))
767
768
        logger.debug('Returning %d results', len(results))
769
        return results
770
771
    def _get_scan_progress_raw(self, scan_id: str) -> Dict:
772
        """Returns a dictionary with scan_id scan's progress information."""
773
        current_progress = dict()
774
775
        current_progress[
776
            'current_hosts'
777
        ] = self.scan_collection.get_current_target_progress(scan_id)
778
        current_progress['overall'] = self.get_scan_progress(scan_id)
779
        current_progress['count_alive'] = self.scan_collection.get_count_alive(
780
            scan_id
781
        )
782
        current_progress['count_dead'] = self.scan_collection.get_count_dead(
783
            scan_id
784
        )
785
        current_progress[
786
            'count_excluded'
787
        ] = self.scan_collection.simplify_exclude_host_count(scan_id)
788
        current_progress['count_total'] = self.scan_collection.get_count_total(
789
            scan_id
790
        )
791
792
        logging.debug(
793
            "%s: Current progress: \n%s",
794
            scan_id,
795
            pformat(current_progress),
796
        )
797
        return current_progress
798
799
    def _get_scan_progress_xml(self, scan_id: str):
800
        """Gets scan_id scan's progress in XML format.
801
802
        Returns:
803
            String of scan progress in xml.
804
        """
805
        current_progress = self._get_scan_progress_raw(scan_id)
806
        return get_progress_xml(current_progress)
807
808
    def get_scan_xml(
809
        self,
810
        scan_id: str,
811
        detailed: bool = True,
812
        pop_res: bool = False,
813
        max_res: int = 0,
814
        progress: bool = False,
815
    ):
816
        """Gets scan in XML format.
817
818
        Returns:
819
            String of scan in XML format.
820
        """
821
        if not scan_id:
822
            return Element('scan')
823
824
        if self.get_scan_status(scan_id) == ScanStatus.QUEUED:
825
            target = ''
826
            scan_progress = 0
827
            status = self.get_scan_status(scan_id)
828
            start_time = 0
829
            end_time = 0
830
            response = Element('scan')
831
            detailed = False
832
            progress = False
833
            response.append(Element('results'))
834
        else:
835
            target = self.get_scan_host(scan_id)
836
            scan_progress = self.get_scan_progress(scan_id)
837
            status = self.get_scan_status(scan_id)
838
            start_time = self.get_scan_start_time(scan_id)
839
            end_time = self.get_scan_end_time(scan_id)
840
            response = Element('scan')
841
842
        for name, value in [
843
            ('id', scan_id),
844
            ('target', target),
845
            ('progress', scan_progress),
846
            ('status', status.name.lower()),
847
            ('start_time', start_time),
848
            ('end_time', end_time),
849
        ]:
850
            response.set(name, str(value))
851
        if detailed:
852
            response.append(
853
                self.get_scan_results_xml(scan_id, pop_res, max_res)
854
            )
855
        if progress:
856
            response.append(self._get_scan_progress_xml(scan_id))
857
858
        return response
859
860
    @staticmethod
861
    def get_custom_vt_as_xml_str(  # pylint: disable=unused-argument
862
        vt_id: str, custom: Dict
863
    ) -> str:
864
        """Create a string representation of the XML object from the
865
        custom data object.
866
        This needs to be implemented by each ospd wrapper, in case
867
        custom elements for VTs are used.
868
869
        The custom XML object which is returned will be embedded
870
        into a <custom></custom> element.
871
872
        Returns:
873
            XML object as string for custom data.
874
        """
875
        return ''
876
877
    @staticmethod
878
    def get_params_vt_as_xml_str(  # pylint: disable=unused-argument
879
        vt_id: str, vt_params
880
    ) -> str:
881
        """Create a string representation of the XML object from the
882
        vt_params data object.
883
        This needs to be implemented by each ospd wrapper, in case
884
        vt_params elements for VTs are used.
885
886
        The params XML object which is returned will be embedded
887
        into a <params></params> element.
888
889
        Returns:
890
            XML object as string for vt parameters data.
891
        """
892
        return ''
893
894
    @staticmethod
895
    def get_refs_vt_as_xml_str(  # pylint: disable=unused-argument
896
        vt_id: str, vt_refs
897
    ) -> str:
898
        """Create a string representation of the XML object from the
899
        refs data object.
900
        This needs to be implemented by each ospd wrapper, in case
901
        refs elements for VTs are used.
902
903
        The refs XML object which is returned will be embedded
904
        into a <refs></refs> element.
905
906
        Returns:
907
            XML object as string for vt references data.
908
        """
909
        return ''
910
911
    @staticmethod
912
    def get_dependencies_vt_as_xml_str(  # pylint: disable=unused-argument
913
        vt_id: str, vt_dependencies
914
    ) -> str:
915
        """Create a string representation of the XML object from the
916
        vt_dependencies data object.
917
        This needs to be implemented by each ospd wrapper, in case
918
        vt_dependencies elements for VTs are used.
919
920
        The vt_dependencies XML object which is returned will be embedded
921
        into a <dependencies></dependencies> element.
922
923
        Returns:
924
            XML object as string for vt dependencies data.
925
        """
926
        return ''
927
928
    @staticmethod
929
    def get_creation_time_vt_as_xml_str(  # pylint: disable=unused-argument
930
        vt_id: str, vt_creation_time
931
    ) -> str:
932
        """Create a string representation of the XML object from the
933
        vt_creation_time data object.
934
        This needs to be implemented by each ospd wrapper, in case
935
        vt_creation_time elements for VTs are used.
936
937
        The vt_creation_time XML object which is returned will be embedded
938
        into a <vt_creation_time></vt_creation_time> element.
939
940
        Returns:
941
            XML object as string for vt creation time data.
942
        """
943
        return ''
944
945
    @staticmethod
946
    def get_modification_time_vt_as_xml_str(  # pylint: disable=unused-argument
947
        vt_id: str, vt_modification_time
948
    ) -> str:
949
        """Create a string representation of the XML object from the
950
        vt_modification_time data object.
951
        This needs to be implemented by each ospd wrapper, in case
952
        vt_modification_time elements for VTs are used.
953
954
        The vt_modification_time XML object which is returned will be embedded
955
        into a <vt_modification_time></vt_modification_time> element.
956
957
        Returns:
958
            XML object as string for vt references data.
959
        """
960
        return ''
961
962
    @staticmethod
963
    def get_summary_vt_as_xml_str(  # pylint: disable=unused-argument
964
        vt_id: str, summary
965
    ) -> str:
966
        """Create a string representation of the XML object from the
967
        summary data object.
968
        This needs to be implemented by each ospd wrapper, in case
969
        summary elements for VTs are used.
970
971
        The summary XML object which is returned will be embedded
972
        into a <summary></summary> element.
973
974
        Returns:
975
            XML object as string for summary data.
976
        """
977
        return ''
978
979
    @staticmethod
980
    def get_impact_vt_as_xml_str(  # pylint: disable=unused-argument
981
        vt_id: str, impact
982
    ) -> str:
983
        """Create a string representation of the XML object from the
984
        impact data object.
985
        This needs to be implemented by each ospd wrapper, in case
986
        impact elements for VTs are used.
987
988
        The impact XML object which is returned will be embedded
989
        into a <impact></impact> element.
990
991
        Returns:
992
            XML object as string for impact data.
993
        """
994
        return ''
995
996
    @staticmethod
997
    def get_affected_vt_as_xml_str(  # pylint: disable=unused-argument
998
        vt_id: str, affected
999
    ) -> str:
1000
        """Create a string representation of the XML object from the
1001
        affected data object.
1002
        This needs to be implemented by each ospd wrapper, in case
1003
        affected elements for VTs are used.
1004
1005
        The affected XML object which is returned will be embedded
1006
        into a <affected></affected> element.
1007
1008
        Returns:
1009
            XML object as string for affected data.
1010
        """
1011
        return ''
1012
1013
    @staticmethod
1014
    def get_insight_vt_as_xml_str(  # pylint: disable=unused-argument
1015
        vt_id: str, insight
1016
    ) -> str:
1017
        """Create a string representation of the XML object from the
1018
        insight data object.
1019
        This needs to be implemented by each ospd wrapper, in case
1020
        insight elements for VTs are used.
1021
1022
        The insight XML object which is returned will be embedded
1023
        into a <insight></insight> element.
1024
1025
        Returns:
1026
            XML object as string for insight data.
1027
        """
1028
        return ''
1029
1030
    @staticmethod
1031
    def get_solution_vt_as_xml_str(  # pylint: disable=unused-argument
1032
        vt_id: str, solution, solution_type=None, solution_method=None
1033
    ) -> str:
1034
        """Create a string representation of the XML object from the
1035
        solution data object.
1036
        This needs to be implemented by each ospd wrapper, in case
1037
        solution elements for VTs are used.
1038
1039
        The solution XML object which is returned will be embedded
1040
        into a <solution></solution> element.
1041
1042
        Returns:
1043
            XML object as string for solution data.
1044
        """
1045
        return ''
1046
1047
    @staticmethod
1048
    def get_detection_vt_as_xml_str(  # pylint: disable=unused-argument
1049
        vt_id: str, detection=None, qod_type=None, qod=None
1050
    ) -> str:
1051
        """Create a string representation of the XML object from the
1052
        detection data object.
1053
        This needs to be implemented by each ospd wrapper, in case
1054
        detection elements for VTs are used.
1055
1056
        The detection XML object which is returned is an element with
1057
        tag <detection></detection> element
1058
1059
        Returns:
1060
            XML object as string for detection data.
1061
        """
1062
        return ''
1063
1064
    @staticmethod
1065
    def get_severities_vt_as_xml_str(  # pylint: disable=unused-argument
1066
        vt_id: str, severities
1067
    ) -> str:
1068
        """Create a string representation of the XML object from the
1069
        severities data object.
1070
        This needs to be implemented by each ospd wrapper, in case
1071
        severities elements for VTs are used.
1072
1073
        The severities XML objects which are returned will be embedded
1074
        into a <severities></severities> element.
1075
1076
        Returns:
1077
            XML object as string for severities data.
1078
        """
1079
        return ''
1080
1081
    def get_vt_iterator(  # pylint: disable=unused-argument
1082
        self, vt_selection: List[str] = None, details: bool = True
1083
    ) -> Iterator[Tuple[str, Dict]]:
1084
        """Return iterator object for getting elements
1085
        from the VTs dictionary."""
1086
        return self.vts.items()
1087
1088
    def get_vt_xml(self, single_vt: Tuple[str, Dict]) -> Element:
1089
        """Gets a single vulnerability test information in XML format.
1090
1091
        Returns:
1092
            String of single vulnerability test information in XML format.
1093
        """
1094
        if not single_vt or single_vt[1] is None:
1095
            return Element('vt')
1096
1097
        vt_id, vt = single_vt
1098
1099
        name = vt.get('name')
1100
        vt_xml = Element('vt')
1101
        vt_xml.set('id', vt_id)
1102
1103
        for name, value in [('name', name)]:
1104
            elem = SubElement(vt_xml, name)
1105
            elem.text = str(value)
1106
1107
        if vt.get('vt_params'):
1108
            params_xml_str = self.get_params_vt_as_xml_str(
1109
                vt_id, vt.get('vt_params')
1110
            )
1111
            vt_xml.append(secET.fromstring(params_xml_str))
1112
1113
        if vt.get('vt_refs'):
1114
            refs_xml_str = self.get_refs_vt_as_xml_str(vt_id, vt.get('vt_refs'))
1115
            vt_xml.append(secET.fromstring(refs_xml_str))
1116
1117
        if vt.get('vt_dependencies'):
1118
            dependencies = self.get_dependencies_vt_as_xml_str(
1119
                vt_id, vt.get('vt_dependencies')
1120
            )
1121
            vt_xml.append(secET.fromstring(dependencies))
1122
1123
        if vt.get('creation_time'):
1124
            vt_ctime = self.get_creation_time_vt_as_xml_str(
1125
                vt_id, vt.get('creation_time')
1126
            )
1127
            vt_xml.append(secET.fromstring(vt_ctime))
1128
1129
        if vt.get('modification_time'):
1130
            vt_mtime = self.get_modification_time_vt_as_xml_str(
1131
                vt_id, vt.get('modification_time')
1132
            )
1133
            vt_xml.append(secET.fromstring(vt_mtime))
1134
1135
        if vt.get('summary'):
1136
            summary_xml_str = self.get_summary_vt_as_xml_str(
1137
                vt_id, vt.get('summary')
1138
            )
1139
            vt_xml.append(secET.fromstring(summary_xml_str))
1140
1141
        if vt.get('impact'):
1142
            impact_xml_str = self.get_impact_vt_as_xml_str(
1143
                vt_id, vt.get('impact')
1144
            )
1145
            vt_xml.append(secET.fromstring(impact_xml_str))
1146
1147
        if vt.get('affected'):
1148
            affected_xml_str = self.get_affected_vt_as_xml_str(
1149
                vt_id, vt.get('affected')
1150
            )
1151
            vt_xml.append(secET.fromstring(affected_xml_str))
1152
1153
        if vt.get('insight'):
1154
            insight_xml_str = self.get_insight_vt_as_xml_str(
1155
                vt_id, vt.get('insight')
1156
            )
1157
            vt_xml.append(secET.fromstring(insight_xml_str))
1158
1159
        if vt.get('solution'):
1160
            solution_xml_str = self.get_solution_vt_as_xml_str(
1161
                vt_id,
1162
                vt.get('solution'),
1163
                vt.get('solution_type'),
1164
                vt.get('solution_method'),
1165
            )
1166
            vt_xml.append(secET.fromstring(solution_xml_str))
1167
1168
        if vt.get('detection') or vt.get('qod_type') or vt.get('qod'):
1169
            detection_xml_str = self.get_detection_vt_as_xml_str(
1170
                vt_id, vt.get('detection'), vt.get('qod_type'), vt.get('qod')
1171
            )
1172
            vt_xml.append(secET.fromstring(detection_xml_str))
1173
1174
        if vt.get('severities'):
1175
            severities_xml_str = self.get_severities_vt_as_xml_str(
1176
                vt_id, vt.get('severities')
1177
            )
1178
            vt_xml.append(secET.fromstring(severities_xml_str))
1179
1180
        if vt.get('custom'):
1181
            custom_xml_str = self.get_custom_vt_as_xml_str(
1182
                vt_id, vt.get('custom')
1183
            )
1184
            vt_xml.append(secET.fromstring(custom_xml_str))
1185
1186
        return vt_xml
1187
1188
    def get_vts_selection_list(
1189
        self, vt_id: str = None, filtered_vts: Dict = None
1190
    ) -> Iterable[str]:
1191
        """
1192
        Get list of VT's OID.
1193
        If vt_id is specified, the collection will contain only this vt, if
1194
        found.
1195
        If no vt_id is specified or filtered_vts is None (default), the
1196
        collection will contain all vts. Otherwise those vts passed
1197
        in filtered_vts or vt_id are returned. In case of both vt_id and
1198
        filtered_vts are given, filtered_vts has priority.
1199
1200
        Arguments:
1201
            vt_id (vt_id, optional): ID of the vt to get.
1202
            filtered_vts (list, optional): Filtered VTs collection.
1203
1204
        Returns:
1205
            List of selected VT's OID.
1206
        """
1207
        vts_xml = []
1208
1209
        # No match for the filter
1210
        if filtered_vts is not None and len(filtered_vts) == 0:
1211
            return vts_xml
1212
1213
        if filtered_vts:
1214
            vts_list = filtered_vts
1215
        elif vt_id:
1216
            vts_list = [vt_id]
1217
        else:
1218
            vts_list = self.vts.keys()
1219
1220
        return vts_list
1221
1222
    def handle_command(self, data: bytes, stream: Stream) -> None:
1223
        """Handles an osp command in a string."""
1224
        try:
1225
            tree = secET.fromstring(data)
1226
        except secET.ParseError as e:
1227
            logger.debug("Erroneous client input: %s", data)
1228
            raise OspdCommandError('Invalid data') from e
1229
1230
        command_name = tree.tag
1231
1232
        logger.debug('Handling %s command request.', command_name)
1233
1234
        command = self.commands.get(command_name, None)
1235
        if not command and command_name != "authenticate":
1236
            raise OspdCommandError('Bogus command name')
1237
1238
        if not self.initialized and command.must_be_initialized:
1239
            exception = OspdCommandError(
1240
                '%s is still starting' % self.daemon_info['name'], 'error'
1241
            )
1242
            response = exception.as_xml()
1243
            stream.write(response)
1244
            return
1245
1246
        response = command.handle_xml(tree)
1247
1248
        write_success = True
1249
        if isinstance(response, bytes):
1250
            write_success = stream.write(response)
1251
        else:
1252
            for data in response:
1253
                write_success = stream.write(data)
1254
                if not write_success:
1255
                    break
1256
1257
        scan_id = tree.get('scan_id')
1258
        if self.scan_exists(scan_id) and command_name == "get_scans":
1259
            if write_success:
1260
                logger.debug(
1261
                    '%s: Results sent successfully to the client. Cleaning '
1262
                    'temporary result list.',
1263
                    scan_id,
1264
                )
1265
                self.scan_collection.clean_temp_result_list(scan_id)
1266
            else:
1267
                logger.debug(
1268
                    '%s: Failed sending results to the client. Restoring '
1269
                    'result list into the cache.',
1270
                    scan_id,
1271
                )
1272
                self.scan_collection.restore_temp_result_list(scan_id)
1273
1274
    def check(self):
1275
        """ Asserts to False. Should be implemented by subclass. """
1276
        raise NotImplementedError
1277
1278
    def run(self) -> None:
1279
        """Starts the Daemon, handling commands until interrupted."""
1280
1281
        try:
1282
            while True:
1283
                time.sleep(SCHEDULER_CHECK_PERIOD)
1284
                self.scheduler()
1285
                self.clean_forgotten_scans()
1286
                self.start_queued_scans()
1287
                self.wait_for_children()
1288
        except KeyboardInterrupt:
1289
            logger.info("Received Ctrl-C shutting-down ...")
1290
1291
    def start_queued_scans(self) -> None:
1292
        """ Starts a queued scan if it is allowed """
1293
1294
        current_queued_scans = self.get_count_queued_scans()
1295
        if not current_queued_scans:
1296
            return
1297
1298
        if not self.initialized:
1299
            logger.info(
1300
                "Queued task can not be started because a feed "
1301
                "update is being performed."
1302
            )
1303
            return
1304
1305
        logger.info('Currently %d queued scans.', current_queued_scans)
1306
1307
        for scan_id in self.scan_collection.ids_iterator():
1308
            scan_allowed = (
1309
                self.is_new_scan_allowed() and self.is_enough_free_memory()
1310
            )
1311
            scan_is_queued = self.get_scan_status(scan_id) == ScanStatus.QUEUED
1312
1313
            if scan_is_queued and scan_allowed:
1314
                try:
1315
                    self.scan_collection.unpickle_scan_info(scan_id)
1316
                except OspdCommandError as e:
1317
                    logger.error("Start scan error %s", e)
1318
                    self.stop_scan(scan_id)
1319
                    continue
1320
1321
                scan_func = self.start_scan
1322
                scan_process = create_process(func=scan_func, args=(scan_id,))
1323
                self.scan_processes[scan_id] = scan_process
1324
                scan_process.start()
1325
                self.set_scan_status(scan_id, ScanStatus.INIT)
1326
1327
                current_queued_scans = current_queued_scans - 1
1328
                logger.info('Starting scan %s.', scan_id)
1329
            elif scan_is_queued and not scan_allowed:
1330
                return
1331
1332
    def is_new_scan_allowed(self) -> bool:
1333
        """Check if max_scans has been reached.
1334
1335
        Returns:
1336
            True if a new scan can be launch.
1337
        """
1338
        if (self.max_scans != 0) and (
1339
            len(self.scan_processes) >= self.max_scans
1340
        ):
1341
            logger.info(
1342
                'Not possible to run a new scan. Max scan limit set '
1343
                'to %d reached.',
1344
                self.max_scans,
1345
            )
1346
            return False
1347
1348
        return True
1349
1350
    def is_enough_free_memory(self) -> bool:
1351
        """Check if there is enough free memory in the system to run
1352
        a new scan. The necessary memory is a rough calculation and very
1353
        conservative.
1354
1355
        Returns:
1356
            True if there is enough memory for a new scan.
1357
        """
1358
        if not self.min_free_mem_scan_queue:
1359
            return True
1360
1361
        free_mem = psutil.virtual_memory().available / (1024 * 1024)
1362
1363
        if free_mem > self.min_free_mem_scan_queue:
1364
            return True
1365
1366
        logger.info(
1367
            'Not possible to run a new scan. Not enough free memory. '
1368
            'Only %d MB available but at least %d are required',
1369
            free_mem,
1370
            self.min_free_mem_scan_queue,
1371
        )
1372
1373
        return False
1374
1375
    def scheduler(self):
1376
        """Should be implemented by subclass in case of need
1377
        to run tasks periodically."""
1378
1379
    def wait_for_children(self):
1380
        """ Join the zombie process to releases resources."""
1381
        for scan_id in self.scan_processes:
1382
            self.scan_processes[scan_id].join(0)
1383
1384
    def create_scan(
1385
        self,
1386
        scan_id: str,
1387
        targets: Dict,
1388
        options: Optional[Dict],
1389
        vt_selection: Dict,
1390
    ) -> Optional[str]:
1391
        """Creates a new scan.
1392
1393
        Arguments:
1394
            target: Target to scan.
1395
            options: Miscellaneous scan options supplied via <scanner_params>
1396
                  XML element.
1397
1398
        Returns:
1399
            New scan's ID. None if the scan_id already exists.
1400
        """
1401
        status = None
1402
        scan_exists = self.scan_exists(scan_id)
1403
        if scan_id and scan_exists:
1404
            status = self.get_scan_status(scan_id)
1405
            logger.info(
1406
                "Scan %s exists with status %s.", scan_id, status.name.lower()
1407
            )
1408
            return
1409
1410
        return self.scan_collection.create_scan(
1411
            scan_id, targets, options, vt_selection
1412
        )
1413
1414
    def get_scan_options(self, scan_id: str) -> str:
1415
        """ Gives a scan's list of options. """
1416
        return self.scan_collection.get_options(scan_id)
1417
1418
    def set_scan_option(self, scan_id: str, name: str, value: Any) -> None:
1419
        """ Sets a scan's option to a provided value. """
1420
        return self.scan_collection.set_option(scan_id, name, value)
1421
1422
    def set_scan_total_hosts(self, scan_id: str, count_total: int) -> None:
1423
        """Sets a scan's total hosts. Allow the scanner to update
1424
        the total count of host to be scanned."""
1425
        self.scan_collection.update_count_total(scan_id, count_total)
1426
1427
    def clean_forgotten_scans(self) -> None:
1428
        """Check for old stopped or finished scans which have not been
1429
        deleted and delete them if the are older than the set value."""
1430
1431
        if not self.scaninfo_store_time:
1432
            return
1433
1434
        for scan_id in list(self.scan_collection.ids_iterator()):
1435
            end_time = int(self.get_scan_end_time(scan_id))
1436
            scan_status = self.get_scan_status(scan_id)
1437
1438
            if (
1439
                scan_status == ScanStatus.STOPPED
1440
                or scan_status == ScanStatus.FINISHED
1441
                or scan_status == ScanStatus.INTERRUPTED
1442
            ) and end_time:
1443
                stored_time = int(time.time()) - end_time
1444
                if stored_time > self.scaninfo_store_time * 3600:
1445
                    logger.debug(
1446
                        'Scan %s is older than %d hours and seems have been '
1447
                        'forgotten. Scan info will be deleted from the '
1448
                        'scan table',
1449
                        scan_id,
1450
                        self.scaninfo_store_time,
1451
                    )
1452
                    self.delete_scan(scan_id)
1453
1454
    def check_scan_process(self, scan_id: str) -> None:
1455
        """ Check the scan's process, and terminate the scan if not alive. """
1456
        status = self.get_scan_status(scan_id)
1457
        if status == ScanStatus.QUEUED:
1458
            return
1459
1460
        scan_process = self.scan_processes.get(scan_id)
1461
        progress = self.get_scan_progress(scan_id)
1462
1463
        if (
1464
            progress < ScanProgress.FINISHED
1465
            and scan_process
1466
            and not scan_process.is_alive()
1467
        ):
1468
            if not status == ScanStatus.STOPPED:
1469
                self.add_scan_error(
1470
                    scan_id, name="", host="", value="Scan process Failure"
1471
                )
1472
1473
                logger.info(
1474
                    "%s: Scan process is dead and its progress is %d",
1475
                    scan_id,
1476
                    progress,
1477
                )
1478
                self.interrupt_scan(scan_id)
1479
1480
        elif progress == ScanProgress.FINISHED:
1481
            scan_process.join(0)
1482
1483
        logger.debug(
1484
            "%s: Check scan process: \n\tProgress %d\n\t Status: %s",
1485
            scan_id,
1486
            progress,
1487
            status.name,
1488
        )
1489
1490
    def get_count_queued_scans(self) -> int:
1491
        """ Get the amount of scans with queued status """
1492
        count = 0
1493
        for scan_id in self.scan_collection.ids_iterator():
1494
            if self.get_scan_status(scan_id) == ScanStatus.QUEUED:
1495
                count += 1
1496
        return count
1497
1498
    def get_scan_progress(self, scan_id: str) -> int:
1499
        """ Gives a scan's current progress value. """
1500
        progress = self.scan_collection.get_progress(scan_id)
1501
        logger.debug('%s: Current scan progress: %s,', scan_id, progress)
1502
        return progress
1503
1504
    def get_scan_host(self, scan_id: str) -> str:
1505
        """ Gives a scan's target. """
1506
        return self.scan_collection.get_host_list(scan_id)
1507
1508
    def get_scan_ports(self, scan_id: str) -> str:
1509
        """ Gives a scan's ports list. """
1510
        return self.scan_collection.get_ports(scan_id)
1511
1512
    def get_scan_exclude_hosts(self, scan_id: str):
1513
        """Gives a scan's exclude host list. If a target is passed gives
1514
        the exclude host list for the given target."""
1515
        return self.scan_collection.get_exclude_hosts(scan_id)
1516
1517
    def get_scan_credentials(self, scan_id: str) -> Dict:
1518
        """Gives a scan's credential list. If a target is passed gives
1519
        the credential list for the given target."""
1520
        return self.scan_collection.get_credentials(scan_id)
1521
1522
    def get_scan_target_options(self, scan_id: str) -> Dict:
1523
        """Gives a scan's target option dict. If a target is passed gives
1524
        the credential list for the given target."""
1525
        return self.scan_collection.get_target_options(scan_id)
1526
1527
    def get_scan_vts(self, scan_id: str) -> Dict:
1528
        """ Gives a scan's vts. """
1529
        return self.scan_collection.get_vts(scan_id)
1530
1531
    def get_scan_start_time(self, scan_id: str) -> str:
1532
        """ Gives a scan's start time. """
1533
        return self.scan_collection.get_start_time(scan_id)
1534
1535
    def get_scan_end_time(self, scan_id: str) -> str:
1536
        """ Gives a scan's end time. """
1537
        return self.scan_collection.get_end_time(scan_id)
1538
1539
    def add_scan_log(
1540
        self,
1541
        scan_id: str,
1542
        host: str = '',
1543
        hostname: str = '',
1544
        name: str = '',
1545
        value: str = '',
1546
        port: str = '',
1547
        test_id: str = '',
1548
        qod: str = '',
1549
        uri: str = '',
1550
    ) -> None:
1551
        """ Adds a log result to scan_id scan. """
1552
1553
        self.scan_collection.add_result(
1554
            scan_id,
1555
            ResultType.LOG,
1556
            host,
1557
            hostname,
1558
            name,
1559
            value,
1560
            port,
1561
            test_id,
1562
            '0.0',
1563
            qod,
1564
            uri,
1565
        )
1566
1567
    def add_scan_error(
1568
        self,
1569
        scan_id: str,
1570
        host: str = '',
1571
        hostname: str = '',
1572
        name: str = '',
1573
        value: str = '',
1574
        port: str = '',
1575
        test_id='',
1576
        uri: str = '',
1577
    ) -> None:
1578
        """ Adds an error result to scan_id scan. """
1579
        self.scan_collection.add_result(
1580
            scan_id,
1581
            ResultType.ERROR,
1582
            host,
1583
            hostname,
1584
            name,
1585
            value,
1586
            port,
1587
            test_id,
1588
            uri,
1589
        )
1590
1591
    def add_scan_host_detail(
1592
        self,
1593
        scan_id: str,
1594
        host: str = '',
1595
        hostname: str = '',
1596
        name: str = '',
1597
        value: str = '',
1598
        uri: str = '',
1599
    ) -> None:
1600
        """ Adds a host detail result to scan_id scan. """
1601
        self.scan_collection.add_result(
1602
            scan_id, ResultType.HOST_DETAIL, host, hostname, name, value, uri
1603
        )
1604
1605
    def add_scan_alarm(
1606
        self,
1607
        scan_id: str,
1608
        host: str = '',
1609
        hostname: str = '',
1610
        name: str = '',
1611
        value: str = '',
1612
        port: str = '',
1613
        test_id: str = '',
1614
        severity: str = '',
1615
        qod: str = '',
1616
        uri: str = '',
1617
    ) -> None:
1618
        """ Adds an alarm result to scan_id scan. """
1619
        self.scan_collection.add_result(
1620
            scan_id,
1621
            ResultType.ALARM,
1622
            host,
1623
            hostname,
1624
            name,
1625
            value,
1626
            port,
1627
            test_id,
1628
            severity,
1629
            qod,
1630
            uri,
1631
        )
1632