|
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
|
|
|
""" Access management for redis-based OpenVAS Scanner Database.""" |
|
21
|
|
|
import logging |
|
22
|
|
|
import sys |
|
23
|
|
|
import time |
|
24
|
|
|
|
|
25
|
|
|
from typing import List, NewType, Optional, Iterable, Iterator, Tuple |
|
26
|
|
|
from queue import Queue |
|
27
|
|
|
|
|
28
|
|
|
import redis |
|
29
|
|
|
import paho.mqtt.client as mqtt |
|
30
|
|
|
|
|
31
|
|
|
from ospd.errors import RequiredArgument |
|
32
|
|
|
from ospd_openvas.errors import OspdOpenvasError |
|
33
|
|
|
from ospd_openvas.openvas import Openvas |
|
34
|
|
|
|
|
35
|
|
|
SOCKET_TIMEOUT = 60 # in seconds |
|
36
|
|
|
LIST_FIRST_POS = 0 |
|
37
|
|
|
LIST_LAST_POS = -1 |
|
38
|
|
|
LIST_ALL = 0 |
|
39
|
|
|
|
|
40
|
|
|
# Possible positions of nvt values in cache list. |
|
41
|
|
|
NVT_META_FIELDS = [ |
|
42
|
|
|
"NVT_FILENAME_POS", |
|
43
|
|
|
"NVT_REQUIRED_KEYS_POS", |
|
44
|
|
|
"NVT_MANDATORY_KEYS_POS", |
|
45
|
|
|
"NVT_EXCLUDED_KEYS_POS", |
|
46
|
|
|
"NVT_REQUIRED_UDP_PORTS_POS", |
|
47
|
|
|
"NVT_REQUIRED_PORTS_POS", |
|
48
|
|
|
"NVT_DEPENDENCIES_POS", |
|
49
|
|
|
"NVT_TAGS_POS", |
|
50
|
|
|
"NVT_CVES_POS", |
|
51
|
|
|
"NVT_BIDS_POS", |
|
52
|
|
|
"NVT_XREFS_POS", |
|
53
|
|
|
"NVT_CATEGORY_POS", |
|
54
|
|
|
"NVT_TIMEOUT_POS", |
|
55
|
|
|
"NVT_FAMILY_POS", |
|
56
|
|
|
"NVT_NAME_POS", |
|
57
|
|
|
] |
|
58
|
|
|
|
|
59
|
|
|
# Name of the namespace usage bitmap in redis. |
|
60
|
|
|
DBINDEX_NAME = "GVM.__GlobalDBIndex" |
|
61
|
|
|
|
|
62
|
|
|
logger = logging.getLogger(__name__) |
|
63
|
|
|
|
|
64
|
|
|
# Types |
|
65
|
|
|
RedisCtx = NewType('RedisCtx', redis.Redis) |
|
66
|
|
|
|
|
67
|
|
|
|
|
68
|
|
|
class OpenvasDB: |
|
69
|
|
|
"""Class to connect to redis, to perform queries, and to move |
|
70
|
|
|
from a KB to another.""" |
|
71
|
|
|
|
|
72
|
|
|
_db_address = None |
|
73
|
|
|
|
|
74
|
|
|
@classmethod |
|
75
|
|
|
def get_database_address(cls) -> Optional[str]: |
|
76
|
|
|
if not cls._db_address: |
|
77
|
|
|
settings = Openvas.get_settings() |
|
78
|
|
|
|
|
79
|
|
|
cls._db_address = settings.get('db_address') |
|
80
|
|
|
|
|
81
|
|
|
return cls._db_address |
|
82
|
|
|
|
|
83
|
|
|
@classmethod |
|
84
|
|
|
def create_context( |
|
85
|
|
|
cls, dbnum: Optional[int] = 0, encoding: Optional[str] = 'latin-1' |
|
86
|
|
|
) -> RedisCtx: |
|
87
|
|
|
"""Connect to redis to the given database or to the default db 0 . |
|
88
|
|
|
|
|
89
|
|
|
Arguments: |
|
90
|
|
|
dbnum: The db number to connect to. |
|
91
|
|
|
encoding: The encoding to be used to read and write. |
|
92
|
|
|
|
|
93
|
|
|
Return a new redis context on success. |
|
94
|
|
|
""" |
|
95
|
|
|
tries = 5 |
|
96
|
|
|
while tries: |
|
97
|
|
|
try: |
|
98
|
|
|
ctx = redis.Redis( |
|
99
|
|
|
unix_socket_path=cls.get_database_address(), |
|
100
|
|
|
db=dbnum, |
|
101
|
|
|
socket_timeout=SOCKET_TIMEOUT, |
|
102
|
|
|
encoding=encoding, |
|
103
|
|
|
decode_responses=True, |
|
104
|
|
|
) |
|
105
|
|
|
ctx.keys("test") |
|
106
|
|
|
except (redis.exceptions.ConnectionError, FileNotFoundError) as err: |
|
107
|
|
|
logger.debug( |
|
108
|
|
|
'Redis connection lost: %s. Trying again in 5 seconds.', err |
|
109
|
|
|
) |
|
110
|
|
|
tries = tries - 1 |
|
111
|
|
|
time.sleep(5) |
|
112
|
|
|
continue |
|
113
|
|
|
break |
|
114
|
|
|
|
|
115
|
|
|
if not tries: |
|
116
|
|
|
logger.error('Redis Error: Not possible to connect to the kb.') |
|
117
|
|
|
sys.exit(1) |
|
118
|
|
|
|
|
119
|
|
|
return ctx |
|
|
|
|
|
|
120
|
|
|
|
|
121
|
|
|
@classmethod |
|
122
|
|
|
def find_database_by_pattern( |
|
123
|
|
|
cls, pattern: str, max_database_index: int |
|
124
|
|
|
) -> Tuple[Optional[RedisCtx], Optional[int]]: |
|
125
|
|
|
"""Search a pattern inside all kbs up to max_database_index. |
|
126
|
|
|
|
|
127
|
|
|
Returns the redis context for the db and its index as a tuple or |
|
128
|
|
|
None, None if the db with the pattern couldn't be found. |
|
129
|
|
|
""" |
|
130
|
|
|
for i in range(0, max_database_index): |
|
131
|
|
|
ctx = cls.create_context(i) |
|
132
|
|
|
if ctx.keys(pattern): |
|
133
|
|
|
return (ctx, i) |
|
134
|
|
|
|
|
135
|
|
|
return (None, None) |
|
136
|
|
|
|
|
137
|
|
|
@staticmethod |
|
138
|
|
|
def select_database(ctx: RedisCtx, kbindex: str): |
|
139
|
|
|
"""Use an existent redis connection and select a redis kb. |
|
140
|
|
|
|
|
141
|
|
|
Arguments: |
|
142
|
|
|
ctx: Redis context to use. |
|
143
|
|
|
kbindex: The new kb to select |
|
144
|
|
|
""" |
|
145
|
|
|
if not ctx: |
|
146
|
|
|
raise RequiredArgument('select_database', 'ctx') |
|
147
|
|
|
if not kbindex: |
|
148
|
|
|
raise RequiredArgument('select_database', 'kbindex') |
|
149
|
|
|
|
|
150
|
|
|
ctx.execute_command('SELECT ' + str(kbindex)) |
|
151
|
|
|
|
|
152
|
|
|
@staticmethod |
|
153
|
|
|
def get_list_item( |
|
154
|
|
|
ctx: RedisCtx, |
|
155
|
|
|
name: str, |
|
156
|
|
|
start: Optional[int] = LIST_FIRST_POS, |
|
157
|
|
|
end: Optional[int] = LIST_LAST_POS, |
|
158
|
|
|
) -> Optional[list]: |
|
159
|
|
|
"""Returns the specified elements from `start` to `end` of the |
|
160
|
|
|
list stored as `name`. |
|
161
|
|
|
|
|
162
|
|
|
Arguments: |
|
163
|
|
|
ctx: Redis context to use. |
|
164
|
|
|
name: key name of a list. |
|
165
|
|
|
start: first range element to get. |
|
166
|
|
|
end: last range element to get. |
|
167
|
|
|
|
|
168
|
|
|
Return List specified elements in the key. |
|
169
|
|
|
""" |
|
170
|
|
|
if not ctx: |
|
171
|
|
|
raise RequiredArgument('get_list_item', 'ctx') |
|
172
|
|
|
if not name: |
|
173
|
|
|
raise RequiredArgument('get_list_item', 'name') |
|
174
|
|
|
|
|
175
|
|
|
return ctx.lrange(name, start, end) |
|
176
|
|
|
|
|
177
|
|
|
@staticmethod |
|
178
|
|
|
def get_last_list_item(ctx: RedisCtx, name: str) -> str: |
|
179
|
|
|
if not ctx: |
|
180
|
|
|
raise RequiredArgument('get_last_list_item', 'ctx') |
|
181
|
|
|
if not name: |
|
182
|
|
|
raise RequiredArgument('get_last_list_item', 'name') |
|
183
|
|
|
|
|
184
|
|
|
return ctx.rpop(name) |
|
185
|
|
|
|
|
186
|
|
|
@staticmethod |
|
187
|
|
|
def pop_list_items(ctx: RedisCtx, name: str) -> List[str]: |
|
188
|
|
|
if not ctx: |
|
189
|
|
|
raise RequiredArgument('pop_list_items', 'ctx') |
|
190
|
|
|
if not name: |
|
191
|
|
|
raise RequiredArgument('pop_list_items', 'name') |
|
192
|
|
|
|
|
193
|
|
|
pipe = ctx.pipeline() |
|
194
|
|
|
pipe.lrange(name, LIST_FIRST_POS, LIST_LAST_POS) |
|
195
|
|
|
pipe.delete(name) |
|
196
|
|
|
results, redis_return_code = pipe.execute() |
|
197
|
|
|
|
|
198
|
|
|
# The results are left-pushed. To preserver the order |
|
199
|
|
|
# the result list must be reversed. |
|
200
|
|
|
if redis_return_code: |
|
201
|
|
|
results.reverse() |
|
202
|
|
|
else: |
|
203
|
|
|
results = [] |
|
204
|
|
|
|
|
205
|
|
|
return results |
|
206
|
|
|
|
|
207
|
|
|
@staticmethod |
|
208
|
|
|
def get_key_count(ctx: RedisCtx, pattern: Optional[str] = None) -> int: |
|
209
|
|
|
"""Get the number of keys matching with the pattern. |
|
210
|
|
|
|
|
211
|
|
|
Arguments: |
|
212
|
|
|
ctx: Redis context to use. |
|
213
|
|
|
pattern: pattern used as filter. |
|
214
|
|
|
""" |
|
215
|
|
|
if not pattern: |
|
216
|
|
|
pattern = "*" |
|
217
|
|
|
|
|
218
|
|
|
if not ctx: |
|
219
|
|
|
raise RequiredArgument('get_key_count', 'ctx') |
|
220
|
|
|
|
|
221
|
|
|
return len(ctx.keys(pattern)) |
|
222
|
|
|
|
|
223
|
|
|
@staticmethod |
|
224
|
|
|
def remove_list_item(ctx: RedisCtx, key: str, value: str): |
|
225
|
|
|
"""Remove item from the key list. |
|
226
|
|
|
|
|
227
|
|
|
Arguments: |
|
228
|
|
|
ctx: Redis context to use. |
|
229
|
|
|
key: key name of a list. |
|
230
|
|
|
value: Value to be removed from the key. |
|
231
|
|
|
""" |
|
232
|
|
|
if not ctx: |
|
233
|
|
|
raise RequiredArgument('remove_list_item ', 'ctx') |
|
234
|
|
|
if not key: |
|
235
|
|
|
raise RequiredArgument('remove_list_item', 'key') |
|
236
|
|
|
if not value: |
|
237
|
|
|
raise RequiredArgument('remove_list_item ', 'value') |
|
238
|
|
|
|
|
239
|
|
|
ctx.lrem(key, count=LIST_ALL, value=value) |
|
240
|
|
|
|
|
241
|
|
|
@staticmethod |
|
242
|
|
|
def get_single_item( |
|
243
|
|
|
ctx: RedisCtx, |
|
244
|
|
|
name: str, |
|
245
|
|
|
index: Optional[int] = LIST_FIRST_POS, |
|
246
|
|
|
) -> Optional[str]: |
|
247
|
|
|
"""Get a single KB element. |
|
248
|
|
|
|
|
249
|
|
|
Arguments: |
|
250
|
|
|
ctx: Redis context to use. |
|
251
|
|
|
name: key name of a list. |
|
252
|
|
|
index: index of the element to be return. |
|
253
|
|
|
Defaults to the first element in the list. |
|
254
|
|
|
|
|
255
|
|
|
Return the first element of the list or None if the name couldn't be |
|
256
|
|
|
found. |
|
257
|
|
|
""" |
|
258
|
|
|
if not ctx: |
|
259
|
|
|
raise RequiredArgument('get_single_item', 'ctx') |
|
260
|
|
|
if not name: |
|
261
|
|
|
raise RequiredArgument('get_single_item', 'name') |
|
262
|
|
|
|
|
263
|
|
|
return ctx.lindex(name, index) |
|
264
|
|
|
|
|
265
|
|
|
@staticmethod |
|
266
|
|
|
def add_single_list(ctx: RedisCtx, name: str, values: Iterable): |
|
267
|
|
|
"""Add a single KB element with one or more values. |
|
268
|
|
|
The values can be repeated. If the key already exists will |
|
269
|
|
|
be removed an completely replaced. |
|
270
|
|
|
|
|
271
|
|
|
Arguments: |
|
272
|
|
|
ctx: Redis context to use. |
|
273
|
|
|
name: key name of a list. |
|
274
|
|
|
value: Elements to add to the key. |
|
275
|
|
|
""" |
|
276
|
|
|
if not ctx: |
|
277
|
|
|
raise RequiredArgument('add_single_list', 'ctx') |
|
278
|
|
|
if not name: |
|
279
|
|
|
raise RequiredArgument('add_single_list', 'name') |
|
280
|
|
|
if not values: |
|
281
|
|
|
raise RequiredArgument('add_single_list', 'value') |
|
282
|
|
|
|
|
283
|
|
|
pipe = ctx.pipeline() |
|
284
|
|
|
pipe.delete(name) |
|
285
|
|
|
pipe.rpush(name, *values) |
|
286
|
|
|
pipe.execute() |
|
287
|
|
|
|
|
288
|
|
|
@staticmethod |
|
289
|
|
|
def add_single_item(ctx: RedisCtx, name: str, values: Iterable): |
|
290
|
|
|
"""Add a single KB element with one or more values. Don't add |
|
291
|
|
|
duplicated values during this operation, but if the the same |
|
292
|
|
|
values already exists under the key, this will not be overwritten. |
|
293
|
|
|
|
|
294
|
|
|
Arguments: |
|
295
|
|
|
ctx: Redis context to use. |
|
296
|
|
|
name: key name of a list. |
|
297
|
|
|
value: Elements to add to the key. |
|
298
|
|
|
""" |
|
299
|
|
|
if not ctx: |
|
300
|
|
|
raise RequiredArgument('add_single_item', 'ctx') |
|
301
|
|
|
if not name: |
|
302
|
|
|
raise RequiredArgument('add_single_item', 'name') |
|
303
|
|
|
if not values: |
|
304
|
|
|
raise RequiredArgument('add_single_item', 'value') |
|
305
|
|
|
|
|
306
|
|
|
ctx.rpush(name, *set(values)) |
|
307
|
|
|
|
|
308
|
|
|
@staticmethod |
|
309
|
|
|
def set_single_item(ctx: RedisCtx, name: str, value: Iterable): |
|
310
|
|
|
"""Set (replace) a single KB element. If the same key exists |
|
311
|
|
|
in the kb, it is completed removed. Values added are unique. |
|
312
|
|
|
|
|
313
|
|
|
Arguments: |
|
314
|
|
|
ctx: Redis context to use. |
|
315
|
|
|
name: key name of a list. |
|
316
|
|
|
value: New elements to add to the key. |
|
317
|
|
|
""" |
|
318
|
|
|
if not ctx: |
|
319
|
|
|
raise RequiredArgument('set_single_item', 'ctx') |
|
320
|
|
|
if not name: |
|
321
|
|
|
raise RequiredArgument('set_single_item', 'name') |
|
322
|
|
|
if not value: |
|
323
|
|
|
raise RequiredArgument('set_single_item', 'value') |
|
324
|
|
|
|
|
325
|
|
|
pipe = ctx.pipeline() |
|
326
|
|
|
pipe.delete(name) |
|
327
|
|
|
pipe.rpush(name, *set(value)) |
|
328
|
|
|
pipe.execute() |
|
329
|
|
|
|
|
330
|
|
|
@staticmethod |
|
331
|
|
|
def get_pattern(ctx: RedisCtx, pattern: str) -> List: |
|
332
|
|
|
"""Get all items stored under a given pattern. |
|
333
|
|
|
|
|
334
|
|
|
Arguments: |
|
335
|
|
|
ctx: Redis context to use. |
|
336
|
|
|
pattern: key pattern to match. |
|
337
|
|
|
|
|
338
|
|
|
Return a list with the elements under the matched key. |
|
339
|
|
|
""" |
|
340
|
|
|
if not ctx: |
|
341
|
|
|
raise RequiredArgument('get_pattern', 'ctx') |
|
342
|
|
|
if not pattern: |
|
343
|
|
|
raise RequiredArgument('get_pattern', 'pattern') |
|
344
|
|
|
|
|
345
|
|
|
items = ctx.keys(pattern) |
|
346
|
|
|
|
|
347
|
|
|
elem_list = [] |
|
348
|
|
|
for item in items: |
|
349
|
|
|
elem_list.append( |
|
350
|
|
|
[ |
|
351
|
|
|
item, |
|
352
|
|
|
ctx.lrange(item, start=LIST_FIRST_POS, end=LIST_LAST_POS), |
|
353
|
|
|
] |
|
354
|
|
|
) |
|
355
|
|
|
return elem_list |
|
356
|
|
|
|
|
357
|
|
|
@classmethod |
|
358
|
|
|
def get_keys_by_pattern(cls, ctx: RedisCtx, pattern: str) -> List[str]: |
|
359
|
|
|
"""Get all items with index 'index', stored under |
|
360
|
|
|
a given pattern. |
|
361
|
|
|
|
|
362
|
|
|
Arguments: |
|
363
|
|
|
ctx: Redis context to use. |
|
364
|
|
|
pattern: key pattern to match. |
|
365
|
|
|
|
|
366
|
|
|
Return a sorted list with the elements under the matched key |
|
367
|
|
|
""" |
|
368
|
|
|
if not ctx: |
|
369
|
|
|
raise RequiredArgument('get_elem_pattern_by_index', 'ctx') |
|
370
|
|
|
if not pattern: |
|
371
|
|
|
raise RequiredArgument('get_elem_pattern_by_index', 'pattern') |
|
372
|
|
|
|
|
373
|
|
|
return sorted(ctx.keys(pattern)) |
|
374
|
|
|
|
|
375
|
|
|
@classmethod |
|
376
|
|
|
def get_filenames_and_oids( |
|
377
|
|
|
cls, |
|
378
|
|
|
ctx: RedisCtx, |
|
379
|
|
|
) -> Iterable[Tuple[str, str]]: |
|
380
|
|
|
"""Get all items with index 'index', stored under |
|
381
|
|
|
a given pattern. |
|
382
|
|
|
|
|
383
|
|
|
Arguments: |
|
384
|
|
|
ctx: Redis context to use. |
|
385
|
|
|
|
|
386
|
|
|
Return an iterable where each single tuple contains the filename |
|
387
|
|
|
as first element and the oid as the second one. |
|
388
|
|
|
""" |
|
389
|
|
|
if not ctx: |
|
390
|
|
|
raise RequiredArgument('get_filenames_and_oids', 'ctx') |
|
391
|
|
|
|
|
392
|
|
|
items = cls.get_keys_by_pattern(ctx, 'nvt:*') |
|
393
|
|
|
|
|
394
|
|
|
return ((ctx.lindex(item, 0), item[4:]) for item in items) |
|
|
|
|
|
|
395
|
|
|
|
|
396
|
|
|
|
|
397
|
|
|
class BaseDB: |
|
398
|
|
|
def __init__(self, kbindex: int, ctx: Optional[RedisCtx] = None): |
|
399
|
|
|
if ctx is None: |
|
400
|
|
|
self.ctx = OpenvasDB.create_context(kbindex) |
|
401
|
|
|
else: |
|
402
|
|
|
self.ctx = ctx |
|
403
|
|
|
|
|
404
|
|
|
self.index = kbindex |
|
405
|
|
|
|
|
406
|
|
|
def flush(self): |
|
407
|
|
|
"""Flush the database""" |
|
408
|
|
|
self.ctx.flushdb() |
|
409
|
|
|
|
|
410
|
|
|
|
|
411
|
|
|
class MQTTDB: |
|
412
|
|
|
def __init__(self, host): |
|
413
|
|
|
self.results = Queue() |
|
414
|
|
|
self.client = mqtt.Client( |
|
415
|
|
|
client_id="result_handler", |
|
416
|
|
|
protocol=mqtt.MQTTv5, |
|
417
|
|
|
userdata=self.results, |
|
418
|
|
|
) |
|
419
|
|
|
self.client.connect(host) |
|
420
|
|
|
self.client.on_message = self.on_message |
|
421
|
|
|
self.client.subscribe("scanner/results") |
|
422
|
|
|
self.client.loop_start() |
|
423
|
|
|
|
|
424
|
|
|
@staticmethod |
|
425
|
|
|
def on_message(client, userdata, msg): |
|
426
|
|
|
if msg.topic == "scanner/results": |
|
427
|
|
|
userdata.put(str(msg.payload.decode("utf-8")), False) |
|
428
|
|
|
|
|
429
|
|
|
def get_result(self): |
|
430
|
|
|
"""Get all result from the result Queue""" |
|
431
|
|
|
ret = [] |
|
432
|
|
|
while not self.results.empty(): |
|
433
|
|
|
ret.append(self.results.get(False)) |
|
434
|
|
|
return ret |
|
435
|
|
|
|
|
436
|
|
|
|
|
437
|
|
|
class BaseKbDB(BaseDB): |
|
438
|
|
|
def _add_single_item( |
|
439
|
|
|
self, name: str, values: Iterable, utf8_enc: Optional[bool] = False |
|
440
|
|
|
): |
|
441
|
|
|
"""Changing the encoding format of an existing redis context |
|
442
|
|
|
is not possible. Therefore a new temporary redis context is |
|
443
|
|
|
created to store key-values encoded with utf-8.""" |
|
444
|
|
|
if utf8_enc: |
|
445
|
|
|
ctx = OpenvasDB.create_context(self.index, encoding='utf-8') |
|
446
|
|
|
OpenvasDB.add_single_item(ctx, name, values) |
|
447
|
|
|
else: |
|
448
|
|
|
OpenvasDB.add_single_item(self.ctx, name, values) |
|
449
|
|
|
|
|
450
|
|
|
def _set_single_item(self, name: str, value: Iterable): |
|
451
|
|
|
"""Set (replace) a single KB element. |
|
452
|
|
|
|
|
453
|
|
|
Arguments: |
|
454
|
|
|
name: key name of a list. |
|
455
|
|
|
value: New elements to add to the key. |
|
456
|
|
|
""" |
|
457
|
|
|
OpenvasDB.set_single_item(self.ctx, name, value) |
|
458
|
|
|
|
|
459
|
|
|
def _get_single_item(self, name: str) -> Optional[str]: |
|
460
|
|
|
"""Get a single KB element. |
|
461
|
|
|
|
|
462
|
|
|
Arguments: |
|
463
|
|
|
name: key name of a list. |
|
464
|
|
|
""" |
|
465
|
|
|
return OpenvasDB.get_single_item(self.ctx, name) |
|
466
|
|
|
|
|
467
|
|
|
def _get_list_item( |
|
468
|
|
|
self, |
|
469
|
|
|
name: str, |
|
470
|
|
|
) -> Optional[List]: |
|
471
|
|
|
"""Returns the specified elements from `start` to `end` of the |
|
472
|
|
|
list stored as `name`. |
|
473
|
|
|
|
|
474
|
|
|
Arguments: |
|
475
|
|
|
name: key name of a list. |
|
476
|
|
|
|
|
477
|
|
|
Return List specified elements in the key. |
|
478
|
|
|
""" |
|
479
|
|
|
return OpenvasDB.get_list_item(self.ctx, name) |
|
480
|
|
|
|
|
481
|
|
|
def _pop_list_items(self, name: str) -> List: |
|
482
|
|
|
return OpenvasDB.pop_list_items(self.ctx, name) |
|
483
|
|
|
|
|
484
|
|
|
def _remove_list_item(self, key: str, value: str): |
|
485
|
|
|
"""Remove item from the key list. |
|
486
|
|
|
|
|
487
|
|
|
Arguments: |
|
488
|
|
|
key: key name of a list. |
|
489
|
|
|
value: Value to be removed from the key. |
|
490
|
|
|
""" |
|
491
|
|
|
OpenvasDB.remove_list_item(self.ctx, key, value) |
|
492
|
|
|
|
|
493
|
|
|
def get_result(self) -> Optional[str]: |
|
494
|
|
|
"""Get and remove the oldest result from the list. |
|
495
|
|
|
|
|
496
|
|
|
Return the oldest scan results |
|
497
|
|
|
""" |
|
498
|
|
|
return self._pop_list_items("internal/results") |
|
499
|
|
|
|
|
500
|
|
|
def get_status(self, openvas_scan_id: str) -> Optional[str]: |
|
501
|
|
|
"""Return the status of the host scan""" |
|
502
|
|
|
return self._get_single_item('internal/{}'.format(openvas_scan_id)) |
|
503
|
|
|
|
|
504
|
|
|
def __repr__(self): |
|
505
|
|
|
return '<{} index={}>'.format(self.__class__.__name__, self.index) |
|
506
|
|
|
|
|
507
|
|
|
|
|
508
|
|
|
class ScanDB(BaseKbDB): |
|
509
|
|
|
"""Database for a scanning a single host""" |
|
510
|
|
|
|
|
511
|
|
|
def select(self, kbindex: int) -> "ScanDB": |
|
512
|
|
|
"""Select a redis kb. |
|
513
|
|
|
|
|
514
|
|
|
Arguments: |
|
515
|
|
|
kbindex: The new kb to select |
|
516
|
|
|
""" |
|
517
|
|
|
OpenvasDB.select_database(self.ctx, kbindex) |
|
518
|
|
|
self.index = kbindex |
|
519
|
|
|
return self |
|
520
|
|
|
|
|
521
|
|
|
|
|
522
|
|
|
class KbDB(BaseKbDB): |
|
523
|
|
|
def get_scan_databases(self) -> Iterator[ScanDB]: |
|
524
|
|
|
"""Returns an iterator yielding corresponding ScanDBs |
|
525
|
|
|
|
|
526
|
|
|
The returned Iterator can't be converted to an Iterable like a List. |
|
527
|
|
|
Each yielded ScanDB must be used independently in a for loop. If the |
|
528
|
|
|
Iterator gets converted into an Iterable all returned ScanDBs will use |
|
529
|
|
|
the same redis context pointing to the same redis database. |
|
530
|
|
|
""" |
|
531
|
|
|
dbs = self._get_list_item('internal/dbindex') |
|
532
|
|
|
scan_db = ScanDB(self.index) |
|
533
|
|
|
for kbindex in dbs: |
|
534
|
|
|
if kbindex == self.index: |
|
535
|
|
|
continue |
|
536
|
|
|
|
|
537
|
|
|
yield scan_db.select(kbindex) |
|
538
|
|
|
|
|
539
|
|
|
def add_scan_id(self, scan_id: str): |
|
540
|
|
|
self._add_single_item('internal/{}'.format(scan_id), ['new']) |
|
541
|
|
|
self._add_single_item('internal/scanid', [scan_id]) |
|
542
|
|
|
|
|
543
|
|
|
def add_scan_preferences(self, openvas_scan_id: str, preferences: Iterable): |
|
544
|
|
|
self._add_single_item( |
|
545
|
|
|
'internal/{}/scanprefs'.format(openvas_scan_id), preferences |
|
546
|
|
|
) |
|
547
|
|
|
|
|
548
|
|
|
def add_credentials_to_scan_preferences( |
|
549
|
|
|
self, openvas_scan_id: str, preferences: Iterable |
|
550
|
|
|
): |
|
551
|
|
|
"""Force the usage of the utf-8 encoding, since some credentials |
|
552
|
|
|
contain special chars not supported by latin-1 encoding.""" |
|
553
|
|
|
self._add_single_item( |
|
554
|
|
|
'internal/{}/scanprefs'.format(openvas_scan_id), |
|
555
|
|
|
preferences, |
|
556
|
|
|
utf8_enc=True, |
|
557
|
|
|
) |
|
558
|
|
|
|
|
559
|
|
|
def add_scan_process_id(self, pid: int): |
|
560
|
|
|
self._add_single_item('internal/ovas_pid', [pid]) |
|
561
|
|
|
|
|
562
|
|
|
def get_scan_process_id(self) -> Optional[str]: |
|
563
|
|
|
return self._get_single_item('internal/ovas_pid') |
|
564
|
|
|
|
|
565
|
|
|
def remove_scan_database(self, scan_db: ScanDB): |
|
566
|
|
|
self._remove_list_item('internal/dbindex', scan_db.index) |
|
567
|
|
|
|
|
568
|
|
|
def target_is_finished(self, scan_id: str) -> bool: |
|
569
|
|
|
"""Check if a target has finished.""" |
|
570
|
|
|
|
|
571
|
|
|
status = self._get_single_item('internal/{}'.format(scan_id)) |
|
572
|
|
|
|
|
573
|
|
|
return status == 'finished' or status is None |
|
574
|
|
|
|
|
575
|
|
|
def stop_scan(self, openvas_scan_id: str): |
|
576
|
|
|
self._set_single_item( |
|
577
|
|
|
'internal/{}'.format(openvas_scan_id), ['stop_all'] |
|
578
|
|
|
) |
|
579
|
|
|
|
|
580
|
|
|
def scan_is_stopped(self, scan_id: str) -> bool: |
|
581
|
|
|
"""Check if the scan should be stopped""" |
|
582
|
|
|
status = self._get_single_item('internal/%s' % scan_id) |
|
583
|
|
|
return status == 'stop_all' |
|
584
|
|
|
|
|
585
|
|
|
def get_scan_status(self) -> List: |
|
586
|
|
|
"""Get and remove the oldest host scan status from the list. |
|
587
|
|
|
|
|
588
|
|
|
Return a string which represents the host scan status. |
|
589
|
|
|
""" |
|
590
|
|
|
return self._pop_list_items("internal/status") |
|
591
|
|
|
|
|
592
|
|
|
|
|
593
|
|
|
class MainDB(BaseDB): |
|
594
|
|
|
"""Main Database""" |
|
595
|
|
|
|
|
596
|
|
|
DEFAULT_INDEX = 0 |
|
597
|
|
|
|
|
598
|
|
|
def __init__(self, ctx=None): |
|
599
|
|
|
super().__init__(self.DEFAULT_INDEX, ctx) |
|
600
|
|
|
|
|
601
|
|
|
self._max_dbindex = None |
|
602
|
|
|
|
|
603
|
|
|
@property |
|
604
|
|
|
def max_database_index(self): |
|
605
|
|
|
"""Set the number of databases have been configured into kbr struct.""" |
|
606
|
|
|
if self._max_dbindex is None: |
|
607
|
|
|
resp = self.ctx.config_get('databases') |
|
608
|
|
|
|
|
609
|
|
|
if len(resp) == 1: |
|
610
|
|
|
self._max_dbindex = int(resp.get('databases')) |
|
611
|
|
|
else: |
|
612
|
|
|
raise OspdOpenvasError( |
|
613
|
|
|
'Redis Error: Not possible to get max_dbindex.' |
|
614
|
|
|
) from None |
|
615
|
|
|
|
|
616
|
|
|
return self._max_dbindex |
|
617
|
|
|
|
|
618
|
|
|
def try_database(self, index: int) -> bool: |
|
619
|
|
|
"""Check if a redis db is already in use. If not, set it |
|
620
|
|
|
as in use and return. |
|
621
|
|
|
|
|
622
|
|
|
Arguments: |
|
623
|
|
|
ctx: Redis object connected to the kb with the |
|
624
|
|
|
DBINDEX_NAME key. |
|
625
|
|
|
index: Number intended to be used. |
|
626
|
|
|
|
|
627
|
|
|
Return True if it is possible to use the db. False if the given db |
|
628
|
|
|
number is already in use. |
|
629
|
|
|
""" |
|
630
|
|
|
_in_use = 1 |
|
631
|
|
|
try: |
|
632
|
|
|
resp = self.ctx.hsetnx(DBINDEX_NAME, index, _in_use) |
|
633
|
|
|
except: |
|
634
|
|
|
raise OspdOpenvasError( |
|
635
|
|
|
'Redis Error: Not possible to set %s.' % DBINDEX_NAME |
|
636
|
|
|
) from None |
|
637
|
|
|
|
|
638
|
|
|
return resp == 1 |
|
639
|
|
|
|
|
640
|
|
|
def get_new_kb_database(self) -> Optional[KbDB]: |
|
641
|
|
|
"""Return a new kb db to an empty kb.""" |
|
642
|
|
|
for index in range(1, self.max_database_index): |
|
643
|
|
|
if self.try_database(index): |
|
644
|
|
|
kbdb = KbDB(index) |
|
645
|
|
|
kbdb.flush() |
|
646
|
|
|
return kbdb |
|
647
|
|
|
|
|
648
|
|
|
return None |
|
649
|
|
|
|
|
650
|
|
|
def find_kb_database_by_scan_id( |
|
651
|
|
|
self, scan_id: str |
|
652
|
|
|
) -> Tuple[Optional[str], Optional["KbDB"]]: |
|
653
|
|
|
"""Find a kb db by via a scan id""" |
|
654
|
|
|
for index in range(1, self.max_database_index): |
|
655
|
|
|
ctx = OpenvasDB.create_context(index) |
|
656
|
|
|
if OpenvasDB.get_key_count(ctx, 'internal/{}'.format(scan_id)): |
|
657
|
|
|
return KbDB(index, ctx) |
|
658
|
|
|
|
|
659
|
|
|
return None |
|
660
|
|
|
|
|
661
|
|
|
def release_database(self, database: BaseDB): |
|
662
|
|
|
self.release_database_by_index(database.index) |
|
663
|
|
|
database.flush() |
|
664
|
|
|
|
|
665
|
|
|
def release_database_by_index(self, index: int): |
|
666
|
|
|
self.ctx.hdel(DBINDEX_NAME, index) |
|
667
|
|
|
|
|
668
|
|
|
def release(self): |
|
669
|
|
|
self.release_database(self) |
|
670
|
|
|
|