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