Skip to content

Instantly share code, notes, and snippets.

@oreoshake
Last active June 1, 2016 22:12
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oreoshake/cd33d8f3734227c3b57ec0e5010f2440 to your computer and use it in GitHub Desktop.
Save oreoshake/cd33d8f3734227c3b57ec0e5010f2440 to your computer and use it in GitHub Desktop.
HackerOne -> GitHub chatops code
#!/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
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
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