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

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ResourceHelper

included, #make_get_request, #make_post_request, #parse_response

Constructor Details

#initialize(report) ⇒ Report

Returns a new instance of Report.



41
42
43
# File 'lib/hackerone/client/report.rb', line 41

def initialize(report)
  @report = report
end

Class Method Details

.add_on_state_change_hook(proc) ⇒ Object



28
29
30
# File 'lib/hackerone/client/report.rb', line 28

def add_on_state_change_hook(proc)
  on_state_change_hooks << proc
end

.clear_on_state_change_hooksObject



32
33
34
# File 'lib/hackerone/client/report.rb', line 32

def clear_on_state_change_hooks
  @on_state_change_hooks = []
end

.on_state_change_hooksObject



36
37
38
# File 'lib/hackerone/client/report.rb', line 36

def on_state_change_hooks
  @on_state_change_hooks ||= []
end

Instance Method Details

#activitiesObject



124
125
126
127
128
129
130
# File 'lib/hackerone/client/report.rb', line 124

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”



249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/hackerone/client/report.rb', line 249

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.



219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/hackerone/client/report.rb', line 219

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



269
270
271
272
# File 'lib/hackerone/client/report.rb', line 269

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

#assign_to_user(name) ⇒ Object



264
265
266
267
# File 'lib/hackerone/client/report.rb', line 264

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

#assigneeObject



76
77
78
79
80
81
82
# File 'lib/hackerone/client/report.rb', line 76

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

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



136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/hackerone/client/report.rb', line 136

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



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

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

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

#classification_labelObject



115
116
117
# File 'lib/hackerone/client/report.rb', line 115

def classification_label
  weakness.to_owasp
end

#created_atObject



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

def created_at
  attributes[:created_at]
end

#idObject



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

def id
  @report[:id]
end

#issue_tracker_reference_idObject



61
62
63
# File 'lib/hackerone/client/report.rb', line 61

def issue_tracker_reference_id
  attributes[:issue_tracker_reference_id]
end

#issue_tracker_reference_urlObject



57
58
59
# File 'lib/hackerone/client/report.rb', line 57

def issue_tracker_reference_url
  attributes[:issue_tracker_reference_url]
end

#payment_totalObject



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

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

#programObject



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

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

#reporterObject



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

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



94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/hackerone/client/report.rb', line 94

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

#stateObject



65
66
67
# File 'lib/hackerone/client/report.rb', line 65

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)


183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/hackerone/client/report.rb', line 183

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



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

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

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



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

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



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

def summary
  attributes[:vulnerability_information]
end

#titleObject



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

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.



239
240
241
242
# File 'lib/hackerone/client/report.rb', line 239

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

#unassignObject



274
275
276
# File 'lib/hackerone/client/report.rb', line 274

def unassign
  _assign_to(nil, :nobody)
end

#weaknessObject



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

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.



120
121
122
# File 'lib/hackerone/client/report.rb', line 120

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