Completed
Push — master ( 922de2...d10835 )
by
unknown
19s queued 10s
created

ospd.ospd.OSPDaemon.check_scan_process()   B

Complexity

Conditions 7

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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