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