Passed
Pull Request — master (#244)
by
unknown
01:25
created

check-gmp.gmp.InstanceManager.check_pid()   A

Complexity

Conditions 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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