Completed
Push — master ( 49ba58...c1f282 )
by Juan José
13s queued 11s
created

ospd.ospd.OSPDaemon.set_scan_total_hosts()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 3
dl 0
loc 4
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 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 = __version__
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
        return self.vts_version
260
261
    def command_exists(self, name: str) -> bool:
262
        """ Checks if a commands exists. """
263
        return name in self.commands
264
265
    def get_scanner_name(self) -> str:
266
        """ Gives the wrapped scanner's name. """
267
        return self.scanner_info['name']
268
269
    def get_scanner_version(self) -> str:
270
        """ Gives the wrapped scanner's version. """
271
        return self.scanner_info['version']
272
273
    def get_scanner_description(self) -> str:
274
        """ Gives the wrapped scanner's description. """
275
        return self.scanner_info['description']
276
277
    def get_server_version(self) -> str:
278
        """ Gives the specific OSP server's version. """
279
        assert self.server_version
280
        return self.server_version
281
282
    def get_protocol_version(self) -> str:
283
        """ Gives the OSP's version. """
284
        return self.protocol_version
285
286
    def preprocess_scan_params(self, xml_params):
287
        """ Processes the scan parameters. """
288
        params = {}
289
290
        for param in xml_params:
291
            params[param.tag] = param.text or ''
292
293
        # Validate values.
294
        for key in params:
295
            param_type = self.get_scanner_param_type(key)
296
            if not param_type:
297
                continue
298
299
            if param_type in ['integer', 'boolean']:
300
                try:
301
                    params[key] = int(params[key])
302
                except ValueError:
303
                    raise OspdCommandError(
304
                        'Invalid %s value' % key, 'start_scan'
305
                    ) from None
306
307
            if param_type == 'boolean':
308
                if params[key] not in [0, 1]:
309
                    raise OspdCommandError(
310
                        'Invalid %s value' % key, 'start_scan'
311
                    )
312
            elif param_type == 'selection':
313
                selection = self.get_scanner_param_default(key).split('|')
314
                if params[key] not in selection:
315
                    raise OspdCommandError(
316
                        'Invalid %s value' % key, 'start_scan'
317
                    )
318
            if self.get_scanner_param_mandatory(key) and params[key] == '':
319
                raise OspdCommandError(
320
                    'Mandatory %s value is missing' % key, 'start_scan'
321
                )
322
323
        return params
324
325
    def process_scan_params(self, params: Dict) -> Dict:
326
        """This method is to be overridden by the child classes if necessary"""
327
        return params
328
329
    def stop_scan(self, scan_id: str) -> None:
330
        if (
331
            scan_id in self.scan_collection.ids_iterator()
332
            and self.get_scan_status(scan_id) == ScanStatus.QUEUED
333
        ):
334
            logger.info('Scan %s has been removed from the queue.', scan_id)
335
            self.scan_collection.remove_file_pickled_scan_info(scan_id)
336
            self.set_scan_status(scan_id, ScanStatus.STOPPED)
337
338
            return
339
340
        scan_process = self.scan_processes.get(scan_id)
341
        if not scan_process:
342
            raise OspdCommandError(
343
                'Scan not found {0}.'.format(scan_id), 'stop_scan'
344
            )
345
        if not scan_process.is_alive():
346
            raise OspdCommandError(
347
                'Scan already stopped or finished.', 'stop_scan'
348
            )
349
350
        self.set_scan_status(scan_id, ScanStatus.STOPPED)
351
352
        logger.info(
353
            '%s: Stopping Scan with the PID %s.', scan_id, scan_process.ident
354
        )
355
356
        self.stop_scan_cleanup(scan_id)
357
358
        try:
359
            scan_process.terminate()
360
        except AttributeError:
361
            logger.debug('%s: The scanner task stopped unexpectedly.', scan_id)
362
363
        try:
364
            _terminate_process_group(scan_process)
365
        except ProcessLookupError:
366
            logger.info(
367
                '%s: Scan with the PID %s is already stopped.',
368
                scan_id,
369
                scan_process.pid,
370
            )
371
372
        if scan_process.ident != os.getpid():
373
            scan_process.join(0)
374
375
        logger.info('%s: Scan stopped.', scan_id)
376
377
    @staticmethod
378
    def stop_scan_cleanup(scan_id: str):
379
        """Should be implemented by subclass in case of a clean up before
380
        terminating is needed."""
381
382
    @staticmethod
383
    def target_is_finished(scan_id: str):
384
        """Should be implemented by subclass in case of a check before
385
        stopping is needed."""
386
387
    def exec_scan(self, scan_id: str):
388
        """ Asserts to False. Should be implemented by subclass. """
389
        raise NotImplementedError
390
391
    def finish_scan(self, scan_id: str) -> None:
392
        """ Sets a scan as finished. """
393
        self.scan_collection.set_progress(scan_id, ScanProgress.FINISHED.value)
394
        self.set_scan_status(scan_id, ScanStatus.FINISHED)
395
        logger.info("%s: Scan finished.", scan_id)
396
397
    def interrupt_scan(self, scan_id: str) -> None:
398
        """ Set scan status as interrupted. """
399
        self.set_scan_status(scan_id, ScanStatus.INTERRUPTED)
400
        logger.info("%s: Scan interrupted.", scan_id)
401
402
    def daemon_exit_cleanup(self) -> None:
403
        """ Perform a cleanup before exiting """
404
        self.scan_collection.clean_up_pickled_scan_info()
405
406
        # Stop scans which are not already stopped.
407
        for scan_id in self.scan_collection.ids_iterator():
408
            status = self.get_scan_status(scan_id)
409
            if (
410
                status != ScanStatus.STOPPED
411
                and status != ScanStatus.FINISHED
412
                and status != ScanStatus.INTERRUPTED
413
            ):
414
                self.stop_scan(scan_id)
415
416
        # Wait for scans to be in some stopped state.
417
        while True:
418
            all_stopped = True
419
            for scan_id in self.scan_collection.ids_iterator():
420
                status = self.get_scan_status(scan_id)
421
                if (
422
                    status != ScanStatus.STOPPED
423
                    and status != ScanStatus.FINISHED
424
                    and status != ScanStatus.INTERRUPTED
425
                ):
426
                    all_stopped = False
427
428
            if all_stopped:
429
                return
430
431
            time.sleep(1)
432
433
    def get_daemon_name(self) -> str:
434
        """ Gives osp daemon's name. """
435
        return self.daemon_info['name']
436
437
    def get_daemon_version(self) -> str:
438
        """ Gives osp daemon's version. """
439
        return self.daemon_info['version']
440
441
    def get_scanner_param_type(self, param: str):
442
        """ Returns type of a scanner parameter. """
443
        assert isinstance(param, str)
444
        entry = self.scanner_params.get(param)
445
        if not entry:
446
            return None
447
        return entry.get('type')
448
449
    def get_scanner_param_mandatory(self, param: str):
450
        """ Returns if a scanner parameter is mandatory. """
451
        assert isinstance(param, str)
452
        entry = self.scanner_params.get(param)
453
        if not entry:
454
            return False
455
        return entry.get('mandatory')
456
457
    def get_scanner_param_default(self, param: str):
458
        """ Returns default value of a scanner parameter. """
459
        assert isinstance(param, str)
460
        entry = self.scanner_params.get(param)
461
        if not entry:
462
            return None
463
        return entry.get('default')
464
465
    def handle_client_stream(self, stream: Stream) -> None:
466
        """ Handles stream of data received from client. """
467
        data = b''
468
469
        request_parser = RequestParser()
470
471
        while True:
472
            try:
473
                buf = stream.read()
474
                if not buf:
475
                    break
476
477
                data += buf
478
479
                if request_parser.has_ended(buf):
480
                    break
481
            except (AttributeError, ValueError) as message:
482
                logger.error(message)
483
                return
484
            except (ssl.SSLError) as exception:
485
                logger.debug('Error: %s', exception)
486
                break
487
            except (socket.timeout) as exception:
488
                logger.debug('Request timeout: %s', exception)
489
                break
490
491
        if len(data) <= 0:
492
            logger.debug("Empty client stream")
493
            return
494
495
        response = None
496
        try:
497
            self.handle_command(data, stream)
498
        except OspdCommandError as exception:
499
            response = exception.as_xml()
500
            logger.debug('Command error: %s', exception.message)
501
        except Exception:  # pylint: disable=broad-except
502
            logger.exception('While handling client command:')
503
            exception = OspdCommandError('Fatal error', 'error')
504
            response = exception.as_xml()
505
506
        if response:
507
            stream.write(response)
508
509
        stream.close()
510
511
    def process_finished_hosts(self, scan_id: str) -> None:
512
        """ Process the finished hosts before launching the scans."""
513
514
        finished_hosts = self.scan_collection.get_finished_hosts(scan_id)
515
        if not finished_hosts:
516
            return
517
518
        exc_finished_hosts_list = target_str_to_list(finished_hosts)
519
        self.scan_collection.set_host_finished(scan_id, exc_finished_hosts_list)
520
521
    def start_scan(self, scan_id: str) -> None:
522
        """ Starts the scan with scan_id. """
523
        os.setsid()
524
525
        self.process_finished_hosts(scan_id)
526
527
        try:
528
            self.set_scan_status(scan_id, ScanStatus.RUNNING)
529
            self.exec_scan(scan_id)
530
        except Exception as e:  # pylint: disable=broad-except
531
            self.add_scan_error(
532
                scan_id,
533
                name='',
534
                host=self.get_scan_host(scan_id),
535
                value='Host process failure (%s).' % e,
536
            )
537
            logger.exception('While scanning: %s', scan_id)
538
        else:
539
            logger.info("%s: Host scan finished.", scan_id)
540
541
        is_stopped = self.get_scan_status(scan_id) == ScanStatus.STOPPED
542
        self.set_scan_progress(scan_id)
543
        progress = self.get_scan_progress(scan_id)
544
545
        if not is_stopped and progress == ScanProgress.FINISHED:
546
            self.finish_scan(scan_id)
547
        elif not is_stopped:
548
            self.interrupt_scan(scan_id)
549
550
    def dry_run_scan(self, scan_id: str, target: Dict) -> None:
551
        """ Dry runs a scan. """
552
553
        os.setsid()
554
555
        host = resolve_hostname(target.get('hosts'))
556
        if host is None:
557
            logger.info("Couldn't resolve %s.", self.get_scan_host(scan_id))
558
559
        port = self.get_scan_ports(scan_id)
560
561
        logger.info("%s:%s: Dry run mode.", host, port)
562
563
        self.add_scan_log(scan_id, name='', host=host, value='Dry run result')
564
565
        self.finish_scan(scan_id)
566
567
    def handle_timeout(self, scan_id: str, host: str) -> None:
568
        """ Handles scanner reaching timeout error. """
569
        self.add_scan_error(
570
            scan_id,
571
            host=host,
572
            name="Timeout",
573
            value="{0} exec timeout.".format(self.get_scanner_name()),
574
        )
575
576
    def sort_host_finished(
577
        self,
578
        scan_id: str,
579
        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,
621
        scan_id: str,
622
        host: str = None,
623
        progress: int = None,
624
    ) -> None:
625
        """Sets host's progress which is part of target.
626
        Each time a host progress is updated, the scan progress
627
        is updated too.
628
        """
629
        if host is None or progress is None:
630
            return
631
632
        if not isinstance(progress, int):
633
            try:
634
                progress = int(progress)
635
            except (TypeError, ValueError):
636
                return
637
638
        host_progress = {host: progress}
639
        self.set_scan_progress_batch(scan_id, host_progress)
640
641
    def set_scan_status(self, scan_id: str, status: ScanStatus) -> None:
642
        """ Set the scan's status."""
643
        self.scan_collection.set_status(scan_id, status)
644
645
    def get_scan_status(self, scan_id: str) -> ScanStatus:
646
        """ Get scan_id scans's status."""
647
        return self.scan_collection.get_status(scan_id)
648
649
    def scan_exists(self, scan_id: str) -> bool:
650
        """Checks if a scan with ID scan_id is in collection.
651
652
        @return: 1 if scan exists, 0 otherwise.
653
        """
654
        return self.scan_collection.id_exists(scan_id)
655
656
    def get_help_text(self) -> str:
657
        """ Returns the help output in plain text format."""
658
659
        txt = ''
660
        for name, info in self.commands.items():
661
            description = info.get_description()
662
            attributes = info.get_attributes()
663
            elements = info.get_elements()
664
665
            command_txt = "\t{0: <22} {1}\n".format(name, description)
666
667
            if attributes:
668
                command_txt = ''.join([command_txt, "\t Attributes:\n"])
669
670
                for attrname, attrdesc in attributes.items():
671
                    attr_txt = "\t  {0: <22} {1}\n".format(attrname, attrdesc)
672
                    command_txt = ''.join([command_txt, attr_txt])
673
674
            if elements:
675
                command_txt = ''.join(
676
                    [
677
                        command_txt,
678
                        "\t Elements:\n",
679
                        elements_as_text(elements),
680
                    ]
681
                )
682
683
            txt += command_txt
684
685
        return txt
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_count_total(
745
            scan_id
746
        )
747
748
        return get_progress_xml(current_progress)
749
750
    def get_scan_xml(
751
        self,
752
        scan_id: str,
753
        detailed: bool = True,
754
        pop_res: bool = False,
755
        max_res: int = 0,
756
        progress: bool = False,
757
    ):
758
        """Gets scan in XML format.
759
760
        @return: String of scan in XML format.
761
        """
762
        if not scan_id:
763
            return Element('scan')
764
765
        if self.get_scan_status(scan_id) == ScanStatus.QUEUED:
766
            target = ''
767
            scan_progress = 0
768
            status = self.get_scan_status(scan_id)
769
            start_time = 0
770
            end_time = 0
771
            response = Element('scan')
772
            detailed = False
773
            progress = False
774
            response.append(Element('results'))
775
        else:
776
            target = self.get_scan_host(scan_id)
777
            scan_progress = self.get_scan_progress(scan_id)
778
            status = self.get_scan_status(scan_id)
779
            start_time = self.get_scan_start_time(scan_id)
780
            end_time = self.get_scan_end_time(scan_id)
781
            response = Element('scan')
782
783
        for name, value in [
784
            ('id', scan_id),
785
            ('target', target),
786
            ('progress', scan_progress),
787
            ('status', status.name.lower()),
788
            ('start_time', start_time),
789
            ('end_time', end_time),
790
        ]:
791
            response.set(name, str(value))
792
        if detailed:
793
            response.append(
794
                self.get_scan_results_xml(scan_id, pop_res, max_res)
795
            )
796
        if progress:
797
            response.append(self._get_scan_progress_xml(scan_id))
798
799
        return response
800
801
    @staticmethod
802
    def get_custom_vt_as_xml_str(  # pylint: disable=unused-argument
803
        vt_id: str, custom: Dict
804
    ) -> str:
805
        """Create a string representation of the XML object from the
806
        custom data object.
807
        This needs to be implemented by each ospd wrapper, in case
808
        custom elements for VTs are used.
809
810
        The custom XML object which is returned will be embedded
811
        into a <custom></custom> element.
812
813
        @return: XML object as string for custom data.
814
        """
815
        return ''
816
817
    @staticmethod
818
    def get_params_vt_as_xml_str(  # pylint: disable=unused-argument
819
        vt_id: str, vt_params
820
    ) -> str:
821
        """Create a string representation of the XML object from the
822
        vt_params data object.
823
        This needs to be implemented by each ospd wrapper, in case
824
        vt_params elements for VTs are used.
825
826
        The params XML object which is returned will be embedded
827
        into a <params></params> element.
828
829
        @return: XML object as string for vt parameters data.
830
        """
831
        return ''
832
833
    @staticmethod
834
    def get_refs_vt_as_xml_str(  # pylint: disable=unused-argument
835
        vt_id: str, vt_refs
836
    ) -> str:
837
        """Create a string representation of the XML object from the
838
        refs data object.
839
        This needs to be implemented by each ospd wrapper, in case
840
        refs elements for VTs are used.
841
842
        The refs XML object which is returned will be embedded
843
        into a <refs></refs> element.
844
845
        @return: XML object as string for vt references data.
846
        """
847
        return ''
848
849
    @staticmethod
850
    def get_dependencies_vt_as_xml_str(  # pylint: disable=unused-argument
851
        vt_id: str, vt_dependencies
852
    ) -> str:
853
        """Create a string representation of the XML object from the
854
        vt_dependencies data object.
855
        This needs to be implemented by each ospd wrapper, in case
856
        vt_dependencies elements for VTs are used.
857
858
        The vt_dependencies XML object which is returned will be embedded
859
        into a <dependencies></dependencies> element.
860
861
        @return: XML object as string for vt dependencies data.
862
        """
863
        return ''
864
865
    @staticmethod
866
    def get_creation_time_vt_as_xml_str(  # pylint: disable=unused-argument
867
        vt_id: str, vt_creation_time
868
    ) -> str:
869
        """Create a string representation of the XML object from the
870
        vt_creation_time data object.
871
        This needs to be implemented by each ospd wrapper, in case
872
        vt_creation_time elements for VTs are used.
873
874
        The vt_creation_time XML object which is returned will be embedded
875
        into a <vt_creation_time></vt_creation_time> element.
876
877
        @return: XML object as string for vt creation time data.
878
        """
879
        return ''
880
881
    @staticmethod
882
    def get_modification_time_vt_as_xml_str(  # pylint: disable=unused-argument
883
        vt_id: str, vt_modification_time
884
    ) -> str:
885
        """Create a string representation of the XML object from the
886
        vt_modification_time data object.
887
        This needs to be implemented by each ospd wrapper, in case
888
        vt_modification_time elements for VTs are used.
889
890
        The vt_modification_time XML object which is returned will be embedded
891
        into a <vt_modification_time></vt_modification_time> element.
892
893
        @return: XML object as string for vt references data.
894
        """
895
        return ''
896
897
    @staticmethod
898
    def get_summary_vt_as_xml_str(  # pylint: disable=unused-argument
899
        vt_id: str, summary
900
    ) -> str:
901
        """Create a string representation of the XML object from the
902
        summary data object.
903
        This needs to be implemented by each ospd wrapper, in case
904
        summary elements for VTs are used.
905
906
        The summary XML object which is returned will be embedded
907
        into a <summary></summary> element.
908
909
        @return: XML object as string for summary data.
910
        """
911
        return ''
912
913
    @staticmethod
914
    def get_impact_vt_as_xml_str(  # pylint: disable=unused-argument
915
        vt_id: str, impact
916
    ) -> str:
917
        """Create a string representation of the XML object from the
918
        impact data object.
919
        This needs to be implemented by each ospd wrapper, in case
920
        impact elements for VTs are used.
921
922
        The impact XML object which is returned will be embedded
923
        into a <impact></impact> element.
924
925
        @return: XML object as string for impact data.
926
        """
927
        return ''
928
929
    @staticmethod
930
    def get_affected_vt_as_xml_str(  # pylint: disable=unused-argument
931
        vt_id: str, affected
932
    ) -> str:
933
        """Create a string representation of the XML object from the
934
        affected data object.
935
        This needs to be implemented by each ospd wrapper, in case
936
        affected elements for VTs are used.
937
938
        The affected XML object which is returned will be embedded
939
        into a <affected></affected> element.
940
941
        @return: XML object as string for affected data.
942
        """
943
        return ''
944
945
    @staticmethod
946
    def get_insight_vt_as_xml_str(  # pylint: disable=unused-argument
947
        vt_id: str, insight
948
    ) -> str:
949
        """Create a string representation of the XML object from the
950
        insight data object.
951
        This needs to be implemented by each ospd wrapper, in case
952
        insight elements for VTs are used.
953
954
        The insight XML object which is returned will be embedded
955
        into a <insight></insight> element.
956
957
        @return: XML object as string for insight data.
958
        """
959
        return ''
960
961
    @staticmethod
962
    def get_solution_vt_as_xml_str(  # pylint: disable=unused-argument
963
        vt_id: str, solution, solution_type=None, solution_method=None
964
    ) -> str:
965
        """Create a string representation of the XML object from the
966
        solution data object.
967
        This needs to be implemented by each ospd wrapper, in case
968
        solution elements for VTs are used.
969
970
        The solution XML object which is returned will be embedded
971
        into a <solution></solution> element.
972
973
        @return: XML object as string for solution data.
974
        """
975
        return ''
976
977
    @staticmethod
978
    def get_detection_vt_as_xml_str(  # pylint: disable=unused-argument
979
        vt_id: str, detection=None, qod_type=None, qod=None
980
    ) -> str:
981
        """Create a string representation of the XML object from the
982
        detection data object.
983
        This needs to be implemented by each ospd wrapper, in case
984
        detection elements for VTs are used.
985
986
        The detection XML object which is returned is an element with
987
        tag <detection></detection> element
988
989
        @return: XML object as string for detection data.
990
        """
991
        return ''
992
993
    @staticmethod
994
    def get_severities_vt_as_xml_str(  # pylint: disable=unused-argument
995
        vt_id: str, severities
996
    ) -> str:
997
        """Create a string representation of the XML object from the
998
        severities data object.
999
        This needs to be implemented by each ospd wrapper, in case
1000
        severities elements for VTs are used.
1001
1002
        The severities XML objects which are returned will be embedded
1003
        into a <severities></severities> element.
1004
1005
        @return: XML object as string for severities data.
1006
        """
1007
        return ''
1008
1009
    def get_vt_iterator(  # pylint: disable=unused-argument
1010
        self, vt_selection: List[str] = None, details: bool = True
1011
    ) -> Iterator[Tuple[str, Dict]]:
1012
        """Return iterator object for getting elements
1013
        from the VTs dictionary."""
1014
        return self.vts.items()
1015
1016
    def get_vt_xml(self, single_vt: Tuple[str, Dict]) -> Element:
1017
        """Gets a single vulnerability test information in XML format.
1018
1019
        @return: String of single vulnerability test information in XML format.
1020
        """
1021
        if not single_vt or single_vt[1] is None:
1022
            return Element('vt')
1023
1024
        vt_id, vt = single_vt
1025
1026
        name = vt.get('name')
1027
        vt_xml = Element('vt')
1028
        vt_xml.set('id', vt_id)
1029
1030
        for name, value in [('name', name)]:
1031
            elem = SubElement(vt_xml, name)
1032
            elem.text = str(value)
1033
1034
        if vt.get('vt_params'):
1035
            params_xml_str = self.get_params_vt_as_xml_str(
1036
                vt_id, vt.get('vt_params')
1037
            )
1038
            vt_xml.append(secET.fromstring(params_xml_str))
1039
1040
        if vt.get('vt_refs'):
1041
            refs_xml_str = self.get_refs_vt_as_xml_str(vt_id, vt.get('vt_refs'))
1042
            vt_xml.append(secET.fromstring(refs_xml_str))
1043
1044
        if vt.get('vt_dependencies'):
1045
            dependencies = self.get_dependencies_vt_as_xml_str(
1046
                vt_id, vt.get('vt_dependencies')
1047
            )
1048
            vt_xml.append(secET.fromstring(dependencies))
1049
1050
        if vt.get('creation_time'):
1051
            vt_ctime = self.get_creation_time_vt_as_xml_str(
1052
                vt_id, vt.get('creation_time')
1053
            )
1054
            vt_xml.append(secET.fromstring(vt_ctime))
1055
1056
        if vt.get('modification_time'):
1057
            vt_mtime = self.get_modification_time_vt_as_xml_str(
1058
                vt_id, vt.get('modification_time')
1059
            )
1060
            vt_xml.append(secET.fromstring(vt_mtime))
1061
1062
        if vt.get('summary'):
1063
            summary_xml_str = self.get_summary_vt_as_xml_str(
1064
                vt_id, vt.get('summary')
1065
            )
1066
            vt_xml.append(secET.fromstring(summary_xml_str))
1067
1068
        if vt.get('impact'):
1069
            impact_xml_str = self.get_impact_vt_as_xml_str(
1070
                vt_id, vt.get('impact')
1071
            )
1072
            vt_xml.append(secET.fromstring(impact_xml_str))
1073
1074
        if vt.get('affected'):
1075
            affected_xml_str = self.get_affected_vt_as_xml_str(
1076
                vt_id, vt.get('affected')
1077
            )
1078
            vt_xml.append(secET.fromstring(affected_xml_str))
1079
1080
        if vt.get('insight'):
1081
            insight_xml_str = self.get_insight_vt_as_xml_str(
1082
                vt_id, vt.get('insight')
1083
            )
1084
            vt_xml.append(secET.fromstring(insight_xml_str))
1085
1086
        if vt.get('solution'):
1087
            solution_xml_str = self.get_solution_vt_as_xml_str(
1088
                vt_id,
1089
                vt.get('solution'),
1090
                vt.get('solution_type'),
1091
                vt.get('solution_method'),
1092
            )
1093
            vt_xml.append(secET.fromstring(solution_xml_str))
1094
1095
        if vt.get('detection') or vt.get('qod_type') or vt.get('qod'):
1096
            detection_xml_str = self.get_detection_vt_as_xml_str(
1097
                vt_id, vt.get('detection'), vt.get('qod_type'), vt.get('qod')
1098
            )
1099
            vt_xml.append(secET.fromstring(detection_xml_str))
1100
1101
        if vt.get('severities'):
1102
            severities_xml_str = self.get_severities_vt_as_xml_str(
1103
                vt_id, vt.get('severities')
1104
            )
1105
            vt_xml.append(secET.fromstring(severities_xml_str))
1106
1107
        if vt.get('custom'):
1108
            custom_xml_str = self.get_custom_vt_as_xml_str(
1109
                vt_id, vt.get('custom')
1110
            )
1111
            vt_xml.append(secET.fromstring(custom_xml_str))
1112
1113
        return vt_xml
1114
1115
    def get_vts_selection_list(
1116
        self, vt_id: str = None, filtered_vts: Dict = None
1117
    ) -> Iterable[str]:
1118
        """
1119
        Get list of VT's OID.
1120
        If vt_id is specified, the collection will contain only this vt, if
1121
        found.
1122
        If no vt_id is specified or filtered_vts is None (default), the
1123
        collection will contain all vts. Otherwise those vts passed
1124
        in filtered_vts or vt_id are returned. In case of both vt_id and
1125
        filtered_vts are given, filtered_vts has priority.
1126
1127
        Arguments:
1128
            vt_id (vt_id, optional): ID of the vt to get.
1129
            filtered_vts (list, optional): Filtered VTs collection.
1130
1131
        Return:
1132
            List of selected VT's OID.
1133
        """
1134
        vts_xml = []
1135
1136
        # No match for the filter
1137
        if filtered_vts is not None and len(filtered_vts) == 0:
1138
            return vts_xml
1139
1140
        if filtered_vts:
1141
            vts_list = filtered_vts
1142
        elif vt_id:
1143
            vts_list = [vt_id]
1144
        else:
1145
            vts_list = self.vts.keys()
1146
1147
        return vts_list
1148
1149
    def handle_command(self, data: bytes, stream: Stream) -> None:
1150
        """Handles an osp command in a string."""
1151
        try:
1152
            tree = secET.fromstring(data)
1153
        except secET.ParseError as e:
1154
            logger.debug("Erroneous client input: %s", data)
1155
            raise OspdCommandError('Invalid data') from e
1156
1157
        command_name = tree.tag
1158
1159
        logger.debug('Handling %s command request.', command_name)
1160
1161
        command = self.commands.get(command_name, None)
1162
        if not command and command_name != "authenticate":
1163
            raise OspdCommandError('Bogus command name')
1164
1165
        if not self.initialized and command.must_be_initialized:
1166
            exception = OspdCommandError(
1167
                '%s is still starting' % self.daemon_info['name'], 'error'
1168
            )
1169
            response = exception.as_xml()
1170
            stream.write(response)
1171
            return
1172
1173
        response = command.handle_xml(tree)
1174
1175
        write_success = True
1176
        if isinstance(response, bytes):
1177
            write_success = stream.write(response)
1178
        else:
1179
            for data in response:
1180
                write_success = stream.write(data)
1181
                if not write_success:
1182
                    break
1183
1184
        scan_id = tree.get('scan_id')
1185
        if self.scan_exists(scan_id) and command_name == "get_scans":
1186
            if write_success:
1187
                self.scan_collection.clean_temp_result_list(scan_id)
1188
            else:
1189
                self.scan_collection.restore_temp_result_list(scan_id)
1190
1191
    def check(self):
1192
        """ Asserts to False. Should be implemented by subclass. """
1193
        raise NotImplementedError
1194
1195
    def run(self) -> None:
1196
        """Starts the Daemon, handling commands until interrupted."""
1197
1198
        try:
1199
            while True:
1200
                time.sleep(SCHEDULER_CHECK_PERIOD)
1201
                self.scheduler()
1202
                self.clean_forgotten_scans()
1203
                self.start_queued_scans()
1204
                self.wait_for_children()
1205
        except KeyboardInterrupt:
1206
            logger.info("Received Ctrl-C shutting-down ...")
1207
1208
    def start_queued_scans(self) -> None:
1209
        """ Starts a queued scan if it is allowed """
1210
1211
        current_queued_scans = self.get_count_queued_scans()
1212
        if not current_queued_scans:
1213
            return
1214
1215
        if not self.initialized:
1216
            logger.info(
1217
                "Queued task can not be started because a feed "
1218
                "update is being performed."
1219
            )
1220
            return
1221
1222
        logger.info('Currently %d queued scans.', current_queued_scans)
1223
1224
        for scan_id in self.scan_collection.ids_iterator():
1225
            scan_allowed = (
1226
                self.is_new_scan_allowed() and self.is_enough_free_memory()
1227
            )
1228
            scan_is_queued = self.get_scan_status(scan_id) == ScanStatus.QUEUED
1229
1230
            if scan_is_queued and scan_allowed:
1231
                try:
1232
                    self.scan_collection.unpickle_scan_info(scan_id)
1233
                except OspdCommandError as e:
1234
                    logger.error("Start scan error %s", e)
1235
                    self.stop_scan(scan_id)
1236
                    continue
1237
1238
                scan_func = self.start_scan
1239
                scan_process = create_process(func=scan_func, args=(scan_id,))
1240
                self.scan_processes[scan_id] = scan_process
1241
                scan_process.start()
1242
                self.set_scan_status(scan_id, ScanStatus.INIT)
1243
1244
                current_queued_scans = current_queued_scans - 1
1245
                logger.info('Starting scan %s.', scan_id)
1246
            elif scan_is_queued and not scan_allowed:
1247
                return
1248
1249
    def is_new_scan_allowed(self) -> bool:
1250
        """Check if max_scans has been reached.
1251
1252
        Return:
1253
            True if a new scan can be launch.
1254
        """
1255
        if (self.max_scans != 0) and (
1256
            len(self.scan_processes) >= self.max_scans
1257
        ):
1258
            logger.info(
1259
                'Not possible to run a new scan. Max scan limit set '
1260
                'to %d reached.',
1261
                self.max_scans,
1262
            )
1263
            return False
1264
1265
        return True
1266
1267
    def is_enough_free_memory(self) -> bool:
1268
        """Check if there is enough free memory in the system to run
1269
        a new scan. The necessary memory is a rough calculation and very
1270
        conservative.
1271
1272
        Return:
1273
            True if there is enough memory for a new scan.
1274
        """
1275
        if not self.min_free_mem_scan_queue:
1276
            return True
1277
1278
        free_mem = psutil.virtual_memory().available / (1024 * 1024)
1279
1280
        if free_mem > self.min_free_mem_scan_queue:
1281
            return True
1282
1283
        logger.info(
1284
            'Not possible to run a new scan. Not enough free memory. '
1285
            'Only %d MB available but at least %d are required',
1286
            free_mem,
1287
            self.min_free_mem_scan_queue,
1288
        )
1289
1290
        return False
1291
1292
    def scheduler(self):
1293
        """Should be implemented by subclass in case of need
1294
        to run tasks periodically."""
1295
1296
    def wait_for_children(self):
1297
        """ Join the zombie process to releases resources."""
1298
        for scan_id in self.scan_processes:
1299
            self.scan_processes[scan_id].join(0)
1300
1301
    def create_scan(
1302
        self,
1303
        scan_id: str,
1304
        targets: Dict,
1305
        options: Optional[Dict],
1306
        vt_selection: Dict,
1307
    ) -> Optional[str]:
1308
        """Creates a new scan.
1309
1310
        @target: Target to scan.
1311
        @options: Miscellaneous scan options supplied via <scanner_params>
1312
                  XML element.
1313
1314
        @return: New scan's ID. None if the scan_id already exists.
1315
        """
1316
        status = None
1317
        scan_exists = self.scan_exists(scan_id)
1318
        if scan_id and scan_exists:
1319
            status = self.get_scan_status(scan_id)
1320
            logger.info(
1321
                "Scan %s exists with status %s.", scan_id, status.name.lower()
1322
            )
1323
            return
1324
1325
        return self.scan_collection.create_scan(
1326
            scan_id, targets, options, vt_selection
1327
        )
1328
1329
    def get_scan_options(self, scan_id: str) -> str:
1330
        """ Gives a scan's list of options. """
1331
        return self.scan_collection.get_options(scan_id)
1332
1333
    def set_scan_option(self, scan_id: str, name: str, value: Any) -> None:
1334
        """ Sets a scan's option to a provided value. """
1335
        return self.scan_collection.set_option(scan_id, name, value)
1336
1337
    def set_scan_total_hosts(self, scan_id: str, count_total: int) -> None:
1338
        """Sets a scan's total hosts. Allow the scanner to update
1339
        the total count of host to be scanned."""
1340
        self.scan_collection.update_count_total(scan_id, count_total)
1341
1342
    def clean_forgotten_scans(self) -> None:
1343
        """Check for old stopped or finished scans which have not been
1344
        deleted and delete them if the are older than the set value."""
1345
1346
        if not self.scaninfo_store_time:
1347
            return
1348
1349
        for scan_id in list(self.scan_collection.ids_iterator()):
1350
            end_time = int(self.get_scan_end_time(scan_id))
1351
            scan_status = self.get_scan_status(scan_id)
1352
1353
            if (
1354
                scan_status == ScanStatus.STOPPED
1355
                or scan_status == ScanStatus.FINISHED
1356
                or scan_status == ScanStatus.INTERRUPTED
1357
            ) and end_time:
1358
                stored_time = int(time.time()) - end_time
1359
                if stored_time > self.scaninfo_store_time * 3600:
1360
                    logger.debug(
1361
                        'Scan %s is older than %d hours and seems have been '
1362
                        'forgotten. Scan info will be deleted from the '
1363
                        'scan table',
1364
                        scan_id,
1365
                        self.scaninfo_store_time,
1366
                    )
1367
                    self.delete_scan(scan_id)
1368
1369
    def check_scan_process(self, scan_id: str) -> None:
1370
        """ Check the scan's process, and terminate the scan if not alive. """
1371
        if self.get_scan_status(scan_id) == ScanStatus.QUEUED:
1372
            return
1373
1374
        scan_process = self.scan_processes.get(scan_id)
1375
        progress = self.get_scan_progress(scan_id)
1376
1377
        if (
1378
            progress < ScanProgress.FINISHED
1379
            and scan_process
1380
            and not scan_process.is_alive()
1381
        ):
1382
            if not self.get_scan_status(scan_id) == ScanStatus.STOPPED:
1383
                self.set_scan_status(scan_id, ScanStatus.STOPPED)
1384
                self.add_scan_error(
1385
                    scan_id, name="", host="", value="Scan process failure."
1386
                )
1387
1388
                logger.info("%s: Scan stopped with errors.", scan_id)
1389
                self.interrupt_scan(scan_id)
1390
1391
        elif progress == ScanProgress.FINISHED:
1392
            scan_process.join(0)
1393
1394
    def get_count_queued_scans(self) -> int:
1395
        """ Get the amount of scans with queued status """
1396
        count = 0
1397
        for scan_id in self.scan_collection.ids_iterator():
1398
            if self.get_scan_status(scan_id) == ScanStatus.QUEUED:
1399
                count += 1
1400
        return count
1401
1402
    def get_scan_progress(self, scan_id: str) -> int:
1403
        """ Gives a scan's current progress value. """
1404
        return self.scan_collection.get_progress(scan_id)
1405
1406
    def get_scan_host(self, scan_id: str) -> str:
1407
        """ Gives a scan's target. """
1408
        return self.scan_collection.get_host_list(scan_id)
1409
1410
    def get_scan_ports(self, scan_id: str) -> str:
1411
        """ Gives a scan's ports list. """
1412
        return self.scan_collection.get_ports(scan_id)
1413
1414
    def get_scan_exclude_hosts(self, scan_id: str):
1415
        """Gives a scan's exclude host list. If a target is passed gives
1416
        the exclude host list for the given target."""
1417
        return self.scan_collection.get_exclude_hosts(scan_id)
1418
1419
    def get_scan_credentials(self, scan_id: str) -> Dict:
1420
        """Gives a scan's credential list. If a target is passed gives
1421
        the credential list for the given target."""
1422
        return self.scan_collection.get_credentials(scan_id)
1423
1424
    def get_scan_target_options(self, scan_id: str) -> Dict:
1425
        """Gives a scan's target option dict. If a target is passed gives
1426
        the credential list for the given target."""
1427
        return self.scan_collection.get_target_options(scan_id)
1428
1429
    def get_scan_vts(self, scan_id: str) -> Dict:
1430
        """ Gives a scan's vts. """
1431
        return self.scan_collection.get_vts(scan_id)
1432
1433
    def get_scan_start_time(self, scan_id: str) -> str:
1434
        """ Gives a scan's start time. """
1435
        return self.scan_collection.get_start_time(scan_id)
1436
1437
    def get_scan_end_time(self, scan_id: str) -> str:
1438
        """ Gives a scan's end time. """
1439
        return self.scan_collection.get_end_time(scan_id)
1440
1441
    def add_scan_log(
1442
        self,
1443
        scan_id: str,
1444
        host: str = '',
1445
        hostname: str = '',
1446
        name: str = '',
1447
        value: str = '',
1448
        port: str = '',
1449
        test_id: str = '',
1450
        qod: str = '',
1451
        uri: str = '',
1452
    ) -> None:
1453
        """ Adds a log result to scan_id scan. """
1454
1455
        self.scan_collection.add_result(
1456
            scan_id,
1457
            ResultType.LOG,
1458
            host,
1459
            hostname,
1460
            name,
1461
            value,
1462
            port,
1463
            test_id,
1464
            '0.0',
1465
            qod,
1466
            uri,
1467
        )
1468
1469
    def add_scan_error(
1470
        self,
1471
        scan_id: str,
1472
        host: str = '',
1473
        hostname: str = '',
1474
        name: str = '',
1475
        value: str = '',
1476
        port: str = '',
1477
        test_id='',
1478
        uri: str = '',
1479
    ) -> None:
1480
        """ Adds an error result to scan_id scan. """
1481
        self.scan_collection.add_result(
1482
            scan_id,
1483
            ResultType.ERROR,
1484
            host,
1485
            hostname,
1486
            name,
1487
            value,
1488
            port,
1489
            test_id,
1490
            uri,
1491
        )
1492
1493
    def add_scan_host_detail(
1494
        self,
1495
        scan_id: str,
1496
        host: str = '',
1497
        hostname: str = '',
1498
        name: str = '',
1499
        value: str = '',
1500
        uri: str = '',
1501
    ) -> None:
1502
        """ Adds a host detail result to scan_id scan. """
1503
        self.scan_collection.add_result(
1504
            scan_id, ResultType.HOST_DETAIL, host, hostname, name, value, uri
1505
        )
1506
1507
    def add_scan_alarm(
1508
        self,
1509
        scan_id: str,
1510
        host: str = '',
1511
        hostname: str = '',
1512
        name: str = '',
1513
        value: str = '',
1514
        port: str = '',
1515
        test_id: str = '',
1516
        severity: str = '',
1517
        qod: str = '',
1518
        uri: str = '',
1519
    ) -> None:
1520
        """ Adds an alarm result to scan_id scan. """
1521
        self.scan_collection.add_result(
1522
            scan_id,
1523
            ResultType.ALARM,
1524
            host,
1525
            hostname,
1526
            name,
1527
            value,
1528
            port,
1529
            test_id,
1530
            severity,
1531
            qod,
1532
            uri,
1533
        )
1534