1 | # -*- coding: utf-8 -*- |
||
2 | # Copyright (C) 2014-2021 Greenbone Networks GmbH |
||
3 | # |
||
4 | # SPDX-License-Identifier: AGPL-3.0-or-later |
||
5 | # |
||
6 | # This program is free software: you can redistribute it and/or modify |
||
7 | # it under the terms of the GNU Affero General Public License as |
||
8 | # published by the Free Software Foundation, either version 3 of the |
||
9 | # License, or (at your option) any later version. |
||
10 | # |
||
11 | # This program is distributed in the hope that it will be useful, |
||
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
14 | # GNU Affero General Public License for more details. |
||
15 | # |
||
16 | # You should have received a copy of the GNU Affero General Public License |
||
17 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
18 | |||
19 | |||
20 | # pylint: disable=too-many-lines |
||
21 | |||
22 | """ Setup for the OSP OpenVAS Server. """ |
||
23 | |||
24 | import logging |
||
25 | import time |
||
26 | import copy |
||
27 | |||
28 | from typing import Optional, Dict, List, Tuple, Iterator |
||
29 | from datetime import datetime |
||
30 | |||
31 | from pathlib import Path |
||
32 | from os import geteuid, environ |
||
33 | from lxml.etree import tostring, SubElement, Element |
||
34 | |||
35 | import psutil |
||
36 | |||
37 | from ospd.ospd import OSPDaemon |
||
38 | from ospd.scan import ScanProgress, ScanStatus |
||
39 | from ospd.server import BaseServer |
||
40 | from ospd.main import main as daemon_main |
||
41 | from ospd.vtfilter import VtsFilter |
||
42 | from ospd.resultlist import ResultList |
||
43 | |||
44 | from ospd_openvas import __version__ |
||
45 | from ospd_openvas.errors import OspdOpenvasError |
||
46 | |||
47 | from ospd_openvas.dryrun import DryRun |
||
48 | from ospd_openvas.nvticache import NVTICache |
||
49 | from ospd_openvas.db import MainDB, BaseDB |
||
50 | from ospd_openvas.lock import LockFile |
||
51 | from ospd_openvas.preferencehandler import PreferenceHandler |
||
52 | from ospd_openvas.openvas import Openvas |
||
53 | from ospd_openvas.vthelper import VtHelper |
||
54 | |||
55 | SENTRY_DSN_OSPD_OPENVAS = environ.get("SENTRY_DSN_OSPD_OPENVAS") |
||
56 | if SENTRY_DSN_OSPD_OPENVAS: |
||
57 | import sentry_sdk |
||
58 | |||
59 | sentry_sdk.init( # pylint: disable=abstract-class-instantiated |
||
60 | SENTRY_DSN_OSPD_OPENVAS, |
||
61 | traces_sample_rate=1.0, |
||
62 | server_name=environ.get('SENTRY_SERVER_NAME'), |
||
63 | environment=environ.get('SENTRY_ENVIRONMENT'), |
||
64 | ) |
||
65 | |||
66 | logger = logging.getLogger(__name__) |
||
67 | |||
68 | |||
69 | OSPD_DESC = """ |
||
70 | This scanner runs OpenVAS to scan the target hosts. |
||
71 | |||
72 | OpenVAS (Open Vulnerability Assessment Scanner) is a powerful scanner |
||
73 | for vulnerabilities in IT infrastrucutres. The capabilities include |
||
74 | unauthenticated scanning as well as authenticated scanning for |
||
75 | various types of systems and services. |
||
76 | |||
77 | For more details about OpenVAS see: |
||
78 | http://www.openvas.org/ |
||
79 | |||
80 | The current version of ospd-openvas is a simple frame, which sends |
||
81 | the server parameters to the Greenbone Vulnerability Manager daemon (GVMd) and |
||
82 | checks the existence of OpenVAS binary. But it can not run scans yet. |
||
83 | """ |
||
84 | |||
85 | OSPD_PARAMS = { |
||
86 | 'auto_enable_dependencies': { |
||
87 | 'type': 'boolean', |
||
88 | 'name': 'auto_enable_dependencies', |
||
89 | 'default': 1, |
||
90 | 'mandatory': 1, |
||
91 | 'visible_for_client': True, |
||
92 | 'description': 'Automatically enable the plugins that are depended on', |
||
93 | }, |
||
94 | 'cgi_path': { |
||
95 | 'type': 'string', |
||
96 | 'name': 'cgi_path', |
||
97 | 'default': '/cgi-bin:/scripts', |
||
98 | 'mandatory': 1, |
||
99 | 'visible_for_client': True, |
||
100 | 'description': 'Look for default CGIs in /cgi-bin and /scripts', |
||
101 | }, |
||
102 | 'checks_read_timeout': { |
||
103 | 'type': 'integer', |
||
104 | 'name': 'checks_read_timeout', |
||
105 | 'default': 5, |
||
106 | 'mandatory': 1, |
||
107 | 'visible_for_client': True, |
||
108 | 'description': ( |
||
109 | 'Number of seconds that the security checks will ' |
||
110 | + 'wait for when doing a recv()' |
||
111 | ), |
||
112 | }, |
||
113 | 'non_simult_ports': { |
||
114 | 'type': 'string', |
||
115 | 'name': 'non_simult_ports', |
||
116 | 'default': '139, 445, 3389, Services/irc', |
||
117 | 'mandatory': 1, |
||
118 | 'visible_for_client': True, |
||
119 | 'description': ( |
||
120 | 'Prevent to make two connections on the same given ' |
||
121 | + 'ports at the same time.' |
||
122 | ), |
||
123 | }, |
||
124 | 'open_sock_max_attempts': { |
||
125 | 'type': 'integer', |
||
126 | 'name': 'open_sock_max_attempts', |
||
127 | 'default': 5, |
||
128 | 'mandatory': 0, |
||
129 | 'visible_for_client': True, |
||
130 | 'description': ( |
||
131 | 'Number of unsuccessful retries to open the socket ' |
||
132 | + 'before to set the port as closed.' |
||
133 | ), |
||
134 | }, |
||
135 | 'timeout_retry': { |
||
136 | 'type': 'integer', |
||
137 | 'name': 'timeout_retry', |
||
138 | 'default': 5, |
||
139 | 'mandatory': 0, |
||
140 | 'visible_for_client': True, |
||
141 | 'description': ( |
||
142 | 'Number of retries when a socket connection attempt ' + 'timesout.' |
||
143 | ), |
||
144 | }, |
||
145 | 'optimize_test': { |
||
146 | 'type': 'boolean', |
||
147 | 'name': 'optimize_test', |
||
148 | 'default': 1, |
||
149 | 'mandatory': 0, |
||
150 | 'visible_for_client': True, |
||
151 | 'description': ( |
||
152 | 'By default, optimize_test is enabled which means openvas does ' |
||
153 | + 'trust the remote host banners and is only launching plugins ' |
||
154 | + 'against the services they have been designed to check. ' |
||
155 | + 'For example it will check a web server claiming to be IIS only ' |
||
156 | + 'for IIS related flaws but will skip plugins testing for Apache ' |
||
157 | + 'flaws, and so on. This default behavior is used to optimize ' |
||
158 | + 'the scanning performance and to avoid false positives. ' |
||
159 | + 'If you are not sure that the banners of the remote host ' |
||
160 | + 'have been tampered with, you can disable this option.' |
||
161 | ), |
||
162 | }, |
||
163 | 'plugins_timeout': { |
||
164 | 'type': 'integer', |
||
165 | 'name': 'plugins_timeout', |
||
166 | 'default': 5, |
||
167 | 'mandatory': 0, |
||
168 | 'visible_for_client': True, |
||
169 | 'description': 'This is the maximum lifetime, in seconds of a plugin.', |
||
170 | }, |
||
171 | 'report_host_details': { |
||
172 | 'type': 'boolean', |
||
173 | 'name': 'report_host_details', |
||
174 | 'default': 1, |
||
175 | 'mandatory': 1, |
||
176 | 'visible_for_client': True, |
||
177 | 'description': '', |
||
178 | }, |
||
179 | 'safe_checks': { |
||
180 | 'type': 'boolean', |
||
181 | 'name': 'safe_checks', |
||
182 | 'default': 1, |
||
183 | 'mandatory': 1, |
||
184 | 'visible_for_client': True, |
||
185 | 'description': ( |
||
186 | 'Disable the plugins with potential to crash ' |
||
187 | + 'the remote services' |
||
188 | ), |
||
189 | }, |
||
190 | 'scanner_plugins_timeout': { |
||
191 | 'type': 'integer', |
||
192 | 'name': 'scanner_plugins_timeout', |
||
193 | 'default': 36000, |
||
194 | 'mandatory': 1, |
||
195 | 'visible_for_client': True, |
||
196 | 'description': 'Like plugins_timeout, but for ACT_SCANNER plugins.', |
||
197 | }, |
||
198 | 'time_between_request': { |
||
199 | 'type': 'integer', |
||
200 | 'name': 'time_between_request', |
||
201 | 'default': 0, |
||
202 | 'mandatory': 0, |
||
203 | 'visible_for_client': True, |
||
204 | 'description': ( |
||
205 | 'Allow to set a wait time between two actions ' |
||
206 | + '(open, send, close).' |
||
207 | ), |
||
208 | }, |
||
209 | 'unscanned_closed': { |
||
210 | 'type': 'boolean', |
||
211 | 'name': 'unscanned_closed', |
||
212 | 'default': 1, |
||
213 | 'mandatory': 1, |
||
214 | 'visible_for_client': True, |
||
215 | 'description': '', |
||
216 | }, |
||
217 | 'unscanned_closed_udp': { |
||
218 | 'type': 'boolean', |
||
219 | 'name': 'unscanned_closed_udp', |
||
220 | 'default': 1, |
||
221 | 'mandatory': 1, |
||
222 | 'visible_for_client': True, |
||
223 | 'description': '', |
||
224 | }, |
||
225 | 'expand_vhosts': { |
||
226 | 'type': 'boolean', |
||
227 | 'name': 'expand_vhosts', |
||
228 | 'default': 1, |
||
229 | 'mandatory': 0, |
||
230 | 'visible_for_client': True, |
||
231 | 'description': 'Whether to expand the target hosts ' |
||
232 | + 'list of vhosts with values gathered from sources ' |
||
233 | + 'such as reverse-lookup queries and VT checks ' |
||
234 | + 'for SSL/TLS certificates.', |
||
235 | }, |
||
236 | 'test_empty_vhost': { |
||
237 | 'type': 'boolean', |
||
238 | 'name': 'test_empty_vhost', |
||
239 | 'default': 0, |
||
240 | 'mandatory': 0, |
||
241 | 'visible_for_client': True, |
||
242 | 'description': 'If set to yes, the scanner will ' |
||
243 | + 'also test the target by using empty vhost value ' |
||
244 | + 'in addition to the targets associated vhost values.', |
||
245 | }, |
||
246 | 'max_hosts': { |
||
247 | 'type': 'integer', |
||
248 | 'name': 'max_hosts', |
||
249 | 'default': 30, |
||
250 | 'mandatory': 0, |
||
251 | 'visible_for_client': False, |
||
252 | 'description': ( |
||
253 | 'The maximum number of hosts to test at the same time which ' |
||
254 | + 'should be given to the client (which can override it). ' |
||
255 | + 'This value must be computed given your bandwidth, ' |
||
256 | + 'the number of hosts you want to test, your amount of ' |
||
257 | + 'memory and the performance of your processor(s).' |
||
258 | ), |
||
259 | }, |
||
260 | 'max_checks': { |
||
261 | 'type': 'integer', |
||
262 | 'name': 'max_checks', |
||
263 | 'default': 10, |
||
264 | 'mandatory': 0, |
||
265 | 'visible_for_client': False, |
||
266 | 'description': ( |
||
267 | 'The number of plugins that will run against each host being ' |
||
268 | + 'tested. Note that the total number of process will be max ' |
||
269 | + 'checks x max_hosts so you need to find a balance between ' |
||
270 | + 'these two options. Note that launching too many plugins at ' |
||
271 | + 'the same time may disable the remote host, either temporarily ' |
||
272 | + '(ie: inetd closes its ports) or definitely (the remote host ' |
||
273 | + 'crash because it is asked to do too many things at the ' |
||
274 | + 'same time), so be careful.' |
||
275 | ), |
||
276 | }, |
||
277 | 'port_range': { |
||
278 | 'type': 'string', |
||
279 | 'name': 'port_range', |
||
280 | 'default': '', |
||
281 | 'mandatory': 0, |
||
282 | 'visible_for_client': False, |
||
283 | 'description': ( |
||
284 | 'This is the default range of ports that the scanner plugins will ' |
||
285 | + 'probe. The syntax of this option is flexible, it can be a ' |
||
286 | + 'single range ("1-1500"), several ports ("21,23,80"), several ' |
||
287 | + 'ranges of ports ("1-1500,32000-33000"). Note that you can ' |
||
288 | + 'specify UDP and TCP ports by prefixing each range by T or U. ' |
||
289 | + 'For instance, the following range will make openvas scan UDP ' |
||
290 | + 'ports 1 to 1024 and TCP ports 1 to 65535 : ' |
||
291 | + '"T:1-65535,U:1-1024".' |
||
292 | ), |
||
293 | }, |
||
294 | 'test_alive_hosts_only': { |
||
295 | 'type': 'boolean', |
||
296 | 'name': 'test_alive_hosts_only', |
||
297 | 'default': 0, |
||
298 | 'mandatory': 0, |
||
299 | 'visible_for_client': False, |
||
300 | 'description': ( |
||
301 | 'If this option is set, openvas will scan the target list for ' |
||
302 | + 'alive hosts in a separate process while only testing those ' |
||
303 | + 'hosts which are identified as alive. This boosts the scan ' |
||
304 | + 'speed of target ranges with a high amount of dead hosts ' |
||
305 | + 'significantly.' |
||
306 | ), |
||
307 | }, |
||
308 | 'hosts_allow': { |
||
309 | 'type': 'string', |
||
310 | 'name': 'hosts_allow', |
||
311 | 'default': '', |
||
312 | 'mandatory': 0, |
||
313 | 'visible_for_client': False, |
||
314 | 'description': ( |
||
315 | 'Comma-separated list of the only targets that are authorized ' |
||
316 | + 'to be scanned. Supports the same syntax as the list targets. ' |
||
317 | + 'Both target hostnames and the address to which they resolve ' |
||
318 | + 'are checked. Hostnames in hosts_allow list are not resolved ' |
||
319 | + 'however.' |
||
320 | ), |
||
321 | }, |
||
322 | 'hosts_deny': { |
||
323 | 'type': 'string', |
||
324 | 'name': 'hosts_deny', |
||
325 | 'default': '', |
||
326 | 'mandatory': 0, |
||
327 | 'visible_for_client': False, |
||
328 | 'description': ( |
||
329 | 'Comma-separated list of targets that are not authorized to ' |
||
330 | + 'be scanned. Supports the same syntax as the list targets. ' |
||
331 | + 'Both target hostnames and the address to which they resolve ' |
||
332 | + 'are checked. Hostnames in hosts_deny list are not ' |
||
333 | + 'resolved however.' |
||
334 | ), |
||
335 | }, |
||
336 | 'results_per_host': { |
||
337 | 'type': 'integer', |
||
338 | 'name': 'results_per_host', |
||
339 | 'default': 10, |
||
340 | 'mandatory': 0, |
||
341 | 'visible_for_client': True, |
||
342 | 'description': ( |
||
343 | 'Amount of fake results generated per each host in the target ' |
||
344 | + 'list for a dry run scan.' |
||
345 | ), |
||
346 | }, |
||
347 | } |
||
348 | |||
349 | VT_BASE_OID = "1.3.6.1.4.1.25623." |
||
350 | |||
351 | |||
352 | def safe_int(value: str) -> Optional[int]: |
||
353 | """Convert a string into an integer and return None in case of errors |
||
354 | during conversion |
||
355 | """ |
||
356 | try: |
||
357 | return int(value) |
||
358 | except (ValueError, TypeError): |
||
359 | return None |
||
360 | |||
361 | |||
362 | class OpenVasVtsFilter(VtsFilter): |
||
363 | |||
364 | """Methods to overwrite the ones in the original class.""" |
||
365 | |||
366 | def __init__(self, nvticache: NVTICache) -> None: |
||
367 | super().__init__() |
||
368 | |||
369 | self.nvti = nvticache |
||
370 | |||
371 | def format_vt_modification_time(self, value: str) -> str: |
||
372 | """Convert the string seconds since epoch into a 19 character |
||
373 | string representing YearMonthDayHourMinuteSecond, |
||
374 | e.g. 20190319122532. This always refers to UTC. |
||
375 | """ |
||
376 | |||
377 | return datetime.utcfromtimestamp(int(value)).strftime("%Y%m%d%H%M%S") |
||
378 | |||
379 | def get_filtered_vts_list(self, vts, vt_filter: str) -> Optional[List[str]]: |
||
380 | """Gets a collection of vulnerability test from the redis cache, |
||
381 | which match the filter. |
||
382 | |||
383 | Arguments: |
||
384 | vt_filter: Filter to apply to the vts collection. |
||
385 | vts: The complete vts collection. |
||
386 | |||
387 | Returns: |
||
388 | List with filtered vulnerability tests. The list can be empty. |
||
389 | None in case of filter parse failure. |
||
390 | """ |
||
391 | filters = self.parse_filters(vt_filter) |
||
392 | if not filters: |
||
393 | return None |
||
394 | |||
395 | if not self.nvti: |
||
396 | return None |
||
397 | |||
398 | vt_oid_list = [vtlist[1] for vtlist in self.nvti.get_oids()] |
||
399 | vt_oid_list_temp = copy.copy(vt_oid_list) |
||
400 | vthelper = VtHelper(self.nvti) |
||
401 | |||
402 | for element, oper, filter_val in filters: |
||
403 | for vt_oid in vt_oid_list_temp: |
||
404 | if vt_oid not in vt_oid_list: |
||
405 | continue |
||
406 | |||
407 | vt = vthelper.get_single_vt(vt_oid) |
||
408 | if vt is None or not vt.get(element): |
||
409 | vt_oid_list.remove(vt_oid) |
||
410 | continue |
||
411 | |||
412 | elem_val = vt.get(element) |
||
413 | val = self.format_filter_value(element, elem_val) |
||
414 | |||
415 | if self.filter_operator[oper](val, filter_val): |
||
416 | continue |
||
417 | else: |
||
418 | vt_oid_list.remove(vt_oid) |
||
419 | |||
420 | return vt_oid_list |
||
421 | |||
422 | |||
423 | class OSPDopenvas(OSPDaemon): |
||
424 | |||
425 | """Class for ospd-openvas daemon.""" |
||
426 | |||
427 | def __init__( |
||
428 | self, *, niceness=None, lock_file_dir='/var/lib/openvas', **kwargs |
||
429 | ): |
||
430 | """Initializes the ospd-openvas daemon's internal data.""" |
||
431 | self.main_db = MainDB() |
||
432 | self.nvti = NVTICache(self.main_db) |
||
433 | |||
434 | super().__init__( |
||
435 | customvtfilter=OpenVasVtsFilter(self.nvti), |
||
436 | storage=dict, |
||
437 | file_storage_dir=lock_file_dir, |
||
438 | **kwargs, |
||
439 | ) |
||
440 | |||
441 | self.server_version = __version__ |
||
442 | |||
443 | self._niceness = str(niceness) |
||
444 | |||
445 | self.feed_lock = LockFile(Path(lock_file_dir) / 'feed-update.lock') |
||
446 | self.daemon_info['name'] = 'OSPd OpenVAS' |
||
447 | self.scanner_info['name'] = 'openvas' |
||
448 | self.scanner_info['version'] = '' # achieved during self.init() |
||
449 | self.scanner_info['description'] = OSPD_DESC |
||
450 | |||
451 | for name, param in OSPD_PARAMS.items(): |
||
452 | self.set_scanner_param(name, param) |
||
453 | |||
454 | self._sudo_available = None |
||
455 | self._is_running_as_root = None |
||
456 | |||
457 | self.scan_only_params = dict() |
||
458 | |||
459 | def init(self, server: BaseServer) -> None: |
||
460 | |||
461 | self.scan_collection.init() |
||
462 | |||
463 | server.start(self.handle_client_stream) |
||
464 | |||
465 | self.scanner_info['version'] = Openvas.get_version() |
||
466 | |||
467 | self.set_params_from_openvas_settings() |
||
468 | |||
469 | with self.feed_lock.wait_for_lock(): |
||
470 | Openvas.load_vts_into_redis() |
||
471 | current_feed = self.nvti.get_feed_version() |
||
472 | self.set_vts_version(vts_version=current_feed) |
||
473 | |||
474 | logger.debug("Calculating vts integrity check hash...") |
||
475 | vthelper = VtHelper(self.nvti) |
||
476 | self.vts.sha256_hash = vthelper.calculate_vts_collection_hash() |
||
477 | |||
478 | self.initialized = True |
||
479 | |||
480 | def set_params_from_openvas_settings(self): |
||
481 | """Set OSPD_PARAMS with the params taken from the openvas executable.""" |
||
482 | param_list = Openvas.get_settings() |
||
483 | |||
484 | for elem in param_list: # pylint: disable=consider-using-dict-items |
||
485 | if elem not in OSPD_PARAMS: |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||
486 | self.scan_only_params[elem] = param_list[elem] |
||
487 | else: |
||
488 | OSPD_PARAMS[elem]['default'] = param_list[elem] |
||
489 | |||
490 | def feed_is_outdated(self, current_feed: str) -> Optional[bool]: |
||
491 | """Compare the current feed with the one in the disk. |
||
492 | |||
493 | Return: |
||
494 | False if there is no new feed. |
||
495 | True if the feed version in disk is newer than the feed in |
||
496 | redis cache. |
||
497 | None if there is no feed on the disk. |
||
498 | """ |
||
499 | plugins_folder = self.scan_only_params.get('plugins_folder') |
||
500 | if not plugins_folder: |
||
501 | raise OspdOpenvasError("Error: Path to plugins folder not found.") |
||
502 | |||
503 | feed_info_file = Path(plugins_folder) / 'plugin_feed_info.inc' |
||
504 | if not feed_info_file.exists(): |
||
505 | self.set_params_from_openvas_settings() |
||
506 | logger.debug('Plugins feed file %s not found.', feed_info_file) |
||
507 | return None |
||
508 | |||
509 | current_feed = safe_int(current_feed) |
||
510 | if current_feed is None: |
||
511 | logger.debug( |
||
512 | "Wrong PLUGIN_SET format in plugins feed file %s. Format has to" |
||
513 | " be yyyymmddhhmm. For example 'PLUGIN_SET = \"201910251033\"'", |
||
514 | feed_info_file, |
||
515 | ) |
||
516 | |||
517 | feed_date = None |
||
518 | with feed_info_file.open() as fcontent: |
||
519 | for line in fcontent: |
||
520 | if "PLUGIN_SET" in line: |
||
521 | feed_date = line.split('=', 1)[1] |
||
522 | feed_date = feed_date.strip() |
||
523 | feed_date = feed_date.replace(';', '') |
||
524 | feed_date = feed_date.replace('"', '') |
||
525 | feed_date = safe_int(feed_date) |
||
526 | break |
||
527 | |||
528 | logger.debug("Current feed version: %s", current_feed) |
||
529 | logger.debug("Plugin feed version: %s", feed_date) |
||
530 | |||
531 | return ( |
||
532 | (not feed_date) or (not current_feed) or (current_feed < feed_date) |
||
533 | ) |
||
534 | |||
535 | def check_feed(self): |
||
536 | """Check if there is a feed update. |
||
537 | |||
538 | Wait until all the running scans finished. Set a flag to announce there |
||
539 | is a pending feed update, which avoids to start a new scan. |
||
540 | """ |
||
541 | if not self.vts.is_cache_available: |
||
542 | return |
||
543 | |||
544 | current_feed = self.nvti.get_feed_version() |
||
545 | is_outdated = self.feed_is_outdated(current_feed) |
||
546 | |||
547 | # Check if the nvticache in redis is outdated |
||
548 | if not current_feed or is_outdated: |
||
549 | with self.feed_lock as fl: |
||
550 | if fl.has_lock(): |
||
551 | self.initialized = False |
||
552 | Openvas.load_vts_into_redis() |
||
553 | current_feed = self.nvti.get_feed_version() |
||
554 | self.set_vts_version(vts_version=current_feed) |
||
555 | |||
556 | vthelper = VtHelper(self.nvti) |
||
557 | self.vts.sha256_hash = ( |
||
558 | vthelper.calculate_vts_collection_hash() |
||
559 | ) |
||
560 | self.initialized = True |
||
561 | else: |
||
562 | logger.debug( |
||
563 | "The feed was not upload or it is outdated, " |
||
564 | "but other process is locking the update. " |
||
565 | "Trying again later..." |
||
566 | ) |
||
567 | return |
||
568 | |||
569 | def scheduler(self): |
||
570 | """This method is called periodically to run tasks.""" |
||
571 | self.check_feed() |
||
572 | |||
573 | def get_vt_iterator( |
||
574 | self, vt_selection: List[str] = None, details: bool = True |
||
575 | ) -> Iterator[Tuple[str, Dict]]: |
||
576 | vthelper = VtHelper(self.nvti) |
||
577 | return vthelper.get_vt_iterator(vt_selection, details) |
||
578 | |||
579 | @staticmethod |
||
580 | def get_custom_vt_as_xml_str(vt_id: str, custom: Dict) -> str: |
||
581 | """Return an xml element with custom metadata formatted as string. |
||
582 | Arguments: |
||
583 | vt_id: VT OID. Only used for logging in error case. |
||
584 | custom: Dictionary with the custom metadata. |
||
585 | Return: |
||
586 | Xml element as string. |
||
587 | """ |
||
588 | |||
589 | _custom = Element('custom') |
||
590 | for key, val in custom.items(): |
||
591 | xml_key = SubElement(_custom, key) |
||
592 | try: |
||
593 | xml_key.text = val |
||
594 | except ValueError as e: |
||
595 | logger.warning( |
||
596 | "Not possible to parse custom tag for VT %s: %s", vt_id, e |
||
597 | ) |
||
598 | return tostring(_custom).decode('utf-8') |
||
599 | |||
600 | @staticmethod |
||
601 | def get_severities_vt_as_xml_str(vt_id: str, severities: Dict) -> str: |
||
602 | """Return an xml element with severities as string. |
||
603 | Arguments: |
||
604 | vt_id: VT OID. Only used for logging in error case. |
||
605 | severities: Dictionary with the severities. |
||
606 | Return: |
||
607 | Xml element as string. |
||
608 | """ |
||
609 | _severities = Element('severities') |
||
610 | _severity = SubElement(_severities, 'severity') |
||
611 | if 'severity_base_vector' in severities: |
||
612 | try: |
||
613 | _value = SubElement(_severity, 'value') |
||
614 | _value.text = severities.get('severity_base_vector') |
||
615 | except ValueError as e: |
||
616 | logger.warning( |
||
617 | "Not possible to parse severity tag for vt %s: %s", vt_id, e |
||
618 | ) |
||
619 | if 'severity_origin' in severities: |
||
620 | _origin = SubElement(_severity, 'origin') |
||
621 | _origin.text = severities.get('severity_origin') |
||
622 | if 'severity_date' in severities: |
||
623 | _date = SubElement(_severity, 'date') |
||
624 | _date.text = severities.get('severity_date') |
||
625 | if 'severity_type' in severities: |
||
626 | _severity.set('type', severities.get('severity_type')) |
||
627 | |||
628 | return tostring(_severities).decode('utf-8') |
||
629 | |||
630 | @staticmethod |
||
631 | def get_params_vt_as_xml_str(vt_id: str, vt_params: Dict) -> str: |
||
632 | """Return an xml element with params formatted as string. |
||
633 | Arguments: |
||
634 | vt_id: VT OID. Only used for logging in error case. |
||
635 | vt_params: Dictionary with the VT parameters. |
||
636 | Return: |
||
637 | Xml element as string. |
||
638 | """ |
||
639 | vt_params_xml = Element('params') |
||
640 | for _pref_id, prefs in vt_params.items(): |
||
641 | vt_param = Element('param') |
||
642 | vt_param.set('type', prefs['type']) |
||
643 | vt_param.set('id', _pref_id) |
||
644 | xml_name = SubElement(vt_param, 'name') |
||
645 | try: |
||
646 | xml_name.text = prefs['name'] |
||
647 | except ValueError as e: |
||
648 | logger.warning( |
||
649 | "Not possible to parse parameter for VT %s: %s", vt_id, e |
||
650 | ) |
||
651 | if prefs['default']: |
||
652 | xml_def = SubElement(vt_param, 'default') |
||
653 | try: |
||
654 | xml_def.text = prefs['default'] |
||
655 | except ValueError as e: |
||
656 | logger.warning( |
||
657 | "Not possible to parse default parameter for VT %s: %s", |
||
658 | vt_id, |
||
659 | e, |
||
660 | ) |
||
661 | vt_params_xml.append(vt_param) |
||
662 | |||
663 | return tostring(vt_params_xml).decode('utf-8') |
||
664 | |||
665 | @staticmethod |
||
666 | def get_refs_vt_as_xml_str(vt_id: str, vt_refs: Dict) -> str: |
||
667 | """Return an xml element with references formatted as string. |
||
668 | Arguments: |
||
669 | vt_id: VT OID. Only used for logging in error case. |
||
670 | vt_refs: Dictionary with the VT references. |
||
671 | Return: |
||
672 | Xml element as string. |
||
673 | """ |
||
674 | vt_refs_xml = Element('refs') |
||
675 | for ref_type, ref_values in vt_refs.items(): |
||
676 | for value in ref_values: |
||
677 | vt_ref = Element('ref') |
||
678 | if ref_type == "xref" and value: |
||
679 | for xref in value.split(', '): |
||
680 | try: |
||
681 | _type, _id = xref.split(':', 1) |
||
682 | except ValueError as e: |
||
683 | logger.error( |
||
684 | 'Not possible to parse xref "%s" for VT %s: %s', |
||
685 | xref, |
||
686 | vt_id, |
||
687 | e, |
||
688 | ) |
||
689 | continue |
||
690 | vt_ref.set('type', _type.lower()) |
||
691 | vt_ref.set('id', _id) |
||
692 | elif value: |
||
693 | vt_ref.set('type', ref_type.lower()) |
||
694 | vt_ref.set('id', value) |
||
695 | else: |
||
696 | continue |
||
697 | vt_refs_xml.append(vt_ref) |
||
698 | |||
699 | return tostring(vt_refs_xml).decode('utf-8') |
||
700 | |||
701 | @staticmethod |
||
702 | def get_dependencies_vt_as_xml_str( |
||
703 | vt_id: str, vt_dependencies: List |
||
704 | ) -> str: |
||
705 | """Return an xml element with dependencies as string. |
||
706 | Arguments: |
||
707 | vt_id: VT OID. Only used for logging in error case. |
||
708 | vt_dependencies: List with the VT dependencies. |
||
709 | Return: |
||
710 | Xml element as string. |
||
711 | """ |
||
712 | vt_deps_xml = Element('dependencies') |
||
713 | for dep in vt_dependencies: |
||
714 | _vt_dep = Element('dependency') |
||
715 | if VT_BASE_OID in dep: |
||
716 | _vt_dep.set('vt_id', dep) |
||
717 | else: |
||
718 | logger.error( |
||
719 | 'Not possible to add dependency %s for VT %s', dep, vt_id |
||
720 | ) |
||
721 | continue |
||
722 | vt_deps_xml.append(_vt_dep) |
||
723 | |||
724 | return tostring(vt_deps_xml).decode('utf-8') |
||
725 | |||
726 | @staticmethod |
||
727 | def get_creation_time_vt_as_xml_str( |
||
728 | vt_id: str, vt_creation_time: str |
||
729 | ) -> str: |
||
730 | """Return creation time as string. |
||
731 | Arguments: |
||
732 | vt_id: VT OID. Only used for logging in error case. |
||
733 | vt_creation_time: String with the VT creation time. |
||
734 | Return: |
||
735 | Xml element as string. |
||
736 | """ |
||
737 | _time = Element('creation_time') |
||
738 | try: |
||
739 | _time.text = vt_creation_time |
||
740 | except ValueError as e: |
||
741 | logger.warning( |
||
742 | "Not possible to parse creation time for VT %s: %s", vt_id, e |
||
743 | ) |
||
744 | return tostring(_time).decode('utf-8') |
||
745 | |||
746 | @staticmethod |
||
747 | def get_modification_time_vt_as_xml_str( |
||
748 | vt_id: str, vt_modification_time: str |
||
749 | ) -> str: |
||
750 | """Return modification time as string. |
||
751 | Arguments: |
||
752 | vt_id: VT OID. Only used for logging in error case. |
||
753 | vt_modification_time: String with the VT modification time. |
||
754 | Return: |
||
755 | Xml element as string. |
||
756 | """ |
||
757 | _time = Element('modification_time') |
||
758 | try: |
||
759 | _time.text = vt_modification_time |
||
760 | except ValueError as e: |
||
761 | logger.warning( |
||
762 | "Not possible to parse modification time for VT %s: %s", |
||
763 | vt_id, |
||
764 | e, |
||
765 | ) |
||
766 | return tostring(_time).decode('utf-8') |
||
767 | |||
768 | @staticmethod |
||
769 | def get_summary_vt_as_xml_str(vt_id: str, summary: str) -> str: |
||
770 | """Return summary as string. |
||
771 | Arguments: |
||
772 | vt_id: VT OID. Only used for logging in error case. |
||
773 | summary: String with a VT summary. |
||
774 | Return: |
||
775 | Xml element as string. |
||
776 | """ |
||
777 | _summary = Element('summary') |
||
778 | try: |
||
779 | _summary.text = summary |
||
780 | except ValueError as e: |
||
781 | logger.warning( |
||
782 | "Not possible to parse summary tag for VT %s: %s", vt_id, e |
||
783 | ) |
||
784 | return tostring(_summary).decode('utf-8') |
||
785 | |||
786 | @staticmethod |
||
787 | def get_impact_vt_as_xml_str(vt_id: str, impact) -> str: |
||
788 | """Return impact as string. |
||
789 | |||
790 | Arguments: |
||
791 | vt_id (str): VT OID. Only used for logging in error case. |
||
792 | impact (str): String which explain the vulneravility impact. |
||
793 | Return: |
||
794 | string: xml element as string. |
||
795 | """ |
||
796 | _impact = Element('impact') |
||
797 | try: |
||
798 | _impact.text = impact |
||
799 | except ValueError as e: |
||
800 | logger.warning( |
||
801 | "Not possible to parse impact tag for VT %s: %s", vt_id, e |
||
802 | ) |
||
803 | return tostring(_impact).decode('utf-8') |
||
804 | |||
805 | @staticmethod |
||
806 | def get_affected_vt_as_xml_str(vt_id: str, affected: str) -> str: |
||
807 | """Return affected as string. |
||
808 | Arguments: |
||
809 | vt_id: VT OID. Only used for logging in error case. |
||
810 | affected: String which explain what is affected. |
||
811 | Return: |
||
812 | Xml element as string. |
||
813 | """ |
||
814 | _affected = Element('affected') |
||
815 | try: |
||
816 | _affected.text = affected |
||
817 | except ValueError as e: |
||
818 | logger.warning( |
||
819 | "Not possible to parse affected tag for VT %s: %s", vt_id, e |
||
820 | ) |
||
821 | return tostring(_affected).decode('utf-8') |
||
822 | |||
823 | @staticmethod |
||
824 | def get_insight_vt_as_xml_str(vt_id: str, insight: str) -> str: |
||
825 | """Return insight as string. |
||
826 | Arguments: |
||
827 | vt_id: VT OID. Only used for logging in error case. |
||
828 | insight: String giving an insight of the vulnerability. |
||
829 | Return: |
||
830 | Xml element as string. |
||
831 | """ |
||
832 | _insight = Element('insight') |
||
833 | try: |
||
834 | _insight.text = insight |
||
835 | except ValueError as e: |
||
836 | logger.warning( |
||
837 | "Not possible to parse insight tag for VT %s: %s", vt_id, e |
||
838 | ) |
||
839 | return tostring(_insight).decode('utf-8') |
||
840 | |||
841 | @staticmethod |
||
842 | def get_solution_vt_as_xml_str( |
||
843 | vt_id: str, |
||
844 | solution: str, |
||
845 | solution_type: Optional[str] = None, |
||
846 | solution_method: Optional[str] = None, |
||
847 | ) -> str: |
||
848 | """Return solution as string. |
||
849 | Arguments: |
||
850 | vt_id: VT OID. Only used for logging in error case. |
||
851 | solution: String giving a possible solution. |
||
852 | solution_type: A solution type |
||
853 | solution_method: A solution method |
||
854 | Return: |
||
855 | Xml element as string. |
||
856 | """ |
||
857 | _solution = Element('solution') |
||
858 | try: |
||
859 | _solution.text = solution |
||
860 | except ValueError as e: |
||
861 | logger.warning( |
||
862 | "Not possible to parse solution tag for VT %s: %s", vt_id, e |
||
863 | ) |
||
864 | if solution_type: |
||
865 | _solution.set('type', solution_type) |
||
866 | if solution_method: |
||
867 | _solution.set('method', solution_method) |
||
868 | return tostring(_solution).decode('utf-8') |
||
869 | |||
870 | @staticmethod |
||
871 | def get_detection_vt_as_xml_str( |
||
872 | vt_id: str, |
||
873 | detection: Optional[str] = None, |
||
874 | qod_type: Optional[str] = None, |
||
875 | qod: Optional[str] = None, |
||
876 | ) -> str: |
||
877 | """Return detection as string. |
||
878 | Arguments: |
||
879 | vt_id: VT OID. Only used for logging in error case. |
||
880 | detection: String which explain how the vulnerability |
||
881 | was detected. |
||
882 | qod_type: qod type. |
||
883 | qod: qod value. |
||
884 | Return: |
||
885 | Xml element as string. |
||
886 | """ |
||
887 | _detection = Element('detection') |
||
888 | if detection: |
||
889 | try: |
||
890 | _detection.text = detection |
||
891 | except ValueError as e: |
||
892 | logger.warning( |
||
893 | "Not possible to parse detection tag for VT %s: %s", |
||
894 | vt_id, |
||
895 | e, |
||
896 | ) |
||
897 | if qod_type: |
||
898 | _detection.set('qod_type', qod_type) |
||
899 | elif qod: |
||
900 | _detection.set('qod', qod) |
||
901 | |||
902 | return tostring(_detection).decode('utf-8') |
||
903 | |||
904 | @property |
||
905 | def is_running_as_root(self) -> bool: |
||
906 | """Check if it is running as root user.""" |
||
907 | if self._is_running_as_root is not None: |
||
908 | return self._is_running_as_root |
||
909 | |||
910 | self._is_running_as_root = False |
||
911 | if geteuid() == 0: |
||
912 | self._is_running_as_root = True |
||
913 | |||
914 | return self._is_running_as_root |
||
915 | |||
916 | @property |
||
917 | def sudo_available(self) -> bool: |
||
918 | """Checks that sudo is available""" |
||
919 | if self._sudo_available is not None: |
||
920 | return self._sudo_available |
||
921 | |||
922 | if self.is_running_as_root: |
||
923 | self._sudo_available = False |
||
924 | return self._sudo_available |
||
925 | |||
926 | self._sudo_available = Openvas.check_sudo() |
||
927 | |||
928 | return self._sudo_available |
||
929 | |||
930 | def check(self) -> bool: |
||
931 | """Checks that openvas command line tool is found and |
||
932 | is executable.""" |
||
933 | has_openvas = Openvas.check() |
||
934 | if not has_openvas: |
||
935 | logger.error( |
||
936 | 'openvas executable not available. Please install openvas' |
||
937 | ' into your PATH.' |
||
938 | ) |
||
939 | return has_openvas |
||
940 | |||
941 | def report_openvas_scan_status(self, kbdb: BaseDB, scan_id: str): |
||
942 | """Get all status entries from redis kb. |
||
943 | |||
944 | Arguments: |
||
945 | kbdb: KB context where to get the status from. |
||
946 | scan_id: Scan ID to identify the current scan. |
||
947 | """ |
||
948 | all_status = kbdb.get_scan_status() |
||
949 | all_hosts = dict() |
||
950 | finished_hosts = list() |
||
951 | for res in all_status: |
||
952 | try: |
||
953 | current_host, launched, total = res.split('/') |
||
954 | except ValueError: |
||
955 | continue |
||
956 | |||
957 | try: |
||
958 | if float(total) == 0: |
||
959 | continue |
||
960 | elif float(total) == ScanProgress.DEAD_HOST: |
||
961 | host_prog = ScanProgress.DEAD_HOST |
||
962 | else: |
||
963 | host_prog = int((float(launched) / float(total)) * 100) |
||
964 | except TypeError: |
||
965 | continue |
||
966 | |||
967 | all_hosts[current_host] = host_prog |
||
968 | |||
969 | if ( |
||
970 | host_prog == ScanProgress.DEAD_HOST |
||
971 | or host_prog == ScanProgress.FINISHED |
||
972 | ): |
||
973 | finished_hosts.append(current_host) |
||
974 | |||
975 | logger.debug( |
||
976 | '%s: Host %s has progress: %d', scan_id, current_host, host_prog |
||
977 | ) |
||
978 | |||
979 | self.set_scan_progress_batch(scan_id, host_progress=all_hosts) |
||
980 | |||
981 | self.sort_host_finished(scan_id, finished_hosts) |
||
982 | |||
983 | def report_openvas_results(self, db: BaseDB, scan_id: str) -> bool: |
||
984 | """Get all result entries from redis kb.""" |
||
985 | |||
986 | vthelper = VtHelper(self.nvti) |
||
987 | |||
988 | # Result messages come in the next form, with optional uri field |
||
989 | # type ||| host ip ||| hostname ||| port ||| OID ||| value [|||uri] |
||
990 | all_results = db.get_result() |
||
991 | res_list = ResultList() |
||
992 | total_dead = 0 |
||
993 | for res in all_results: |
||
994 | if not res: |
||
995 | continue |
||
996 | |||
997 | msg = res.split('|||') |
||
998 | roid = msg[4].strip() |
||
999 | rqod = '' |
||
1000 | rname = '' |
||
1001 | current_host = msg[1].strip() if msg[1] else '' |
||
1002 | rhostname = msg[2].strip() if msg[2] else '' |
||
1003 | host_is_dead = "Host dead" in msg[5] or msg[0] == "DEADHOST" |
||
1004 | host_deny = "Host access denied" in msg[5] |
||
1005 | start_end_msg = msg[0] == "HOST_START" or msg[0] == "HOST_END" |
||
1006 | host_count = msg[0] == "HOSTS_COUNT" |
||
1007 | vt_aux = None |
||
1008 | |||
1009 | # URI is optional and msg list length must be checked |
||
1010 | ruri = '' |
||
1011 | if len(msg) > 6: |
||
1012 | ruri = msg[6] |
||
1013 | |||
1014 | if ( |
||
1015 | roid |
||
1016 | and not host_is_dead |
||
1017 | and not host_deny |
||
1018 | and not start_end_msg |
||
1019 | and not host_count |
||
1020 | ): |
||
1021 | vt_aux = vthelper.get_single_vt(roid) |
||
1022 | |||
1023 | if ( |
||
1024 | not vt_aux |
||
1025 | and not host_is_dead |
||
1026 | and not host_deny |
||
1027 | and not start_end_msg |
||
1028 | and not host_count |
||
1029 | ): |
||
1030 | logger.warning('Invalid VT oid %s for a result', roid) |
||
1031 | |||
1032 | if vt_aux: |
||
1033 | if vt_aux.get('qod_type'): |
||
1034 | qod_t = vt_aux.get('qod_type') |
||
1035 | rqod = self.nvti.QOD_TYPES[qod_t] |
||
1036 | elif vt_aux.get('qod'): |
||
1037 | rqod = vt_aux.get('qod') |
||
1038 | |||
1039 | rname = vt_aux.get('name') |
||
1040 | |||
1041 | if msg[0] == 'ERRMSG': |
||
1042 | res_list.add_scan_error_to_list( |
||
1043 | host=current_host, |
||
1044 | hostname=rhostname, |
||
1045 | name=rname, |
||
1046 | value=msg[5], |
||
1047 | port=msg[3], |
||
1048 | test_id=roid, |
||
1049 | uri=ruri, |
||
1050 | ) |
||
1051 | |||
1052 | elif msg[0] == 'HOST_START' or msg[0] == 'HOST_END': |
||
1053 | res_list.add_scan_log_to_list( |
||
1054 | host=current_host, |
||
1055 | name=msg[0], |
||
1056 | value=msg[5], |
||
1057 | ) |
||
1058 | |||
1059 | elif msg[0] == 'LOG': |
||
1060 | res_list.add_scan_log_to_list( |
||
1061 | host=current_host, |
||
1062 | hostname=rhostname, |
||
1063 | name=rname, |
||
1064 | value=msg[5], |
||
1065 | port=msg[3], |
||
1066 | qod=rqod, |
||
1067 | test_id=roid, |
||
1068 | uri=ruri, |
||
1069 | ) |
||
1070 | |||
1071 | elif msg[0] == 'HOST_DETAIL': |
||
1072 | res_list.add_scan_host_detail_to_list( |
||
1073 | host=current_host, |
||
1074 | hostname=rhostname, |
||
1075 | name=rname, |
||
1076 | value=msg[5], |
||
1077 | uri=ruri, |
||
1078 | ) |
||
1079 | |||
1080 | elif msg[0] == 'ALARM': |
||
1081 | rseverity = vthelper.get_severity_score(vt_aux) |
||
1082 | res_list.add_scan_alarm_to_list( |
||
1083 | host=current_host, |
||
1084 | hostname=rhostname, |
||
1085 | name=rname, |
||
1086 | value=msg[5], |
||
1087 | port=msg[3], |
||
1088 | test_id=roid, |
||
1089 | severity=rseverity, |
||
1090 | qod=rqod, |
||
1091 | uri=ruri, |
||
1092 | ) |
||
1093 | |||
1094 | # To process non-scanned dead hosts when |
||
1095 | # test_alive_host_only in openvas is enable |
||
1096 | elif msg[0] == 'DEADHOST': |
||
1097 | try: |
||
1098 | total_dead = int(msg[5]) |
||
1099 | except TypeError: |
||
1100 | logger.debug('Error processing dead host count') |
||
1101 | |||
1102 | # To update total host count |
||
1103 | if msg[0] == 'HOSTS_COUNT': |
||
1104 | try: |
||
1105 | count_total = int(msg[5]) |
||
1106 | logger.debug( |
||
1107 | '%s: Set total hosts counted by OpenVAS: %d', |
||
1108 | scan_id, |
||
1109 | count_total, |
||
1110 | ) |
||
1111 | self.set_scan_total_hosts(scan_id, count_total) |
||
1112 | except TypeError: |
||
1113 | logger.debug('Error processing total host count') |
||
1114 | |||
1115 | # Insert result batch into the scan collection table. |
||
1116 | if len(res_list): |
||
1117 | self.scan_collection.add_result_list(scan_id, res_list) |
||
1118 | logger.debug( |
||
1119 | '%s: Inserting %d results into scan collection table', |
||
1120 | scan_id, |
||
1121 | len(res_list), |
||
1122 | ) |
||
1123 | if total_dead: |
||
1124 | logger.debug( |
||
1125 | '%s: Set dead hosts counted by OpenVAS: %d', |
||
1126 | scan_id, |
||
1127 | total_dead, |
||
1128 | ) |
||
1129 | self.scan_collection.set_amount_dead_hosts( |
||
1130 | scan_id, total_dead=total_dead |
||
1131 | ) |
||
1132 | |||
1133 | return len(res_list) > 0 |
||
1134 | |||
1135 | @staticmethod |
||
1136 | def is_openvas_process_alive(openvas_process: psutil.Popen) -> bool: |
||
1137 | |||
1138 | if openvas_process.status() == psutil.STATUS_ZOMBIE: |
||
1139 | logger.debug("Process is a Zombie, waiting for it to clean up") |
||
1140 | openvas_process.wait() |
||
1141 | return openvas_process.is_running() |
||
1142 | |||
1143 | def stop_scan_cleanup( |
||
1144 | self, |
||
1145 | kbdb: BaseDB, |
||
1146 | scan_id: str, |
||
1147 | ovas_process: psutil.Popen, # pylint: disable=arguments-differ |
||
1148 | ): |
||
1149 | """Set a key in redis to indicate the wrapper is stopped. |
||
1150 | It is done through redis because it is a new multiprocess |
||
1151 | instance and it is not possible to reach the variables |
||
1152 | of the grandchild process. |
||
1153 | Indirectly sends SIGUSR1 to the running openvas scan process |
||
1154 | via an invocation of openvas with the --scan-stop option to |
||
1155 | stop it.""" |
||
1156 | |||
1157 | if kbdb: |
||
1158 | # Set stop flag in redis |
||
1159 | kbdb.stop_scan(scan_id) |
||
1160 | |||
1161 | # Check if openvas is running |
||
1162 | if ovas_process.is_running(): |
||
1163 | # Cleaning in case of Zombie Process |
||
1164 | if ovas_process.status() == psutil.STATUS_ZOMBIE: |
||
1165 | logger.debug( |
||
1166 | '%s: Process with PID %s is a Zombie process.' |
||
1167 | ' Cleaning up...', |
||
1168 | scan_id, |
||
1169 | ovas_process.pid, |
||
1170 | ) |
||
1171 | ovas_process.wait() |
||
1172 | # Stop openvas process and wait until it stopped |
||
1173 | else: |
||
1174 | can_stop_scan = Openvas.stop_scan( |
||
1175 | scan_id, |
||
1176 | not self.is_running_as_root and self.sudo_available, |
||
1177 | ) |
||
1178 | if not can_stop_scan: |
||
1179 | logger.debug( |
||
1180 | 'Not possible to stop scan process: %s.', |
||
1181 | ovas_process, |
||
1182 | ) |
||
1183 | return |
||
1184 | |||
1185 | logger.debug('Stopping process: %s', ovas_process) |
||
1186 | |||
1187 | while ovas_process.is_running(): |
||
1188 | if ovas_process.status() == psutil.STATUS_ZOMBIE: |
||
1189 | ovas_process.wait() |
||
1190 | else: |
||
1191 | time.sleep(0.1) |
||
1192 | else: |
||
1193 | logger.debug( |
||
1194 | "%s: Process with PID %s already stopped", |
||
1195 | scan_id, |
||
1196 | ovas_process.pid, |
||
1197 | ) |
||
1198 | |||
1199 | # Clean redis db |
||
1200 | for scan_db in kbdb.get_scan_databases(): |
||
1201 | self.main_db.release_database(scan_db) |
||
1202 | |||
1203 | def exec_scan(self, scan_id: str): |
||
1204 | """Starts the OpenVAS scanner for scan_id scan.""" |
||
1205 | params = self.scan_collection.get_options(scan_id) |
||
1206 | if params.get("dry_run"): |
||
1207 | dryrun = DryRun(self) |
||
1208 | dryrun.exec_dry_run_scan(scan_id, self.nvti, OSPD_PARAMS) |
||
1209 | return |
||
1210 | |||
1211 | do_not_launch = False |
||
1212 | kbdb = self.main_db.get_new_kb_database() |
||
1213 | scan_prefs = PreferenceHandler( |
||
1214 | scan_id, kbdb, self.scan_collection, self.nvti |
||
1215 | ) |
||
1216 | kbdb.add_scan_id(scan_id) |
||
1217 | scan_prefs.prepare_target_for_openvas() |
||
1218 | |||
1219 | if not scan_prefs.prepare_ports_for_openvas(): |
||
1220 | self.add_scan_error( |
||
1221 | scan_id, name='', host='', value='Invalid port list.' |
||
1222 | ) |
||
1223 | do_not_launch = True |
||
1224 | |||
1225 | # Set credentials |
||
1226 | if not scan_prefs.prepare_credentials_for_openvas(): |
||
1227 | error = ( |
||
1228 | 'All authentifications contain errors.' |
||
1229 | + 'Starting unauthenticated scan instead.' |
||
1230 | ) |
||
1231 | self.add_scan_error( |
||
1232 | scan_id, |
||
1233 | name='', |
||
1234 | host='', |
||
1235 | value=error, |
||
1236 | ) |
||
1237 | logger.error(error) |
||
1238 | errors = scan_prefs.get_error_messages() |
||
1239 | for e in errors: |
||
1240 | error = 'Malformed credential. ' + e |
||
1241 | self.add_scan_error( |
||
1242 | scan_id, |
||
1243 | name='', |
||
1244 | host='', |
||
1245 | value=error, |
||
1246 | ) |
||
1247 | logger.error(error) |
||
1248 | |||
1249 | if not scan_prefs.prepare_plugins_for_openvas(): |
||
1250 | self.add_scan_error( |
||
1251 | scan_id, name='', host='', value='No VTS to run.' |
||
1252 | ) |
||
1253 | do_not_launch = True |
||
1254 | |||
1255 | scan_prefs.prepare_main_kbindex_for_openvas() |
||
1256 | scan_prefs.prepare_host_options_for_openvas() |
||
1257 | scan_prefs.prepare_scan_params_for_openvas(OSPD_PARAMS) |
||
1258 | scan_prefs.prepare_reverse_lookup_opt_for_openvas() |
||
1259 | scan_prefs.prepare_alive_test_option_for_openvas() |
||
1260 | |||
1261 | # VT preferences are stored after all preferences have been processed, |
||
1262 | # since alive tests preferences have to be able to overwrite default |
||
1263 | # preferences of ping_host.nasl for the classic method. |
||
1264 | scan_prefs.prepare_nvt_preferences() |
||
1265 | scan_prefs.prepare_boreas_alive_test() |
||
1266 | |||
1267 | # Release memory used for scan preferences. |
||
1268 | del scan_prefs |
||
1269 | |||
1270 | if do_not_launch or kbdb.scan_is_stopped(scan_id): |
||
1271 | self.main_db.release_database(kbdb) |
||
1272 | return |
||
1273 | |||
1274 | openvas_process = Openvas.start_scan( |
||
1275 | scan_id, |
||
1276 | not self.is_running_as_root and self.sudo_available, |
||
1277 | self._niceness, |
||
1278 | ) |
||
1279 | |||
1280 | if openvas_process is None: |
||
1281 | self.main_db.release_database(kbdb) |
||
1282 | return |
||
1283 | |||
1284 | kbdb.add_scan_process_id(openvas_process.pid) |
||
1285 | logger.debug('pid = %s', openvas_process.pid) |
||
1286 | |||
1287 | # Wait until the scanner starts and loads all the preferences. |
||
1288 | while kbdb.get_status(scan_id) == 'new': |
||
1289 | res = openvas_process.poll() |
||
1290 | if res and res < 0: |
||
1291 | self.stop_scan_cleanup(kbdb, scan_id, openvas_process) |
||
1292 | logger.error( |
||
1293 | 'It was not possible run the task %s, since openvas ended ' |
||
1294 | 'unexpectedly with errors during launching.', |
||
1295 | scan_id, |
||
1296 | ) |
||
1297 | return |
||
1298 | |||
1299 | time.sleep(1) |
||
1300 | |||
1301 | got_results = False |
||
1302 | while True: |
||
1303 | |||
1304 | openvas_process_is_alive = self.is_openvas_process_alive( |
||
1305 | openvas_process |
||
1306 | ) |
||
1307 | target_is_finished = kbdb.target_is_finished(scan_id) |
||
1308 | scan_stopped = self.get_scan_status(scan_id) == ScanStatus.STOPPED |
||
1309 | |||
1310 | # Report new Results and update status |
||
1311 | got_results = self.report_openvas_results(kbdb, scan_id) |
||
1312 | self.report_openvas_scan_status(kbdb, scan_id) |
||
1313 | |||
1314 | # Check if the client stopped the whole scan |
||
1315 | if scan_stopped: |
||
1316 | logger.debug('%s: Scan stopped by the client', scan_id) |
||
1317 | |||
1318 | self.stop_scan_cleanup(kbdb, scan_id, openvas_process) |
||
1319 | |||
1320 | # clean main_db, but wait for scanner to finish. |
||
1321 | while not kbdb.target_is_finished(scan_id): |
||
1322 | logger.debug('%s: Waiting for openvas to finish', scan_id) |
||
1323 | time.sleep(1) |
||
1324 | self.main_db.release_database(kbdb) |
||
1325 | return |
||
1326 | |||
1327 | # Scan end. No kb in use for this scan id |
||
1328 | if target_is_finished: |
||
1329 | logger.debug('%s: Target is finished', scan_id) |
||
1330 | break |
||
1331 | |||
1332 | if not openvas_process_is_alive: |
||
1333 | logger.error( |
||
1334 | 'Task %s was unexpectedly stopped or killed.', |
||
1335 | scan_id, |
||
1336 | ) |
||
1337 | self.add_scan_error( |
||
1338 | scan_id, |
||
1339 | name='', |
||
1340 | host='', |
||
1341 | value='Task was unexpectedly stopped or killed.', |
||
1342 | ) |
||
1343 | |||
1344 | # check for scanner error messages before leaving. |
||
1345 | self.report_openvas_results(kbdb, scan_id) |
||
1346 | |||
1347 | kbdb.stop_scan(scan_id) |
||
1348 | |||
1349 | for scan_db in kbdb.get_scan_databases(): |
||
1350 | self.main_db.release_database(scan_db) |
||
1351 | self.main_db.release_database(kbdb) |
||
1352 | return |
||
1353 | |||
1354 | # Wait a second before trying to get result from redis if there |
||
1355 | # was no results before. |
||
1356 | # Otherwise, wait 50 msec to give access other process to redis. |
||
1357 | if not got_results: |
||
1358 | time.sleep(1) |
||
1359 | else: |
||
1360 | time.sleep(0.05) |
||
1361 | got_results = False |
||
1362 | |||
1363 | # Delete keys from KB related to this scan task. |
||
1364 | logger.debug('%s: End Target. Release main database', scan_id) |
||
1365 | self.main_db.release_database(kbdb) |
||
1366 | |||
1367 | |||
1368 | def main(): |
||
1369 | """OSP openvas main function.""" |
||
1370 | daemon_main('OSPD - openvas', OSPDopenvas) |
||
1371 | |||
1372 | |||
1373 | if __name__ == '__main__': |
||
1374 | main() |
||
1375 |