Passed
Pull Request — master (#316)
by Juan José
01:53
created

ospd.ospd.OSPDaemon.process_vts_params()   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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