liferay_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 +7 -0
- data/bin/liferay-scan +177 -0
- data/data/names.txt +1179 -0
- data/data/users.txt +9 -0
- data/lib/liferay_scan.rb +489 -0
- metadata +75 -0
data/data/users.txt
ADDED
data/lib/liferay_scan.rb
ADDED
@@ -0,0 +1,489 @@
|
|
1
|
+
#
|
2
|
+
# This file is part of LiferayScan
|
3
|
+
# https://github.com/bcoles/liferay_scan
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'uri'
|
7
|
+
require 'cgi'
|
8
|
+
require 'logger'
|
9
|
+
require 'net/http'
|
10
|
+
require 'openssl'
|
11
|
+
require 'stringio'
|
12
|
+
|
13
|
+
class LiferayScan
|
14
|
+
VERSION = '0.0.2'.freeze
|
15
|
+
@resource_path = File.join(File.dirname(File.expand_path(__FILE__)), '../data')
|
16
|
+
|
17
|
+
class << self
|
18
|
+
attr_reader :logger
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_writer :logger
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.insecure
|
26
|
+
@insecure ||= false
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
attr_writer :insecure
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Check if URL is running Liferay
|
35
|
+
#
|
36
|
+
# @param [String] URL
|
37
|
+
#
|
38
|
+
# @return [Boolean]
|
39
|
+
#
|
40
|
+
def self.detectLiferay(url)
|
41
|
+
return true if detectLiferayFromLogin(url)
|
42
|
+
return true if detectLiferayFromHome(url)
|
43
|
+
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Check if URL is running Liferay using login page
|
49
|
+
#
|
50
|
+
# @param [String] URL
|
51
|
+
#
|
52
|
+
# @return [Boolean]
|
53
|
+
#
|
54
|
+
def self.detectLiferayFromLogin(url)
|
55
|
+
url += '/' unless url.to_s.end_with?('/')
|
56
|
+
|
57
|
+
res = sendHttpRequest("#{url}c/portal/login")
|
58
|
+
|
59
|
+
return false unless res
|
60
|
+
return false unless res.code.to_i == 302
|
61
|
+
|
62
|
+
return true if res['liferay-portal'].to_s.start_with?('Liferay')
|
63
|
+
|
64
|
+
# old Liferay <= 6.x
|
65
|
+
return true if res['location'] =~ /p_p_id=58/
|
66
|
+
|
67
|
+
# new Liferay >= 7.x
|
68
|
+
return true if res['location'] =~ /p_p_id=com_liferay_login_web_portlet_LoginPortlet/
|
69
|
+
|
70
|
+
false
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Check if URL is running Liferay using home page
|
75
|
+
#
|
76
|
+
# @param [String] URL
|
77
|
+
#
|
78
|
+
# @return [Boolean]
|
79
|
+
#
|
80
|
+
def self.detectLiferayFromHome(url)
|
81
|
+
url += '/' unless url.to_s.end_with?('/')
|
82
|
+
|
83
|
+
res = sendHttpRequest("#{url}home")
|
84
|
+
|
85
|
+
return false unless res
|
86
|
+
return false unless res.code.to_i == 200
|
87
|
+
|
88
|
+
return true if res['liferay-portal'].to_s.start_with?('Liferay')
|
89
|
+
return true if res.body.to_s.include?('var Liferay = Liferay || {};')
|
90
|
+
return true if res.body.to_s.include?('var Liferay = {')
|
91
|
+
|
92
|
+
false
|
93
|
+
end
|
94
|
+
|
95
|
+
# Get Liferay version
|
96
|
+
#
|
97
|
+
# @param [String] URL
|
98
|
+
#
|
99
|
+
# @return [String] Liferay version
|
100
|
+
#
|
101
|
+
def self.getVersion(url)
|
102
|
+
version = getVersionFromLogin(url)
|
103
|
+
return version if version
|
104
|
+
|
105
|
+
version = getVersionFromGuestHome(url)
|
106
|
+
return version if version
|
107
|
+
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Get Liferay version from login page
|
113
|
+
#
|
114
|
+
# @param [String] URL
|
115
|
+
#
|
116
|
+
# @return [String] Liferay version
|
117
|
+
#
|
118
|
+
def self.getVersionFromLogin(url)
|
119
|
+
url += '/' unless url.to_s.end_with?('/')
|
120
|
+
|
121
|
+
res = sendHttpRequest("#{url}")
|
122
|
+
|
123
|
+
return unless res
|
124
|
+
|
125
|
+
if res['liferay-portal'].to_s.start_with?('Liferay') && res['liferay-portal'].to_s.include?('.')
|
126
|
+
return res['liferay-portal'].to_s
|
127
|
+
end
|
128
|
+
|
129
|
+
res.body.to_s.scan(/<div class="clearfix component-paragraph text-break" data-lfr-editable-id="element-text" data-lfr-editable-type="rich-text">\s*(Welcome to )?(Liferay [^<]+)\s*</).flatten[1]
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# Get Liferay version from guest home page
|
134
|
+
#
|
135
|
+
# @param [String] URL
|
136
|
+
#
|
137
|
+
# @return [String] Liferay version
|
138
|
+
#
|
139
|
+
def self.getVersionFromGuestHome(url)
|
140
|
+
url += '/' unless url.to_s.end_with?('/')
|
141
|
+
|
142
|
+
res = sendHttpRequest("#{url}web/guest/home")
|
143
|
+
|
144
|
+
return unless res
|
145
|
+
|
146
|
+
if res['liferay-portal'].to_s.start_with?('Liferay') && res['liferay-portal'].to_s.include?('.')
|
147
|
+
return res['liferay-portal'].to_s
|
148
|
+
end
|
149
|
+
|
150
|
+
# Hello World default post
|
151
|
+
res.body.to_s.scan(%r{<div class="portlet-body">\s*Welcome to (Liferay Portal [^<]+)\.\s*</div>}).flatten.first
|
152
|
+
end
|
153
|
+
|
154
|
+
# Get server from server error page
|
155
|
+
#
|
156
|
+
# @param [String] URL
|
157
|
+
#
|
158
|
+
# @return [String] Server version
|
159
|
+
#
|
160
|
+
def self.getServerVersion(url)
|
161
|
+
url += '/' unless url.to_s.end_with?('/')
|
162
|
+
|
163
|
+
res = sendHttpRequest("#{url}api/liferay")
|
164
|
+
|
165
|
+
return unless res
|
166
|
+
|
167
|
+
tomcat = res.body.scan(%r{>(Apache Tomcat/[0-9.]+)}).flatten.first
|
168
|
+
return tomcat if tomcat
|
169
|
+
|
170
|
+
glassfish = res.body.scan(/>(GlassFish Server Open Source Edition [0-9.]+)/).flatten.first
|
171
|
+
return glassfish if glassfish
|
172
|
+
|
173
|
+
nil
|
174
|
+
end
|
175
|
+
|
176
|
+
# Get client IP address from server error page
|
177
|
+
# This may disclose the IP address of intermediary proxy servers
|
178
|
+
#
|
179
|
+
# @param [String] URL
|
180
|
+
#
|
181
|
+
# @return [String] Client IP address
|
182
|
+
#
|
183
|
+
def self.getClientIpAddress(url)
|
184
|
+
url += '/' unless url.to_s.end_with?('/')
|
185
|
+
|
186
|
+
res = sendHttpRequest("#{url}api/liferay")
|
187
|
+
|
188
|
+
return unless res
|
189
|
+
|
190
|
+
res.body.scan(/>Access denied for ([\d.]+)</).flatten.first
|
191
|
+
end
|
192
|
+
|
193
|
+
#
|
194
|
+
# Get Liferay default language
|
195
|
+
#
|
196
|
+
# @param [String] URL
|
197
|
+
#
|
198
|
+
# @return [String] Liferay language
|
199
|
+
#
|
200
|
+
def self.getLanguage(url)
|
201
|
+
url += '/' unless url.to_s.end_with?('/')
|
202
|
+
|
203
|
+
res = sendHttpRequest(url)
|
204
|
+
|
205
|
+
return unless res
|
206
|
+
|
207
|
+
res['set-cookie'].to_s.scan(/GUEST_LANGUAGE_ID=([a-z]{2,3}_[A-Z]{2,3})/).flatten.first
|
208
|
+
end
|
209
|
+
|
210
|
+
#
|
211
|
+
# Retrieve organisation email address domain
|
212
|
+
#
|
213
|
+
# @param [String] URL
|
214
|
+
#
|
215
|
+
# @return [String] Organisation email address domain
|
216
|
+
#
|
217
|
+
def self.getOrganisationEmail(url)
|
218
|
+
url += '/' unless url.to_s.end_with?('/')
|
219
|
+
|
220
|
+
# old Liferay <= 6.x
|
221
|
+
res = sendHttpRequest("#{url}web/guest/home?p_p_id=58&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&saveLastPath=false")
|
222
|
+
if res && res.body =~ (/name="_58_login"[^>]+type="text"\s*value="@([^"]+)"/)
|
223
|
+
return CGI.unescapeHTML(::Regexp.last_match(1))
|
224
|
+
end
|
225
|
+
|
226
|
+
# new Liferay >= 7.x
|
227
|
+
res = sendHttpRequest("#{url}home?p_p_id=com_liferay_login_web_portlet_LoginPortlet&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&saveLastPath=false")
|
228
|
+
if res
|
229
|
+
return res.body.scan(/name="_com_liferay_login_web_portlet_LoginPortlet_login"\s*type="text"\s*value="@([^"]+)"/).flatten.first
|
230
|
+
end
|
231
|
+
|
232
|
+
nil
|
233
|
+
end
|
234
|
+
|
235
|
+
#
|
236
|
+
# Retrieve names from open search
|
237
|
+
#
|
238
|
+
# @param [String] URL
|
239
|
+
#
|
240
|
+
# @return [Array] list of users
|
241
|
+
#
|
242
|
+
def self.getUsersFromSearch(url)
|
243
|
+
url += '/' unless url.to_s.end_with?('/')
|
244
|
+
|
245
|
+
res = sendHttpRequest("#{url}c/search/open_search")
|
246
|
+
|
247
|
+
return [] unless res
|
248
|
+
return [] unless res.body
|
249
|
+
|
250
|
+
valid_users = []
|
251
|
+
res.body.encode('UTF-8', invalid: :replace, undef: :replace).scan(/\[Users » ([^\]]+)\]/).each do |full_name|
|
252
|
+
next if full_name.empty?
|
253
|
+
|
254
|
+
valid_users << [nil, full_name.flatten.first]
|
255
|
+
end
|
256
|
+
|
257
|
+
valid_users
|
258
|
+
rescue StandardError
|
259
|
+
@logger.error("#{e.message}")
|
260
|
+
[]
|
261
|
+
end
|
262
|
+
|
263
|
+
#
|
264
|
+
# Check if account registration is enabled
|
265
|
+
#
|
266
|
+
# @param [String] URL
|
267
|
+
#
|
268
|
+
# @return [Boolean]
|
269
|
+
#
|
270
|
+
def self.userRegistration(url)
|
271
|
+
url += '/' unless url.to_s.end_with?('/')
|
272
|
+
|
273
|
+
# new Liferay >= 7.x
|
274
|
+
res = sendHttpRequest("#{url}web/guest/home?p_p_id=com_liferay_login_web_portlet_LoginPortlet&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&_com_liferay_login_web_portlet_LoginPortlet_mvcRenderCommandName=%2Flogin%2Fcreate_account&saveLastPath=false")
|
275
|
+
if res && res.body.include?('_com_liferay_login_web_portlet_LoginPortlet_firstName') && res.body.include?('_com_liferay_login_web_portlet_LoginPortlet_lastName')
|
276
|
+
return true
|
277
|
+
end
|
278
|
+
|
279
|
+
# old Liferay <= 6.x
|
280
|
+
res = sendHttpRequest("#{url}web/guest/home?p_p_id=58&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&saveLastPath=false&_58_struts_action=%2Flogin%2Fcreate_account")
|
281
|
+
return true if res && res.body =~ /_58_firstName/ && res.body =~ /_58_lastName/
|
282
|
+
|
283
|
+
false
|
284
|
+
end
|
285
|
+
|
286
|
+
#
|
287
|
+
# Check if Single SignOn (SSO) authentication is enabled
|
288
|
+
#
|
289
|
+
# @param [String] URL
|
290
|
+
#
|
291
|
+
# @return [Boolean] SSO authentication is enabled
|
292
|
+
#
|
293
|
+
def self.ssoAuthEnabled(url)
|
294
|
+
url += '/' unless url.to_s.end_with?('/')
|
295
|
+
|
296
|
+
res = sendHttpRequest("#{url}c/portal/login")
|
297
|
+
|
298
|
+
return false unless res
|
299
|
+
|
300
|
+
return true if res.body.to_s.include?('name="SAMLRequest"')
|
301
|
+
return true if res.body =~ /id="idpEntityId"\s+name="idpEntityId"/
|
302
|
+
|
303
|
+
false
|
304
|
+
end
|
305
|
+
|
306
|
+
#
|
307
|
+
# Check if remote access to the SOAP API is allowed
|
308
|
+
# https://help.liferay.com/hc/en-us/articles/360018161151-SOAP-Web-Services
|
309
|
+
#
|
310
|
+
# @param [String] URL
|
311
|
+
#
|
312
|
+
# @return [Boolean]
|
313
|
+
#
|
314
|
+
def self.remoteSoapApi(url)
|
315
|
+
url += '/' unless url.to_s.end_with?('/')
|
316
|
+
|
317
|
+
res = sendHttpRequest("#{url}api/axis")
|
318
|
+
|
319
|
+
return false unless res
|
320
|
+
return false unless res.code.to_i == 200
|
321
|
+
|
322
|
+
return true if res.body.to_s.include?('<h2>And now... Some Services</h2>')
|
323
|
+
|
324
|
+
false
|
325
|
+
end
|
326
|
+
|
327
|
+
#
|
328
|
+
# Check if remote access to the JSON API is allowed
|
329
|
+
# https://liferay.atlassian.net/browse/LPSA-86672
|
330
|
+
# https://help.liferay.com/hc/en-us/articles/360018179011-Portal-Configuration-of-JSON-Web-Services
|
331
|
+
# https://help.liferay.com/hc/en-us/articles/360017882172-Configuring-JSON-Web-Services-#controlling-public-access
|
332
|
+
#
|
333
|
+
# @param [String] URL
|
334
|
+
#
|
335
|
+
# @return [Boolean]
|
336
|
+
#
|
337
|
+
def self.remoteJsonApi(url)
|
338
|
+
url += '/' unless url.to_s.end_with?('/')
|
339
|
+
|
340
|
+
res = sendHttpRequest("#{url}api/jsonws")
|
341
|
+
|
342
|
+
return false unless res
|
343
|
+
return false unless res.code.to_i == 200
|
344
|
+
|
345
|
+
return true if res.body.to_s.include?('<title>json-web-services-api</title>')
|
346
|
+
return true if res.body.to_s.include?('"JSONWS API"')
|
347
|
+
|
348
|
+
false
|
349
|
+
end
|
350
|
+
|
351
|
+
#
|
352
|
+
# Check if Forgot Password is enabled
|
353
|
+
#
|
354
|
+
# @param [String] URL
|
355
|
+
#
|
356
|
+
# @return [Boolean]
|
357
|
+
#
|
358
|
+
def self.passwordResetEnabled(url)
|
359
|
+
url += '/' unless url.to_s.end_with?('/')
|
360
|
+
|
361
|
+
# new Liferay >= 7.x
|
362
|
+
res = sendHttpRequest("#{url}home?p_p_id=com_liferay_login_web_portlet_LoginPortlet&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&_com_liferay_login_web_portlet_LoginPortlet_mvcRenderCommandName=%2Flogin%2Fforgot_password&saveLastPath=false")
|
363
|
+
return true if res && res.body.to_s.include?('Forgot Password')
|
364
|
+
|
365
|
+
# old Liferay <= 6.x
|
366
|
+
res = sendHttpRequest("#{url}web/guest/home?p_p_id=58&p_p_lifecycle=0&p_p_state=exclusive&p_p_mode=view&_58_struts_action=%2Flogin%2Fforgot_password")
|
367
|
+
return true if res && res.body.to_s.include?('Forgot Password')
|
368
|
+
|
369
|
+
false
|
370
|
+
end
|
371
|
+
|
372
|
+
#
|
373
|
+
# Check if Forgot Password requires CAPTCHA
|
374
|
+
#
|
375
|
+
# @param [String] URL
|
376
|
+
#
|
377
|
+
# @return [Boolean]
|
378
|
+
#
|
379
|
+
def self.passwordResetUsesCaptcha(url)
|
380
|
+
url += '/' unless url.to_s.end_with?('/')
|
381
|
+
|
382
|
+
# new Liferay >= 7.x
|
383
|
+
res = sendHttpRequest("#{url}home?p_p_id=com_liferay_login_web_portlet_LoginPortlet&p_p_lifecycle=0&p_p_state=maximized&p_p_mode=view&_com_liferay_login_web_portlet_LoginPortlet_mvcRenderCommandName=%2Flogin%2Fforgot_password&saveLastPath=false")
|
384
|
+
if res
|
385
|
+
return true if res.body.to_s.include?('id="_58_captcha"')
|
386
|
+
return true if res.body.to_s.include?('id="_com_liferay_login_web_portlet_LoginPortlet_captchaText"')
|
387
|
+
return true if res.body.to_s.include?('RecaptchaOptions')
|
388
|
+
end
|
389
|
+
|
390
|
+
# old Liferay <= 6.x
|
391
|
+
res = sendHttpRequest("#{url}web/guest/home?p_p_id=58&p_p_lifecycle=0&p_p_state=exclusive&p_p_mode=view&_58_struts_action=%2Flogin%2Fforgot_password")
|
392
|
+
if res
|
393
|
+
return true if res.body.to_s.include?('id="_58_captcha"')
|
394
|
+
return true if res.body.to_s.include?('id="_com_liferay_login_web_portlet_LoginPortlet_captchaText"')
|
395
|
+
return true if res.body.to_s.include?('RecaptchaOptions')
|
396
|
+
end
|
397
|
+
|
398
|
+
false
|
399
|
+
end
|
400
|
+
|
401
|
+
#
|
402
|
+
# Enumerate users
|
403
|
+
#
|
404
|
+
# @param [String] URL
|
405
|
+
#
|
406
|
+
# @return [Array] list of users
|
407
|
+
#
|
408
|
+
def self.enumerateUsersFromBlogRss(url)
|
409
|
+
url += '/' unless url.to_s.end_with?('/')
|
410
|
+
|
411
|
+
# load potential usernames from gem data files
|
412
|
+
users = File.readlines("#{@resource_path}/users.txt")
|
413
|
+
users.concat(File.readlines("#{@resource_path}/names.txt"))
|
414
|
+
|
415
|
+
# enumerate common user names from blog RSS feed
|
416
|
+
valid_users = []
|
417
|
+
users.sort.uniq.each do |screen_name|
|
418
|
+
next if screen_name.start_with?('#')
|
419
|
+
|
420
|
+
screen_name.chomp!
|
421
|
+
|
422
|
+
next if screen_name.empty?
|
423
|
+
|
424
|
+
res = sendHttpRequest("#{url}web/#{URI::Parser.new.escape(screen_name)}/home/-/blogs/rss")
|
425
|
+
|
426
|
+
next if res.nil?
|
427
|
+
next if res.code.to_i != 200
|
428
|
+
|
429
|
+
full_name = res.body.to_s.scan(%r{<subtitle>(.+?)</subtitle>}).flatten.first
|
430
|
+
|
431
|
+
next unless full_name
|
432
|
+
|
433
|
+
valid_users << [screen_name, full_name]
|
434
|
+
end
|
435
|
+
|
436
|
+
valid_users
|
437
|
+
rescue StandardError => e
|
438
|
+
@logger.error("#{e.message}")
|
439
|
+
[]
|
440
|
+
end
|
441
|
+
|
442
|
+
#
|
443
|
+
# Fetch URL
|
444
|
+
#
|
445
|
+
# @param [String] URL
|
446
|
+
#
|
447
|
+
# @return [Net::HTTPResponse] HTTP response
|
448
|
+
#
|
449
|
+
def self.sendHttpRequest(url)
|
450
|
+
target = URI.parse(url)
|
451
|
+
@logger.info("Fetching #{target}")
|
452
|
+
|
453
|
+
http = Net::HTTP.new(target.host, target.port)
|
454
|
+
if target.scheme.to_s.eql?('https')
|
455
|
+
http.use_ssl = true
|
456
|
+
http.verify_mode = @insecure ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
|
457
|
+
end
|
458
|
+
http.open_timeout = 20
|
459
|
+
http.read_timeout = 20
|
460
|
+
headers = {}
|
461
|
+
headers['User-Agent'] = "LiferayScan/#{VERSION}"
|
462
|
+
headers['Accept-Encoding'] = 'gzip,deflate'
|
463
|
+
|
464
|
+
begin
|
465
|
+
res = http.request(Net::HTTP::Get.new(target, headers.to_hash))
|
466
|
+
rescue Timeout::Error, Errno::ETIMEDOUT
|
467
|
+
@logger.error("Could not retrieve URL #{target}: Timeout")
|
468
|
+
return nil
|
469
|
+
rescue StandardError => e
|
470
|
+
@logger.error("Could not retrieve URL #{target}: #{e}")
|
471
|
+
return nil
|
472
|
+
end
|
473
|
+
|
474
|
+
@logger.info("Received reply (#{res.body.length} bytes)")
|
475
|
+
|
476
|
+
begin
|
477
|
+
if res.body && res['Content-Encoding'].eql?('gzip')
|
478
|
+
sio = StringIO.new(res.body)
|
479
|
+
gz = Zlib::GzipReader.new(sio)
|
480
|
+
res.body = gz.read
|
481
|
+
end
|
482
|
+
rescue Zlib::GzipFile::Error => e
|
483
|
+
# Not compressed? Return raw response.
|
484
|
+
@logger.info("Gzip decompression failed: #{e.message}")
|
485
|
+
end
|
486
|
+
|
487
|
+
res
|
488
|
+
end
|
489
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: liferay_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: 2025-04-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: terminal-table
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: logger
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
description: A simple remote scanner for Liferay Portal
|
42
|
+
email: bcoles@gmail.com
|
43
|
+
executables:
|
44
|
+
- liferay-scan
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- bin/liferay-scan
|
49
|
+
- data/names.txt
|
50
|
+
- data/users.txt
|
51
|
+
- lib/liferay_scan.rb
|
52
|
+
homepage: https://github.com/bcoles/liferay_scan
|
53
|
+
licenses:
|
54
|
+
- MIT
|
55
|
+
metadata: {}
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 3.0.0
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
requirements: []
|
71
|
+
rubygems_version: 3.3.27
|
72
|
+
signing_key:
|
73
|
+
specification_version: 4
|
74
|
+
summary: Liferay scanner
|
75
|
+
test_files: []
|