Passed
Pull Request — master (#352)
by Juan José
04:07
created

ospd.ospd.OSPDaemon.get_scan_credentials()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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