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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +11 -0
- data/Guardfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +47 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/fixtures/vcr_cassettes/empty_report_list.yml +71 -0
- data/fixtures/vcr_cassettes/missing_report.yml +69 -0
- data/fixtures/vcr_cassettes/report.yml +464 -0
- data/fixtures/vcr_cassettes/report_list.yml +240 -0
- data/hackerone-client.gemspec +30 -0
- data/lib/hackerone/client.rb +113 -0
- data/lib/hackerone/client/report.rb +115 -0
- data/lib/hackerone/client/version.rb +5 -0
- metadata +167 -0
@@ -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
|