Subversion Repositories vnag

Rev

Rev 42 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
8 daniel-mar 1
<?php /* <ViaThinkSoftSignature>
59 daniel-mar 2
d5Ch9xyUxv3Bu3N2jbIjfBn2mP062FT5Kdj1z31EUW9w+rJMXfvaeidWQfLapqU7c
3
uIAztDKoRCMsAsvZYSB1zD8+pi0ClJYGbiB+/7ACt1TSVOvtrWxE9lsWXIOjLA0Ft
4
YeccOh4Mf/JFtLbLyo/xQofsIy+9umKQ1fbhgrcmJfaWY07QeoZVE7HHauTZA+ld6
5
HyjNklTAc9b3tcFQBp9bgB3p4Pt28y6irIqDhaqvBja8F2oCK7FCZQFwE/JF0UKT/
6
PMOJ0VtzC1p72SiOWMg4U0+hMi4Sre48SuSOiUtLaF14GdzfATGOxOqyFvFAFXClh
7
qUqgxXmQTFJCZzGW2rga9BJw35zTYy/jtWLN8nGFG+S2c7e+IJAqN+iDMD7I+NyRd
8
jjZKqDjXDZrIolL6KP2yl4WPtVD8iaJcgrxUQD+TQiJkcjvSEp2DVN0OETDFUzlx2
9
Jr9hc14n7z78ebkTgNToAGauBvfyFRCEyRBcXyS9VluTd23g3ICXY0qdWJUhf4zpz
10
PJPdgAVmO1X8kPaVB97GI2iQDqIMFLr6PEre6GTaB893B3lrej8sTdAiV8WPlzKmZ
11
LHxxUr2uQdIT2NkLbvvzNSoJKsWXKdmbvWk3Oy+VYGH9LEjw+bRTweubH36gYf4to
12
QpoiUcnQ4B1f9GxDUp455aPtN9HmY7dOb4YehyWCFKge/1PUoT3/omz4cRlz5+8QM
13
aYOB1/UgdeCUS674R3tzhr28LhNfJ8J4pyXzOVjVsmBkmMd+he/4MJcXWyDYDWpVl
14
AMA700hkscLcwktA25TDd/Qh5vjF4i6dWcNlIbFxdaKyN9PTs/0T+Jza0JaDmZagW
15
gYD5R4+ei44asrZ37oAg9CjOeDm7FKRuMYiMaw/08LAYGQKFxgIAsMdcKOHya4By/
16
5yVVEVLihCLKdvdn0u9d1d2BQHZS0n2KNG4cMQxnNKVjjTXDybRbyB2C0pp2S9YXN
17
V9WWzveDY5m8jYrZ9wFjvbHeJHfFNXMrkN1k4U7mYM5Be2Mu2+MYA/Xw2oLtzmmyt
18
9WxiCO0B4TrEKOJYV1jC7BDWoAAHNnDxFoOJo+dm/hjTU3XOaWSpsxTURYFR/Ltn5
19
G1yakcUq18mS5c2BEXpwXaJq5+f3QP5COzEcrvgGqBJRfpy0JxsRtKAzTQpADzRtR
20
8jI68XrP4y9FrcUazM4RPp+U8dRY2zjMas559t7Xb7RysHRpFxf2SZ+Xj0SVQZEsj
21
YOATRaQdV+a6haeDvB346iJIjVaNY6SaaMPI5JEhAr79BM6nJCXIu7HT9DCBWvhbG
22
MCePzFjOa4XRB1vaCJo5jolQUWyy0pFLKD5VbimI63wd1a5KqRPmtSFAu+/nQ3aKC
23
Q==
8 daniel-mar 24
</ViaThinkSoftSignature> */ ?>
25
<?php
26
 
27
/*
28
 * VNag - Nagios Framework for PHP
29
 * Developed by Daniel Marschall, ViaThinkSoft <www.viathinksoft.com>
30
 * Licensed under the terms of the Apache 2.0 license
31
 *
42 daniel-mar 32
 * Revision 2021-12-23
8 daniel-mar 33
 */
34
 
35
declare(ticks=1);
36
 
37
class OpenBugBountyCheck extends VNag {
38
        protected $argDomain = null;
15 daniel-mar 39
        protected $argPrivateAPI = null;
17 daniel-mar 40
        protected $argIgnoredIds = null;
8 daniel-mar 41
 
42
        public function __construct() {
43
                parent::__construct();
44
 
45
                $this->registerExpectedStandardArguments('Vvht');
46
 
47
                $this->getHelpManager()->setPluginName('check_openbugbounty');
15 daniel-mar 48
                $this->getHelpManager()->setVersion('1.1');
8 daniel-mar 49
                $this->getHelpManager()->setShortDescription('This plugin checks if a domain has unfixed vulnerabilities listed at OpenBugBounty.org.');
50
                $this->getHelpManager()->setCopyright('Copyright (C) 2011-$CURYEAR$ Daniel Marschall, ViaThinkSoft.');
17 daniel-mar 51
                $this->getHelpManager()->setSyntax('$SCRIPTNAME$ [-d <SingleDomain[,SingleDomain,[...]]> | -d <DomainListFile> | -p <PrivateApiUrl> | -i <IgnoredId,IgnoredId,...> ]');
8 daniel-mar 52
                $this->getHelpManager()->setFootNotes('If you encounter bugs, please contact ViaThinkSoft at www.viathinksoft.com');
53
 
54
                // Individual (non-standard) arguments:
9 daniel-mar 55
                $this->addExpectedArgument($this->argDomain = new VNagArgument('d', 'domain', VNagArgument::VALUE_REQUIRED, 'domainOrFile', 'Domain(s) or subdomain(s), separated by comma, to be checked or a file containing domain names.'));
15 daniel-mar 56
                $this->addExpectedArgument($this->argPrivateAPI = new VNagArgument('p', 'privateapi', VNagArgument::VALUE_REQUIRED, 'privateApiUrl', 'A link to your private API (https://www.openbugbounty.org/api/2/...../). Cannot be used together with argument \'-d\'.'));
17 daniel-mar 57
                $this->addExpectedArgument($this->argIgnoredIds = new VNagArgument('i', 'ignoredids', VNagArgument::VALUE_REQUIRED, 'ignoredIds', 'Comma separated list of submission IDs that shall be defined as fixed (because OpenBugBounty often does not mark fixed bugs as fixed, even if you tell them that you have fixed them...)'));
8 daniel-mar 58
        }
59
 
17 daniel-mar 60
        function is_ignored($id) {
61
                $ids = $this->argIgnoredIds->getValue();
62
                if (empty($ids)) return false;
63
 
64
                $ids = explode(',', $ids);
65
                foreach ($ids as $test) {
66
                        if ($id == $test) return true;
67
                }
68
                return false;
69
        }
70
 
71
        static function extract_id_from_url($url) {
72
                // https://www.openbugbounty.org/reports/1019234/
73
                $parts = explode('/', $url);
74
                foreach ($parts as $part) {
75
                        if (is_numeric($part)) return $part;
76
                }
77
                return -1;
78
        }
79
 
80
        function num_open_bugs_v1($domain, $max_cache_time = 3600) { // TODO: make cache time configurable via config
81
                //assert(!empty($this->argDomain->getValue()));
82
                //assert(empty($this->argPrivateAPI->getValue()));
83
 
84
                $fixed = 0;
85
                $unfixed = 0;
42 daniel-mar 86
                $unfixed_ignored = 0;
17 daniel-mar 87
 
88
                $this->setStatus(VNag::STATUS_OK);
89
 
8 daniel-mar 90
                $domain = strtolower($domain);
59 daniel-mar 91
                $url = 'https://www.openbugbounty.org/api/1/search/?domain='.urlencode($domain);
92
                $cache_file = $this->get_cache_dir() . '/' . sha1($url);
8 daniel-mar 93
 
94
                if (file_exists($cache_file) && (time()-filemtime($cache_file) < $max_cache_time)) {
39 daniel-mar 95
                        $cont = @file_get_contents($cache_file);
40 daniel-mar 96
                        if (!$cont) throw new Exception("Failed to get contents from $cache_file");
8 daniel-mar 97
                } else {
39 daniel-mar 98
                        $cont = @file_get_contents($url);
99
                        if (!$cont) throw new Exception("Failed to get contents from $url");
8 daniel-mar 100
                        file_put_contents($cache_file, $cont);
101
                }
102
 
103
                $xml = simplexml_load_string($cont);
104
                foreach ($xml as $x) {
17 daniel-mar 105
                        $submission = $x->url;
106
 
42 daniel-mar 107
                        if ($x->fixed == '1') {
108
                                $fixed++;
109
                                $this->addVerboseMessage("Fixed issue found at $domain: $submission", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
110
                                $this->setStatus(VNag::STATUS_OK);
111
                        } else if ($this->is_ignored($this->extract_id_from_url($submission))) {
112
                                $unfixed_ignored++;
113
                                $this->addVerboseMessage("Ignored issue found at $domain: $submission", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
114
                                $this->setStatus(VNag::STATUS_OK);
115
                        } else {
17 daniel-mar 116
                                $unfixed++;
117
                                $this->addVerboseMessage("Unfixed issue found at $domain: $submission", VNag::VERBOSITY_SUMMARY);
118
                                $this->setStatus(VNag::STATUS_WARNING);
119
                                // TODO: Unlike the "private" API, the "normal" API does not show if a bug is disclosed (= critical instead of warning)
120
                                //       But we could check if the report is older than XXX months, and then we know that it must be disclosed.
121
                        }
122
 
8 daniel-mar 123
                }
124
 
42 daniel-mar 125
                return array($fixed, $unfixed, $unfixed_ignored);
8 daniel-mar 126
        }
127
 
15 daniel-mar 128
        function get_privateapi_data($url, $max_cache_time = 3600) { // TODO: make cache time configurable via config
129
                $url = strtolower($url);
59 daniel-mar 130
                $cache_file = $this->get_cache_dir() . '/' . sha1($url);
15 daniel-mar 131
 
132
                if (file_exists($cache_file) && (time()-filemtime($cache_file) < $max_cache_time)) {
39 daniel-mar 133
                        $cont = @file_get_contents($cache_file);
134
                        if (!$cont) throw new Exception("Failed to get contents from $url");
15 daniel-mar 135
                } else {
39 daniel-mar 136
                        $cont = @file_get_contents($url);
137
                        if (!$cont) throw new Exception("Failed to get contents from $url");
15 daniel-mar 138
                        file_put_contents($cache_file, $cont);
139
                }
140
 
141
                $ary = @json_decode($cont,true);
142
                if (!$ary) throw new Exception("This is probably not a correct Private API URL, or the service is down (JSON Decode failed)");
143
                return $ary;
144
        }
145
 
17 daniel-mar 146
        function num_open_bugs_v2($privateapi, $max_cache_time = 3600) { // TODO: make cache time configurable via config
147
                //assert(empty($this->argDomain->getValue()));
148
                //assert(!empty($this->argPrivateAPI->getValue()));
149
 
150
                $sum_fixed = 0;
151
                $sum_unfixed_pending = 0;
152
                $sum_unfixed_disclosed = 0;
42 daniel-mar 153
                $sum_unfixed_ignored = 0;
17 daniel-mar 154
 
155
                $this->setStatus(VNag::STATUS_OK);
156
 
157
                $ary = $this->get_privateapi_data($privateapi, $max_cache_time);
158
                foreach ($ary as $id => $data) {
159
                        /*
160
                        [Vulnerability Reported] => 21 September, 2017 05:13
161
                        [Vulnerability Verified] => 21 September, 2017 05:14
162
                        [Scheduled Public Disclosure] => 21 October, 2017 05:13
163
                        [Path Status] => Patched
164
                        [Vulnerability Fixed] => 7 August, 2018 21:47
165
                        [Report Url] => https://openbugbounty.org/reports/.../
166
                        [Host] => ...
167
                        [Researcher] => https://openbugbounty.org/researchers/.../
168
                        */
169
 
170
                        if (empty($data['Vulnerability Reported'])) throw new Exception("This is probably not a correct Private API URL, or the service is down (Missing fields in structure)");
171
 
172
                        $status = isset($data['Patch Status']) ? $data['Patch Status'] : $data['Path Status']; // sic! There is a typo in their API (reported, but not fixed)
173
 
174
                        $submission = $data['Report Url'];
175
                        $domain = $data['Host'];
176
 
177
                        if ($status == 'Patched') {
178
                                $sum_fixed++;
42 daniel-mar 179
                                $fixed_date = $data['Vulnerability Fixed'];
17 daniel-mar 180
                                $this->addVerboseMessage("Fixed issue found at $domain: $submission (fixed: $fixed_date)", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
181
                                $this->setStatus(VNag::STATUS_OK);
42 daniel-mar 182
                        } else if ($this->is_ignored($this->extract_id_from_url($submission))) {
183
                                $sum_unfixed_ignored++;
184
                                $this->addVerboseMessage("Ignored issue found at $domain: $submission", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
185
                                $this->setStatus(VNag::STATUS_OK);
17 daniel-mar 186
                        } else {
187
                                $disclosure = $data['Scheduled Public Disclosure'];
188
                                $time = strtotime(str_replace(',', '', $disclosure));
189
                                if (time() > $time) {
190
                                        $sum_unfixed_disclosed++;
191
                                        $this->addVerboseMessage("Disclosed unfixed issue found at $domain: $submission (disclosure: $disclosure)", VNag::VERBOSITY_SUMMARY);
192
                                        $this->setStatus(VNag::STATUS_CRITICAL);
193
                                } else {
194
                                        $sum_unfixed_pending++;
195
                                        $this->addVerboseMessage("Undisclosed unfixed issue found at $domain: $submission (disclosure: $disclosure)", VNag::VERBOSITY_SUMMARY);
196
                                        $this->setStatus(VNag::STATUS_WARNING);
197
                                }
198
                        }
199
                }
200
 
42 daniel-mar 201
                return array($sum_fixed, $sum_unfixed_pending, $sum_unfixed_disclosed, $sum_unfixed_ignored);
17 daniel-mar 202
        }
203
 
8 daniel-mar 204
        protected function cbRun($optional_args=array()) {
205
                $domain = $this->argDomain->getValue();
15 daniel-mar 206
                $privateapi = $this->argPrivateAPI->getValue();
207
 
208
                if (empty($domain) && empty($privateapi)) {
209
                        throw new Exception("Please specify a domain or subdomain, a list of domains, or a private API Url.");
8 daniel-mar 210
                }
211
 
15 daniel-mar 212
                if (!empty($domain) && !empty($privateapi)) {
213
                        throw new Exception("You can either use argument '-d' or '-p', but not both.");
214
                }
215
 
216
                if (!empty($privateapi)) {
217
                        // Possibility 1: Private API (showing all bugs for all of your domains, with detailled information)
218
                        //                https://www.openbugbounty.org/api/2/.../
219
                        $sum_fixed = 0;
220
                        $sum_unfixed_pending = 0;
221
                        $sum_unfixed_disclosed = 0;
42 daniel-mar 222
                        $sum_unfixed_ignored = 0;
223
                        list($sum_fixed, $sum_unfixed_pending, $sum_unfixed_disclosed, $sum_unfixed_ignored) = $this->num_open_bugs_v2($privateapi);
224
                        $this->setHeadline("$sum_fixed fixed and ".($sum_unfixed_pending + $sum_unfixed_disclosed + $sum_unfixed_ignored)." unfixed ($sum_unfixed_pending pending, $sum_unfixed_disclosed disclosed, $sum_unfixed_ignored ignored) issues found at your domain(s)", true);
15 daniel-mar 225
                } else if (file_exists($domain)) {
226
                        // Possibility 2: File containing a list of domains
8 daniel-mar 227
                        $domains = file($domain);
228
                        $sum_fixed = 0;
229
                        $sum_unfixed = 0;
42 daniel-mar 230
                        $sum_unfixed_ignored = 0;
8 daniel-mar 231
                        $count = 0;
232
                        foreach ($domains as $domain) {
233
                                $domain = trim($domain);
234
                                if ($domain == '') continue;
235
                                if ($domain[0] == '#') continue;
42 daniel-mar 236
                                list($fixed, $unfixed, $unfixed_ignored) = $this->num_open_bugs_v1($domain);
8 daniel-mar 237
                                $sum_fixed += $fixed;
238
                                $sum_unfixed += $unfixed;
42 daniel-mar 239
                                $sum_unfixed_ignored += $unfixed_ignored;
8 daniel-mar 240
                                $count++;
42 daniel-mar 241
                                $this->addVerboseMessage("$fixed fixed, $unfixed_ignored ignored, and $unfixed unfixed issues found at $domain", $unfixed > 0 ? VNag::VERBOSITY_SUMMARY : VNag::VERBOSITY_ADDITIONAL_INFORMATION);
8 daniel-mar 242
                        }
42 daniel-mar 243
                        $this->setHeadline("$sum_fixed fixed and ".($sum_unfixed + $sum_unfixed_ignored)." unfixed (including $sum_unfixed_ignored ignored) issues found at $count domains", true);
10 daniel-mar 244
                } else if (strpos($domain, ',') !== false) {
15 daniel-mar 245
                        // Possibility 3: Domains separated with comma
9 daniel-mar 246
                        $domains = explode(',', $domain);
247
                        $sum_fixed = 0;
248
                        $sum_unfixed = 0;
42 daniel-mar 249
                        $sum_unfixed_ignored = 0;
9 daniel-mar 250
                        $count = 0;
251
                        foreach ($domains as $domain) {
42 daniel-mar 252
                                list($fixed, $unfixed, $unfixed_ignored) = $this->num_open_bugs_v1($domain);
9 daniel-mar 253
                                $sum_fixed += $fixed;
254
                                $sum_unfixed += $unfixed;
42 daniel-mar 255
                                $sum_unfixed_ignored += $unfixed_ignored;
9 daniel-mar 256
                                $count++;
42 daniel-mar 257
                                $this->addVerboseMessage("$fixed fixed, $unfixed_ignored ignored,  and $unfixed unfixed issues found at $domain", $unfixed > 0 ? VNag::VERBOSITY_SUMMARY : VNag::VERBOSITY_ADDITIONAL_INFORMATION);
9 daniel-mar 258
                        }
42 daniel-mar 259
                        $this->setHeadline("$sum_fixed fixed and ".($sum_unfixed + $sum_unfixed_ignored)." unfixed (including $sum_unfixed_ignored ignored) issues found at $count domains", true);
8 daniel-mar 260
                } else {
15 daniel-mar 261
                        // Possibility 4: Single domain
42 daniel-mar 262
                        list($sum_fixed, $sum_unfixed, $sum_unfixed_ignored) = $this->num_open_bugs_v1($domain);
263
                        $this->setHeadline("$sum_fixed fixed and ".($sum_unfixed + $sum_unfixed_ignored)." unfixed (including $sum_unfixed_ignored ignored) issues found at $domain", true);
8 daniel-mar 264
                }
265
        }
266
}
9 daniel-mar 267