Completed
Push — master ( 745830...54fe4d )
by Juan José
17s queued 13s
created

ospd.scan   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 206
dl 0
loc 405
rs 5.5199
c 0
b 0
f 0
wmc 56

35 Methods

Rating   Name   Duplication   Size   Complexity  
A ScanCollection.__init__() 0 7 1
A ScanCollection.id_exists() 0 4 1
A ScanCollection.get_host_count() 0 6 1
A ScanCollection.set_host_dead() 0 6 1
A ScanCollection.get_count_dead() 0 4 1
A ScanCollection.get_credentials() 0 5 1
A ScanCollection.add_result_list() 0 12 1
A ScanCollection.results_iterator() 0 21 4
A ScanCollection.add_result() 0 33 1
A ScanCollection.get_options() 0 4 1
A ScanCollection.get_finished_hosts() 0 4 1
A ScanCollection.get_target_options() 0 6 1
A ScanCollection.set_amount_dead_hosts() 0 5 1
A ScanCollection.get_progress() 0 4 1
A ScanCollection.set_host_progress() 0 11 1
A ScanCollection.get_host_list() 0 4 1
A ScanCollection.get_exclude_hosts() 0 4 1
A ScanCollection.get_ports() 0 7 1
A ScanCollection.remove_hosts_from_target_progress() 0 16 4
A ScanCollection.get_end_time() 0 4 1
A ScanCollection.calculate_target_progress() 0 24 2
A ScanCollection.get_vts() 0 7 1
A ScanCollection.get_status() 0 4 1
A ScanCollection.set_host_finished() 0 8 1
A ScanCollection.ids_iterator() 0 4 1
B ScanCollection.simplify_exclude_host_count() 0 22 6
A ScanCollection.get_count_alive() 0 4 1
A ScanCollection.set_progress() 0 8 4
A ScanCollection.delete_scan() 0 11 2
B ScanCollection.create_scan() 0 35 5
A ScanCollection.get_current_target_progress() 0 3 1
A ScanCollection.set_status() 0 5 2
A ScanCollection.get_start_time() 0 4 1
A ScanCollection.set_option() 0 4 1
A ScanCollection.init() 0 2 1

How to fix   Complexity   

Complexity

Complex classes like ospd.scan often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# Copyright (C) 2014-2020 Greenbone Networks GmbH
2
#
3
# SPDX-License-Identifier: AGPL-3.0-or-later
4
#
5
# This program is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Affero General Public License as
7
# published by the Free Software Foundation, either version 3 of the
8
# License, or (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU Affero General Public License for more details.
14
#
15
# You should have received a copy of the GNU Affero General Public License
16
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18
import logging
19
import multiprocessing
20
import time
21
import uuid
22
23
from collections import OrderedDict
24
from enum import Enum
25
from typing import List, Any, Dict, Iterator, Optional, Iterable
26
27
from ospd.network import target_str_to_list
28
29
LOGGER = logging.getLogger(__name__)
30
31
32
class ScanStatus(Enum):
33
    """Scan status. """
34
35
    PENDING = 0
36
    INIT = 1
37
    RUNNING = 2
38
    STOPPED = 3
39
    FINISHED = 4
40
41
42
class ScanCollection:
43
44
    """ Scans collection, managing scans and results read and write, exposing
45
    only needed information.
46
47
    Each scan has meta-information such as scan ID, current progress (from 0 to
48
    100), start time, end time, scan target and options and a list of results.
49
50
    There are 4 types of results: Alarms, Logs, Errors and Host Details.
51
52
    Todo:
53
    - Better checking for Scan ID existence and handling otherwise.
54
    - More data validation.
55
    - Mutex access per table/scan_info.
56
57
    """
58
59
    def __init__(self) -> None:
60
        """ Initialize the Scan Collection. """
61
62
        self.data_manager = (
63
            None
64
        )  # type: Optional[multiprocessing.managers.SyncManager]
65
        self.scans_table = dict()  # type: Dict
66
67
    def init(self):
68
        self.data_manager = multiprocessing.Manager()
69
70
    def add_result(
71
        self,
72
        scan_id: str,
73
        result_type: int,
74
        host: str = '',
75
        hostname: str = '',
76
        name: str = '',
77
        value: str = '',
78
        port: str = '',
79
        test_id: str = '',
80
        severity: str = '',
81
        qod: str = '',
82
    ) -> None:
83
        """ Add a result to a scan in the table. """
84
85
        assert scan_id
86
        assert len(name) or len(value)
87
88
        result = OrderedDict()  # type: Dict
89
        result['type'] = result_type
90
        result['name'] = name
91
        result['severity'] = severity
92
        result['test_id'] = test_id
93
        result['value'] = value
94
        result['host'] = host
95
        result['hostname'] = hostname
96
        result['port'] = port
97
        result['qod'] = qod
98
        results = self.scans_table[scan_id]['results']
99
        results.append(result)
100
101
        # Set scan_info's results to propagate results to parent process.
102
        self.scans_table[scan_id]['results'] = results
103
104
    def add_result_list(
105
        self, scan_id: str, result_list: Iterable[Dict[str, str]]
106
    ) -> None:
107
        """
108
        Add a batch of results to the result's table for the corresponding
109
        scan_id
110
        """
111
        results = self.scans_table[scan_id]['results']
112
        results.extend(result_list)
113
114
        # Set scan_info's results to propagate results to parent process.
115
        self.scans_table[scan_id]['results'] = results
116
117
    def remove_hosts_from_target_progress(
118
        self, scan_id: str, hosts: List
119
    ) -> None:
120
        """Remove a list of hosts from the main scan progress table to avoid
121
        the hosts to be included in the calculation of the scan progress"""
122
        if not hosts:
123
            return
124
125
        target = self.scans_table[scan_id].get('target_progress')
126
        for host in hosts:
127
            if host in target:
128
                del target[host]
129
130
        # Set scan_info's target_progress to propagate progresses
131
        # to parent process.
132
        self.scans_table[scan_id]['target_progress'] = target
133
134
    def set_progress(self, scan_id: str, progress: int) -> None:
135
        """ Sets scan_id scan's progress. """
136
137
        if progress > 0 and progress <= 100:
138
            self.scans_table[scan_id]['progress'] = progress
139
140
        if progress == 100:
141
            self.scans_table[scan_id]['end_time'] = int(time.time())
142
143
    def set_host_progress(
144
        self, scan_id: str, host_progress_batch: Dict[str, int]
145
    ) -> None:
146
        """ Sets scan_id scan's progress. """
147
148
        host_progresses = self.scans_table[scan_id].get('target_progress')
149
        host_progresses.update(host_progress_batch)
150
151
        # Set scan_info's target_progress to propagate progresses
152
        # to parent process.
153
        self.scans_table[scan_id]['target_progress'] = host_progresses
154
155
    def set_host_finished(self, scan_id: str, hosts: List[str]) -> None:
156
        """ Increase the amount of finished hosts which were alive."""
157
158
        total_finished = len(hosts)
159
        count_alive = (
160
            self.scans_table[scan_id].get('count_alive') + total_finished
161
        )
162
        self.scans_table[scan_id]['count_alive'] = count_alive
163
164
    def set_host_dead(self, scan_id: str, hosts: List[str]) -> None:
165
        """ Increase the amount of dead hosts. """
166
167
        total_dead = len(hosts)
168
        count_dead = self.scans_table[scan_id].get('count_dead') + total_dead
169
        self.scans_table[scan_id]['count_dead'] = count_dead
170
171
    def set_amount_dead_hosts(self, scan_id: str, total_dead: int) -> None:
172
        """ Increase the amount of dead hosts. """
173
174
        count_dead = self.scans_table[scan_id].get('count_dead') + total_dead
175
        self.scans_table[scan_id]['count_dead'] = count_dead
176
177
    def results_iterator(
178
        self, scan_id: str, pop_res: bool = False, max_res: int = None
179
    ) -> Iterator[Any]:
180
        """ Returns an iterator over scan_id scan's results. If pop_res is True,
181
        it removed the fetched results from the list.
182
183
        If max_res is None, return all the results.
184
        Otherwise, if max_res = N > 0 return N as maximum number of results.
185
186
        max_res works only together with pop_results.
187
        """
188
        if pop_res and max_res:
189
            result_aux = self.scans_table[scan_id]['results']
190
            self.scans_table[scan_id]['results'] = result_aux[max_res:]
191
            return iter(result_aux[:max_res])
192
        elif pop_res:
193
            result_aux = self.scans_table[scan_id]['results']
194
            self.scans_table[scan_id]['results'] = list()
195
            return iter(result_aux)
196
197
        return iter(self.scans_table[scan_id]['results'])
198
199
    def ids_iterator(self) -> Iterator[str]:
200
        """ Returns an iterator over the collection's scan IDS. """
201
202
        return iter(self.scans_table.keys())
203
204
    def create_scan(
205
        self,
206
        scan_id: str = '',
207
        target: Dict = None,
208
        options: Optional[Dict] = None,
209
        vts: Dict = None,
210
    ) -> str:
211
        """ Creates a new scan with provided scan information. """
212
213
        if not target:
214
            target = {}
215
216
        if not options:
217
            options = dict()
218
219
        scan_info = self.data_manager.dict()  # type: Dict
220
        scan_info['results'] = list()
221
        scan_info['progress'] = 0
222
        scan_info['target_progress'] = dict()
223
        scan_info['count_alive'] = 0
224
        scan_info['count_dead'] = 0
225
        scan_info['target'] = target
226
        scan_info['vts'] = vts
227
        scan_info['options'] = options
228
        scan_info['start_time'] = int(time.time())
229
        scan_info['end_time'] = 0
230
        scan_info['status'] = ScanStatus.PENDING
231
232
        if scan_id is None or scan_id == '':
233
            scan_id = str(uuid.uuid4())
234
235
        scan_info['scan_id'] = scan_id
236
237
        self.scans_table[scan_id] = scan_info
238
        return scan_id
239
240
    def set_status(self, scan_id: str, status: ScanStatus) -> None:
241
        """ Sets scan_id scan's status. """
242
        self.scans_table[scan_id]['status'] = status
243
        if status == ScanStatus.STOPPED:
244
            self.scans_table[scan_id]['end_time'] = int(time.time())
245
246
    def get_status(self, scan_id: str) -> ScanStatus:
247
        """ Get scan_id scans's status."""
248
249
        return self.scans_table[scan_id]['status']
250
251
    def get_options(self, scan_id: str) -> Dict:
252
        """ Get scan_id scan's options list. """
253
254
        return self.scans_table[scan_id]['options']
255
256
    def set_option(self, scan_id, name: str, value: Any) -> None:
257
        """ Set a scan_id scan's name option to value. """
258
259
        self.scans_table[scan_id]['options'][name] = value
260
261
    def get_progress(self, scan_id: str) -> int:
262
        """ Get a scan's current progress value. """
263
264
        return self.scans_table[scan_id]['progress']
265
266
    def get_count_dead(self, scan_id: str) -> int:
267
        """ Get a scan's current dead host count. """
268
269
        return self.scans_table[scan_id]['count_dead']
270
271
    def get_count_alive(self, scan_id: str) -> int:
272
        """ Get a scan's current dead host count. """
273
274
        return self.scans_table[scan_id]['count_alive']
275
276
    def get_current_target_progress(self, scan_id: str) -> Dict[str, int]:
277
        """ Get a scan's current hosts progress """
278
        return self.scans_table[scan_id]['target_progress']
279
280
    def simplify_exclude_host_count(self, scan_id: str) -> int:
281
        """ Remove from exclude_hosts the received hosts in the finished_hosts
282
        list sent by the client.
283
        The finished hosts are sent also as exclude hosts for backward
284
        compatibility purposses.
285
286
        Return:
287
            Count of excluded host.
288
        """
289
290
        exc_hosts_list = target_str_to_list(self.get_exclude_hosts(scan_id))
291
292
        finished_hosts_list = target_str_to_list(
293
            self.get_finished_hosts(scan_id)
294
        )
295
296
        if finished_hosts_list and exc_hosts_list:
297
            for finished in finished_hosts_list:
298
                if finished in exc_hosts_list:
299
                    exc_hosts_list.remove(finished)
300
301
        return len(exc_hosts_list) if exc_hosts_list else 0
302
303
    def calculate_target_progress(self, scan_id: str) -> int:
304
        """ Get a target's current progress value.
305
        The value is calculated with the progress of each single host
306
        in the target."""
307
308
        total_hosts = self.get_host_count(scan_id)
309
        exc_hosts = self.simplify_exclude_host_count(scan_id)
310
        count_alive = self.get_count_alive(scan_id)
311
        count_dead = self.get_count_dead(scan_id)
312
        host_progresses = self.get_current_target_progress(scan_id)
313
314
        try:
315
            t_prog = int(
316
                (sum(host_progresses.values()) + 100 * count_alive)
317
                / (total_hosts - exc_hosts - count_dead)
318
            )
319
        except ZeroDivisionError:
320
            LOGGER.error(
321
                "Zero division error in %s",
322
                self.calculate_target_progress.__name__,
323
            )
324
            raise
325
326
        return t_prog
327
328
    def get_start_time(self, scan_id: str) -> str:
329
        """ Get a scan's start time. """
330
331
        return self.scans_table[scan_id]['start_time']
332
333
    def get_end_time(self, scan_id: str) -> str:
334
        """ Get a scan's end time. """
335
336
        return self.scans_table[scan_id]['end_time']
337
338
    def get_host_list(self, scan_id: str) -> Dict:
339
        """ Get a scan's host list. """
340
341
        return self.scans_table[scan_id]['target'].get('hosts')
342
343
    def get_host_count(self, scan_id: str) -> int:
344
        """ Get total host count in the target. """
345
        host = self.get_host_list(scan_id)
346
        total_hosts = len(target_str_to_list(host))
347
348
        return total_hosts
349
350
    def get_ports(self, scan_id: str):
351
        """ Get a scan's ports list.
352
        """
353
        target = self.scans_table[scan_id].get('target')
354
        ports = target.pop('ports')
355
        self.scans_table[scan_id]['target'] = target
356
        return ports
357
358
    def get_exclude_hosts(self, scan_id: str):
359
        """ Get an exclude host list for a given target.
360
        """
361
        return self.scans_table[scan_id]['target'].get('exclude_hosts')
362
363
    def get_finished_hosts(self, scan_id: str):
364
        """ Get the finished host list sent by the client for a given target.
365
        """
366
        return self.scans_table[scan_id]['target'].get('finished_hosts')
367
368
    def get_credentials(self, scan_id: str):
369
        """ Get a scan's credential list. It return dictionary with
370
        the corresponding credential for a given target.
371
        """
372
        return self.scans_table[scan_id]['target'].get('credentials')
373
374
    def get_target_options(self, scan_id: str):
375
        """ Get a scan's target option dictionary.
376
        It return dictionary with the corresponding options for
377
        a given target.
378
        """
379
        return self.scans_table[scan_id]['target'].get('options')
380
381
    def get_vts(self, scan_id: str) -> Dict:
382
        """ Get a scan's vts. """
383
        scan_info = self.scans_table[scan_id]
384
        vts = scan_info.pop('vts')
385
        self.scans_table[scan_id] = scan_info
386
387
        return vts
388
389
    def id_exists(self, scan_id: str) -> bool:
390
        """ Check whether a scan exists in the table. """
391
392
        return self.scans_table.get(scan_id) is not None
393
394
    def delete_scan(self, scan_id: str) -> bool:
395
        """ Delete a scan if fully finished. """
396
397
        if self.get_status(scan_id) == ScanStatus.RUNNING:
398
            return False
399
400
        scans_table = self.scans_table
401
        del scans_table[scan_id]
402
        self.scans_table = scans_table
403
404
        return True
405