jira_scan 0.0.2
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 +15 -0
- data/bin/jira-scan +164 -0
- data/lib/jira_scan.rb +326 -0
- metadata +47 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YmVmMjdhMTg1NThlZmMzNDBjNzE3Y2Q0MjY3NzQ1MTY5OTRlMjc5Yg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
OTk3Mjc4M2FiNDU0MzdlYjU0ZjhiYTA2MzdlNWRjZjk2YWQ5NzQxOA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZDk2MTdhZjM5ZWI3Y2NjYWIwNzI0MzhiZjU1NzNiNDg3NDU2NjViODJlZjQ2
|
10
|
+
ZDg4YTg3NGQ1YTg4NGMwZTMyMDNlNjVkNzhhOGMxZTEzMGY5ODdiNDgzMzkw
|
11
|
+
ZGYwM2E1MDJkZjkxZDY2NzdjNGU3NTMzZDEzNmY0Zjk2MzQ1ZDI=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NzA2ODczZTI3YzAxYWFhYmEyMWMyMjJjYjJhYzIyN2I0ZDkzYjllZThiMTZi
|
14
|
+
MTNhMWNjYmZkZThhZTNkNThmY2UzOWJjMTQ5YTk2M2NjZWY2ZTRmNDQzZjAy
|
15
|
+
Y2RiMzZhMmQ1YzUzNDg0ZDIxNDA3YjEwOWNkZmQzMjBmMzgzYmE=
|
data/bin/jira-scan
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file is part of JiraScan
|
4
|
+
# https://github.com/bcoles/jira_scan
|
5
|
+
#
|
6
|
+
|
7
|
+
require 'jira_scan'
|
8
|
+
require 'optparse'
|
9
|
+
require 'terminal-table'
|
10
|
+
require 'resolv'
|
11
|
+
|
12
|
+
def banner
|
13
|
+
puts "
|
14
|
+
_ _ _____
|
15
|
+
| (_) / ____|
|
16
|
+
| |_ _ __ __ _| (___ ___ __ _ _ __
|
17
|
+
_ | | | '__/ _` |\\___ \\ / __/ _` | '_ \\
|
18
|
+
| |__| | | | | (_| |____) | (_| (_| | | | |
|
19
|
+
\\____/|_|_| \\__,_|_____/ \\___\\__,_|_| |_|
|
20
|
+
version 0.0.2"
|
21
|
+
puts
|
22
|
+
puts '-' * 60
|
23
|
+
end
|
24
|
+
|
25
|
+
banner
|
26
|
+
options = {}
|
27
|
+
opts = OptionParser.new do |o|
|
28
|
+
o.banner = 'Usage: jira-scan [options]'
|
29
|
+
|
30
|
+
o.on('-u URL', '--url URL', 'Jira URL to scan') do |v|
|
31
|
+
unless v.match(%r{\Ahttps?://})
|
32
|
+
puts "- Invalid URL: #{v}"
|
33
|
+
exit(1)
|
34
|
+
end
|
35
|
+
options[:url] = v
|
36
|
+
end
|
37
|
+
|
38
|
+
o.on('-s', '--skip', 'Skip check for Jira') do
|
39
|
+
options[:skip] = true
|
40
|
+
end
|
41
|
+
|
42
|
+
o.on('-v', '--verbose', 'Enable verbose output') do
|
43
|
+
options[:verbose] = true
|
44
|
+
end
|
45
|
+
|
46
|
+
o.on('-h', '--help', 'Show this help') do
|
47
|
+
puts opts
|
48
|
+
exit
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.parse!
|
53
|
+
|
54
|
+
$VERBOSE = true unless options[:verbose].nil?
|
55
|
+
@check = true unless options[:skip]
|
56
|
+
|
57
|
+
if options[:url].nil?
|
58
|
+
puts opts
|
59
|
+
exit(1)
|
60
|
+
end
|
61
|
+
|
62
|
+
def scan(url)
|
63
|
+
puts "Scan started at #{Time.now.getutc}"
|
64
|
+
puts "URL: #{url}"
|
65
|
+
|
66
|
+
# parse URL
|
67
|
+
target = nil
|
68
|
+
begin
|
69
|
+
target = URI::parse(url.split('?').first)
|
70
|
+
rescue
|
71
|
+
puts "- Could not parse target URL: #{url}"
|
72
|
+
end
|
73
|
+
exit(1) if target.nil?
|
74
|
+
|
75
|
+
# resolve IP address
|
76
|
+
begin
|
77
|
+
ip = Resolv.getaddress(target.host).to_s
|
78
|
+
puts "IP: #{ip}" unless ip.nil?
|
79
|
+
rescue
|
80
|
+
puts "- Could not resolve hostname #{target.host}"
|
81
|
+
end
|
82
|
+
|
83
|
+
puts "Port: #{target.port}"
|
84
|
+
puts '-' * 60
|
85
|
+
|
86
|
+
# Check if the URL is Jira
|
87
|
+
if @check
|
88
|
+
unless JiraScan::isJira(url)
|
89
|
+
puts '- Jira not found'
|
90
|
+
exit(1)
|
91
|
+
end
|
92
|
+
puts '+ Found Jira'
|
93
|
+
end
|
94
|
+
|
95
|
+
# Get Jira version
|
96
|
+
version = JiraScan::getVersion(url)
|
97
|
+
puts "+ Version: #{version}" if version
|
98
|
+
|
99
|
+
# Dev mode enabled
|
100
|
+
dev_mode = JiraScan::devMode(url)
|
101
|
+
puts '+ Dev mode is enabled' if dev_mode
|
102
|
+
|
103
|
+
# User registration enabled
|
104
|
+
register = JiraScan::userRegistration(url)
|
105
|
+
puts '+ User registration is enabled' if register
|
106
|
+
|
107
|
+
# Check if User Picker Browser is accessible
|
108
|
+
user_picker = JiraScan::userPickerBrowser(url)
|
109
|
+
if user_picker
|
110
|
+
puts '+ User Picker Browser is available'
|
111
|
+
# Retrieve list of first 1,000 users
|
112
|
+
users = JiraScan::getUsersFromUserPicker(url)
|
113
|
+
unless users.empty?
|
114
|
+
puts "+ Found users (#{users.length}):"
|
115
|
+
table = Terminal::Table.new :headings => ['Username', 'Full Name', 'Email'], :rows => users
|
116
|
+
puts table
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Check if REST User Picker is accessible
|
121
|
+
rest_user_picker = JiraScan::restUserPicker(url)
|
122
|
+
puts "+ REST UserPicker is available" if rest_user_picker
|
123
|
+
|
124
|
+
# Check if REST Group User Picker is accessible
|
125
|
+
rest_group_user_picker = JiraScan::restGroupUserPicker(url)
|
126
|
+
puts "+ REST GroupUserPicker is available" if rest_group_user_picker
|
127
|
+
|
128
|
+
# Check if ViewUserHover.jspa is accessible
|
129
|
+
view_user_hover = JiraScan::viewUserHover(url)
|
130
|
+
puts "+ ViewUserHover.jspa is available" if view_user_hover
|
131
|
+
|
132
|
+
# Check if META-INF contents are accessible
|
133
|
+
meta_inf = JiraScan::metaInf(url)
|
134
|
+
puts '+ META-INF directory contents are accessible' if meta_inf
|
135
|
+
|
136
|
+
# Retrieve list of dashboards
|
137
|
+
dashboards = JiraScan::getDashboards(url)
|
138
|
+
unless dashboards.empty?
|
139
|
+
puts "+ Found dashboards (#{dashboards.length}):"
|
140
|
+
table = Terminal::Table.new :headings => ['ID', 'Name'], :rows => dashboards
|
141
|
+
puts table
|
142
|
+
end
|
143
|
+
|
144
|
+
# Retrieve list of popular filters
|
145
|
+
filters = JiraScan::getPopularFilters(url)
|
146
|
+
unless filters.empty?
|
147
|
+
puts "+ Found popular filters (#{filters.length}):"
|
148
|
+
table = Terminal::Table.new :headings => ['Filter Name'], :rows => filters
|
149
|
+
puts table
|
150
|
+
end
|
151
|
+
|
152
|
+
# Retrieve list of field names
|
153
|
+
field_names = JiraScan::getFieldNames(url)
|
154
|
+
unless field_names.empty?
|
155
|
+
puts "+ Found field names (#{field_names.length}):"
|
156
|
+
table = Terminal::Table.new :headings => ['Name', 'ID', 'Key', 'IsShown', 'Last Viewed'], :rows => field_names
|
157
|
+
puts table
|
158
|
+
end
|
159
|
+
|
160
|
+
puts "Scan finished at #{Time.now.getutc}"
|
161
|
+
puts '-' * 60
|
162
|
+
end
|
163
|
+
|
164
|
+
scan(options[:url])
|
data/lib/jira_scan.rb
ADDED
@@ -0,0 +1,326 @@
|
|
1
|
+
#
|
2
|
+
# This file is part of JiraScan
|
3
|
+
# https://github.com/bcoles/jira_scan
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'uri'
|
7
|
+
require 'cgi'
|
8
|
+
require 'json'
|
9
|
+
require 'net/http'
|
10
|
+
require 'openssl'
|
11
|
+
|
12
|
+
class JiraScan
|
13
|
+
VERSION = '0.0.2'.freeze
|
14
|
+
|
15
|
+
#
|
16
|
+
# Check if Jira
|
17
|
+
#
|
18
|
+
# @param [String] URL
|
19
|
+
#
|
20
|
+
# @return [Boolean]
|
21
|
+
#
|
22
|
+
def self.isJira(url)
|
23
|
+
url += '/' unless url.to_s.end_with? '/'
|
24
|
+
res = sendHttpRequest("#{url}secure/Dashboard.jspa")
|
25
|
+
|
26
|
+
return false unless res
|
27
|
+
return false unless res.code.to_i == 200
|
28
|
+
|
29
|
+
res.body.to_s.include?('JIRA')
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Get Jira version
|
34
|
+
#
|
35
|
+
# @param [String] URL
|
36
|
+
#
|
37
|
+
# @return [String] Jira version
|
38
|
+
#
|
39
|
+
def self.getVersion(url)
|
40
|
+
url += '/' unless url.to_s.end_with? '/'
|
41
|
+
res = sendHttpRequest("#{url}secure/Dashboard.jspa")
|
42
|
+
|
43
|
+
return unless res
|
44
|
+
return unless res.code.to_i == 200
|
45
|
+
|
46
|
+
version = res.body.to_s.scan(%r{<meta name="ajs-version-number" content="([\d\.]+)">}).flatten.first
|
47
|
+
build = res.body.to_s.scan(%r{<meta name="ajs-build-number" content="(\d+)">}).flatten.first
|
48
|
+
|
49
|
+
unless version && build
|
50
|
+
if res.body.to_s =~ /Version: ([\d\.]+)-#(\d+)/
|
51
|
+
version = $1
|
52
|
+
build = $2
|
53
|
+
else
|
54
|
+
return
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
"#{version}-##{build}"
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Check if dev mode is enabled
|
63
|
+
#
|
64
|
+
# @param [String] URL
|
65
|
+
#
|
66
|
+
# @return [Boolean]
|
67
|
+
#
|
68
|
+
def self.devMode(url)
|
69
|
+
url += '/' unless url.to_s.end_with? '/'
|
70
|
+
res = sendHttpRequest(url)
|
71
|
+
|
72
|
+
return false unless res
|
73
|
+
return false unless res.code.to_i == 200
|
74
|
+
|
75
|
+
res.body.to_s.include?('<meta name="ajs-dev-mode" content="true">')
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Check if account registration is enabled
|
80
|
+
#
|
81
|
+
# @param [String] URL
|
82
|
+
#
|
83
|
+
# @return [Boolean]
|
84
|
+
#
|
85
|
+
def self.userRegistration(url)
|
86
|
+
url += '/' unless url.to_s.end_with? '/'
|
87
|
+
res = sendHttpRequest("#{url}secure/Signup!default.jspa")
|
88
|
+
|
89
|
+
return false unless res
|
90
|
+
return false unless res.code.to_i == 200
|
91
|
+
|
92
|
+
res.body.to_s.include?('<h1>Sign up</h1>')
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# Check if unauthenticated access to UserPickerBrowser.jspa is allowed
|
97
|
+
#
|
98
|
+
# @param [String] URL
|
99
|
+
#
|
100
|
+
# @return [Boolean]
|
101
|
+
#
|
102
|
+
def self.userPickerBrowser(url)
|
103
|
+
url += '/' unless url.to_s.end_with? '/'
|
104
|
+
res = sendHttpRequest("#{url}secure/popups/UserPickerBrowser.jspa")
|
105
|
+
|
106
|
+
return false unless res
|
107
|
+
return false unless res.code.to_i == 200
|
108
|
+
|
109
|
+
res.body.to_s.include?('<h1>User Picker</h1>')
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# Retrieve list of users from UserPickerBrowser
|
114
|
+
#
|
115
|
+
# @param [String] URL
|
116
|
+
#
|
117
|
+
# @return [Array] list of first 1,000 users
|
118
|
+
#
|
119
|
+
def self.getUsersFromUserPickerBrowser(url)
|
120
|
+
url += '/' unless url.to_s.end_with? '/'
|
121
|
+
max = 1_000
|
122
|
+
res = sendHttpRequest("#{url}secure/popups/UserPickerBrowser.jspa?max=#{max}")
|
123
|
+
|
124
|
+
return [] unless res && res.code.to_i == 200 && res.body.to_s.include?('<h1>User Picker</h1>')
|
125
|
+
|
126
|
+
users = []
|
127
|
+
if res.body.to_s.include? 'cell-type-email'
|
128
|
+
res.body.to_s.scan(%r{<td data-cell-type="name" class="user-name">(.*?)</td>\s+<td data-cell-type="fullname" >(.*?)</td>\s+<td data-cell-type="email" class="cell-type-email">(.*?)</td>}m).each do |u|
|
129
|
+
users << u
|
130
|
+
end
|
131
|
+
else
|
132
|
+
res.body.to_s.scan(%r{<td data-cell-type="name" class="user-name">(.*?)</td>\s+<td data-cell-type="fullname" >(.*?)</td>}m).each do |u|
|
133
|
+
users << u
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
users
|
138
|
+
rescue
|
139
|
+
[]
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Check if unauthenticated access to REST UserPicker is allowed (CVE-2019-3403)
|
144
|
+
#
|
145
|
+
# @param [String] URL
|
146
|
+
#
|
147
|
+
# @return [Boolean]
|
148
|
+
#
|
149
|
+
def self.restUserPicker(url)
|
150
|
+
url += '/' unless url.to_s.end_with? '/'
|
151
|
+
res = sendHttpRequest("#{url}rest/api/latest/user/picker")
|
152
|
+
|
153
|
+
return false unless res
|
154
|
+
return false unless res.code.to_i == 400
|
155
|
+
|
156
|
+
res.body.to_s.include?('The username query parameter was not provided')
|
157
|
+
end
|
158
|
+
|
159
|
+
#
|
160
|
+
# Check if unauthenticated access to REST GroupUserPicker is allowed (CVE-2019-8449)
|
161
|
+
#
|
162
|
+
# @param [String] URL
|
163
|
+
#
|
164
|
+
# @return [Boolean]
|
165
|
+
#
|
166
|
+
def self.restGroupUserPicker(url)
|
167
|
+
url += '/' unless url.to_s.end_with? '/'
|
168
|
+
res = sendHttpRequest("#{url}rest/api/latest/groupuserpicker")
|
169
|
+
|
170
|
+
return false unless res
|
171
|
+
return false unless res.code.to_i == 400
|
172
|
+
|
173
|
+
res.body.to_s.include?('The username query parameter was not provided')
|
174
|
+
end
|
175
|
+
|
176
|
+
#
|
177
|
+
# Check if unauthenticated access to ViewUserHover.jspa is allowed (CVE-2020-14181)
|
178
|
+
#
|
179
|
+
# @param [String] URL
|
180
|
+
#
|
181
|
+
# @return [Boolean]
|
182
|
+
#
|
183
|
+
def self.viewUserHover(url)
|
184
|
+
url += '/' unless url.to_s.end_with? '/'
|
185
|
+
res = sendHttpRequest("#{url}secure/ViewUserHover.jspa")
|
186
|
+
|
187
|
+
return false unless res
|
188
|
+
return false unless res.code.to_i == 200
|
189
|
+
|
190
|
+
res.body.to_s.include?('User does not exist')
|
191
|
+
end
|
192
|
+
|
193
|
+
#
|
194
|
+
# Check if META-INF contents are accessible (CVE-2019-8442)
|
195
|
+
#
|
196
|
+
# @param [String] URL
|
197
|
+
#
|
198
|
+
# @return [Boolean]
|
199
|
+
#
|
200
|
+
def self.metaInf(url)
|
201
|
+
url += '/' unless url.to_s.end_with? '/'
|
202
|
+
res = sendHttpRequest("#{url}s/#{rand(36**6).to_s(36)}/_/META-INF/maven/com.atlassian.jira/atlassian-jira-webapp/pom.xml")
|
203
|
+
|
204
|
+
return false unless res
|
205
|
+
return false unless res.code.to_i == 200
|
206
|
+
|
207
|
+
res.body.to_s.start_with?('<project')
|
208
|
+
end
|
209
|
+
|
210
|
+
#
|
211
|
+
# Retrieve list of popular filters
|
212
|
+
#
|
213
|
+
# @param [String] URL
|
214
|
+
#
|
215
|
+
# @return [Array] list of popular filters
|
216
|
+
#
|
217
|
+
def self.getPopularFilters(url)
|
218
|
+
url += '/' unless url.to_s.end_with? '/'
|
219
|
+
res = sendHttpRequest("#{url}secure/ManageFilters.jspa?filter=popular&filterView=popular")
|
220
|
+
|
221
|
+
return [] unless res
|
222
|
+
return [] unless res.code.to_i == 200
|
223
|
+
return [] unless res.body.to_s.include?('<h1>Manage Filters</h1>')
|
224
|
+
|
225
|
+
return res.body.to_s.scan(%r{requestId=\d+">(.+?)</a>}) if res.body.to_s =~ /requestId=\d/
|
226
|
+
return res.body.to_s.scan(%r{filter=\d+">(.+?)</a>}) if res.body.to_s =~ /filter=\d/
|
227
|
+
|
228
|
+
[]
|
229
|
+
rescue
|
230
|
+
[]
|
231
|
+
end
|
232
|
+
|
233
|
+
#
|
234
|
+
# Retrieve list of dashboards
|
235
|
+
#
|
236
|
+
# @param [String] URL
|
237
|
+
#
|
238
|
+
# @return [Array] list of dashboards
|
239
|
+
#
|
240
|
+
def self.getDashboards(url)
|
241
|
+
url += '/' unless url.to_s.end_with? '/'
|
242
|
+
max = 1_000
|
243
|
+
res = sendHttpRequest("#{url}rest/api/2/dashboard?maxResults=#{max}")
|
244
|
+
|
245
|
+
return [] unless res
|
246
|
+
return [] unless res.code.to_i == 200
|
247
|
+
return [] unless res.body.to_s.start_with?('{"startAt"')
|
248
|
+
|
249
|
+
JSON.parse(res.body.to_s, symbolize_names: true)[:dashboards].map {|d| [d[:id], d[:name]] }
|
250
|
+
rescue
|
251
|
+
[]
|
252
|
+
end
|
253
|
+
|
254
|
+
#
|
255
|
+
# Retrieve list of field names from QueryComponent!Default.jspa (CVE-2020-14179)
|
256
|
+
#
|
257
|
+
# @param [String] URL
|
258
|
+
#
|
259
|
+
# @return [Array] list of field names
|
260
|
+
#
|
261
|
+
def self.getFieldNames(url)
|
262
|
+
url += '/' unless url.to_s.end_with? '/'
|
263
|
+
res = sendHttpRequest("#{url}secure/QueryComponent!Default.jspa")
|
264
|
+
|
265
|
+
return [] unless res
|
266
|
+
return [] unless res.code.to_i == 200
|
267
|
+
return [] unless res.body.to_s.start_with?('{"searchers"')
|
268
|
+
|
269
|
+
searchers = JSON.parse(res.body.to_s)["searchers"]
|
270
|
+
return [] if searchers.empty?
|
271
|
+
|
272
|
+
groups = searchers['groups']
|
273
|
+
return [] if groups.empty?
|
274
|
+
|
275
|
+
field_names = []
|
276
|
+
groups.each do |g|
|
277
|
+
g['searchers'].each do |s|
|
278
|
+
field_names << s
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
JSON.parse(field_names.to_json, symbolize_names: true).map {|f| [f[:name], f[:id], f[:key], f[:isShown].to_s, f[:lastViewed]] }
|
283
|
+
rescue
|
284
|
+
[]
|
285
|
+
end
|
286
|
+
|
287
|
+
private
|
288
|
+
|
289
|
+
#
|
290
|
+
# Fetch URL
|
291
|
+
#
|
292
|
+
# @param [String] URL
|
293
|
+
#
|
294
|
+
# @return [Net::HTTPResponse] HTTP response
|
295
|
+
#
|
296
|
+
def self.sendHttpRequest(url)
|
297
|
+
target = URI.parse(url)
|
298
|
+
puts "* Fetching #{target}" if $VERBOSE
|
299
|
+
http = Net::HTTP.new(target.host, target.port)
|
300
|
+
if target.scheme.to_s.eql?('https')
|
301
|
+
http.use_ssl = true
|
302
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
303
|
+
# http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
304
|
+
end
|
305
|
+
http.open_timeout = 20
|
306
|
+
http.read_timeout = 20
|
307
|
+
headers = {}
|
308
|
+
headers['User-Agent'] = "JiraScan/#{VERSION}"
|
309
|
+
headers['Accept-Encoding'] = 'gzip,deflate'
|
310
|
+
|
311
|
+
begin
|
312
|
+
res = http.request(Net::HTTP::Get.new(target, headers.to_hash))
|
313
|
+
if res.body && res['Content-Encoding'].eql?('gzip')
|
314
|
+
sio = StringIO.new(res.body)
|
315
|
+
gz = Zlib::GzipReader.new(sio)
|
316
|
+
res.body = gz.read
|
317
|
+
end
|
318
|
+
rescue Timeout::Error, Errno::ETIMEDOUT
|
319
|
+
puts "- Error: Timeout retrieving #{target}" if $VERBOSE
|
320
|
+
rescue => e
|
321
|
+
puts "- Error: Could not retrieve URL #{target}\n#{e}" if $VERBOSE
|
322
|
+
end
|
323
|
+
puts "+ Received reply (#{res.body.length} bytes)" if $VERBOSE
|
324
|
+
res
|
325
|
+
end
|
326
|
+
end
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jira_scan
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brendan Coles
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-03-23 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A simple remote scanner for Atlassian Jira
|
14
|
+
email: bcoles@gmail.com
|
15
|
+
executables:
|
16
|
+
- jira-scan
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- bin/jira-scan
|
21
|
+
- lib/jira_scan.rb
|
22
|
+
homepage: https://github.com/bcoles/jira_scan
|
23
|
+
licenses:
|
24
|
+
- MIT
|
25
|
+
metadata: {}
|
26
|
+
post_install_message:
|
27
|
+
rdoc_options: []
|
28
|
+
require_paths:
|
29
|
+
- lib
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ! '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 2.0.0
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
requirements: []
|
41
|
+
rubyforge_project:
|
42
|
+
rubygems_version: 2.2.2
|
43
|
+
signing_key:
|
44
|
+
specification_version: 4
|
45
|
+
summary: Jira scanner
|
46
|
+
test_files: []
|
47
|
+
has_rdoc:
|