Subversion Repositories vnag

Rev

Rev 39 | 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>
40 daniel-mar 2
RbPHJmgu6/76ESjp6DP2+iHQJB9V7WnEiJNQhlJiVkjcs2nxUecWAEbxuX7/u4zX+
3
gSbXQoo75yZD+pgxifWdiFIZjpk5U6NfKyXhLtMy6TG7LF5Ekb8hqDRxNyzIlpin4
4
YMVicW6HoqSFDzdO3Xby7UcPFqpP7IqameNwLojYO4ll3bB0uPWh4sa8ybGbelc5D
5
oq4EtMpfAsqqq1PNgc77SbHB1RWEf0MkO66khEPI/HGhqh4Du0nrPIehy/HU521ai
6
Uz4eiT7jOktWITkAVzbCQJLu3F0lQEWB5zgLKoCAF4qMKbO3Sf8gqnSB9OdzWJerp
7
o8ZCfUNCMDUzgujpaTuYP7N38QoUmKJfs8a2LbgoHOWAdP7GvzdranSz8CBfPWJfc
8
r1ut45B4Do3Tr1uMn34WhN3qgRldDhVxYvVYAREa0LhKjDXUMVHaY5I/A97JhJ23V
9
+9bFiLzWnYx+VV0TkddQEPiRDjw7nTnqs5Wuw4cHKOB9sRnWe2BQmoVL4z2aushFF
10
hp8ojwt2xEDFigEdsXZQEqklhdJFHygKc81AuWkw/UaTD3OZtMEa7wSLoDZ3WBpqJ
11
cPkWqOPOxXseuoFnMmd5xuNZyEOXtFXhobiYnK8n121bhY5TGMs9rT7N+VpzUwJuM
12
oEhVXefd/p59f8vYdUyb1C+z2LUK6RZS4mNuCREaJ8QQ9aKULJfhTESjFl8RgTyrv
13
FQ4kUAK+OsPusNg72PtR65aYlHIUeUo4wGJJ922tQGxty/y4Kke2i/Wtfd7cQGtaG
14
EhPNHgccMSnCxbqjz6y98TletC61sDMwgaZ5EEhlE0zc0X4BWp+fMirf/0s7QBacx
15
zuIMuOKTaqevAUcrnXsxjfKtOGydpInLB4v34V1iEhWyvdQjB3Y0tQ0D4zxANQ+2Q
16
l/NicUfDOTLYXuyzLYXOSVNwS/vyBW17MMqham+zb3TkhWVTQI3u+1GsC8Xau40z8
17
g1iupYXunYD7seuqcV3ZsiPJz8WHBLj4zwTIe2wa0fQ5HTxEVqyZumQdryB2KrmTj
18
XUgz5v5zyQcwqrClIAO4XtJO1hnIinMh4Vrj85d5Tqbp38wwhAqeQUxrxwiM+fNKc
19
uG0sFOnl7NVeKeB7sQ/rDFevWm8aayHnqsgz0JTnAmEdhzzRWX7PYlFVNfu/E0j6l
20
VJlpv4EI+eMoZmGMv3JhNWAp7yhkJqz7Xf5UKsXLi/0bgs0m4gMuSPl/JF7G7njfi
21
oEK/dKIblZGsqTOxdgxZ100BHyuOTbHCfRjEosb1xGO6054g9ZI1y1njwnX9dG2Q5
22
zF5p/wElp16/VtxftI8yrJtVep/sipCI77lkYEeYLzB+/pMsayvbr5zGT5SYn3QHD
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
 *
39 daniel-mar 32
 * Revision 2021-12-06
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;
112
 
113
                $this->setStatus(VNag::STATUS_OK);
114
 
8 daniel-mar 115
                $domain = strtolower($domain);
116
                $cache_file = $this->get_cache_dir() . '/' . md5($domain);
117
 
118
                if (file_exists($cache_file) && (time()-filemtime($cache_file) < $max_cache_time)) {
39 daniel-mar 119
                        $cont = @file_get_contents($cache_file);
40 daniel-mar 120
                        if (!$cont) throw new Exception("Failed to get contents from $cache_file");
8 daniel-mar 121
                } else {
122
                        $url = 'https://www.openbugbounty.org/api/1/search/?domain='.urlencode($domain);
39 daniel-mar 123
                        $cont = @file_get_contents($url);
124
                        if (!$cont) throw new Exception("Failed to get contents from $url");
8 daniel-mar 125
                        file_put_contents($cache_file, $cont);
126
                }
127
 
128
                $xml = simplexml_load_string($cont);
129
                foreach ($xml as $x) {
17 daniel-mar 130
                        $submission = $x->url;
131
 
132
                        if ($fake_fix = $this->is_ignored($this->extract_id_from_url($submission))) $x->fixed = '1';
133
 
134
                        if ($x->fixed == '0') {
135
                                $unfixed++;
136
                                $this->addVerboseMessage("Unfixed issue found at $domain: $submission", VNag::VERBOSITY_SUMMARY);
137
                                $this->setStatus(VNag::STATUS_WARNING);
138
                                // TODO: Unlike the "private" API, the "normal" API does not show if a bug is disclosed (= critical instead of warning)
139
                                //       But we could check if the report is older than XXX months, and then we know that it must be disclosed.
140
                        }
141
 
142
                        if ($x->fixed == '1') {
143
                                $fixed++;
144
                                $tmp = $fake_fix ? ' (fix asserted by operator)' : '';
145
                                $this->addVerboseMessage("Fixed issue found at $domain: $submission$tmp", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
146
                                $this->setStatus(VNag::STATUS_OK);
147
                        }
8 daniel-mar 148
                }
149
 
150
                return array($fixed, $unfixed);
151
        }
152
 
15 daniel-mar 153
        function get_privateapi_data($url, $max_cache_time = 3600) { // TODO: make cache time configurable via config
154
                $url = strtolower($url);
155
                $cache_file = $this->get_cache_dir() . '/' . md5($url);
156
 
157
                if (file_exists($cache_file) && (time()-filemtime($cache_file) < $max_cache_time)) {
39 daniel-mar 158
                        $cont = @file_get_contents($cache_file);
159
                        if (!$cont) throw new Exception("Failed to get contents from $url");
15 daniel-mar 160
                } else {
39 daniel-mar 161
                        $cont = @file_get_contents($url);
162
                        if (!$cont) throw new Exception("Failed to get contents from $url");
15 daniel-mar 163
                        file_put_contents($cache_file, $cont);
164
                }
165
 
166
                $ary = @json_decode($cont,true);
167
                if (!$ary) throw new Exception("This is probably not a correct Private API URL, or the service is down (JSON Decode failed)");
168
                return $ary;
169
        }
170
 
17 daniel-mar 171
        function num_open_bugs_v2($privateapi, $max_cache_time = 3600) { // TODO: make cache time configurable via config
172
                //assert(empty($this->argDomain->getValue()));
173
                //assert(!empty($this->argPrivateAPI->getValue()));
174
 
175
                $sum_fixed = 0;
176
                $sum_unfixed_pending = 0;
177
                $sum_unfixed_disclosed = 0;
178
 
179
                $this->setStatus(VNag::STATUS_OK);
180
 
181
                $ary = $this->get_privateapi_data($privateapi, $max_cache_time);
182
                foreach ($ary as $id => $data) {
183
                        /*
184
                        [Vulnerability Reported] => 21 September, 2017 05:13
185
                        [Vulnerability Verified] => 21 September, 2017 05:14
186
                        [Scheduled Public Disclosure] => 21 October, 2017 05:13
187
                        [Path Status] => Patched
188
                        [Vulnerability Fixed] => 7 August, 2018 21:47
189
                        [Report Url] => https://openbugbounty.org/reports/.../
190
                        [Host] => ...
191
                        [Researcher] => https://openbugbounty.org/researchers/.../
192
                        */
193
 
194
                        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)");
195
 
196
                        $status = isset($data['Patch Status']) ? $data['Patch Status'] : $data['Path Status']; // sic! There is a typo in their API (reported, but not fixed)
197
 
198
                        $submission = $data['Report Url'];
199
                        if ($fake_fix = $this->is_ignored($this->extract_id_from_url($submission))) $status = 'Patched';
200
 
201
                        $domain = $data['Host'];
202
 
203
                        if ($status == 'Patched') {
204
                                $sum_fixed++;
205
                                $fixed_date = $fake_fix ? 'asserted by operator' : $data['Vulnerability Fixed'];
206
                                $this->addVerboseMessage("Fixed issue found at $domain: $submission (fixed: $fixed_date)", VNag::VERBOSITY_ADDITIONAL_INFORMATION);
207
                                $this->setStatus(VNag::STATUS_OK);
208
                        } else {
209
                                $disclosure = $data['Scheduled Public Disclosure'];
210
                                $time = strtotime(str_replace(',', '', $disclosure));
211
                                if (time() > $time) {
212
                                        $sum_unfixed_disclosed++;
213
                                        $this->addVerboseMessage("Disclosed unfixed issue found at $domain: $submission (disclosure: $disclosure)", VNag::VERBOSITY_SUMMARY);
214
                                        $this->setStatus(VNag::STATUS_CRITICAL);
215
                                } else {
216
                                        $sum_unfixed_pending++;
217
                                        $this->addVerboseMessage("Undisclosed unfixed issue found at $domain: $submission (disclosure: $disclosure)", VNag::VERBOSITY_SUMMARY);
218
                                        $this->setStatus(VNag::STATUS_WARNING);
219
                                }
220
                        }
221
                }
222
 
223
                return array($sum_fixed, $sum_unfixed_pending, $sum_unfixed_disclosed);
224
        }
225
 
8 daniel-mar 226
        protected function cbRun($optional_args=array()) {
227
                $domain = $this->argDomain->getValue();
15 daniel-mar 228
                $privateapi = $this->argPrivateAPI->getValue();
229
 
230
                if (empty($domain) && empty($privateapi)) {
231
                        throw new Exception("Please specify a domain or subdomain, a list of domains, or a private API Url.");
8 daniel-mar 232
                }
233
 
15 daniel-mar 234
                if (!empty($domain) && !empty($privateapi)) {
235
                        throw new Exception("You can either use argument '-d' or '-p', but not both.");
236
                }
237
 
238
                if (!empty($privateapi)) {
239
                        // Possibility 1: Private API (showing all bugs for all of your domains, with detailled information)
240
                        //                https://www.openbugbounty.org/api/2/.../
241
                        $sum_fixed = 0;
242
                        $sum_unfixed_pending = 0;
243
                        $sum_unfixed_disclosed = 0;
17 daniel-mar 244
                        list($sum_fixed, $sum_unfixed_pending, $sum_unfixed_disclosed) = $this->num_open_bugs_v2($privateapi);
16 daniel-mar 245
                        if ($this->getVerbosityLevel() == VNag::VERBOSITY_SUMMARY) {
246
                                $this->setHeadline(($sum_unfixed_pending + $sum_unfixed_disclosed)." unfixed ($sum_unfixed_pending pending, $sum_unfixed_disclosed disclosed) issues found at your domains", true);
247
                        } else {
248
                                $this->setHeadline("$sum_fixed fixed and ".($sum_unfixed_pending + $sum_unfixed_disclosed)." unfixed ($sum_unfixed_pending pending, $sum_unfixed_disclosed disclosed) issues found at your domains", true);
249
                        }
15 daniel-mar 250
                } else if (file_exists($domain)) {
251
                        // Possibility 2: File containing a list of domains
8 daniel-mar 252
                        $domains = file($domain);
253
                        $sum_fixed = 0;
254
                        $sum_unfixed = 0;
255
                        $count = 0;
256
                        foreach ($domains as $domain) {
257
                                $domain = trim($domain);
258
                                if ($domain == '') continue;
259
                                if ($domain[0] == '#') continue;
17 daniel-mar 260
                                list($fixed, $unfixed) = $this->num_open_bugs_v1($domain);
8 daniel-mar 261
                                $sum_fixed += $fixed;
262
                                $sum_unfixed += $unfixed;
263
                                $count++;
10 daniel-mar 264
                                $this->addVerboseMessage("$fixed fixed and $unfixed unfixed issues found at $domain", $unfixed > 0 ? VNag::VERBOSITY_SUMMARY : VNag::VERBOSITY_ADDITIONAL_INFORMATION);
8 daniel-mar 265
                        }
16 daniel-mar 266
                        if ($this->getVerbosityLevel() == VNag::VERBOSITY_SUMMARY) {
267
                                $this->setHeadline("$sum_unfixed unfixed issues found at $count domains", true);
268
                        } else {
269
                                $this->setHeadline("$sum_fixed fixed and $sum_unfixed unfixed issues found at $count domains", true);
270
                        }
10 daniel-mar 271
                } else if (strpos($domain, ',') !== false) {
15 daniel-mar 272
                        // Possibility 3: Domains separated with comma
9 daniel-mar 273
                        $domains = explode(',', $domain);
274
                        $sum_fixed = 0;
275
                        $sum_unfixed = 0;
276
                        $count = 0;
277
                        foreach ($domains as $domain) {
17 daniel-mar 278
                                list($fixed, $unfixed) = $this->num_open_bugs_v1($domain);
9 daniel-mar 279
                                $sum_fixed += $fixed;
280
                                $sum_unfixed += $unfixed;
281
                                $count++;
10 daniel-mar 282
                                $this->addVerboseMessage("$fixed fixed and $unfixed unfixed issues found at $domain", $unfixed > 0 ? VNag::VERBOSITY_SUMMARY : VNag::VERBOSITY_ADDITIONAL_INFORMATION);
9 daniel-mar 283
                        }
16 daniel-mar 284
                        if ($this->getVerbosityLevel() == VNag::VERBOSITY_SUMMARY) {
285
                                $this->setHeadline("$sum_unfixed unfixed issues found at $count domains", true);
286
                        } else {
287
                                $this->setHeadline("$sum_fixed fixed and $sum_unfixed unfixed issues found at $count domains", true);
288
                        }
8 daniel-mar 289
                } else {
15 daniel-mar 290
                        // Possibility 4: Single domain
17 daniel-mar 291
                        list($fixed, $unfixed) = $this->num_open_bugs_v1($domain);
16 daniel-mar 292
                        if ($this->getVerbosityLevel() == VNag::VERBOSITY_SUMMARY) {
293
                                $this->setHeadline("$unfixed unfixed issues found at $domain", true);
294
                        } else {
295
                                $this->setHeadline("$fixed fixed and $unfixed unfixed issues found at $domain", true);
296
                        }
8 daniel-mar 297
                }
298
        }
299
}
9 daniel-mar 300