hackerone-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,240 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://api.hackerone.com/v1/reports?filter%5Bcreated_at__gt%5D=2017-02-11T16:00:44-10:00&filter%5Bprogram%5D%5B0%5D=github&filter%5Bstate%5D%5B0%5D=new
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Authorization:
11
+ - Basic nope=
12
+ User-Agent:
13
+ - Faraday v0.11.0
14
+ Accept-Encoding:
15
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
16
+ Accept:
17
+ - "*/*"
18
+ response:
19
+ status:
20
+ code: 200
21
+ message: OK
22
+ headers:
23
+ Date:
24
+ - Sat, 18 Feb 2017 19:16:35 GMT
25
+ Content-Type:
26
+ - application/json; charset=utf-8
27
+ Transfer-Encoding:
28
+ - chunked
29
+ Connection:
30
+ - keep-alive
31
+ Set-Cookie:
32
+ - __cfduid=123; expires=Sun, 18-Feb-18
33
+ 19:16:35 GMT; path=/; Domain=api.hackerone.com; HttpOnly
34
+ X-Request-Id:
35
+ - 123
36
+ Etag:
37
+ - W/"e337505dbc57f7e1f85685911c939b6e"
38
+ Cache-Control:
39
+ - max-age=0, private, must-revalidate
40
+ Strict-Transport-Security:
41
+ - max-age=31536000; includeSubDomains; preload
42
+ Content-Security-Policy:
43
+ - default-src 'none'; connect-src 'self' www.google-analytics.com errors.hackerone.net;
44
+ font-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self'
45
+ 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; report-uri https://errors.hackerone.net/api/30/csp-report/?sentry_key=61c1e2f50d21487c97a071737701f598
46
+ X-Content-Type-Options:
47
+ - nosniff
48
+ X-Download-Options:
49
+ - noopen
50
+ X-Frame-Options:
51
+ - DENY
52
+ X-Permitted-Cross-Domain-Policies:
53
+ - none
54
+ X-Xss-Protection:
55
+ - 1; mode=block
56
+ Public-Key-Pins-Report-Only:
57
+ - pin-sha256="WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18="; pin-sha256="r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=";
58
+ pin-sha256="K87oWBWM9UZfyddvDfoxL+8lpNyoUB2ptGtn0fv6G2Q="; pin-sha256="iie1VXtL7HzAMF+/PVPR9xzT80kQxdZeJ+zduCB3uj0=";
59
+ pin-sha256="cGuxAXyFXFkWm61cF4HPWX8S0srS9j0aSqN0k4AP+4A="; pin-sha256="bIlWcjiKq1mftH/xd7Hw1JO77Cr+Gv+XYcGUQWwO+A4=";
60
+ pin-sha256="tXD+dGAP8rGY4PW1be90cOYEwg7pZ4G+yPZmIZWPTSg="; max-age=600; includeSubDomains;
61
+ report-uri="https://hackerone.report-uri.io/r/default/hpkp/reportOnly"
62
+ Server:
63
+ - cloudflare-nginx
64
+ Cf-Ray:
65
+ - 3333d0794c920d4f-LAX
66
+ body:
67
+ encoding: ASCII-8BIT
68
+ string: '{
69
+ "data": [
70
+ {
71
+ "id": "207385",
72
+ "type": "report",
73
+ "attributes": {
74
+ "title": "What is my purpose",
75
+ "state": "new",
76
+ "created_at": "2017-02-18T18:26:13.283Z",
77
+ "vulnerability_information": "You pass the butter",
78
+ "triaged_at": null,
79
+ "closed_at": null,
80
+ "last_reporter_activity_at": "2017-02-18T18:26:13.387Z",
81
+ "first_program_activity_at": null,
82
+ "last_program_activity_at": null,
83
+ "bounty_awarded_at": null,
84
+ "swag_awarded_at": null,
85
+ "disclosed_at": null,
86
+ "last_activity_at": "2017-02-18T18:26:13.387Z"
87
+ },
88
+ "relationships": {
89
+ "reporter": {
90
+ "data": {
91
+ "id": "123",
92
+ "type": "user",
93
+ "attributes": {
94
+ "username": "rickestofallthericks",
95
+ "name": "Rick Sanchez",
96
+ "disabled": false,
97
+ "created_at": "2017-02-18T18:21:28.638Z",
98
+ "profile_picture": {
99
+ "62x62": "/assets/avatars/default-123.png",
100
+ "82x82": "/assets/avatars/default-123.png",
101
+ "110x110": "/assets/avatars/default-123.png",
102
+ "260x260": "/assets/avatars/default-123.png"
103
+ }
104
+ }
105
+ }
106
+ },
107
+ "program": {
108
+ "data": {
109
+ "id": "1894",
110
+ "type": "program",
111
+ "attributes": {
112
+ "handle": "github",
113
+ "created_at": "2015-05-29T20:12:03.091Z",
114
+ "updated_at": "2017-02-17T15:18:04.114Z"
115
+ }
116
+ }
117
+ },
118
+ "severity": {
119
+ "data": {
120
+ "id": "26662",
121
+ "type": "severity",
122
+ "attributes": {
123
+ "rating": "critical",
124
+ "author_type": "User",
125
+ "user_id": 123,
126
+ "created_at": "2017-02-18T18:26:13.373Z",
127
+ "score": 9.1,
128
+ "attack_complexity": "low",
129
+ "attack_vector": "network",
130
+ "availability": "none",
131
+ "confidentiality": "high",
132
+ "integrity": "high",
133
+ "privileges_required": "none",
134
+ "user_interaction": "none",
135
+ "scope": "unchanged"
136
+ }
137
+ }
138
+ },
139
+ "vulnerability_types": {
140
+ "data": [
141
+ {
142
+ "id": "25110",
143
+ "type": "vulnerability-type",
144
+ "attributes": {
145
+ "name": "Information Disclosure",
146
+ "description": "nope",
147
+ "created_at": "2016-01-28T13:34:08.945Z"
148
+ }
149
+ }
150
+ ]
151
+ },
152
+ "bounties": {
153
+ "data": []
154
+ }
155
+ }
156
+ },
157
+ {
158
+ "id": "207228",
159
+ "type": "report",
160
+ "attributes": {
161
+ "title": "Take 2 Strokes off of Jerry''s swing",
162
+ "state": "new",
163
+ "created_at": "2017-02-17T21:51:00.916Z",
164
+ "vulnerability_information": "Hai",
165
+ "triaged_at": null,
166
+ "closed_at": null,
167
+ "last_reporter_activity_at": "2017-02-17T21:51:01.110Z",
168
+ "first_program_activity_at": null,
169
+ "last_program_activity_at": null,
170
+ "bounty_awarded_at": null,
171
+ "swag_awarded_at": null,
172
+ "disclosed_at": null,
173
+ "last_activity_at": "2017-02-17T21:51:01.110Z"
174
+ },
175
+ "relationships": {
176
+ "reporter": {
177
+ "data": {
178
+ "id": "1234",
179
+ "type": "user",
180
+ "attributes": {
181
+ "username": "ImMrMeeseeks",
182
+ "name": "Mr Meeseeks",
183
+ "disabled": false,
184
+ "created_at": "2017-02-17T20:52:56.666Z",
185
+ "profile_picture": {
186
+ "62x62": "/assets/avatars/default-123.png",
187
+ "82x82": "/assets/avatars/default-123.png",
188
+ "110x110": "/assets/avatars/default-123.png",
189
+ "260x260": "/assets/avatars/default-123.png"
190
+ }
191
+ }
192
+ }
193
+ },
194
+ "program": {
195
+ "data": {
196
+ "id": "1894",
197
+ "type": "program",
198
+ "attributes": {
199
+ "handle": "github",
200
+ "created_at": "2015-05-29T20:12:03.091Z",
201
+ "updated_at": "2017-02-17T15:18:04.114Z"
202
+ }
203
+ }
204
+ },
205
+ "severity": {
206
+ "data": {
207
+ "id": "26541",
208
+ "type": "severity",
209
+ "attributes": {
210
+ "rating": "low",
211
+ "author_type": "User",
212
+ "user_id": 144214,
213
+ "created_at": "2017-02-17T21:51:01.093Z"
214
+ }
215
+ }
216
+ },
217
+ "vulnerability_types": {
218
+ "data": [
219
+ {
220
+ "id": "25110",
221
+ "type": "vulnerability-type",
222
+ "attributes": {
223
+ "name": "Information Disclosure",
224
+ "description": "Exposure of system information, sensitive or private information, fingerprinting, etc.\n",
225
+ "created_at": "2016-01-28T13:34:08.945Z"
226
+ }
227
+ }
228
+ ]
229
+ },
230
+ "bounties": {
231
+ "data": []
232
+ }
233
+ }
234
+ }
235
+ ],
236
+ "links": {}
237
+ }'
238
+ http_version:
239
+ recorded_at: Sat, 18 Feb 2017 19:16:35 GMT
240
+ recorded_with: VCR 3.0.3
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hackerone/client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hackerone-client"
8
+ spec.version = Hackerone::Client::VERSION
9
+ spec.authors = ["Neil Matatall"]
10
+ spec.email = ["neil.matatall@gmail.com"]
11
+
12
+ spec.summary = %q{A limited client for the HackerOne API}
13
+ spec.homepage = "https://github.com/oreoshake/hackerone-client"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.14"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rspec", "~> 3.0"
26
+ spec.add_development_dependency "vcr", "~> 3.0"
27
+ spec.add_development_dependency "webmock", "~> 2.3"
28
+ spec.add_runtime_dependency "faraday"
29
+ spec.add_runtime_dependency 'activesupport', '~> 3.0', '> 3.0'
30
+ end
@@ -0,0 +1,113 @@
1
+ require "faraday"
2
+ require 'active_support/time'
3
+ require_relative "client/version"
4
+ require_relative "client/report"
5
+
6
+ module HackerOne
7
+ module Client
8
+ class NotConfiguredError < StandardError; end
9
+
10
+ DEFAULT_LOW_RANGE = 1...999
11
+ DEFAULT_MEDIUM_RANGE = 1000...2499
12
+ DEFAULT_HIGH_RANGE = 2500...4999
13
+ DEFAULT_CRITICAL_RANGE = 5000...100_000_000
14
+
15
+ class << self
16
+ ATTRS = [:low_range, :medium_range, :high_range, :critical_range].freeze
17
+ attr_accessor :program
18
+ attr_reader *ATTRS
19
+
20
+ ATTRS.each do |attr|
21
+ define_method "#{attr}=" do |value|
22
+ raise ArgumentError, "value must be a range object" unless value.is_a?(Range)
23
+ instance_variable_set :"@#{attr}", value
24
+ end
25
+ end
26
+ end
27
+
28
+ class Api
29
+ def initialize(program = nil)
30
+ @program = program
31
+ end
32
+
33
+ def program
34
+ @program || HackerOne::Client.program
35
+ end
36
+
37
+ ## Returns all open reports, optionally with a time bound
38
+ #
39
+ # program: the HackerOne program to search on (configure globally with Hackerone::Client.program=)
40
+ # since (optional): a time bound, don't include reports earlier than +since+. Must be a DateTime object.
41
+ #
42
+ # returns all open reports or an empty array
43
+ def reports(since: 3.days.ago)
44
+ raise ArgumentError, "Program cannot be nil" unless program
45
+ response = self.class.hackerone_api_connection.get do |req|
46
+ options = {
47
+ "filter[state][]" => "new",
48
+ "filter[program][]" => program,
49
+ "filter[created_at__gt]" => since.iso8601
50
+ }
51
+ req.url "reports", options
52
+ end
53
+
54
+ data = JSON.parse(response.body, :symbolize_names => true)[:data]
55
+ if data.nil?
56
+ raise RuntimeError, "Expected data attribute in response: #{response.body}"
57
+ end
58
+
59
+ data.map do |report|
60
+ Report.new(report)
61
+ end
62
+ end
63
+
64
+ ## Public: retrieve a report
65
+ #
66
+ # id: the ID of a specific report
67
+ #
68
+ # returns an HackerOne::Client::Report object or raises an error if
69
+ # no report is found.
70
+ def report(id)
71
+ response = with_retry do
72
+ self.class.hackerone_api_connection.get do |req|
73
+ req.url "reports/#{id}"
74
+ end
75
+ end
76
+
77
+ if response.success?
78
+ Report.new(JSON.parse(response.body, :symbolize_names => true)[:data])
79
+ else
80
+ raise ArgumentError, "Could not retrieve HackerOne report ##{id}: #{response.body}"
81
+ end
82
+ end
83
+
84
+ private
85
+ def self.hackerone_api_connection
86
+ unless ENV["HACKERONE_TOKEN_NAME"] && ENV["HACKERONE_TOKEN"]
87
+ raise NotConfiguredError, "HACKERONE_TOKEN_NAME HACKERONE_TOKEN environment variables must be set"
88
+ end
89
+
90
+ @connection ||= Faraday.new(:url => "https://api.hackerone.com/v1") do |faraday|
91
+ faraday.basic_auth(ENV["HACKERONE_TOKEN_NAME"], ENV["HACKERONE_TOKEN"])
92
+ faraday.adapter Faraday.default_adapter
93
+ end
94
+ end
95
+
96
+ def with_retry(attempts=3, &block)
97
+ attempts_remaining = attempts
98
+
99
+ begin
100
+ yield
101
+ rescue StandardError
102
+ if attempts_remaining > 0
103
+ attempts_remaining -= 1
104
+ sleep (attempts - attempts_remaining)
105
+ retry
106
+ else
107
+ raise
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,115 @@
1
+ module HackerOne
2
+ module Client
3
+ class Report
4
+ PAYOUT_ACTIVITY_KEY = "activity-bounty-awarded"
5
+ CLASSIFICATION_MAPPING = {
6
+ "None Applicable" => "A0-Other",
7
+ "Denial of Service" => "A0-Other",
8
+ "Memory Corruption" => "A0-Other",
9
+ "Cryptographic Issue" => "A0-Other",
10
+ "Privilege Escalation" => "A0-Other",
11
+ "UI Redressing (Clickjacking)" => "A0-Other",
12
+ "Command Injection" => "A1-Injection",
13
+ "Remote Code Execution" => "A1-Injection",
14
+ "SQL Injection" => "A1-Injection",
15
+ "Authentication" => "A2-AuthSession",
16
+ "Cross-Site Scripting (XSS)" => "A3-XSS",
17
+ "Information Disclosure" => "A6-DataExposure",
18
+ "Cross-Site Request Forgery (CSRF)" => "A8-CSRF",
19
+ "Unvalidated / Open Redirect" => "A10-Redirects"
20
+ }
21
+
22
+ def initialize(report)
23
+ @report = report
24
+ end
25
+
26
+ def id
27
+ @report[:id]
28
+ end
29
+
30
+ def title
31
+ attributes[:title]
32
+ end
33
+
34
+ def vulnerability_information
35
+ attributes[:vulnerability_information]
36
+ end
37
+
38
+ def reporter
39
+ relationships
40
+ .fetch(:reporter, {})
41
+ .fetch(:data, {})
42
+ .fetch(:attributes, {})
43
+ end
44
+
45
+ def payment_total
46
+ payments.reduce(0) { |total, payment| total + payment_amount(payment) }
47
+ end
48
+
49
+ # Excludes reports where the payout amount is 0 indicating swag-only or no
50
+ # payout for the issue supplied
51
+ def risk
52
+ case payment_total
53
+ when HackerOne::Client.low_range || DEFAULT_LOW_RANGE
54
+ "low"
55
+ when HackerOne::Client.medium_range || DEFAULT_MEDIUM_RANGE
56
+ "medium"
57
+ when HackerOne::Client.high_range || DEFAULT_HIGH_RANGE
58
+ "high"
59
+ when HackerOne::Client.critical_range || DEFAULT_CRITICAL_RANGE
60
+ "critical"
61
+ end
62
+ end
63
+
64
+ def summary
65
+ summaries = relationships.fetch(:summaries, {}).fetch(:data, []).select {|summary| summary[:type] == "report-summary" }
66
+ return unless summaries
67
+
68
+ summaries.select { |summary| summary[:attributes][:category] == "team" }.map do |summary|
69
+ summary[:attributes][:content]
70
+ end.join("\n")
71
+ end
72
+
73
+ # Do our best to map the value that hackerone provides and the reporter sets
74
+ # to the OWASP Top 10. Take the first match since multiple values can be set.
75
+ # This is used for the issue label.
76
+ def classification_label
77
+ owasp_mapping = vulnerability_types.map do |vuln_type|
78
+ CLASSIFICATION_MAPPING[vuln_type[:attributes][:name]]
79
+ end.flatten.first
80
+
81
+ owasp_mapping || CLASSIFICATION_MAPPING["None Applicable"]
82
+ end
83
+
84
+ # Bounty writeups just use the key, and not the label value.
85
+ def writeup_classification
86
+ classification_label().split("-").first
87
+ end
88
+
89
+ private
90
+ def payments
91
+ activities.select { |activity| activity[:type] == PAYOUT_ACTIVITY_KEY }
92
+ end
93
+
94
+ def payment_amount(payment)
95
+ payment.fetch(:attributes, {}).fetch(:bounty_amount, 0).gsub(/[^\d]/, "").to_i
96
+ end
97
+
98
+ def activities
99
+ relationships.fetch(:activities, {}).fetch(:data, [])
100
+ end
101
+
102
+ def attributes
103
+ @report[:attributes]
104
+ end
105
+
106
+ def relationships
107
+ @report[:relationships]
108
+ end
109
+
110
+ def vulnerability_types
111
+ relationships.fetch(:vulnerability_types, {}).fetch(:data, [])
112
+ end
113
+ end
114
+ end
115
+ end