Subversion Repositories vnag

Rev

Rev 40 | 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>
42 daniel-mar 2
tnq+qJ0FVMFjtw0VxxXU3yGcuwQtR+YxngRuF612pYRja0m6907iUi6E2uuTAiK6d
3
lCx969+n5MBG0N9yM/vPLKowBodxgEAaE5PlS5cfU2WKqbPbrAI6yWjfMruy5OzOi
4
KV2wIIWY+QR9HuNDKvO5TQjFMaLvXdOqNZp+bCP/YDiLJ4oq8s470/z4MZu/jz1ou
5
2pLzyjyDdaMAjrphpGrG9BY0eS1j9EQo88Kv9sFJrmOR+QNRawiMSL1Vuy5XxbriO
6
VH65ZkZ6hs7NPsojgKM50OQUUmRiMi99S2CCqQHPh2O0VaZMB9hQ4NiWN5wyjExMN
7
v5vVpWLFDwG20YKNWdfPd/hADcJ+W3E17RuDbRqphzEJlHcgKgLMULmCCT0H7XWb3
8
NP3iKqVJOGnt7SVDXPKsNbjP2oA6/gAOpBZptV/i95f0kplJ69T7AxVmoNg9dWJnA
9
JMOmpteZCmdZQV7vKbPvCLVOTMh9/Q9OFe877kjRaEAQJaPtrdus4Q8uhvghRFiiL
10
yuJbsZgIAnZvliEe9jDBPCFxTC4tMDqoG5rXRltz4J+Ig52L9AWq0bSf9+AywMjdT
11
c1jS22mBcqC0rx2cmKZl/AWutrBisVeQweAaipRncW85wyZMWgSB3lowbMKZHNqZV
12
YCZt7QSxUGPZAIKy51i6QivhJaaQhvnCZW3lkQGZLqruuXU7QJzw6BzW+aMz+kWqM
13
wMHANFDgw/VusaSWW4a+oaYCyygKRiRkb2YQE8U2EObxkaDDEhquWLHhqEJ8F8kly
14
2aZghC94ryvIkMmjUCOhxJ9a429MyDrochi4RLI9OkYF4WmF4AkqFnqYJWf73kRUV
15
mLpohXJGLaRp5e0Q7dxJto9hy/I/6yntTREvnDkm19cY8lHceJPRv3YbuSVybMha4
16
9nf3KgaF4hmAwogIqTcSb5f18uqMC+Pp4sZaChQnpbC+K7StY7lI3dWL/MINHUGRX
17
yM702pX2l/WSbflcWcvHaPoOkfkvJwP+R5BZ/GIB5F5Yv5Q4K4BDNs23u2stvbzuK
18
6NyheDgjSRF+PckMy8AmIHtGMn4wBTbw+mH+nmBnN6HmQgqM6zHpU1CwVw1Q/c2IP
19
xihKexQelORhik6WyUWXR8GPT4PAFUOkIKV3ayKibd2zLDAd3YM3J4uDbEwp3vg/b
20
neIWy36vzf6xGnPFig0qobZGIisfVMMpvnZkXA9c67K6LFNEx1eOlW6Cx068NZqZW
21
l1s/Q8qJ8UjnPurbrQ4k1v62ZHMy3s9LbiNRyMEt5kdjCMFNuc1jWGpLwp2rw5WKC
22
O5yx+62O5GPX+qZpkIjxZwe/3woj5dGiFFdsCo4afv4KitzI3czXMCDgiL4oTanQQ
40 daniel-mar 23
g==
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
 
60
        protected function get_cache_dir() {
61
                $homedir = @getenv('HOME');
62
                if ($homedir) {
63
                        $try = "${homedir}/.vnag_obb_cache";
64
                        if (is_dir($try)) return $try;
65
                        if (@mkdir($try)) return $try;
66
                }
67
 
68
                $user = posix_getpwuid(posix_geteuid());
69
                if (isset($user['dir'])) {
70
                        $homedir = $user['dir'];
71
                        $try = "${homedir}/.vnag_obb_cache";
72
                        if (is_dir($try)) return $try;
73
                        if (@mkdir($try)) return $try;
74
                }
75
 
76
                if (isset($user['name'])) {
77
                        $username = $user['name'];
78
                        $try = "/tmp/vnag_obb_cache";
79
                        if (is_dir($try)) return $try;
80
                        if (@mkdir($try)) return $try;
81
                }
82
 
83
                return false; // should usually never happen
84
        }
85
 
17 daniel-mar 86
        function is_ignored($id) {
87
                $ids = $this->argIgnoredIds->getValue();
88
                if (empty($ids)) return false;
89
 
90
                $ids = explode(',', $ids);
91
                foreach ($ids as $test) {
92
                        if ($id == $test) return true;
93
                }
94
                return false;
95
        }
96
 
97
        static function extract_id_from_url($url) {
98
                // https://www.openbugbounty.org/reports/1019234/
99
                $parts = explode('/', $url);
100
                foreach ($parts as $part) {
101
                        if (is_numeric($part)) return $part;
102
                }
103
                return -1;
104
        }
105
 
106
        function num_open_bugs_v1($domain, $max_cache_time = 3600) { // TODO: make cache time configurable via config
107
                //assert(!empty($this->argDomain->getValue()));
108
                //assert(empty($this->argPrivateAPI->getValue()));
109
 
110
                $fixed = 0;
111
                $unfixed = 0;
42 daniel-mar 112
                $unfixed_ignored = 0;
17 daniel-mar 113
 
114
                $this->setStatus(VNag::STATUS_OK);
115
 
8 daniel-mar 116
                $domain = strtolower($domain);
117
                $cache_file = $this->get_cache_dir() . '/' . md5($domain);
118
 
119
                if (file_exists($cache_file) && (time()-filemtime($cache_file) < $max_cache_time)) {
39 daniel-mar 120
                        $cont = @file_get_contents($cache_file);
40 daniel-mar 121
                        if (!$cont) throw new Exception("Failed to get contents from $cache_file");
8 daniel-mar 122
                } else {
123
                        $url = 'https://www.openbugbounty.org/api/1/search/?domain='.urlencode($domain);
39 daniel-mar 124
                        $cont = @file_get_contents($url);
125
                        if (!$cont) throw new Exception("Failed to get contents from $url");
8 daniel-mar 126
                        file_put_contents($cache_file, $cont);
127
                }
128
 
129
                $xml = simplexml_load_string($cont);
130
                foreach ($xml as $x) {
17 daniel-mar 131
                        $submission = $x->url;
132
 
42 daniel-mar 133
                        if ($x->fixed == '1') {
134
                                $fixed++;
135
                                $this->addVerboseMessage("Fixed issue found at $domain: $submission", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
136
                                $this->setStatus(VNag::STATUS_OK);
137
                        } else if ($this->is_ignored($this->extract_id_from_url($submission))) {
138
                                $unfixed_ignored++;
139
                                $this->addVerboseMessage("Ignored issue found at $domain: $submission", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
140
                                $this->setStatus(VNag::STATUS_OK);
141
                        } else {
17 daniel-mar 142
                                $unfixed++;
143
                                $this->addVerboseMessage("Unfixed issue found at $domain: $submission", VNag::VERBOSITY_SUMMARY);
144
                                $this->setStatus(VNag::STATUS_WARNING);
145
                                // TODO: Unlike the "private" API, the "normal" API does not show if a bug is disclosed (= critical instead of warning)
146
                                //       But we could check if the report is older than XXX months, and then we know that it must be disclosed.
147
                        }
148
 
8 daniel-mar 149
                }
150
 
42 daniel-mar 151
                return array($fixed, $unfixed, $unfixed_ignored);
8 daniel-mar 152
        }
153
 
15 daniel-mar 154
        function get_privateapi_data($url, $max_cache_time = 3600) { // TODO: make cache time configurable via config
155
                $url = strtolower($url);
156
                $cache_file = $this->get_cache_dir() . '/' . md5($url);
157
 
158
                if (file_exists($cache_file) && (time()-filemtime($cache_file) < $max_cache_time)) {
39 daniel-mar 159
                        $cont = @file_get_contents($cache_file);
160
                        if (!$cont) throw new Exception("Failed to get contents from $url");
15 daniel-mar 161
                } else {
39 daniel-mar 162
                        $cont = @file_get_contents($url);
163
                        if (!$cont) throw new Exception("Failed to get contents from $url");
15 daniel-mar 164
                        file_put_contents($cache_file, $cont);
165
                }
166
 
167
                $ary = @json_decode($cont,true);
168
                if (!$ary) throw new Exception("This is probably not a correct Private API URL, or the service is down (JSON Decode failed)");
169
                return $ary;
170
        }
171
 
17 daniel-mar 172
        function num_open_bugs_v2($privateapi, $max_cache_time = 3600) { // TODO: make cache time configurable via config
173
                //assert(empty($this->argDomain->getValue()));
174
                //assert(!empty($this->argPrivateAPI->getValue()));
175
 
176
                $sum_fixed = 0;
177
                $sum_unfixed_pending = 0;
178
                $sum_unfixed_disclosed = 0;
42 daniel-mar 179
                $sum_unfixed_ignored = 0;
17 daniel-mar 180
 
181
                $this->setStatus(VNag::STATUS_OK);
182
 
183
                $ary = $this->get_privateapi_data($privateapi, $max_cache_time);
184
                foreach ($ary as $id => $data) {
185
                        /*
186
                        [Vulnerability Reported] => 21 September, 2017 05:13
187
                        [Vulnerability Verified] => 21 September, 2017 05:14
188
                        [Scheduled Public Disclosure] => 21 October, 2017 05:13
189
                        [Path Status] => Patched
190
                        [Vulnerability Fixed] => 7 August, 2018 21:47
191
                        [Report Url] => https://openbugbounty.org/reports/.../
192
                        [Host] => ...
193
                        [Researcher] => https://openbugbounty.org/researchers/.../
194
                        */
195
 
196
                        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)");
197
 
198
                        $status = isset($data['Patch Status']) ? $data['Patch Status'] : $data['Path Status']; // sic! There is a typo in their API (reported, but not fixed)
199
 
200
                        $submission = $data['Report Url'];
201
                        $domain = $data['Host'];
202
 
203
                        if ($status == 'Patched') {
204
                                $sum_fixed++;
42 daniel-mar 205
                                $fixed_date = $data['Vulnerability Fixed'];
17 daniel-mar 206
                                $this->addVerboseMessage("Fixed issue found at $domain: $submission (fixed: $fixed_date)", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
207
                                $this->setStatus(VNag::STATUS_OK);
42 daniel-mar 208
                        } else if ($this->is_ignored($this->extract_id_from_url($submission))) {
209
                                $sum_unfixed_ignored++;
210
                                $this->addVerboseMessage("Ignored issue found at $domain: $submission", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
211
                                $this->setStatus(VNag::STATUS_OK);
17 daniel-mar 212
                        } else {
213
                                $disclosure = $data['Scheduled Public Disclosure'];
214
                                $time = strtotime(str_replace(',', '', $disclosure));
215
                                if (time() > $time) {
216
                                        $sum_unfixed_disclosed++;
217
                                        $this->addVerboseMessage("Disclosed unfixed issue found at $domain: $submission (disclosure: $disclosure)", VNag::VERBOSITY_SUMMARY);
218
                                        $this->setStatus(VNag::STATUS_CRITICAL);
219
                                } else {
220
                                        $sum_unfixed_pending++;
221
                                        $this->addVerboseMessage("Undisclosed unfixed issue found at $domain: $submission (disclosure: $disclosure)", VNag::VERBOSITY_SUMMARY);
222
                                        $this->setStatus(VNag::STATUS_WARNING);
223
                                }
224
                        }
225
                }
226
 
42 daniel-mar 227
                return array($sum_fixed, $sum_unfixed_pending, $sum_unfixed_disclosed, $sum_unfixed_ignored);
17 daniel-mar 228
        }
229
 
8 daniel-mar 230
        protected function cbRun($optional_args=array()) {
231
                $domain = $this->argDomain->getValue();
15 daniel-mar 232
                $privateapi = $this->argPrivateAPI->getValue();
233
 
234
                if (empty($domain) && empty($privateapi)) {
235
                        throw new Exception("Please specify a domain or subdomain, a list of domains, or a private API Url.");
8 daniel-mar 236
                }
237
 
15 daniel-mar 238
                if (!empty($domain) && !empty($privateapi)) {
239
                        throw new Exception("You can either use argument '-d' or '-p', but not both.");
240
                }
241
 
242
                if (!empty($privateapi)) {
243
                        // Possibility 1: Private API (showing all bugs for all of your domains, with detailled information)
244
                        //                https://www.openbugbounty.org/api/2/.../
245
                        $sum_fixed = 0;
246
                        $sum_unfixed_pending = 0;
247
                        $sum_unfixed_disclosed = 0;
42 daniel-mar 248
                        $sum_unfixed_ignored = 0;
249
                        list($sum_fixed, $sum_unfixed_pending, $sum_unfixed_disclosed, $sum_unfixed_ignored) = $this->num_open_bugs_v2($privateapi);
250
                        $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 251
                } else if (file_exists($domain)) {
252
                        // Possibility 2: File containing a list of domains
8 daniel-mar 253
                        $domains = file($domain);
254
                        $sum_fixed = 0;
255
                        $sum_unfixed = 0;
42 daniel-mar 256
                        $sum_unfixed_ignored = 0;
8 daniel-mar 257
                        $count = 0;
258
                        foreach ($domains as $domain) {
259
                                $domain = trim($domain);
260
                                if ($domain == '') continue;
261
                                if ($domain[0] == '#') continue;
42 daniel-mar 262
                                list($fixed, $unfixed, $unfixed_ignored) = $this->num_open_bugs_v1($domain);
8 daniel-mar 263
                                $sum_fixed += $fixed;
264
                                $sum_unfixed += $unfixed;
42 daniel-mar 265
                                $sum_unfixed_ignored += $unfixed_ignored;
8 daniel-mar 266
                                $count++;
42 daniel-mar 267
                                $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 268
                        }
42 daniel-mar 269
                        $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 270
                } else if (strpos($domain, ',') !== false) {
15 daniel-mar 271
                        // Possibility 3: Domains separated with comma
9 daniel-mar 272
                        $domains = explode(',', $domain);
273
                        $sum_fixed = 0;
274
                        $sum_unfixed = 0;
42 daniel-mar 275
                        $sum_unfixed_ignored = 0;
9 daniel-mar 276
                        $count = 0;
277
                        foreach ($domains as $domain) {
42 daniel-mar 278
                                list($fixed, $unfixed, $unfixed_ignored) = $this->num_open_bugs_v1($domain);
9 daniel-mar 279
                                $sum_fixed += $fixed;
280
                                $sum_unfixed += $unfixed;
42 daniel-mar 281
                                $sum_unfixed_ignored += $unfixed_ignored;
9 daniel-mar 282
                                $count++;
42 daniel-mar 283
                                $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 284
                        }
42 daniel-mar 285
                        $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 286
                } else {
15 daniel-mar 287
                        // Possibility 4: Single domain
42 daniel-mar 288
                        list($sum_fixed, $sum_unfixed, $sum_unfixed_ignored) = $this->num_open_bugs_v1($domain);
289
                        $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 290
                }
291
        }
292
}
9 daniel-mar 293