Completed
Push — master ( b78836...6b9a52 )
by
unknown
18s queued 13s
created

ospd.command.command.BaseCommand.get_attributes()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
# Copyright (C) 2014-2020 Greenbone Networks GmbH
2
#
3
# SPDX-License-Identifier: AGPL-3.0-or-later
4
#
5
# This program is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Affero General Public License as
7
# published by the Free Software Foundation, either version 3 of the
8
# License, or (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU Affero General Public License for more details.
14
#
15
# You should have received a copy of the GNU Affero General Public License
16
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18
import multiprocessing
19
import re
20
import logging
21
import subprocess
22
23
from decimal import Decimal
24
from typing import Optional, Dict, Any, Union, Iterator
25
26
from xml.etree.ElementTree import Element, SubElement
27
28
import psutil
29
30
from ospd.errors import OspdCommandError
31
from ospd.misc import valid_uuid, create_process
32
from ospd.protocol import OspRequest, OspResponse
33
from ospd.xml import (
34
    simple_response_str,
35
    get_elements_from_dict,
36
    XmlStringHelper,
37
)
38
39
from .initsubclass import InitSubclassMeta
40
from .registry import register_command
41
42
logger = logging.getLogger(__name__)
43
44
45
class BaseCommand(metaclass=InitSubclassMeta):
46
47
    name = None
48
    description = None
49
    attributes = None
50
    elements = None
51
    must_be_initialized = None
52
53
    def __init_subclass__(cls, **kwargs):
54
        super_cls = super()
55
56
        if hasattr(super_cls, '__init_subclass__'):
57
            super_cls.__init_subclass__(**kwargs)
58
59
        register_command(cls)
60
61
    def __init__(self, daemon):
62
        self._daemon = daemon
63
64
    def get_name(self) -> str:
65
        return self.name
66
67
    def get_description(self) -> str:
68
        return self.description
69
70
    def get_attributes(self) -> Optional[Dict[str, Any]]:
71
        return self.attributes
72
73
    def get_elements(self) -> Optional[Dict[str, Any]]:
74
        return self.elements
75
76
    def handle_xml(self, xml: Element) -> Union[bytes, Iterator[bytes]]:
77
        raise NotImplementedError()
78
79
    def as_dict(self):
80
        return {
81
            'name': self.get_name(),
82
            'attributes': self.get_attributes(),
83
            'description': self.get_description(),
84
            'elements': self.get_elements(),
85
        }
86
87
    def __repr__(self):
88
        return '<{} description="{}" attributes={} elements={}>'.format(
89
            self.name, self.description, self.attributes, self.elements
90
        )
91
92
93
class HelpCommand(BaseCommand):
94
    name = "help"
95
    description = 'Print the commands help.'
96
    attributes = {'format': 'Help format. Could be text or xml.'}
97
    must_be_initialized = False
98
99
    def handle_xml(self, xml: Element) -> bytes:
100
        help_format = xml.get('format')
101
102
        if help_format is None or help_format == "text":
103
            # Default help format is text.
104
            return simple_response_str(
105
                'help', 200, 'OK', self._daemon.get_help_text()
106
            )
107
        elif help_format == "xml":
108
            text = get_elements_from_dict(
109
                {k: v.as_dict() for k, v in self._daemon.commands.items()}
110
            )
111
            return simple_response_str('help', 200, 'OK', text)
112
113
        raise OspdCommandError('Bogus help format', 'help')
114
115
116
class GetVersion(BaseCommand):
117
    name = "get_version"
118
    description = 'Return various version information'
119
    must_be_initialized = False
120
121
    def handle_xml(self, xml: Element) -> bytes:
122
        """ Handles <get_version> command.
123
124
        Return:
125
            Response string for <get_version> command.
126
        """
127
        protocol = Element('protocol')
128
129
        for name, value in [
130
            ('name', 'OSP'),
131
            ('version', self._daemon.get_protocol_version()),
132
        ]:
133
            elem = SubElement(protocol, name)
134
            elem.text = value
135
136
        daemon = Element('daemon')
137
        for name, value in [
138
            ('name', self._daemon.get_daemon_name()),
139
            ('version', self._daemon.get_daemon_version()),
140
        ]:
141
            elem = SubElement(daemon, name)
142
            elem.text = value
143
144
        scanner = Element('scanner')
145
        for name, value in [
146
            ('name', self._daemon.get_scanner_name()),
147
            ('version', self._daemon.get_scanner_version()),
148
        ]:
149
            elem = SubElement(scanner, name)
150
            elem.text = value
151
152
        content = [protocol, daemon, scanner]
153
154
        vts_version = self._daemon.get_vts_version()
155
        if vts_version:
156
            vts = Element('vts')
157
            elem = SubElement(vts, 'version')
158
            elem.text = vts_version
159
            content.append(vts)
160
161
        return simple_response_str('get_version', 200, 'OK', content)
162
163
164
GVMCG_TITLES = [
165
    'cpu-*',
166
    'proc',
167
    'mem',
168
    'swap',
169
    'load',
170
    'df-*',
171
    'disk-sd[a-z][0-9]-rw',
172
    'disk-sd[a-z][0-9]-load',
173
    'disk-sd[a-z][0-9]-io-load',
174
    'interface-eth*-traffic',
175
    'interface-eth*-err-rate',
176
    'interface-eth*-err',
177
    'sensors-*_temperature-*',
178
    'sensors-*_fanspeed-*',
179
    'sensors-*_voltage-*',
180
    'titles',
181
]  # type: List[str]
182
183
184
class GetPerformance(BaseCommand):
185
    name = "get_performance"
186
    description = 'Return system report'
187
    attributes = {
188
        'start': 'Time of first data point in report.',
189
        'end': 'Time of last data point in report.',
190
        'title': 'Name of report.',
191
    }
192
    must_be_initialized = False
193
194
    def handle_xml(self, xml: Element) -> bytes:
195
        """ Handles <get_performance> command.
196
197
        @return: Response string for <get_performance> command.
198
        """
199
        start = xml.attrib.get('start')
200
        end = xml.attrib.get('end')
201
        titles = xml.attrib.get('titles')
202
203
        cmd = ['gvmcg']
204
        if start:
205
            try:
206
                int(start)
207
            except ValueError:
208
                raise OspdCommandError(
209
                    'Start argument must be integer.', 'get_performance'
210
                )
211
212
            cmd.append(start)
213
214
        if end:
215
            try:
216
                int(end)
217
            except ValueError:
218
                raise OspdCommandError(
219
                    'End argument must be integer.', 'get_performance'
220
                )
221
222
            cmd.append(end)
223
224
        if titles:
225
            combined = "(" + ")|(".join(GVMCG_TITLES) + ")"
226
            forbidden = "^[^|&;]+$"
227
            if re.match(combined, titles) and re.match(forbidden, titles):
228
                cmd.append(titles)
229
            else:
230
                raise OspdCommandError(
231
                    'Arguments not allowed', 'get_performance'
232
                )
233
234
        try:
235
            output = subprocess.check_output(cmd)
236
        except (subprocess.CalledProcessError, OSError) as e:
237
            raise OspdCommandError(
238
                'Bogus get_performance format. %s' % e, 'get_performance'
239
            )
240
241
        return simple_response_str(
242
            'get_performance', 200, 'OK', output.decode()
243
        )
244
245
246
class GetScannerDetails(BaseCommand):
247
    name = 'get_scanner_details'
248
    description = 'Return scanner description and parameters'
249
    must_be_initialized = True
250
251
    def handle_xml(self, xml: Element) -> bytes:
252
        """ Handles <get_scanner_details> command.
253
254
        @return: Response string for <get_scanner_details> command.
255
        """
256
        desc_xml = Element('description')
257
        desc_xml.text = self._daemon.get_scanner_description()
258
        scanner_params = self._daemon.get_scanner_params()
259
        details = [
260
            desc_xml,
261
            OspResponse.create_scanner_params_xml(scanner_params),
262
        ]
263
        return simple_response_str('get_scanner_details', 200, 'OK', details)
264
265
266
class DeleteScan(BaseCommand):
267
    name = 'delete_scan'
268
    description = 'Delete a finished scan.'
269
    attributes = {'scan_id': 'ID of scan to delete.'}
270
    must_be_initialized = False
271
272
    def handle_xml(self, xml: Element) -> bytes:
273
        """ Handles <delete_scan> command.
274
275
        @return: Response string for <delete_scan> command.
276
        """
277
        scan_id = xml.get('scan_id')
278
        if scan_id is None:
279
            return simple_response_str(
280
                'delete_scan', 404, 'No scan_id attribute'
281
            )
282
283
        if not self._daemon.scan_exists(scan_id):
284
            text = "Failed to find scan '{0}'".format(scan_id)
285
            return simple_response_str('delete_scan', 404, text)
286
287
        self._daemon.check_scan_process(scan_id)
288
289
        if self._daemon.delete_scan(scan_id):
290
            return simple_response_str('delete_scan', 200, 'OK')
291
292
        raise OspdCommandError('Scan in progress', 'delete_scan')
293
294
295
class GetVts(BaseCommand):
296
    name = 'get_vts'
297
    description = 'List of available vulnerability tests.'
298
    attributes = {
299
        'vt_id': 'ID of a specific vulnerability test to get.',
300
        'filter': 'Optional filter to get an specific vt collection.',
301
    }
302
    must_be_initialized = True
303
304
    def handle_xml(self, xml: Element) -> Iterator[bytes]:
305
        """ Handles <get_vts> command.
306
        Writes the vt collection on the stream.
307
        The <get_vts> element accept two optional arguments.
308
        vt_id argument receives a single vt id.
309
        filter argument receives a filter selecting a sub set of vts.
310
        If both arguments are given, the vts which match with the filter
311
        are return.
312
313
        @return: Response string for <get_vts> command on fail.
314
        """
315
        self._daemon.vts.is_cache_available = False
316
317
        xml_helper = XmlStringHelper()
318
319
        vt_id = xml.get('vt_id')
320
        vt_filter = xml.get('filter')
321
        _details = xml.get('details')
322
        version_only = xml.get('version_only')
323
324
        vt_details = False if _details == '0' else True
325
326
        if self._daemon.vts and vt_id and vt_id not in self._daemon.vts:
327
            self._daemon.vts.is_cache_available = True
328
            text = "Failed to find vulnerability test '{0}'".format(vt_id)
329
            raise OspdCommandError(text, 'get_vts', 404)
330
331
        filtered_vts = None
332
        if vt_filter and not version_only:
333
            try:
334
                filtered_vts = self._daemon.vts_filter.get_filtered_vts_list(
335
                    self._daemon.vts, vt_filter
336
                )
337
            except OspdCommandError as filter_error:
338
                self._daemon.vts.is_cache_available = True
339
                raise OspdCommandError(filter_error)
340
341
        if not version_only:
342
            vts_selection = self._daemon.get_vts_selection_list(
343
                vt_id, filtered_vts
344
            )
345
        # List of xml pieces with the generator to be iterated
346
        yield xml_helper.create_response('get_vts')
347
348
        begin_vts_tag = xml_helper.create_element('vts')
349
        begin_vts_tag = xml_helper.add_attr(
350
            begin_vts_tag, "vts_version", self._daemon.get_vts_version()
351
        )
352
        val = len(self._daemon.vts)
353
        begin_vts_tag = xml_helper.add_attr(begin_vts_tag, "total", val)
354
        if filtered_vts and not version_only:
355
            val = len(filtered_vts)
356
            begin_vts_tag = xml_helper.add_attr(begin_vts_tag, "sent", val)
357
358
        if self._daemon.vts.sha256_hash is not None:
359
            begin_vts_tag = xml_helper.add_attr(
360
                begin_vts_tag, "sha256_hash", self._daemon.vts.sha256_hash
361
            )
362
363
        yield begin_vts_tag
364
        if not version_only:
365
            for vt in self._daemon.get_vt_iterator(vts_selection, vt_details):
0 ignored issues
show
introduced by
The variable vts_selection does not seem to be defined in case BooleanNotNode on line 341 is False. Are you sure this can never be the case?
Loading history...
366
                yield xml_helper.add_element(self._daemon.get_vt_xml(vt))
367
368
        yield xml_helper.create_element('vts', end=True)
369
        yield xml_helper.create_response('get_vts', end=True)
370
371
        self._daemon.vts.is_cache_available = True
372
373
374
class StopScan(BaseCommand):
375
    name = 'stop_scan'
376
    description = 'Stop a currently running scan.'
377
    attributes = {'scan_id': 'ID of scan to stop.'}
378
    must_be_initialized = True
379
380
    def handle_xml(self, xml: Element) -> bytes:
381
        """ Handles <stop_scan> command.
382
383
        @return: Response string for <stop_scan> command.
384
        """
385
386
        scan_id = xml.get('scan_id')
387
        if scan_id is None or scan_id == '':
388
            raise OspdCommandError('No scan_id attribute', 'stop_scan')
389
390
        self._daemon.stop_scan(scan_id)
391
392
        # Don't send response until the scan is stopped.
393
        try:
394
            self._daemon.scan_processes[scan_id].join()
395
        except KeyError:
396
            pass
397
398
        return simple_response_str('stop_scan', 200, 'OK')
399
400
401
class GetScans(BaseCommand):
402
    name = 'get_scans'
403
    description = 'List the scans in buffer.'
404
    attributes = {
405
        'scan_id': 'ID of a specific scan to get.',
406
        'details': 'Whether to return the full scan report.',
407
        'pop_results': 'Whether to remove the fetched results.',
408
        'max_results': 'Maximum number of results to fetch.',
409
    }
410
    must_be_initialized = False
411
412
    def handle_xml(self, xml: Element) -> bytes:
413
        """ Handles <get_scans> command.
414
415
        @return: Response string for <get_scans> command.
416
        """
417
418
        scan_id = xml.get('scan_id')
419
        if scan_id is None or scan_id == '':
420
            raise OspdCommandError('No scan_id attribute', 'get_scans')
421
422
        details = xml.get('details')
423
        pop_res = xml.get('pop_results')
424
        max_res = xml.get('max_results')
425
        progress = xml.get('progress')
426
427
        if details and details == '0':
428
            details = False
429
        else:
430
            details = True
431
            pop_res = pop_res and pop_res == '1'
432
433
            if max_res:
434
                max_res = int(max_res)
435
436
        progress = progress and progress == '1'
437
438
        responses = []
439
        if scan_id in self._daemon.scan_collection.ids_iterator():
440
            self._daemon.check_scan_process(scan_id)
441
            scan = self._daemon.get_scan_xml(
442
                scan_id, details, pop_res, max_res, progress
443
            )
444
            responses.append(scan)
445
        else:
446
            text = "Failed to find scan '{0}'".format(scan_id)
447
            return simple_response_str('get_scans', 404, text)
448
449
        return simple_response_str('get_scans', 200, 'OK', responses)
450
451
452
class StartScan(BaseCommand):
453
    name = 'start_scan'
454
    description = 'Start a new scan.'
455
    attributes = {
456
        'target': 'Target host to scan',
457
        'ports': 'Ports list to scan',
458
        'scan_id': 'Optional UUID value to use as scan ID',
459
        'parallel': 'Optional nummer of parallel target to scan',
460
    }
461
    must_be_initialized = False
462
463
    def get_elements(self):
464
        elements = {}
465
466
        if self.elements:
467
            elements.update(self.elements)
468
469
        scanner_params = elements.get('scanner_params', {}).copy()
470
        elements['scanner_params'] = scanner_params
471
472
        scanner_params.update(
473
            {
474
                k: v['description']
475
                for k, v in self._daemon.scanner_params.items()
476
            }
477
        )
478
479
        return elements
480
481
    def handle_xml(self, xml: Element) -> bytes:
482
        """ Handles <start_scan> command.
483
484
        Return:
485
            Response string for <start_scan> command.
486
        """
487
488
        current_queued_scans = self._daemon.get_count_queued_scans()
489
        if (
490
            self._daemon.max_queued_scans
491
            and current_queued_scans >= self._daemon.max_queued_scans
492
        ):
493
            logger.info(
494
                'Maximum number of queued scans set to %d reached.',
495
                self._daemon.max_queued_scans,
496
            )
497
            raise OspdCommandError(
498
                'Maximum number of queued scans set to %d reached.'
499
                % self._daemon.max_queued_scans,
500
                'start_scan',
501
            )
502
503
        target_str = xml.get('target')
504
        ports_str = xml.get('ports')
505
506
        # For backward compatibility, if target and ports attributes are set,
507
        # <targets> element is ignored.
508
        if target_str is None or ports_str is None:
509
            target_element = xml.find('targets/target')
510
            if target_element is None:
511
                raise OspdCommandError('No targets or ports', 'start_scan')
512
            else:
513
                scan_target = OspRequest.process_target_element(target_element)
514
        else:
515
            scan_target = {
516
                'hosts': target_str,
517
                'ports': ports_str,
518
                'credentials': {},
519
                'exclude_hosts': '',
520
                'finished_hosts': '',
521
                'options': {},
522
            }
523
            logger.warning(
524
                "Legacy start scan command format is being used, which "
525
                "is deprecated since 20.08. Please read the documentation "
526
                "for start scan command."
527
            )
528
529
        scan_id = xml.get('scan_id')
530
        if scan_id is not None and scan_id != '' and not valid_uuid(scan_id):
531
            raise OspdCommandError('Invalid scan_id UUID', 'start_scan')
532
533
        if xml.get('parallel'):
534
            logger.warning(
535
                "parallel attribute of start_scan will be ignored, sice "
536
                "parallel scan is not supported by OSPd."
537
            )
538
539
        scanner_params = xml.find('scanner_params')
540
        if scanner_params is None:
541
            raise OspdCommandError('No scanner_params element', 'start_scan')
542
543
        params = self._daemon.preprocess_scan_params(scanner_params)
544
545
        # VTS is an optional element. If present should not be empty.
546
        vt_selection = {}  # type: Dict
547
        scanner_vts = xml.find('vt_selection')
548
        if scanner_vts is not None:
549
            if len(scanner_vts) == 0:
550
                raise OspdCommandError('VTs list is empty', 'start_scan')
551
            else:
552
                vt_selection = OspRequest.process_vts_params(scanner_vts)
553
554
        # Dry run case.
555
        dry_run = 'dry_run' in params and int(params['dry_run'])
556
        if dry_run:
557
            scan_params = None
558
        else:
559
            scan_params = self._daemon.process_scan_params(params)
560
561
        scan_id_aux = scan_id
562
        scan_id = self._daemon.create_scan(
563
            scan_id, scan_target, scan_params, vt_selection
564
        )
565
566
        if not scan_id:
567
            id_ = Element('id')
568
            id_.text = scan_id_aux
569
            return simple_response_str('start_scan', 100, 'Continue', id_)
570
571
        logger.info(
572
            'Scan %s added to the queue in position %d.',
573
            scan_id,
574
            current_queued_scans + 1,
575
        )
576
577
        if dry_run:
578
            scan_func = self._daemon.dry_run_scan
579
            scan_process = create_process(
580
                func=scan_func, args=(scan_id, scan_target)
581
            )
582
            self._daemon.scan_processes[scan_id] = scan_process
583
            scan_process.start()
584
585
        id_ = Element('id')
586
        id_.text = scan_id
587
588
        return simple_response_str('start_scan', 200, 'OK', id_)
589
590
591
class GetMemoryUsage(BaseCommand):
592
593
    name = "get_memory_usage"
594
    description = "print the memory consumption of all processes"
595
    attributes = {
596
        'unit': 'Unit for displaying memory consumption (b = bytes, '
597
        'kb = kilobytes, mb = megabytes). Defaults to b.'
598
    }
599
    must_be_initialized = False
600
601
    @staticmethod
602
    def _get_memory(value: int, unit: str = None) -> str:
603
        if not unit:
604
            return str(value)
605
606
        unit = unit.lower()
607
608
        if unit == 'kb':
609
            return str(Decimal(value) / 1024)
610
611
        if unit == 'mb':
612
            return str(Decimal(value) / (1024 * 1024))
613
614
        return str(value)
615
616
    @staticmethod
617
    def _create_process_element(name: str, pid: int):
618
        process_element = Element('process')
619
        process_element.set('name', name)
620
        process_element.set('pid', str(pid))
621
622
        return process_element
623
624
    @classmethod
625
    def _add_memory_info(
626
        cls, process_element: Element, pid: int, unit: str = None
627
    ):
628
        try:
629
            ps_process = psutil.Process(pid)
630
        except psutil.NoSuchProcess:
631
            return
632
633
        memory = ps_process.memory_info()
634
635
        rss_element = Element('rss')
636
        rss_element.text = cls._get_memory(memory.rss, unit)
637
638
        process_element.append(rss_element)
639
640
        vms_element = Element('vms')
641
        vms_element.text = cls._get_memory(memory.vms, unit)
642
643
        process_element.append(vms_element)
644
645
        shared_element = Element('shared')
646
        shared_element.text = cls._get_memory(memory.shared, unit)
647
648
        process_element.append(shared_element)
649
650
    def handle_xml(self, xml: Element) -> bytes:
651
        processes_element = Element('processes')
652
        unit = xml.get('unit')
653
654
        current_process = multiprocessing.current_process()
655
        process_element = self._create_process_element(
656
            current_process.name, current_process.pid
657
        )
658
659
        self._add_memory_info(process_element, current_process.pid, unit)
660
661
        processes_element.append(process_element)
662
663
        for proc in multiprocessing.active_children():
664
            process_element = self._create_process_element(proc.name, proc.pid)
665
666
            self._add_memory_info(process_element, proc.pid, unit)
667
668
            processes_element.append(process_element)
669
670
        return simple_response_str('get_memory', 200, 'OK', processes_element)
671