Passed
Push — master ( eefdf0...e0b344 )
by Juan José
01:39 queued 11s
created

ospd.ospd.OSPDaemon.get_count_running_scans()   A

Complexity

Conditions 4

Size

Total Lines 8
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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