├── .gitignore ├── setup.cfg ├── config.example.yaml ├── requirements.txt ├── LICENSE.md ├── README.md └── hackerone_alchemy.py /.gitignore: -------------------------------------------------------------------------------- 1 | env/* 2 | *.pyc 3 | *.reports 4 | *.bak 5 | config.yaml 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # E123,E133,E226,E241,E242 are the default ignores 3 | ignore = E123,E133,W602 4 | max-line-length = 95 5 | -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | hackerone_identifier: REPLACEME 2 | hackerone_token: REPLACEME 3 | hackerone_program: REPLACEME 4 | flagged_keywords: [] 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML==3.11 2 | beautifulsoup4==4.4.1 3 | phabricator==0.6.1 4 | python-dateutil==2.5.3 5 | requests==2.10.0 6 | six==1.10.0 7 | wheel==0.24.0 8 | wsgiref==0.1.2 9 | futures==3.0.5 10 | requests-futures==0.9.7 11 | h1==1.4.0 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Uber Technologies, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HackerOne Alchemy 2 | 3 | (This project is deprecated and not maintained.) 4 | 5 | A tool for making bug bounty life easier! This tool generates statistics around reports and also makes it easier to identify reports that need more attention. 6 | 7 | # Example Usage 8 | ``` 9 | (env)~ source env/bin/activate; python hackerone_alchemy.py --metrics 10 | 11 | ██░ ██ ▄▄▄ ▄████▄ ██ ▄█▀▓█████ ██▀███ ▒█████ ███▄ █ ▓█████ 12 | ▓██░ ██▒▒████▄ ▒██▀ ▀█ ██▄█▒ ▓█ ▀ ▓██ ▒ ██▒▒██▒ ██▒ ██ ▀█ █ ▓█ ▀ 13 | ▒██▀▀██░▒██ ▀█▄ ▒▓█ ▄ ▓███▄░ ▒███ ▓██ ░▄█ ▒▒██░ ██▒▓██ ▀█ ██▒▒███ 14 | ░▓█ ░██ ░██▄▄▄▄██ ▒▓▓▄ ▄██▒▓██ █▄ ▒▓█ ▄ ▒██▀▀█▄ ▒██ ██░▓██▒ ▐▌██▒▒▓█ ▄ 15 | ░▓█▒░██▓ ▓█ ▓██▒▒ ▓███▀ ░▒██▒ █▄░▒████▒░██▓ ▒██▒░ ████▓▒░▒██░ ▓██░░▒████▒ 16 | ▒ ░░▒░▒ ▒▒ ▓▒█░░ ░▒ ▒ ░▒ ▒▒ ▓▒░░ ▒░ ░░ ▒▓ ░▒▓░░ ▒░▒░▒░ ░ ▒░ ▒ ▒ ░░ ▒░ ░ 17 | ▒ ░▒░ ░ ▒ ▒▒ ░ ░ ▒ ░ ░▒ ▒░ ░ ░ ░ ░▒ ░ ▒░ ░ ▒ ▒░ ░ ░░ ░ ▒░ ░ ░ ░ 18 | ░ ░░ ░ ░ ▒ ░ ░ ░░ ░ ░ ░░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ 19 | ░ ░ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ 20 | ░ 21 | ▄▄▄ ██▓ ▄████▄ ██░ ██ ▓█████ ███▄ ▄███▓▓██ ██▓ 22 | ▒████▄ ▓██▒ ▒██▀ ▀█ ▓██░ ██▒▓█ ▀ ▓██▒▀█▀ ██▒ ▒██ ██▒ 23 | ▒██ ▀█▄ ▒██░ ▒▓█ ▄ ▒██▀▀██░▒███ ▓██ ▓██░ ▒██ ██░ 24 | ░██▄▄▄▄██ ▒██░ ▒▓▓▄ ▄██▒░▓█ ░██ ▒▓█ ▄ ▒██ ▒██ ░ ▐██▓░ 25 | ▓█ ▓██▒░██████▒▒ ▓███▀ ░░▓█▒░██▓░▒████▒▒██▒ ░██▒ ░ ██▒▓░ 26 | ▒▒ ▓▒█░░ ▒░▓ ░░ ░▒ ▒ ░ ▒ ░░▒░▒░░ ▒░ ░░ ▒░ ░ ░ ██▒▒▒ 27 | ▒ ▒▒ ░░ ░ ▒ ░ ░ ▒ ▒ ░▒░ ░ ░ ░ ░░ ░ ░ ▓██ ░▒░ 28 | ░ ▒ ░ ░ ░ ░ ░░ ░ ░ ░ ░ ▒ ▒ ░░ 29 | ░ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░ 30 | ░ ░ ░ 31 | "so 1337" 32 | "check out dat ascii art" 33 | "actually, it's not ascii, it's unicode" 34 | "k" 35 | 36 | 37 | TOTAL REPORT STATS 38 | ================== 39 | Total reports: 397 40 | Total Resolved: 15 41 | Total Spam: 0 42 | Total Triaged: 28 43 | Total Informative: 153 44 | Total Duplicate: 78 45 | Total Not-Applicable: 45 46 | Total New: 41 47 | Total Needs-More-Info: 37 48 | Mean resolution time: 10 days, 11:08:38.315467 49 | Mean first response time: 1 day, 13:38:19.589763 50 | Signal to Noise ratio: 0.217171717172 51 | Number of flagged reports: 0 52 | Total bounty amount awarded: $87,375.00 53 | Closing reports as 'Spam': Priceless 54 | ``` 55 | 56 | # Setup 57 | First, clone that sweet repo: 58 | 59 | ``` 60 | git clone "https://github.com/uber/HackerOneAlchemy.git" 61 | ``` 62 | 63 | Slide into that directory: 64 | 65 | ``` 66 | cd HackerOneAlchemy 67 | ``` 68 | 69 | Now set up your `virtualenv` and `source` it: 70 | 71 | ``` 72 | virtualenv env 73 | source env/bin/activate 74 | ``` 75 | 76 | Install the requirements: 77 | 78 | ``` 79 | pip install -r requirements.txt 80 | ``` 81 | 82 | You'll need to edit the `config.yaml` with API credentials. You can get a set of API credentials from your HackerOne portal, something like https://hackerone.com/{PROGRAM_NAME}/api. You'll also want to set `hackerone_program` to your program's handle. 83 | 84 | Now that you've setup your `config.yaml` you are good to go! Test it's working with the following command: 85 | 86 | ``` 87 | ./hackerone_alchemy.py --metrics 88 | ``` 89 | 90 | Note: For the Phabricator integrations, you will need an ~/.arcrc file already setup for authentication. 91 | 92 | # Features 93 | 94 | ## Get HackerOne Metrics 95 | If you're writing a bug bounty status update post, or if you want to see the general health of the program you can use `--metrics` to get that data. For example, the following will get metrics on our bug bounty program since the beginning: 96 | 97 | ``` 98 | ./hackerone_alchemy.py --metrics 99 | ``` 100 | 101 | In certain situations you may want metrics from a certain date. This is possible with the `--since-date` flag. For example, the following will grab bug bounty metrics since our launch date of March 22, 2016: 102 | 103 | ``` 104 | ./hackerone_alchemy.py --metrics --since-date "March 22, 2016" # Get metrics for submissions since launch 105 | ``` 106 | 107 | ## Get HackerOne Bonus List 108 | One of the fun features we have is [a bonus system](https://newsroom.uber.com/bug-bounty-program/). HackerOne Alchemy has a feature which allows you to figure out who is eligible for bonuses. 109 | 110 | To get this information use HackerOne Alchemy with the flags `--bonuses` and `--since-date`. The following is an example where the bonus season started on `May 1st, 2016`: 111 | 112 | ``` 113 | ./hackerone_alchemy.py --bonuses --since-date "May 1, 2016" 114 | ``` 115 | 116 | This will print out a list of users and the reports which have made them eligible. Keep in mind the reports have to be in a `Resolved` state (and there must be >=5 of them) before the bot will print them as eligible. 117 | 118 | ## Get Oncall Work 119 | If you're the current bug bounty oncall, you can use `--oncall` to get a list of reports that you should take a look at. This does a few checks such as checking if a report is `Triaged` in HackerOne but `Resolved` in Phabricator and vise versa: 120 | 121 | ``` 122 | ./hackerone_alchemy.py --oncall 123 | ``` 124 | 125 | This project is released under the [MIT License](LICENSE.md). 126 | -------------------------------------------------------------------------------- /hackerone_alchemy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2016 Uber Technologies, Inc. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | from __future__ import print_function, unicode_literals, division, absolute_import 25 | 26 | import argparse 27 | import collections 28 | import datetime as dt 29 | import re 30 | from decimal import Decimal 31 | 32 | import yaml 33 | from dateutil import parser as dateparser 34 | from phabricator import Phabricator 35 | 36 | from h1.client import HackerOneClient 37 | from h1.models import ( 38 | ActivityComment, 39 | ActivityStateChange, 40 | Report, 41 | ) 42 | 43 | BANNER = """ 44 | ██░ ██ ▄▄▄ ▄████▄ ██ ▄█▀▓█████ ██▀███ ▒█████ ███▄ █ ▓█████ 45 | ▓██░ ██▒▒████▄ ▒██▀ ▀█ ██▄█▒ ▓█ ▀ ▓██ ▒ ██▒▒██▒ ██▒ ██ ▀█ █ ▓█ ▀ 46 | ▒██▀▀██░▒██ ▀█▄ ▒▓█ ▄ ▓███▄░ ▒███ ▓██ ░▄█ ▒▒██░ ██▒▓██ ▀█ ██▒▒███ 47 | ░▓█ ░██ ░██▄▄▄▄██ ▒▓▓▄ ▄██▒▓██ █▄ ▒▓█ ▄ ▒██▀▀█▄ ▒██ ██░▓██▒ ▐▌██▒▒▓█ ▄ 48 | ░▓█▒░██▓ ▓█ ▓██▒▒ ▓███▀ ░▒██▒ █▄░▒████▒░██▓ ▒██▒░ ████▓▒░▒██░ ▓██░░▒████▒ 49 | ▒ ░░▒░▒ ▒▒ ▓▒█░░ ░▒ ▒ ░▒ ▒▒ ▓▒░░ ▒░ ░░ ▒▓ ░▒▓░░ ▒░▒░▒░ ░ ▒░ ▒ ▒ ░░ ▒░ ░ 50 | ▒ ░▒░ ░ ▒ ▒▒ ░ ░ ▒ ░ ░▒ ▒░ ░ ░ ░ ░▒ ░ ▒░ ░ ▒ ▒░ ░ ░░ ░ ▒░ ░ ░ ░ 51 | ░ ░░ ░ ░ ▒ ░ ░ ░░ ░ ░ ░░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ 52 | ░ ░ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ 53 | ░ 54 | ▄▄▄ ██▓ ▄████▄ ██░ ██ ▓█████ ███▄ ▄███▓▓██ ██▓ 55 | ▒████▄ ▓██▒ ▒██▀ ▀█ ▓██░ ██▒▓█ ▀ ▓██▒▀█▀ ██▒ ▒██ ██▒ 56 | ▒██ ▀█▄ ▒██░ ▒▓█ ▄ ▒██▀▀██░▒███ ▓██ ▓██░ ▒██ ██░ 57 | ░██▄▄▄▄██ ▒██░ ▒▓▓▄ ▄██▒░▓█ ░██ ▒▓█ ▄ ▒██ ▒██ ░ ▐██▓░ 58 | ▓█ ▓██▒░██████▒▒ ▓███▀ ░░▓█▒░██▓░▒████▒▒██▒ ░██▒ ░ ██▒▓░ 59 | ▒▒ ▓▒█░░ ▒░▓ ░░ ░▒ ▒ ░ ▒ ░░▒░▒░░ ▒░ ░░ ▒░ ░ ░ ██▒▒▒ 60 | ▒ ▒▒ ░░ ░ ▒ ░ ░ ▒ ▒ ░▒░ ░ ░ ░ ░░ ░ ░ ▓██ ░▒░ 61 | ░ ▒ ░ ░ ░ ░ ░░ ░ ░ ░ ░ ▒ ▒ ░░ 62 | ░ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░ 63 | ░ ░ ░ 64 | "so 1337" 65 | "check out dat ascii art" 66 | "actually, it's not ascii, it's unicode" 67 | "k" 68 | """ 69 | 70 | BONUS_PERIOD_DAYS = 90 71 | BONUS_REQUIRED_REPORTS = 4 72 | 73 | phab = Phabricator() # This will use your ~/.arcrc file 74 | 75 | try: 76 | with open('config.yaml', 'rb') as config_fh: 77 | settings = yaml.safe_load(config_fh) 78 | except IOError: 79 | print("Error reading config.yaml, have you created one?") 80 | raise 81 | 82 | 83 | class HackerOneAlchemy(object): 84 | def __init__(self, identifier, token): 85 | self.verbose = True 86 | self.client = HackerOneClient(identifier, token) 87 | 88 | def find_reports(self, filters): 89 | return self.client.find_resources( 90 | Report, 91 | program=[settings["hackerone_program"]], 92 | **filters 93 | ) 94 | 95 | def print_report_stats(self, reports, awarded_reports): 96 | stat_data = self.get_report_stats(reports, awarded_reports) 97 | 98 | print("\nTOTAL REPORT STATS\n==================") 99 | print("Total reports:", stat_data["total_reports"]) 100 | for state, count in stat_data["state_counts"].items(): 101 | print("Total ", state.title(), ": ", count, sep="") 102 | print("Mean resolution time:", stat_data["mean_resolution_time"]) 103 | print("Mean first response time:", stat_data["mean_first_response_time"]) 104 | print("Signal to Noise ratio:", stat_data["signal_to_noise_ratio"]) 105 | print("Number of flagged reports:", stat_data["flagged_reports"]) 106 | print("Total bounty amount awarded:", stat_data["total_bounties_awarded_amount"]) 107 | print("Closing reports as 'Spam': Priceless") 108 | 109 | def get_report_stats(self, reports, awarded_reports): 110 | stat_data = {} 111 | stat_data["total_reports"] = len(reports) 112 | 113 | total_bounties = sum(r.total_bounty for r in awarded_reports) 114 | stat_data["total_bounties_awarded_amount"] = '${:,.2f}'.format(total_bounties) 115 | 116 | state_counts = dict((state, 0) for state in Report.STATES) 117 | for report in reports: 118 | state_counts[report.state] += 1 119 | stat_data["state_counts"] = state_counts 120 | 121 | total_good = sum(state_counts[x] for x in ("resolved", "triaged")) 122 | total_meh = sum(state_counts[x] for x in ("informative", "spam", "not-applicable")) 123 | snr = 0.0 124 | if total_meh: 125 | snr = total_good / float(total_meh) 126 | stat_data["signal_to_noise_ratio"] = snr 127 | 128 | stat_data["flagged_reports"] = len(self.reports_containing_words( 129 | reports, 130 | settings["flagged_keywords"] 131 | )) 132 | 133 | response_times = [] 134 | for r in reports: 135 | if not r.time_to_first_response: 136 | continue 137 | response_times.append(r.time_to_first_response.total_seconds()) 138 | mean_first_response_time = None 139 | if response_times: 140 | mean_first_response_secs = sum(response_times) / len(response_times) 141 | mean_first_response_time = dt.timedelta(seconds=mean_first_response_secs) 142 | stat_data["mean_first_response_time"] = mean_first_response_time 143 | 144 | resolution_times = [] 145 | for r in reports: 146 | if r.state != "resolved": 147 | continue 148 | if not r.time_to_closed: 149 | continue 150 | resolution_times.append(r.time_to_closed.total_seconds()) 151 | mean_resolution_time = None 152 | if resolution_times: 153 | mean_resolution_secs = sum(resolution_times) / len(resolution_times) 154 | mean_resolution_time = dt.timedelta(seconds=mean_resolution_secs) 155 | stat_data["mean_resolution_time"] = mean_resolution_time 156 | 157 | return stat_data 158 | 159 | def reports_containing_words(self, reports, word_list): 160 | matching_reports = [] 161 | for report in reports: 162 | for word in word_list: 163 | if self.word_in_report(report, word): 164 | matching_reports.append(report) 165 | break 166 | return matching_reports 167 | 168 | def word_in_report(self, report, word): 169 | text_fields = (report.vulnerability_information, report.title) 170 | return any(word in field.lower() for field in text_fields) 171 | 172 | def get_bonus_information(self, reports): 173 | # Assumes `reports` is in reverse chronological order by creation date 174 | accepted_by_reporter = collections.defaultdict(list) 175 | 176 | for report in reports: 177 | if report.state in {"resolved", "triaged"}: 178 | accepted_by_reporter[report.reporter].append(report) 179 | 180 | reporter_rewards = {} 181 | for reporter, reports_by_reporter in accepted_by_reporter.items(): 182 | if len(reports_by_reporter) > BONUS_REQUIRED_REPORTS: 183 | reporter_rewards[reporter] = self.calc_report_bonuses(reports_by_reporter) 184 | 185 | return reporter_rewards 186 | 187 | def calc_report_bonuses(self, reports): 188 | report_bonuses = {} 189 | 190 | # The first four reports are not eligible 191 | eligible_reports = reports[:-BONUS_REQUIRED_REPORTS] 192 | 193 | for report in eligible_reports: 194 | other_reports = [r for r in reports if r is not report and r.total_bounty] 195 | report_bonuses[report] = self.calc_average_bounty(other_reports) * Decimal('0.10') 196 | 197 | return report_bonuses 198 | 199 | def calc_average_bounty(self, reports): 200 | awarded_reports = [r for r in reports if r.total_bounty is not None] 201 | if not awarded_reports: 202 | return Decimal('0.00') 203 | return sum(r.total_bounty for r in awarded_reports) / len(awarded_reports) 204 | 205 | def print_bonus_information(self, reports): 206 | bonuses_dict = self.get_bonus_information(reports) 207 | 208 | print("\nUSERS ELIGIBLE FOR BONUS\n==================") 209 | 210 | for reporter, reports_by_reporter in bonuses_dict.items(): 211 | print("Username:", reporter) 212 | print("HackerOne Profile: https://hackerone.com/" + reporter.username) 213 | print("Reports (All eligible bugs received since date): ") 214 | for report, bonus in reports_by_reporter.items(): 215 | print("\t", '${:,.2f}'.format(bonus), 216 | report.html_url, report.state, "'%s'\n" % report.title) 217 | 218 | def comments_since_last_response(self, report): 219 | comments_since_last_response = 0 220 | # Iterate in reverse order so it's in chronological order 221 | for activity in reversed(report.activities): 222 | by_reporter = (activity.actor == report.reporter) 223 | if isinstance(activity, ActivityComment): 224 | if by_reporter: 225 | comments_since_last_response += 1 226 | elif not activity.internal: 227 | comments_since_last_response = 0 228 | # Changing the report state counts as a response 229 | if isinstance(activity, ActivityStateChange) and not by_reporter: 230 | comments_since_last_response = 0 231 | 232 | return comments_since_last_response 233 | 234 | def statusmsg(self, msg): 235 | # TODO: replace this with `logging` module 236 | if self.verbose: 237 | print("[ STATUS ]", msg) 238 | 239 | 240 | def _gen_date_filters(range_name, date_range): 241 | date_filters = {} 242 | if "before_date" in date_range: 243 | date_filters[range_name + "_at__lt"] = date_range["before_date"] 244 | if "since_date" in date_range: 245 | date_filters[range_name + "_at__gt"] = date_range["since_date"] 246 | return date_filters 247 | 248 | 249 | def main(args): 250 | hackerone_bot = HackerOneAlchemy( 251 | settings["hackerone_identifier"], 252 | settings["hackerone_token"] 253 | ) 254 | 255 | print(BANNER) 256 | 257 | date_range = dict(args.date_filters) if args.date_filters else {} 258 | if args.bonuses: 259 | if "since_date" not in date_range: 260 | print("Bonuses flag provided without --since-date, cannot continue.") 261 | return 262 | # The range for `--bonuses` should always cover the 90 days after `since_date` 263 | if "before_date" in date_range: 264 | print("Bonuses flag provided with --before-date, cannot continue.") 265 | return 266 | 267 | bonus_period_delta = dt.timedelta(days=BONUS_PERIOD_DAYS) 268 | date_range["before_date"] = date_range["since_date"] + bonus_period_delta 269 | print("Generating bonuses for period starting on", 270 | date_range["since_date"], "ending on", date_range["before_date"]) 271 | 272 | created_date_filters = _gen_date_filters("created", date_range) 273 | reports = hackerone_bot.find_reports(created_date_filters) 274 | 275 | if args.bonuses: 276 | hackerone_bot.print_bonus_information(reports) 277 | 278 | if args.plsrespond: 279 | for report in reports: 280 | # Make sure we have the `activities` field 281 | report.try_complete() 282 | 283 | comments_since_response = hackerone_bot.comments_since_last_response(report) 284 | if comments_since_response >= 3: 285 | print("HackerOne report", report.html_url, "needs a little love!") 286 | 287 | if args.oncall: 288 | print("Bugs that are out of sync with Phabricator:") 289 | for h1_report in reports: 290 | phabricator_id = h1_report.issue_tracker_reference_id 291 | 292 | # Is the linked task even from Phabricator? 293 | if not phabricator_id or not re.match(r"T[0-9]", phabricator_id): 294 | continue 295 | 296 | phab_report_state = phab.maniphest.info( 297 | task_id=int(phabricator_id.replace("T", "")) 298 | )["status"] 299 | 300 | report_link_str = "HackerOne report " + h1_report.html_url 301 | if h1_report.state == "triaged" and phab_report_state == "resolved": 302 | print(report_link_str, "is triaged but the linked Phab task is resolved!") 303 | if h1_report.state == "resolved" and phab_report_state == "open": 304 | print(report_link_str, "is resolved but the linked Phab task is still open!") 305 | 306 | if args.metrics: 307 | awarded_date_filters = _gen_date_filters("bounty_awarded", date_range) 308 | awarded_reports = hackerone_bot.find_reports(awarded_date_filters) 309 | hackerone_bot.print_report_stats(reports, awarded_reports) 310 | 311 | 312 | if __name__ == "__main__": 313 | def _filter_parser(name, val_type): 314 | def func(value): 315 | return name, val_type(value) 316 | return func 317 | 318 | parser = argparse.ArgumentParser( 319 | description="Use HackerOne's API to get useful stats about our bug bounty program.", 320 | ) 321 | 322 | mode_base_group = parser.add_argument_group(title="Modes") 323 | mode_group = mode_base_group.add_mutually_exclusive_group(required=True) 324 | mode_group.add_argument( 325 | "--metrics", 326 | help="Print out various metrics for the specified reports (default to all reports).", 327 | dest="metrics", 328 | action="store_true", 329 | ) 330 | mode_group.add_argument( 331 | "--bonuses", 332 | help="Print out a list of bug bounty submitters who should receive a bonus from us. " 333 | "Must specify a start date via --since-date", 334 | dest="bonuses", 335 | action="store_true", 336 | ) 337 | mode_group.add_argument( 338 | "--oncall", 339 | help="Get Product Security oncall list of action items.", 340 | dest="oncall", 341 | action="store_true", 342 | ) 343 | mode_group.add_argument( 344 | "--plsrespond", 345 | help="Returns reports which have had >=3 responses from researchers without a " 346 | "response from us.", 347 | dest="plsrespond", 348 | action="store_true", 349 | ) 350 | 351 | filter_group = parser.add_argument_group("Filter") 352 | filter_group.add_argument( 353 | "--since-date", 354 | dest="date_filters", 355 | type=_filter_parser("since_date", dateparser.parse), 356 | action="append", 357 | metavar="", 358 | ) 359 | filter_group.add_argument( 360 | "--before-date", 361 | dest="date_filters", 362 | type=_filter_parser("before_date", dateparser.parse), 363 | action="append", 364 | metavar="", 365 | ) 366 | 367 | main(parser.parse_args()) 368 | --------------------------------------------------------------------------------