Completed
Push — master ( 1ed431...35dbab )
by
unknown
17s queued 13s
created

ospd.ospd.OSPDaemon.start_pending_scans()   A

Complexity

Conditions 3

Size

Total Lines 8
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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