hackerone-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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