check-gmp.gmp.FixedOffset.__eq__()   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nop 2
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
# -*- coding: utf-8 -*-
2
# Copyright (C) 2017-2021 Greenbone Networks GmbH
3
#
4
# SPDX-License-Identifier: GPL-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 General Public License as published by
8
# the Free Software Foundation, either version 3 of the License, or
9
# (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 General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19
# pylint: disable=too-many-lines
20
21
import logging
22
import os
23
import re
24
import signal
25
import sqlite3
26
import sys
27
import tempfile
28
29
from argparse import ArgumentParser, RawTextHelpFormatter
30
from datetime import datetime, timedelta, tzinfo
31
from decimal import Decimal
32
from pathlib import Path
33
34
from lxml import etree
35
36
__version__ = "2.0.0"
37
38
logger = logging.getLogger(__name__)
39
40
HELP_TEXT = """
41
    Check-GMP Nagios Command Plugin {version} (C) 2017-2021 Greenbone Networks GmbH
42
43
    This program is free software: you can redistribute it and/or modify
44
    it under the terms of the GNU General Public License as published by
45
    the Free Software Foundation, either version 3 of the License, or
46
    (at your option) any later version.
47
48
    This program is distributed in the hope that it will be useful,
49
    but WITHOUT ANY WARRANTY; without even the implied warranty of
50
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
51
    GNU General Public License for more details.
52
53
    You should have received a copy of the GNU General Public License
54
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
55
    """.format(
56
    version=__version__
57
)
58
59
NAGIOS_OK = 0
60
NAGIOS_WARNING = 1
61
NAGIOS_CRITICAL = 2
62
NAGIOS_UNKNOWN = 3
63
64
NAGIOS_MSG = ["OK", "WARNING", "CRITICAL", "UNKNOWN"]
65
66
MAX_RUNNING_INSTANCES = 10
67
68
69
class InstanceManager:
70
    """Class for managing instances of this plugin
71
72
    All new reports will be cached in a sqlite database.
73
    The first call with a unknown host takes longer,
74
    because the remote gvmd/openvasmd has to generate the report.
75
    The second call will retrieve the data from the database if the scan
76
    duration does not differ.
77
78
    Additionally this class handles all instances of check-gmp. No more than
79
    MAX_RUNNING_INSTANCES can run simultaneously. Other instances are stopped
80
    and wait for continuation.
81
    """
82
83
    def __init__(self, path, parser):
84
        """Initialise the sqlite database.
85
86
        Create it if it does not exist else connect to it.
87
88
        Arguments:
89
            path (string): Path to the database.
90
        """
91
        self.cursor = None
92
        self.con_db = None
93
        self.db = Path(path)
94
        self.pid = os.getpid()
95
96
        # Try to read file with information about cached reports
97
        # First check whether the file exist or not
98
        try:
99
            exist = self.db.is_file()
100
            logger.debug("DB file exist?: %s ", exist)
101
102
            if not exist:
103
                if not self.db.parent.is_dir():
104
                    self.db.parent.mkdir(parents=True, exist_ok=True)
105
                else:
106
                    self.db.touch()
107
                # Connect to db
108
                self.connect_db()
109
110
                # Create the tables
111
                self.cursor.execute(
112
                    """CREATE TABLE Report(
113
                    host text,
114
                    scan_end text,
115
                    params_used text,
116
                    report text
117
                )"""
118
                )
119
120
                self.cursor.execute(
121
                    """CREATE TABLE Instance(
122
                    created_at text,
123
                    pid integer,
124
                    pending integer default 0
125
                )"""
126
                )
127
128
                logger.debug("Tables created")
129
            else:
130
                self.connect_db()
131
132
        except PermissionError:
133
            parser.error(
134
                "The selected temporary database file {} or the parent dir has"
135
                " not the correct permissions.".format(self.db)
136
            )
137
138
    @staticmethod
139
    def _to_sql_bool(pending):
140
        """Replace True/False with 1/0."""
141
        return '1' if pending else '0'
142
143
    def connect_db(self):
144
        """Connect to the database
145
146
        Simply connect to the database at location <path>
147
        """
148
        try:
149
            logger.debug("connect db: %s", self.db)
150
            self.con_db = sqlite3.connect(str(self.db))
151
            self.cursor = self.con_db.cursor()
152
            logger.debug(sqlite3.sqlite_version)
153
        except Exception as e:  # pylint: disable=broad-except
154
            logger.debug(e)
155
156
    def close_db(self):
157
        """Close database"""
158
        self.con_db.close()
159
160
    def set_host(self, host):
161
        """Sets the host variable
162
163
        Arguments:
164
            host (string): Given ip or hostname of target.
165
        """
166
        self.host = host
167
168
    def is_old_report(self, last_scan_end, params_used):
169
        """Decide whether the current report is old or not
170
171
        At first the last scanend and the params that were used are fetched
172
        from the database. If no report is fetched, then True will be returned.
173
        The next step is to compare the old and the new scanend.
174
        If the scanends matches, then return False, because it is the same
175
        report. Else the old report will be deleted.
176
177
        Arguments:
178
            last_scan_end (string): Last scan end of report
179
            params_used (string): Params used for this check
180
181
        Returns:
182
            True if it is an old report or empty. False if it is the same
183
            report.
184
        """
185
186
        # Before we do anything here, check existing instance
187
188
        # Retrieve the scan_end value
189
        self.cursor.execute(
190
            "SELECT scan_end, params_used FROM Report WHERE" " host=?",
191
            (self.host,),
192
        )
193
        db_entry = self.cursor.fetchone()
194
195
        logger.debug("%s %s", db_entry, last_scan_end)
196
197
        if not db_entry:
198
            return True
199
        else:
200
            old = parse_date(db_entry[0])
201
            new = parse_date(last_scan_end)
202
203
            logger.debug(
204
                "Old time (from db): %s\n" "New time (from rp): %s", old, new
205
            )
206
207
            if new <= old and params_used == db_entry[1]:
208
                return False
209
            else:
210
                # Report is newer. Delete old entry.
211
                logger.debug("Delete old report for host %s", self.host)
212
                self.delete_report()
213
                return True
214
215
    def load_local_report(self):
216
        """Load report from local database
217
218
        Select the report from the database according due the hostname or ip.
219
220
        Returns:
221
            An lxml ElementTree
222
        """
223
        self.cursor.execute(
224
            "SELECT report FROM Report WHERE host=?", (self.host,)
225
        )
226
        db_entry = self.cursor.fetchone()
227
228
        if db_entry:
229
            return etree.fromstring(db_entry[0])
230
        else:
231
            logger.debug("Report from host %s is not in the db", self.host)
232
233
    def add_report(self, scan_end, params_used, report):
234
        """Create new entry with the lxml report
235
236
        Create a string from the lxml object and add it to the database.
237
        Additional data is the scanend and the params used.
238
239
        Arguments:
240
            scan_end (string): Scan end of the report
241
            params_used (string): Params used for this check
242
            report (obj): An lxml ElementTree
243
        """
244
245
        data = etree.tostring(report)
246
247
        logger.debug("add_report: %s, %s, %s", self.host, scan_end, params_used)
248
249
        # Insert values
250
        self.cursor.execute(
251
            "INSERT INTO Report VALUES (?, ?, ?, ?)",
252
            (self.host, scan_end, params_used, data),
253
        )
254
255
        # Save the changes
256
        self.con_db.commit()
257
258
    def delete_report(self):
259
        """Delete report from database"""
260
        self.cursor.execute("DELETE FROM Report WHERE host=?", (self.host,))
261
262
        # Save the changes
263
        self.con_db.commit()
264
265
    def delete_entry_with_ip(self, ip):
266
        """Delete report from database with given ip
267
268
        Arguments:
269
            ip (string): IP-Adress
270
        """
271
        logger.debug("Delete entry with ip: %s", ip)
272
        self.cursor.execute("DELETE FROM Report WHERE host=?", (ip,))
273
        self.con_db.isolation_level = None
274
        self.cursor.execute("VACUUM")
275
        self.con_db.isolation_level = ''  # see: https://github.com/CxAalto/gtfspy/commit/8d05c3c94a6d4ca3ed675d88af93def7d5053bfe # pylint: disable=line-too-long
276
        # Save the changes
277
        self.con_db.commit()
278
279
    def delete_older_entries(self, days):
280
        """Delete reports from database older than given days
281
282
        Arguments:
283
            days (int): Number of days in past
284
        """
285
        logger.debug("Delete entries older than: %s days", days)
286
        self.cursor.execute(
287
            "DELETE FROM Report WHERE scan_end <= "
288
            'date("now", "-%s day")' % days
289
        )
290
        self.cursor.execute("VACUUM")
291
292
        # Save the changes
293
        self.con_db.commit()
294
295
    def has_entries(self, pending):
296
        """Return number of instance entries
297
        Arguments:
298
            pending (bool): True for pending instances. False for running
299
                           instances.
300
301
        Returns:
302
            The number of pending or non pending instances entries.
303
        """
304
        self.cursor.execute(
305
            "SELECT count(*) FROM Instance WHERE pending=?",
306
            (self._to_sql_bool(pending),),
307
        )
308
309
        res = self.cursor.fetchone()
310
311
        return res[0]
312
313
    def check_instances(self):
314
        """This method checks the status of check-gmp instances.
315
316
        Checks whether instances are pending or not and start instances
317
        according to the number saved in the MAX_RUNNING_INSTANCES variable.
318
        """
319
320
        # Need to check whether any instances are in the database that were
321
        # killed f.e. because a restart of nagios
322
        self.clean_orphaned_instances()
323
324
        # How many processes are currently running?
325
        number_instances = self.has_entries(pending=False)
326
327
        # How many pending entries are waiting?
328
        number_pending_instances = self.has_entries(pending=True)
329
330
        logger.debug(
331
            "check_instances: %i %i", number_instances, number_pending_instances
332
        )
333
334
        if (
335
            number_instances < MAX_RUNNING_INSTANCES
336
            and number_pending_instances == 0
337
        ):
338
            # Add entry for running process and go on
339
            logger.debug("Fall 1")
340
            self.add_instance(pending=False)
341
342
        elif (
343
            number_instances < MAX_RUNNING_INSTANCES
344
            and number_pending_instances > 0
345
        ):
346
            # Change pending entries and wake them up until enough instances
347
            # are running
348
            logger.debug("Fall 2")
349
350
            while (
351
                number_instances < MAX_RUNNING_INSTANCES
352
                and number_pending_instances > 0
353
            ):
354
                pending_entries = self.get_oldest_pending_entries(
355
                    MAX_RUNNING_INSTANCES - number_instances
356
                )
357
358
                logger.debug("Oldest pending pids: %s", pending_entries)
359
360
                for entry in pending_entries:
361
                    created_at = entry[0]
362
                    pid = entry[1]
363
364
                    # Change status to not pending and continue the process
365
                    self.update_pending_status(created_at, False)
366
                    self.start_process(pid)
367
368
                # Refresh number of instances for next while loop
369
                number_instances = self.has_entries(pending=False)
370
                number_pending_instances = self.has_entries(pending=True)
371
372
            # TODO: Check if this is really necessary
373
            # self.add_instance(pending=False)
374
            # if number_instances >= MAX_RUNNING_INSTANCES:
375
            # self.stop_process(self.pid)
376
377
        elif (
378
            number_instances >= MAX_RUNNING_INSTANCES
379
            and number_pending_instances == 0
380
        ):
381
            # There are running enough instances and no pending instances
382
            # Add new entry with pending status true and stop this instance
383
            logger.debug("Fall 3")
384
            self.add_instance(pending=True)
385
            self.stop_process(self.pid)
386
387
        elif (
388
            number_instances >= MAX_RUNNING_INSTANCES
389
            and number_pending_instances > 0
390
        ):
391
            # There are running enough instances and there are min one
392
            # pending instance
393
            # Add new entry with pending true and stop this instance
394
            logger.debug("Fall 4")
395
            self.add_instance(pending=True)
396
            self.stop_process(self.pid)
397
398
        # If an entry is pending and the same params at another process is
399
        # starting, then exit with gmp pending since data
400
        # if self.has_pending_entries():
401
        # Check if an pending entry is the same as this process
402
        # If hostname
403
        #    date = datetime.now()
404
        #    end_session('GMP PENDING: since %s' % date, NAGIOS_OK)
405
        #    end_session('GMP RUNNING: since', NAGIOS_OK)
406
407
    def add_instance(self, pending):
408
        """Add new instance entry to database
409
410
        Retrieve the current time in ISO 8601 format. Create a new entry with
411
        pending status and the dedicated pid
412
413
        Arguments:
414
            pending (bool): State of instance
415
        """
416
        current_time = datetime.now().isoformat()
417
418
        # Insert values
419
        self.cursor.execute(
420
            "INSERT INTO Instance VALUES (?, ?, ?)",
421
            (current_time, self.pid, self._to_sql_bool(pending)),
422
        )
423
424
        # Save the changes
425
        self.con_db.commit()
426
427
    def get_oldest_pending_entries(self, number):
428
        """Return the oldest last entries of pending entries from database
429
430
        Return:
431
            the oldest instances with status pending limited by the variable
432
            <number>
433
        """
434
        self.cursor.execute(
435
            "SELECT * FROM Instance WHERE pending=1 ORDER BY "
436
            "created_at LIMIT ? ",
437
            (number,),
438
        )
439
        return self.cursor.fetchall()
440
441
    def update_pending_status(self, date, pending):
442
        """Update pending status of instance
443
444
        The date variable works as a primary key for the instance table.
445
        The entry with date get his pending status updated.
446
447
        Arguments:
448
            date (string):  Date of creation for entry
449
            pending (bool): Status of instance
450
        """
451
        self.cursor.execute(
452
            "UPDATE Instance SET pending=? WHERE created_at=?",
453
            (self._to_sql_bool(pending), date),
454
        )
455
456
        # Save the changes
457
        self.con_db.commit()
458
459
    def delete_instance(self, pid=None):
460
        """Delete instance from database
461
462
        If a pid different from zero is given, then delete the entry with
463
        given pid. Else delete the entry with the pid stored in this class
464
        instance.
465
466
        Keyword Arguments:
467
            pid (number): Process Indentificattion Number (default: {0})
468
        """
469
        if not pid:
470
            pid = self.pid
471
472
        logger.debug("Delete entry with pid: %i", pid)
473
        self.cursor.execute("DELETE FROM Instance WHERE pid=?", (pid,))
474
475
        # Save the changes
476
        self.con_db.commit()
477
478
    def clean_orphaned_instances(self):
479
        """Delete non existing instance entries
480
481
        This method check whether a pid exist on the os and if not then delete
482
        the orphaned entry from database.
483
        """
484
        self.cursor.execute("SELECT pid FROM Instance")
485
486
        pids = self.cursor.fetchall()
487
488
        for pid in pids:
489
            if not self.check_pid(pid[0]):
490
                self.delete_instance(pid[0])
491
492
    def wake_instance(self):
493
        """Wake up a pending instance
494
495
        This method is called at the end of any session from check_gmp.
496
        Get the oldest pending entries and wake them up.
497
        """
498
        # How many processes are currently running?
499
        number_instances = self.has_entries(pending=False)
500
501
        # How many pending entries are waiting?
502
        number_pending_instances = self.has_entries(pending=True)
503
504
        if (
505
            number_instances < MAX_RUNNING_INSTANCES
506
            and number_pending_instances > 0
507
        ):
508
509
            pending_entries = self.get_oldest_pending_entries(
510
                MAX_RUNNING_INSTANCES - number_instances
511
            )
512
513
            logger.debug(
514
                "wake_instance: %i %i",
515
                number_instances,
516
                number_pending_instances,
517
            )
518
519
            for entry in pending_entries:
520
                created_at = entry[0]
521
                pid = entry[1]
522
                # Change status to not pending and continue the process
523
                self.update_pending_status(created_at, False)
524
                self.start_process(pid)
525
526
    def start_process(self, pid):
527
        """Continue a stopped process
528
529
        Send a continue signal to the process with given pid
530
531
        Arguments:
532
            pid (int): Process Identification Number
533
        """
534
        logger.debug("Continue pid: %i", pid)
535
        os.kill(pid, signal.SIGCONT)
536
537
    def stop_process(self, pid):
538
        """Stop a running process
539
540
        Send a stop signal to the process with given pid
541
542
        Arguments:
543
            pid (int): Process Identification Number
544
        """
545
        os.kill(pid, signal.SIGSTOP)
546
547
    def check_pid(self, pid):
548
        """Check for the existence of a process.
549
550
        Arguments:
551
            pid (int): Process Identification Number
552
        """
553
        try:
554
            os.kill(pid, 0)
555
        except OSError:
556
            return False
557
        else:
558
            return True
559
560
561
def ping(gmp, im):
562
    """Checks for connectivity
563
564
    This function sends the get_version command and checks whether the status
565
    is ok or not.
566
    """
567
    version = gmp.get_version()
568
    version_status = version.xpath("@status")
569
570
    if "200" in version_status:
571
        end_session(im, "GMP OK: Ping successful", NAGIOS_OK)
572
    else:
573
        end_session(im, "GMP CRITICAL: Machine dead?", NAGIOS_CRITICAL)
574
575
576
def status(gmp, im, script_args):
577
    """Returns the current status of a host
578
579
    This functions return the current state of a host.
580
    Either directly over the asset management or within a task.
581
582
    For a task you can explicitly ask for the trend.
583
    Otherwise the last report of the task will be filtered.
584
585
    In the asset management the report id in the details is taken
586
    as report for the filter.
587
    If the asset information contains any vulnerabilities, then will the
588
    report be filtered too. With additional parameters it is possible to add
589
    more information about the vulnerabilities.
590
591
    * DFN-Certs
592
    * Logs
593
    * Autofp
594
    * Scanend
595
    * Overrides
596
    """
597
    params_used = "task=%s autofp=%i overrides=%i apply_overrides=%i" % (
598
        script_args.task,
599
        script_args.autofp,
600
        int(script_args.overrides),
601
        int(script_args.apply_overrides),
602
    )
603
604
    if script_args.task:
605
        task = gmp.get_tasks(
606
            filter="permission=any owner=any rows=1 "
607
            'name="%s"' % script_args.task
608
        )
609
        if script_args.trend:
610
            trend = task.xpath("task/trend/text()")
611
612
            if not trend:
613
                end_session(
614
                    im, "GMP UNKNOWN: Trend is not available.", NAGIOS_UNKNOWN
615
                )
616
617
            trend = trend[0]
618
619
            if trend in ["up", "more"]:
620
                end_session(
621
                    im, "GMP CRITICAL: Trend is %s." % trend, NAGIOS_CRITICAL
622
                )
623
            elif trend in ["down", "same", "less"]:
624
                end_session(im, "GMP OK: Trend is %s." % trend, NAGIOS_OK)
625
            else:
626
                end_session(
627
                    im,
628
                    "GMP UNKNOWN: Trend is unknown: %s" % trend,
629
                    NAGIOS_UNKNOWN,
630
                )
631
        else:
632
            last_report_id = task.xpath("task/last_report/report/@id")
633
634
            if not last_report_id:
635
                end_session(
636
                    im, "GMP UNKNOWN: Report is not available", NAGIOS_UNKNOWN
637
                )
638
639
            last_report_id = last_report_id[0]
640
            last_scan_end = task.xpath(
641
                "task/last_report/report/scan_end/text()"
642
            )
643
644
            if last_scan_end:
645
                last_scan_end = last_scan_end[0]
646
            else:
647
                last_scan_end = ""
648
649
            if im.is_old_report(last_scan_end, params_used):
650
                host = script_args.hostaddress
651
652
                full_report = gmp.get_report(
653
                    report_id=last_report_id,
654
                    filter="sort-reverse=id result_hosts_only=1 "
655
                    "min_cvss_base= min_qod= levels=hmlgd autofp={} "
656
                    "notes=0 apply_overrides={} overrides={} first=1 rows=-1 "
657
                    "delta_states=cgns host={}".format(
658
                        script_args.autofp,
659
                        int(script_args.overrides),
660
                        int(script_args.apply_overrides),
661
                        host,
662
                    ),
663
                    details=True,
664
                )
665
666
                im.add_report(last_scan_end, params_used, full_report)
667
                logger.debug("Report added to db")
668
            else:
669
                full_report = im.load_local_report()
670
671
            filter_report(
672
                im, full_report.xpath("report/report")[0], script_args
673
            )
674
675
676
def filter_report(im, report, script_args):
677
    """Filter out the information in a report
678
679
    This function filters the results of a given report.
680
681
    Arguments:
682
        report (obj): Report as lxml ElementTree.
683
    """
684
    report_id = report.xpath("@id")
685
    if report_id:
686
        report_id = report_id[0]
687
    results = report.xpath("//results")
688
    if not results:
689
        end_session(
690
            im, "GMP UNKNOWN: Failed to get results list", NAGIOS_UNKNOWN
691
        )
692
693
    results = results[0]
694
    # Init variables
695
    any_found = False
696
    high_count = 0
697
    medium_count = 0
698
    low_count = 0
699
    log_count = 0
700
    error_count = 0
701
702
    nvts = {"high": [], "medium": [], "low": [], "log": []}
703
704
    all_results = results.xpath("result")
705
706
    for result in all_results:
707
        if script_args.hostaddress:
708
            host = result.xpath("host/text()")
709
            if not host:
710
                end_session(
711
                    im,
712
                    "GMP UNKNOWN: Failed to parse result host",
713
                    NAGIOS_UNKNOWN,
714
                )
715
716
            if script_args.hostaddress != host[0]:
717
                continue
718
            any_found = True
719
720
        threat = result.xpath("threat/text()")
721
        if not threat:
722
            end_session(
723
                im,
724
                "GMP UNKNOWN: Failed to parse result threat.",
725
                NAGIOS_UNKNOWN,
726
            )
727
728
        threat = threat[0]
729
        if threat in "High":
730
            high_count += 1
731
            if script_args.oid:
732
                nvts["high"].append(retrieve_nvt_data(result))
733
        elif threat in "Medium":
734
            medium_count += 1
735
            if script_args.oid:
736
                nvts["medium"].append(retrieve_nvt_data(result))
737
        elif threat in "Low":
738
            low_count += 1
739
            if script_args.oid:
740
                nvts["low"].append(retrieve_nvt_data(result))
741
        elif threat in "Log":
742
            log_count += 1
743
            if script_args.oid:
744
                nvts["log"].append(retrieve_nvt_data(result))
745
        else:
746
            end_session(
747
                im,
748
                "GMP UNKNOWN: Unknown result threat: %s" % threat,
749
                NAGIOS_UNKNOWN,
750
            )
751
752
    errors = report.xpath("errors")
753
754
    if errors:
755
        errors = errors[0]
756
        if script_args.hostaddress:
757
            for error in errors.xpath("error"):
758
                host = error.xpath("host/text()")
759
                if script_args.hostaddress == host[0]:
760
                    error_count += 1
761
        else:
762
            error_count = errors.xpath("count/text()")[0]
763
764
    ret = 0
765
    if high_count > 0:
766
        ret = NAGIOS_CRITICAL
767
    elif medium_count > 0:
768
        ret = NAGIOS_WARNING
769
770
    if script_args.empty_as_unknown and (
771
        not all_results or (not any_found and script_args.hostaddress)
772
    ):
773
        ret = NAGIOS_UNKNOWN
774
775
    print(
776
        "GMP %s: %i vulnerabilities found - High: %i Medium: %i "
777
        "Low: %i"
778
        % (
779
            NAGIOS_MSG[ret],
780
            (high_count + medium_count + low_count),
781
            high_count,
782
            medium_count,
783
            low_count,
784
        )
785
    )
786
787
    if not all_results:
788
        print("Report did not contain any vulnerabilities")
789
790
    elif not any_found and script_args.hostaddress:
791
        print(
792
            "Report did not contain vulnerabilities for IP %s"
793
            % script_args.hostaddress
794
        )
795
796
    if int(error_count) > 0:
797
        if script_args.hostaddress:
798
            print_without_pipe(
799
                "Report did contain %i errors for IP %s"
800
                % (error_count, script_args.hostaddress)
801
            )
802
        else:
803
            print_without_pipe(
804
                "Report did contain %i errors" % int(error_count)
805
            )
806
807
    if script_args.report_link:
808
        print(
809
            "https://%s/omp?cmd=get_report&report_id=%s"
810
            % (script_args.hostname, report_id)
811
        )
812
813
    if script_args.oid:
814
        print_nvt_data(
815
            nvts,
816
            show_log=script_args.showlog,
817
            show_ports=script_args.show_ports,
818
            descr=script_args.descr,
819
            dfn=script_args.dfn,
820
        )
821
822
    if script_args.scanend:
823
        end = report.xpath("//end/text()")
824
        end = end[0] if end else "Timestamp of scan end not given"
825
        print("SCAN_END: %s" % end)
826
827
    if script_args.details:
828
        if script_args.hostname:
829
            print("GSM_Host: %s:%d" % (script_args.hostname, script_args.port))
830
        if script_args.gmp_username:
831
            print("GMP_User: %s" % script_args.gmp_username)
832
        if script_args.task:
833
            print_without_pipe("Task: %s" % script_args.task)
834
835
    end_session(
836
        im,
837
        "|High=%i Medium=%i Low=%i" % (high_count, medium_count, low_count),
838
        ret,
839
    )
840
841
842
def retrieve_nvt_data(result):
843
    """Retrieve the nvt data out of the result object
844
845
    This function parse the xml tree to find the important nvt data.
846
847
    Arguments:
848
        result (obj): Result as lxml ElementTree
849
850
    Returns:
851
        Tuple -- List with oid, name, desc, port and dfn
852
    """
853
    oid = result.xpath("nvt/@oid")
854
    name = result.xpath("nvt/name/text()")
855
    desc = result.xpath("description/text()")
856
    port = result.xpath("port/text()")
857
858
    if oid:
859
        oid = oid[0]
860
861
    if name:
862
        name = name[0]
863
864
    if desc:
865
        desc = desc[0]
866
    else:
867
        desc = ""
868
869
    if port:
870
        port = port[0]
871
    else:
872
        port = ""
873
874
    certs = result.xpath("nvt/cert/cert_ref")
875
876
    dfn_list = []
877
    for ref in certs:
878
        ref_type = ref.xpath("@type")[0]
879
        ref_id = ref.xpath("@id")[0]
880
881
        if ref_type in "DFN-CERT":
882
            dfn_list.append(ref_id)
883
884
    return (oid, name, desc, port, dfn_list)
885
886
887
def print_nvt_data(
888
    nvts, show_log=False, show_ports=False, descr=False, dfn=False
889
):
890
    """Print nvt data
891
892
    Prints for each nvt found in the array the relevant data
893
894
    Arguments:
895
        nvts (obj): Object holding all nvts
896
    """
897
    for key, nvt_data in nvts.items():
898
        if key == "log" and not show_log:
899
            continue
900
        for nvt in nvt_data:
901
            print_without_pipe("NVT: %s (%s) %s" % (nvt[0], key, nvt[1]))
902
            if show_ports:
903
                print_without_pipe("PORT: %s" % (nvt[3]))
904
            if descr:
905
                print_without_pipe("DESCR: %s" % nvt[2])
906
907
            if dfn and nvt[4]:
908
                dfn_list = ", ".join(nvt[4])
909
                if dfn_list:
910
                    print_without_pipe("DFN-CERT: %s" % dfn_list)
911
912
913
def end_session(im, msg, nagios_status):
914
    """End the session
915
916
    Close the socket if open and print the last msg
917
918
    Arguments:
919
        msg string): Message to print
920
        nagios_status (int): Exit status
921
    """
922
    print(msg)
923
924
    # Delete this instance
925
    im.delete_instance()
926
927
    # Activate some waiting instances if possible
928
    im.wake_instance()
929
930
    # Close the connection to database
931
    im.close_db()
932
933
    sys.exit(nagios_status)
934
935
936
def print_without_pipe(msg):
937
    """Prints the message, but without any pipe symbol
938
939
    If any pipe symbol is in the msg string, then it will be replaced with
940
    broken pipe symbol.
941
942
    Arguments:
943
        msg (string): Message to print
944
    """
945
    if "|" in msg:
946
        msg = msg.replace("|", "¦")
947
948
    print(msg)
949
950
951
# ISO 8601 date time string parsing
952
953
# Copyright (c) 2007 - 2015 Michael Twomey
954
955
# Permission is hereby granted, free of charge, to any person obtaining a
956
# copy of this software and associated documentation files (the
957
# "Software"), to deal in the Software without restriction, including
958
# without limitation the rights to use, copy, modify, merge, publish,
959
# distribute, sublicense, and/or sell copies of the Software, and to
960
# permit persons to whom the Software is furnished to do so, subject to
961
# the following conditions:
962
963
# The above copyright notice and this permission notice shall be included
964
# in all copies or substantial portions of the Software.
965
966
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
967
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
968
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
969
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
970
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
971
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
972
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
973
974
__all__ = ["parse_date", "ParseError", "UTC"]
975
976
_basestring = str
977
978
979
# Adapted from http://delete.me.uk/2005/03/iso8601.html
980
ISO8601_REGEX = re.compile(
981
    r"""
982
    (?P<year>[0-9]{4})
983
    (
984
        (
985
            (-(?P<monthdash>[0-9]{1,2}))
986
            |
987
            (?P<month>[0-9]{2})
988
            (?!$)  # Don't allow YYYYMM
989
        )
990
        (
991
            (
992
                (-(?P<daydash>[0-9]{1,2}))
993
                |
994
                (?P<day>[0-9]{2})
995
            )
996
            (
997
                (
998
                    (?P<separator>[ T])
999
                    (?P<hour>[0-9]{2})
1000
                    (:{0,1}(?P<minute>[0-9]{2})){0,1}
1001
                    (
1002
                        :{0,1}(?P<second>[0-9]{1,2})
1003
                        ([.,](?P<second_fraction>[0-9]+)){0,1}
1004
                    ){0,1}
1005
                    (?P<timezone>
1006
                        Z
1007
                        |
1008
                        (
1009
                            (?P<tz_sign>[-+])
1010
                            (?P<tz_hour>[0-9]{2})
1011
                            :{0,1}
1012
                            (?P<tz_minute>[0-9]{2}){0,1}
1013
                        )
1014
                    ){0,1}
1015
                ){0,1}
1016
            )
1017
        ){0,1}  # YYYY-MM
1018
    ){0,1}  # YYYY only
1019
    $
1020
    """,
1021
    re.VERBOSE,
1022
)
1023
1024
1025
class ParseError(Exception):
1026
    """Raised when there is a problem parsing a date string"""
1027
1028
1029
# Yoinked from python docs
1030
ZERO = timedelta(0)
1031
1032
1033
class Utc(tzinfo):
1034
    """UTC Timezone"""
1035
1036
    def utcoffset(self, dt):
1037
        return ZERO
1038
1039
    def tzname(self, dt):
1040
        return "UTC"
1041
1042
    def dst(self, dt):
1043
        return ZERO
1044
1045
    def __repr__(self):
1046
        return "<iso8601.Utc>"
1047
1048
1049
UTC = Utc()
1050
1051
1052
class FixedOffset(tzinfo):
1053
    """Fixed offset in hours and minutes from UTC"""
1054
1055
    def __init__(self, offset_hours, offset_minutes, name):
1056
        self.__offset_hours = offset_hours  # Keep for later __getinitargs__
1057
        # Keep for later __getinitargs__
1058
        self.__offset_minutes = offset_minutes
1059
        self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes)
1060
        self.__name = name
1061
1062
    def __eq__(self, other):
1063
        if isinstance(other, FixedOffset):
1064
            # pylint: disable=protected-access
1065
            return (other.__offset == self.__offset) and (
1066
                other.__name == self.__name
1067
            )
1068
        if isinstance(other, tzinfo):
1069
            return other == self
1070
        return False
1071
1072
    def __getinitargs__(self):
1073
        return (self.__offset_hours, self.__offset_minutes, self.__name)
1074
1075
    def utcoffset(self, dt):
1076
        return self.__offset
1077
1078
    def tzname(self, dt):
1079
        return self.__name
1080
1081
    def dst(self, dt):
1082
        return ZERO
1083
1084
    def __repr__(self):
1085
        return "<FixedOffset %r %r>" % (self.__name, self.__offset)
1086
1087
1088
def to_int(
1089
    source_dict, key, default_to_zero=False, default=None, required=True
1090
):
1091
    """Pull a value from the dict and convert to int
1092
1093
    :param default_to_zero: If the value is None or empty, treat it as zero
1094
    :param default: If the value is missing in the dict use this default
1095
1096
    """
1097
1098
    value = source_dict.get(key)
1099
    if value in [None, ""]:
1100
        value = default
1101
    if (value in ["", None]) and default_to_zero:
1102
        return 0
1103
    if value is None:
1104
        if required:
1105
            raise ParseError("Unable to read %s from %s" % (key, source_dict))
1106
        return value
1107
    else:
1108
        return int(value)
1109
1110
1111
def parse_timezone(matches, default_timezone=UTC):
1112
    """Parses ISO 8601 time zone specs into tzinfo offsets"""
1113
1114
    if matches["timezone"] == "Z":
1115
        return UTC
1116
    # This isn't strictly correct, but it's common to encounter dates without
1117
    # timezones so I'll assume the default (which defaults to UTC).
1118
    # Addresses issue 4.
1119
    if matches["timezone"] is None:
1120
        return default_timezone
1121
    sign = matches["tz_sign"]
1122
    hours = to_int(matches, "tz_hour")
1123
    minutes = to_int(matches, "tz_minute", default_to_zero=True)
1124
    description = "%s%02d:%02d" % (sign, hours, minutes)
1125
    if sign == "-":
1126
        hours = -1 * hours
1127
        minutes = -1 * minutes
1128
    return FixedOffset(hours, minutes, description)
1129
1130
1131
def parse_date(datestring, default_timezone=UTC):
1132
    """Parses ISO 8601 dates into datetime objects
1133
1134
    The timezone is parsed from the date string. However it is quite common to
1135
    have dates without a timezone (not strictly correct). In this case the
1136
    default timezone specified in default_timezone is used. This is UTC by
1137
    default.
1138
1139
    Arguments
1140
        datestring: The date to parse as a string
1141
        default_timezone: A datetime tzinfo instance to use when no timezone
1142
                          is specified in the datestring. If this is set to
1143
                          None then a naive datetime object is returned.
1144
    Returns:
1145
        A datetime.datetime instance
1146
    Raises:
1147
        ParseError when there is a problem parsing the date or
1148
        constructing the datetime instance.
1149
1150
    """
1151
    if not isinstance(datestring, _basestring):
1152
        raise ParseError("Expecting a string %r" % datestring)
1153
1154
    match = ISO8601_REGEX.match(datestring)
1155
    if not match:
1156
        raise ParseError("Unable to parse date string %r" % datestring)
1157
1158
    groups = match.groupdict()
1159
1160
    tz = parse_timezone(groups, default_timezone=default_timezone)
1161
1162
    groups["second_fraction"] = int(
1163
        Decimal("0.%s" % (groups["second_fraction"] or 0))
1164
        * Decimal("1000000.0")
1165
    )
1166
1167
    try:
1168
        return datetime(
1169
            year=to_int(groups, "year"),
1170
            month=to_int(
1171
                groups,
1172
                "month",
1173
                default=to_int(groups, "monthdash", required=False, default=1),
1174
            ),
1175
            day=to_int(
1176
                groups,
1177
                "day",
1178
                default=to_int(groups, "daydash", required=False, default=1),
1179
            ),
1180
            hour=to_int(groups, "hour", default_to_zero=True),
1181
            minute=to_int(groups, "minute", default_to_zero=True),
1182
            second=to_int(groups, "second", default_to_zero=True),
1183
            microsecond=groups["second_fraction"],
1184
            tzinfo=tz,
1185
        )
1186
    except Exception as e:
1187
        raise ParseError(e) from None
1188
1189
1190
def main(gmp, args):
1191
    tmp_path = "%s/check_gmp/" % tempfile.gettempdir()
1192
    tmp_path_db = tmp_path + "reports.db"
1193
1194
    parser = ArgumentParser(
1195
        prog="check-gmp",
1196
        prefix_chars="-",
1197
        description=HELP_TEXT,
1198
        formatter_class=RawTextHelpFormatter,
1199
        add_help=False,
1200
        epilog="""
1201
        usage: gvm-script [connection_type] check-gmp.gmp.py ...
1202
        or: gvm-script [connection_type] check-gmp.gmp.py -H
1203
        or: gvm-script connection_type --help""",
1204
    )
1205
1206
    parser.add_argument(
1207
        "-H", action="help", help="Show this help message and exit."
1208
    )
1209
1210
    parser.add_argument(
1211
        "-V",
1212
        "--version",
1213
        action="version",
1214
        version="%(prog)s {version}".format(version=__version__),
1215
        help="Show program's version number and exit",
1216
    )
1217
1218
    parser.add_argument(
1219
        "--cache",
1220
        nargs="?",
1221
        default=tmp_path_db,
1222
        help="Path to cache file. Default: %s." % tmp_path_db,
1223
    )
1224
1225
    parser.add_argument(
1226
        "--clean", action="store_true", help="Activate to clean the database."
1227
    )
1228
1229
    parser.add_argument(
1230
        "-u", "--gmp-username", help="GMP username.", required=False
1231
    )
1232
1233
    parser.add_argument(
1234
        "-w", "--gmp-password", help="GMP password.", required=False
1235
    )
1236
1237
    parser.add_argument(
1238
        "-F",
1239
        "--hostaddress",
1240
        required=False,
1241
        default="",
1242
        help="Report last report status of host <ip>.",
1243
    )
1244
1245
    parser.add_argument(
1246
        "-T", "--task", required=False, help="Report status of task <task>."
1247
    )
1248
1249
    parser.add_argument(
1250
        "--apply-overrides", action="store_true", help="Apply overrides."
1251
    )
1252
1253
    parser.add_argument(
1254
        "--overrides", action="store_true", help="Include overrides."
1255
    )
1256
1257
    parser.add_argument(
1258
        "-d",
1259
        "--details",
1260
        action="store_true",
1261
        help="Include connection details in output.",
1262
    )
1263
1264
    parser.add_argument(
1265
        "-l",
1266
        "--report-link",
1267
        action="store_true",
1268
        help="Include URL of report in output.",
1269
    )
1270
1271
    parser.add_argument(
1272
        "--dfn",
1273
        action="store_true",
1274
        help="Include DFN-CERT IDs on vulnerabilities in output.",
1275
    )
1276
1277
    parser.add_argument(
1278
        "--oid",
1279
        action="store_true",
1280
        help="Include OIDs of NVTs finding vulnerabilities in output.",
1281
    )
1282
1283
    parser.add_argument(
1284
        "--descr",
1285
        action="store_true",
1286
        help="Include descriptions of NVTs finding vulnerabilities in output.",
1287
    )
1288
1289
    parser.add_argument(
1290
        "--showlog", action="store_true", help="Include log messages in output."
1291
    )
1292
1293
    parser.add_argument(
1294
        "--show-ports",
1295
        action="store_true",
1296
        help="Include port of given vulnerable nvt in output.",
1297
    )
1298
1299
    parser.add_argument(
1300
        "--scanend",
1301
        action="store_true",
1302
        help="Include timestamp of scan end in output.",
1303
    )
1304
1305
    parser.add_argument(
1306
        "--autofp",
1307
        type=int,
1308
        choices=[0, 1, 2],
1309
        default=0,
1310
        help="Trust vendor security updates for automatic false positive"
1311
        " filtering (0=No, 1=full match, 2=partial).",
1312
    )
1313
1314
    parser.add_argument(
1315
        "-e",
1316
        "--empty-as-unknown",
1317
        action="store_true",
1318
        help="Respond with UNKNOWN on empty results.",
1319
    )
1320
1321
    parser.add_argument(
1322
        "-I",
1323
        "--max-running-instances",
1324
        default=10,
1325
        type=int,
1326
        help="Set the maximum simultaneous processes of check-gmp",
1327
    )
1328
1329
    parser.add_argument("--hostname", nargs="?", required=False)
1330
1331
    group = parser.add_mutually_exclusive_group(required=False)
1332
    group.add_argument(
1333
        "--ping", action="store_true", help="Ping the gsm appliance."
1334
    )
1335
1336
    group.add_argument(
1337
        "--status", action="store_true", help="Report status of task."
1338
    )
1339
1340
    group = parser.add_mutually_exclusive_group(required=False)
1341
    group.add_argument(
1342
        "--days",
1343
        type=int,
1344
        help="Delete database entries that are older than" " given days.",
1345
    )
1346
    group.add_argument("--ip", help="Delete database entry for given ip.")
1347
1348
    group = parser.add_mutually_exclusive_group(required=False)
1349
    group.add_argument(
1350
        "--trend", action="store_true", help="Report status by trend."
1351
    )
1352
    group.add_argument(
1353
        "--last-report",
1354
        action="store_true",
1355
        help="Report status by last report.",
1356
    )
1357
1358
    script_args = parser.parse_args(args.script_args)
1359
1360
    aux_parser = ArgumentParser(
1361
        prefix_chars="-", formatter_class=RawTextHelpFormatter
1362
    )
1363
    aux_parser.add_argument("--hostname", nargs="?", required=False)
1364
    gvm_tool_args, _ = aux_parser.parse_known_args(sys.argv)
1365
    if "hostname" in gvm_tool_args:
1366
        script_args.hostname = gvm_tool_args.hostname
1367
1368
    # Set the max running instances variable
1369
    if script_args.max_running_instances:
1370
        # TODO should be passed as local variable instead of using a global one
1371
        # pylint: disable=global-statement
1372
        global MAX_RUNNING_INSTANCES
1373
        MAX_RUNNING_INSTANCES = script_args.max_running_instances
1374
1375
    # Set the report manager
1376
    if script_args.cache:
1377
        tmp_path_db = script_args.cache
1378
    im = InstanceManager(tmp_path_db, parser)
1379
1380
    # Check if command holds clean command
1381
    if script_args.clean:
1382
        if script_args.ip:
1383
            logger.info("Delete entry with ip %s", script_args.ip)
1384
            im.delete_entry_with_ip(script_args.ip)
1385
        elif script_args.days:
1386
            logger.info("Delete entries older than %s days", script_args.days)
1387
            im.delete_older_entries(script_args.days)
1388
        sys.exit(1)
1389
1390
    # Set the host
1391
    im.set_host(script_args.hostaddress)
1392
1393
    # Check if no more than 10 instances of check-gmp runs simultaneously
1394
    im.check_instances()
1395
1396
    try:
1397
        gmp.get_version()
1398
    except Exception as e:  # pylint: disable=broad-except
1399
        end_session(im, "GMP CRITICAL: %s" % str(e), NAGIOS_CRITICAL)
1400
1401
    if script_args.ping:
1402
        ping(gmp, im)
1403
1404
    if "status" in script_args:
1405
        status(gmp, im, script_args)
1406
1407
1408
if __name__ == "__gmp__":
1409
    main(gmp, args)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable args does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable gmp does not seem to be defined.
Loading history...
1410