Class: HackerOne::Client::Report

Inherits:
Object
  • Object
show all
Includes:
ResourceHelper
Defined in:
lib/hackerone/client/report.rb

Constant Summary collapse

STATES =
%w(
  new
  triaged
  needs-more-info
  resolved
  not-applicable
  informative
  duplicate
  spam
).map(&:to_sym).freeze
STATES_REQUIRING_STATE_CHANGE_MESSAGE =
%w(
  needs-more-info
  informative
  duplicate
).map(&:to_sym).freeze
RESOLVED_STATES =
%w(
  resolved
  not-applicable
  informative
  duplicate
  spam
).map(&:to_sym).freeze
SEVERITY_RATINGS =
%w(
  none
  low
  medium
  high
  critical
).freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ResourceHelper

included, #make_get_request, #make_post_request, #make_put_request, #parse_response

Constructor Details

#initialize(report) ⇒ Report

Returns a new instance of Report.



59
60
61
# File 'lib/hackerone/client/report.rb', line 59

def initialize(report)
  @report = report
end

Class Method Details

.add_on_state_change_hook(proc) ⇒ Object



46
47
48
# File 'lib/hackerone/client/report.rb', line 46

def add_on_state_change_hook(proc)
  on_state_change_hooks << proc
end

.clear_on_state_change_hooksObject



50
51
52
# File 'lib/hackerone/client/report.rb', line 50

def clear_on_state_change_hooks
  @on_state_change_hooks = []
end

.on_state_change_hooksObject



54
55
56
# File 'lib/hackerone/client/report.rb', line 54

def on_state_change_hooks
  @on_state_change_hooks ||= []
end

Instance Method Details

#activitiesObject



152
153
154
155
156
157
158
# File 'lib/hackerone/client/report.rb', line 152

def activities
  if ships = relationships.fetch(:activities, {}).fetch(:data, [])
    ships.map do |activity_data|
      Activities.build(activity_data)
    end
  end
end

#add_comment(message, internal: true) ⇒ Object

Add a comment to a report. By default, internal comments will be added.

id: the ID of the report message: the content of the comment that will be created internal: “team only” comment (true, default) or “all participants”



294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/hackerone/client/report.rb', line 294

def add_comment(message, internal: true)
  fail ArgumentError, "message is required" if message.blank?

  body = {
    type: "activity-comment",
    attributes: {
      message: message,
      internal: internal
    }
  }

  response_json = make_post_request("reports/#{id}/activities", request_body: body)
  HackerOne::Client::Activities.build(response_json)
end

#add_report_reference(reference) ⇒ Object

Idempotent: Add a report reference to a project

id: the ID of the report state: value for the reference (e.g. issue number or relative path to cross-repo issue)

returns an HackerOne::Client::Report object or raises an error if no report is found.



264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/hackerone/client/report.rb', line 264

def add_report_reference(reference)
  body = {
    type: "issue-tracker-reference-id",
    attributes: {
      reference: reference
    }
  }

  response_json = make_post_request("reports/#{id}/issue_tracker_reference_id", request_body: body)
  @report = response_json[:relationships][:report][:data]
  self
end

#assign_to_group(name) ⇒ Object



327
328
329
330
# File 'lib/hackerone/client/report.rb', line 327

def assign_to_group(name)
  group = program.find_group(name)
  _assign_to(group.id, :group)
end

#assign_to_user(name) ⇒ Object



322
323
324
325
# File 'lib/hackerone/client/report.rb', line 322

def assign_to_user(name)
  member = program.find_member(name)
  _assign_to(member.user.id, :user)
end

#assigneeObject



98
99
100
101
102
103
104
# File 'lib/hackerone/client/report.rb', line 98

def assignee
  if assignee_relationship = relationships[:assignee]
    HackerOne::Client::User.new(assignee_relationship[:data])
  else
    nil
  end
end

#attachmentsObject



146
147
148
149
150
# File 'lib/hackerone/client/report.rb', line 146

def attachments
  @attachments ||= relationships.fetch(:attachments, {})
      .fetch(:data, [])
      .map { |attachment| HackerOne::Client::Attachment.new(attachment) }
end

#award_bounty(message:, amount:, bonus_amount: nil) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/hackerone/client/report.rb', line 164

def award_bounty(message:, amount:, bonus_amount: nil)
  request_body = {
    message: message,
    amount: amount,
    bonus_amount: bonus_amount
  }

  response_body = make_post_request(
    "reports/#{id}/bounties",
    request_body: request_body
  )
  Bounty.new(response_body)
end

#award_swag(message:) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
# File 'lib/hackerone/client/report.rb', line 178

def award_swag(message:)
  request_body = {
    message: message
  }

  response_body = make_post_request(
    "reports/#{id}/swags",
    request_body: request_body
  )
  Swag.new(response_body, program)
end

#classification_labelObject



137
138
139
# File 'lib/hackerone/client/report.rb', line 137

def classification_label
  weakness.to_owasp
end

#created_atObject



71
72
73
# File 'lib/hackerone/client/report.rb', line 71

def created_at
  attributes[:created_at]
end

#idObject



63
64
65
# File 'lib/hackerone/client/report.rb', line 63

def id
  @report[:id]
end

#issue_tracker_reference_idObject



79
80
81
# File 'lib/hackerone/client/report.rb', line 79

def issue_tracker_reference_id
  attributes[:issue_tracker_reference_id]
end

#issue_tracker_reference_urlObject



75
76
77
# File 'lib/hackerone/client/report.rb', line 75

def issue_tracker_reference_url
  attributes[:issue_tracker_reference_url]
end

#lock!Object



309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/hackerone/client/report.rb', line 309

def lock!
  unless RESOLVED_STATES.include? self.state.to_sym
    raise ArgumentError, "Report must be closed before locking"
  end

  body = {
    type: "activity-comments-closed"
  }

  response_json = make_put_request("reports/#{id}/close_comments", request_body: body)
  HackerOne::Client::Activities.build(response_json)
end

#payment_totalObject



106
107
108
# File 'lib/hackerone/client/report.rb', line 106

def payment_total
  payments.reduce(0) { |total, payment| total + payment_amount(payment) }
end

#programObject



160
161
162
# File 'lib/hackerone/client/report.rb', line 160

def program
  @program || Program.find(relationships[:program][:data][:attributes][:handle])
end

#reporterObject



91
92
93
94
95
96
# File 'lib/hackerone/client/report.rb', line 91

def reporter
  relationships
    .fetch(:reporter, {})
    .fetch(:data, {})
    .fetch(:attributes, {})
end

#riskObject

Excludes reports where the payout amount is 0 indicating swag-only or no payout for the issue supplied



116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/hackerone/client/report.rb', line 116

def risk
  case payment_total
  when HackerOne::Client.low_range || DEFAULT_LOW_RANGE
    "low"
  when HackerOne::Client.medium_range || DEFAULT_MEDIUM_RANGE
    "medium"
  when HackerOne::Client.high_range || DEFAULT_HIGH_RANGE
    "high"
  when HackerOne::Client.critical_range || DEFAULT_CRITICAL_RANGE
    "critical"
  end
end

#severityObject



83
84
85
# File 'lib/hackerone/client/report.rb', line 83

def severity
  attributes[:severity]
end

#stateObject



87
88
89
# File 'lib/hackerone/client/report.rb', line 87

def state
  attributes[:state]
end

#state_change(state, message = nil, attributes = {}) ⇒ Object

Idempotent: change the state of a report. See STATES for valid values.

id: the ID of the report state: the state in which the report is to be put in

returns an HackerOne::Client::Report object or raises an error if no report is found.

Raises:

  • (ArgumentError)


228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/hackerone/client/report.rb', line 228

def state_change(state, message = nil, attributes = {})
  raise ArgumentError, "state (#{state}) must be one of #{STATES}" unless STATES.include?(state)

  old_state = self.state
  body = {
    type: "state-change",
    attributes: {
      state: state
    }
  }

  body[:attributes] = body[:attributes].reverse_merge(attributes)

  if message
    body[:attributes][:message] = message
  elsif STATES_REQUIRING_STATE_CHANGE_MESSAGE.include?(state)
    fail ArgumentError, "State #{state} requires a message. No message was supplied."
  else
    # message is in theory optional, but a value appears to be required.
    body[:attributes][:message] = ""
  end
  response_json = make_post_request("reports/#{id}/state_changes", request_body: body)
  @report = response_json
  self.class.on_state_change_hooks.each do |hook|
    hook.call(self, old_state.to_s, state.to_s)
  end
  self
end

#structured_scopeObject



110
111
112
# File 'lib/hackerone/client/report.rb', line 110

def structured_scope
  StructuredScope.new(relationships[:structured_scope].fetch(:data, {}))
end

#suggest_bounty(message:, amount:, bonus_amount: nil) ⇒ Object



207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/hackerone/client/report.rb', line 207

def suggest_bounty(message:, amount:, bonus_amount: nil)
  request_body = {
    message: message,
    amount: amount,
    bonus_amount: bonus_amount
  }

  response_body = make_post_request(
    "reports/#{id}/bounty_suggestions",
    request_body: request_body
  )
  Activities.build(response_body)
end

#summaryObject



129
130
131
# File 'lib/hackerone/client/report.rb', line 129

def summary
  attributes[:vulnerability_information]
end

#titleObject



67
68
69
# File 'lib/hackerone/client/report.rb', line 67

def title
  attributes[:title]
end

#triage(reference) ⇒ Object

Idempotent: add the issue reference and put the report into the “triage” state.

id: the ID of the report state: value for the reference (e.g. issue number or relative path to cross-repo issue)

returns an HackerOne::Client::Report object or raises an error if no report is found.



284
285
286
287
# File 'lib/hackerone/client/report.rb', line 284

def triage(reference)
  add_report_reference(reference)
  state_change(:triaged)
end

#unassignObject



332
333
334
# File 'lib/hackerone/client/report.rb', line 332

def unassign
  _assign_to(nil, :nobody)
end

#update_severity(rating:) ⇒ Object

Raises:

  • (ArgumentError)


190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/hackerone/client/report.rb', line 190

def update_severity(rating:)
  raise ArgumentError, "Invalid severity rating" unless SEVERITY_RATINGS.include?(rating.to_s)

  request_body = {
    type: "severity",
    attributes: {
      rating: rating
    }
  }
  response_body = make_post_request(
    "reports/#{id}/severities",
    request_body: request_body
  )
  @report[:attributes][:severity] = { rating: rating }
  Activities.build(response_body)
end

#weaknessObject



133
134
135
# File 'lib/hackerone/client/report.rb', line 133

def weakness
  @weakness ||= Weakness.new(relationships.fetch(:weakness, {}).fetch(:data, {}).fetch(:attributes, {}))
end

#writeup_classificationObject

Bounty writeups just use the key, and not the label value.



142
143
144
# File 'lib/hackerone/client/report.rb', line 142

def writeup_classification
  classification_label.split("-").first
end