Last active
June 1, 2016 22:12
-
-
Save oreoshake/cd33d8f3734227c3b57ec0e5010f2440 to your computer and use it in GitHub Desktop.
HackerOne -> GitHub chatops code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env shell-ruby | |
#/ Usage: gh-bounty-writeup hackerone_issue_id github_username [issues_repo] [writeup_repo] | |
#/ | |
require "bounty" | |
raise("HACKERONE_TOKEN must be set") unless ENV["HACKERONE_TOKEN"] | |
raise("HACKERONE_TOKEN_NAME must be set") unless ENV["HACKERONE_TOKEN_NAME"] | |
usage = File.read(__FILE__).lines[1][3..-1] | |
report_id = ARGV[0] || abort(usage) | |
github_username = ARGV[1] || abort(usage) | |
issues_repo = ARGV[2] || "supar sekrit repo name 1" | |
writeup_repo = ARGV[3] || "supar sekrit repo name 2" | |
Bounty::Writeup.new(report_id, github_username, issues_repo, writeup_repo).perform |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Bounty::HackerOneReport | |
PAYOUT_ACTIVITY_KEY = "activity-bounty-awarded" | |
CLASSIFICATION_MAPPING = { | |
"Denial of Service" => "A0-Other", | |
"Memory Corruption" => "A0-Other", | |
"Cryptographic Issue" => "A0-Other", | |
"Privilege Escalation" => "A0-Other", | |
"UI Redressing (Clickjacking)" => "A0-Other", | |
"Command Injection" => "A1-Injection", | |
"Remote Code Execution" => "A1-Injection", | |
"SQL Injection" => "A1-Injection", | |
"Authentication" => "A2-AuthSession", | |
"Cross-Site Scripting (XSS)" => "A3-XSS", | |
"Information Disclosure" => "A6-DataExposure", | |
"Cross-Site Request Forgery (CSRF)" => "A8-CSRF", | |
"Unvalidated / Open Redirect" => "A10-Redirects" | |
} | |
def initialize(report) | |
@report = report | |
end | |
def id | |
@report[:id] | |
end | |
def title | |
@report[:attributes][:title] | |
end | |
def relationships | |
@report[:relationships] | |
end | |
def vulnerability_information | |
@report[:attributes][:vulnerability_information] | |
end | |
def reporter | |
relationships | |
.fetch(:reporter, {}) | |
.fetch(:data, {}) | |
.fetch(:attributes, {}) | |
end | |
def activities | |
relationships.fetch(:activities, {}).fetch(:data, []) | |
end | |
def payments | |
activities.select { |activity| activity[:type] == PAYOUT_ACTIVITY_KEY } | |
end | |
def payment_total | |
payments.reduce(0) { |total, payment| total + payment_amount(payment) } | |
end | |
def payment_amount(payment) | |
@payment_amount ||= payment.fetch(:attributes, {}).fetch(:bounty_amount, 0).gsub(/[^\d]/, "").to_i | |
end | |
# Excludes reports where the payout amount is 0 indicating swag-only or no | |
# payout for the issue supplied | |
def risk | |
@risk ||= begin | |
case payment_total | |
when 1...999 | |
"low" | |
when 1000...2500 | |
"medium" | |
when 2500...5000 | |
"high" | |
when 5000...100_000_000 | |
"critical" | |
end | |
end | |
end | |
def summary | |
summaries = relationships.fetch(:summaries, {}).fetch(:data, []).select {|summary| summary[:type] == "report-summary" } | |
return unless summaries | |
summaries.select { |summary| summary[:attributes][:category] == "team" }.map do |summary| | |
summary[:attributes][:content] | |
end.join("\n") | |
end | |
# Do our best to map the value that hackerone provides and the reporter sets | |
# to the OWASP Top 10. Take the first match since multiple values can be set. | |
# This is used for the issue label. | |
def classification_label | |
vulnerability_types.map do |vuln_type| | |
CLASSIFICATION_MAPPING[vuln_type[:attributes][:name]] | |
end.flatten.first | |
end | |
# Bounty writeups just use the key, and not the label value. | |
def writeup_classification | |
classification_label().split("-").first | |
end | |
def vulnerability_types | |
relationships.fetch(:vulnerability_types, {}).fetch(:data, []) | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "json" | |
require "faraday" | |
require "github/octokit" | |
require_relative "draft_post" | |
require_relative "reward_determination_issue" | |
require_relative "hackerone_report" | |
# Bounty rewards chatops. A demonstration of using the HackerOne API | |
# with the GitHub API to manage a mostly automated, integrated workflow. | |
# | |
# 1. create a draft blog post to be published on bounty.github.com and open a pull request. | |
# 2. create a tracking issue for completing the process. | |
# 3. add researcher to the bounty-hunters team. | |
# | |
# Some information is omitted for brevity or because it's private. | |
class Bounty::Writeup | |
def initialize(report_id, github_username, issues_repo, writeup_repo) | |
@report_id = report_id | |
@github_username = github_username | |
@issues_repo = issues_repo | |
@writeup_repo = writeup_repo | |
end | |
def perform | |
bounty_pr = create_bounty_post_draft | |
puts "Bounty writeup PR created: #{bounty_pr.html_url}" | |
issue = create_reward_determination_issue(bounty_pr) | |
puts "reward determination issue created: #{issue.html_url}" | |
# correllate the bounty_pr with the reward determination issue | |
octokit.update_pull_request(@writeup_repo, bounty_pr.number, :body => "See [#{report.title}](#{issue.html_url})") | |
add_researcher_to_bounty_team | |
end | |
def self.hackerone_api_connection | |
@connection ||= Faraday.new(:url => "https://api.hackerone.com/v1") do |faraday| | |
faraday.basic_auth(ENV["HACKERONE_TOKEN_NAME"], ENV["HACKERONE_TOKEN"]) | |
faraday.adapter Faraday.default_adapter | |
end | |
end | |
private | |
def report | |
@report ||= get_report | |
end | |
def github_user_id | |
@github_user_id ||= octokit.user(@github_username).id | |
rescue Octokit::NotFound | |
puts "Could not find GitHub user #{@github_username}" | |
end | |
def add_researcher_to_bounty_team | |
github_teams = octokit.organization_teams(BOUNTY_ORG) | |
bounty_team = github_teams.find { |team| team.slug == BOUNTY_TEAM } | |
octokit.add_team_membership(bounty_team.id, @github_username) | |
puts "Added #{@github_username} to #{BOUNTY_ORG}/#{BOUNTY_TEAM}" | |
end | |
def create_reward_determination_issue(bounty_pr) | |
issue_title = "Bounty Reward Determination: #{report.title}" | |
issue_body = Bounty::RewardDeterminationIssue.new(report, bounty_pr, @github_username).to_s | |
# add metadata to the issue, useful for tracking within GitHub | |
issue_opts = { | |
:labels => ['bounty', report.risk, report.classification_label].compact, | |
:milestone => octokit.milestones(@issues_repo).find {|milestone| milestone[:title] == BOUNTY_MILESTONE}[:number] | |
} | |
octokit.create_issue(@issues_repo, issue_title, issue_body, issue_opts) | |
end | |
def create_bounty_post_draft | |
# figure out branch point | |
master_sha = octokit.ref(@writeup_repo, "heads/master").object.sha | |
new_branch = octokit.create_ref(@writeup_repo, "heads/#{branch_name}", master_sha) | |
with_retry do | |
octokit.create_contents( | |
@writeup_repo, | |
file_name, | |
"Adding writeup for #{report.title}", | |
Bounty::DraftPost.new(report, @github_username).to_s, | |
:branch => branch_name | |
) | |
end | |
with_retry do | |
octokit.create_pull_request( | |
@writeup_repo, | |
"master", | |
branch_name, | |
"Bounty writeup for #{report.title}", | |
"This should never be seen" | |
) | |
end | |
end | |
def get_report | |
response = with_retry do | |
self.class.hackerone_api_connection.get do |req| | |
req.url "reports/#{@report_id}" | |
end | |
end | |
Bounty::HackerOneReport.new(JSON.parse(response.body, :symbolize_names => true)[:data]) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment