jira_scan 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|