fingerprinter 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/fingerprinter/configs/scan_options.rb +51 -0
- data/lib/fingerprinter/core/detector.rb +45 -0
- data/lib/fingerprinter/core/http_client.rb +70 -0
- data/lib/fingerprinter/technologies/cms/magento.rb +46 -0
- data/lib/fingerprinter/technologies/cms/wordpress.rb +27 -0
- data/lib/fingerprinter/technologies/softwares/apache_ofbiz.rb +40 -0
- data/lib/fingerprinter/technologies/softwares/f5_next_central_manager.rb +10 -0
- data/lib/fingerprinter/technologies/softwares/nexus_repository.rb +30 -0
- data/lib/fingerprinter/technologies/softwares/tinyproxy.rb +14 -0
- data/lib/fingerprinter/utilities/kb.rb +18 -0
- data/lib/fingerprinter/utilities/parser.rb +12 -0
- data/lib/fingerprinter/utilities/urls.rb +31 -0
- data/lib/fingerprinter.rb +72 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1d85553bfd3f1487b0185188933d1e409f12cc8e8a96762e5f2e8e5a2ab7f7d1
|
4
|
+
data.tar.gz: bd5fc3fd49ceac7667bcaca1eaf4cb3e0fb7be4dc68d62bc58bbf15f6ff8751d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e598f2f6025e1db35ca2089aace62176b048cc6e432868086199d5af999138257dcd6a3f8134c6b9c303c189e565d15ffb8e0e1e6736eae8b3be9f45c16f360b
|
7
|
+
data.tar.gz: c9829ca7d4288d7f096b4c3dd7dea07369bebbee12e9b308c743bd749176f03c9898a3de1b937bf30f1ea33b5ef675d317ccf1546aeb3feba22a57f0909fdf91
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This class builds and maintains the list of options provided for the scan to make it available
|
4
|
+
# when needed
|
5
|
+
class ScanOptions
|
6
|
+
def self.build(options)
|
7
|
+
@proxy = options[:proxy]
|
8
|
+
@user_agent = options[:ua]
|
9
|
+
@timeout = options[:timeout]
|
10
|
+
@concurrency = options[:concurrency]
|
11
|
+
@silent = options[:silent]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.proxy?
|
15
|
+
!!@proxy
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.proxy_host
|
19
|
+
uri = URI(@proxy)
|
20
|
+
return '' unless uri
|
21
|
+
|
22
|
+
uri.host.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.proxy_url
|
26
|
+
@proxy
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.proxy_port
|
30
|
+
uri = URI(@proxy)
|
31
|
+
return nil unless uri
|
32
|
+
|
33
|
+
uri.port.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.user_agent
|
37
|
+
@user_agent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.timeout
|
41
|
+
@timeout ? @timeout.to_i : 10
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.http_concurrency
|
45
|
+
@concurrency || 10
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.silent?
|
49
|
+
!!@silent
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Fingerprinter : Entry point
|
4
|
+
module Fingerprinter
|
5
|
+
# Technologies : Groups the different detection methods
|
6
|
+
class Technologies
|
7
|
+
def self.response_headers_check(response, regexes)
|
8
|
+
response.headers&.each do |header, value|
|
9
|
+
regex = regexes[header.downcase]
|
10
|
+
next unless regex
|
11
|
+
|
12
|
+
if value.is_a?(Array)
|
13
|
+
return true if value.any? { |v| regex.match?(v) }
|
14
|
+
else
|
15
|
+
return true if regex.match?(value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.whole_body_check(response, regexes)
|
23
|
+
return false if response.body.nil?
|
24
|
+
|
25
|
+
regexes.each do |regex|
|
26
|
+
return true if response.body.match?(regex)
|
27
|
+
end
|
28
|
+
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.meta_detection(doc, regexes, type = 'generator')
|
33
|
+
nodes = doc.xpath("//meta[@name='#{type}']/@content")
|
34
|
+
nodes&.each do |node|
|
35
|
+
return true if regexes.any? { |regex| node.value.match?(regex) }
|
36
|
+
end
|
37
|
+
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.title_detection(doc, title)
|
42
|
+
doc.title&.downcase == title.downcase
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'typhoeus'
|
4
|
+
|
5
|
+
# HTTP Client used to perform some requests (check for some content for example) or as a fallback in some
|
6
|
+
# browser processing (less resource intensive than using a Chrome instance but won't provide access to the DOM
|
7
|
+
# for example)
|
8
|
+
class HttpClient
|
9
|
+
def initialize
|
10
|
+
Typhoeus::Config.user_agent = ScanOptions.user_agent
|
11
|
+
@hydra = Typhoeus::Hydra.new(max_concurrency: ScanOptions.http_concurrency)
|
12
|
+
end
|
13
|
+
|
14
|
+
def request_options(method, options, body = nil)
|
15
|
+
req_options = {
|
16
|
+
ssl_verifypeer: false,
|
17
|
+
ssl_verifyhost: 0,
|
18
|
+
followlocation: options[:follow_location] || false,
|
19
|
+
method: method,
|
20
|
+
headers: options[:headers] || {},
|
21
|
+
body: body
|
22
|
+
}
|
23
|
+
|
24
|
+
req_options[:headers].merge!({
|
25
|
+
'Priority' => 'u=0, i',
|
26
|
+
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8'
|
27
|
+
})
|
28
|
+
|
29
|
+
req_options[:params] = options[:params] if options[:params]
|
30
|
+
|
31
|
+
req_options[:proxy] = ScanOptions.proxy_url if ScanOptions.proxy?
|
32
|
+
req_options[:timeout] = ScanOptions.timeout
|
33
|
+
|
34
|
+
req_options
|
35
|
+
end
|
36
|
+
|
37
|
+
def get(urls, options = {})
|
38
|
+
responses = {}
|
39
|
+
|
40
|
+
urls = [urls] if urls.is_a?(String)
|
41
|
+
urls.each do |url|
|
42
|
+
http_request = Typhoeus::Request.new(url, request_options(:get, options))
|
43
|
+
|
44
|
+
http_request.on_complete { |response| responses[url] = response }
|
45
|
+
|
46
|
+
@hydra.queue(http_request)
|
47
|
+
end
|
48
|
+
|
49
|
+
@hydra.run
|
50
|
+
|
51
|
+
responses
|
52
|
+
end
|
53
|
+
|
54
|
+
def post(urls, body, options = {})
|
55
|
+
responses = {}
|
56
|
+
|
57
|
+
urls = [urls] if urls.is_a?(String)
|
58
|
+
urls.each do |url|
|
59
|
+
http_request = Typhoeus::Request.new(url, request_options(:post, options, body))
|
60
|
+
|
61
|
+
http_request.on_complete { |response| responses[url] = response }
|
62
|
+
|
63
|
+
@hydra.queue(http_request)
|
64
|
+
end
|
65
|
+
|
66
|
+
@hydra.run
|
67
|
+
|
68
|
+
responses
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Magento Detection
|
4
|
+
class Magento < Fingerprinter::Technologies
|
5
|
+
META_CONTENT_REGEX = [
|
6
|
+
'Magento'
|
7
|
+
].freeze
|
8
|
+
|
9
|
+
HEADERS_REGEX = {
|
10
|
+
'x-magento-debug' => /\d/,
|
11
|
+
'x-magento-cache-control' => /\w/
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
BODY_CONTENT_REGEX = [
|
15
|
+
/Magento_PageCache/,
|
16
|
+
/Mage\.Cookies\.path/,
|
17
|
+
/data-requiremodule="(mage|Magento_)/,
|
18
|
+
/mage\/cookies/,
|
19
|
+
/MAGENTO_/,
|
20
|
+
/Magento Security Scan/,
|
21
|
+
/js\/mage\//,
|
22
|
+
/x-magento-init/
|
23
|
+
].freeze
|
24
|
+
|
25
|
+
def self.get_graphql(url)
|
26
|
+
url = File.join(Utilities::Urls.up_to_port(url), '/graphql?query=+{customerDownloadableProducts+{+items+{+date+download_url}}+}')
|
27
|
+
return if Utilities::Kb.inspected?(self, url)
|
28
|
+
|
29
|
+
Utilities::Kb.inspected(self, url)
|
30
|
+
Fingerprinter.http_client.get(url)[url]
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.run(data)
|
34
|
+
detected = meta_detection(data[:doc], META_CONTENT_REGEX) ||
|
35
|
+
whole_body_check(data[:response], BODY_CONTENT_REGEX)
|
36
|
+
|
37
|
+
if detected
|
38
|
+
'Magento'
|
39
|
+
else
|
40
|
+
response = get_graphql(data[:url])
|
41
|
+
return unless response&.code == 200 && ['The current customer', 'graphql-authorization'].all? { |pattern| response.body.include?(pattern) }
|
42
|
+
|
43
|
+
'Magento'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Wordpress Detection
|
4
|
+
class Wordpress < Fingerprinter::Technologies
|
5
|
+
HEADERS_REGEX = {
|
6
|
+
'link' => %r{rel="https//api\.w\.org/"},
|
7
|
+
'x-pingback' => %r{/xmlrpc.php}
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
META_CONTENT_REGEX = [
|
11
|
+
'WordPress'
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
BODY_CONTENT_REGEX = [
|
15
|
+
%r{<script src=['"]https?://[\w./-]+wp-embed\.min\.js},
|
16
|
+
%r{<link rel=['"]stylesheet['"] id=['"][\w-]+['"]\s+href=['"]https?://[\w.-]+/wp-(?:content|includes)/},
|
17
|
+
%r{<script src=['"]https?://[\w./-]+wp-(?:content|includes)}
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
def self.run(data)
|
21
|
+
return unless response_headers_check(data[:response], HEADERS_REGEX) ||
|
22
|
+
meta_detection(data[:doc], META_CONTENT_REGEX) ||
|
23
|
+
whole_body_check(data[:response], BODY_CONTENT_REGEX)
|
24
|
+
|
25
|
+
'WordPress'
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Apache Ofbiz Detection
|
4
|
+
class ApacheOfbiz < Fingerprinter::Technologies
|
5
|
+
META_CONTENT_REGEX = [
|
6
|
+
'Apache OFBiz'
|
7
|
+
].freeze
|
8
|
+
|
9
|
+
HEADERS_REGEX = {
|
10
|
+
'set-cookie' => /OFBiz\.Visitor/
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
BODY_CONTENT_REGEX = [
|
14
|
+
/Powered by.*OFBiz/
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
def self.get_xmlrpc(url)
|
18
|
+
url = File.join(Utilities::Urls.up_to_port(url), '/webtools/control/xmlrpc')
|
19
|
+
return if Utilities::Kb.inspected?(self, url)
|
20
|
+
|
21
|
+
Utilities::Kb.inspected(self, url)
|
22
|
+
Fingerprinter.http_client.get(url)[url]
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.run(data)
|
26
|
+
response = data[:response]
|
27
|
+
if response.code == 404
|
28
|
+
response = get_xmlrpc(data[:url])
|
29
|
+
return unless response&.code == 200
|
30
|
+
|
31
|
+
doc = Utilities::Parser.doc(response.body)
|
32
|
+
end
|
33
|
+
|
34
|
+
return unless response_headers_check(response, HEADERS_REGEX) ||
|
35
|
+
meta_detection(data[:doc], META_CONTENT_REGEX) ||
|
36
|
+
whole_body_check(response, BODY_CONTENT_REGEX)
|
37
|
+
|
38
|
+
'Apache Ofbiz'
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# F5 Next Central Manager Detection
|
4
|
+
class F5NextCentralManager < Fingerprinter::Technologies
|
5
|
+
def self.run(data)
|
6
|
+
return unless title_detection(data[:doc], 'BIG-IP Next | Central Manager')
|
7
|
+
|
8
|
+
'F5 Big-IP Next Central manager'
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Nexus Repository Detection
|
4
|
+
class NexusRepository < Fingerprinter::Technologies
|
5
|
+
META_CONTENT_REGEX = [
|
6
|
+
'Nexus Repository'
|
7
|
+
].freeze
|
8
|
+
|
9
|
+
def self.check_path(url)
|
10
|
+
url = File.join(Utilities::Urls.up_to_port(url), '/nexus/')
|
11
|
+
return if Utilities::Kb.inspected?(self, url)
|
12
|
+
|
13
|
+
Utilities::Kb.inspected(self, url)
|
14
|
+
Fingerprinter.http_client.get(url, { follow_location: true })[url]
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.run(data)
|
18
|
+
detected = title_detection(data[:doc], 'Sonatype Nexus Repository') || meta_detection(data[:doc], META_CONTENT_REGEX, 'description')
|
19
|
+
unless detected
|
20
|
+
response = check_path(data[:url])
|
21
|
+
return unless response&.code == 200
|
22
|
+
|
23
|
+
doc = Utilities::Parser.doc(response.body)
|
24
|
+
detected = title_detection(doc, 'Sonatype Nexus Repository') || meta_detection(doc, META_CONTENT_REGEX, 'description')
|
25
|
+
end
|
26
|
+
return unless detected
|
27
|
+
|
28
|
+
'Nexus Repository'
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# TinyProxy Detection
|
4
|
+
class TinyProxy < Fingerprinter::Technologies
|
5
|
+
HEADERS_REGEX = {
|
6
|
+
'server' => /tinyproxy/
|
7
|
+
}.freeze
|
8
|
+
|
9
|
+
def self.run(data)
|
10
|
+
return unless response_headers_check(data[:response], HEADERS_REGEX)
|
11
|
+
|
12
|
+
'TinyProxy'
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Utilities
|
4
|
+
# Files : Utilities related to files
|
5
|
+
class Kb
|
6
|
+
def self.coverage_id(plugin, url)
|
7
|
+
"#{plugin}:#{url}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.inspected(plugin, url)
|
11
|
+
Fingerprinter::Technologies.kb[:inspected] << coverage_id(plugin, url)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.inspected?(plugin, url)
|
15
|
+
Fingerprinter::Technologies.kb[:inspected].include?(coverage_id(plugin, url))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Utilities
|
4
|
+
# Urls : Utilities related to urls
|
5
|
+
class Urls
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
def self.uri_parse(url)
|
9
|
+
URI(url)
|
10
|
+
rescue ArgumentError, URI::InvalidURIError
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.up_to_port(url)
|
15
|
+
uri = url.is_a?(URI) ? url : uri_parse(url)
|
16
|
+
return unless uri
|
17
|
+
|
18
|
+
url = "#{uri.scheme}://#{uri.host}"
|
19
|
+
url += ":#{uri.port}" unless [80, 443].include?(uri.port)
|
20
|
+
|
21
|
+
url
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.without_query(url)
|
25
|
+
uri = url.is_a?(URI) ? url : uri_parse(url)
|
26
|
+
return unless uri
|
27
|
+
|
28
|
+
uri.to_s.split('?', 2).first.to_s
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent'
|
4
|
+
|
5
|
+
# Fingerprinter : Entry point
|
6
|
+
module Fingerprinter
|
7
|
+
Dir[File.join(__dir__, 'fingerprinter', 'configs/*.rb')].sort.each { |file| require file }
|
8
|
+
Dir[File.join(__dir__, 'fingerprinter', 'core/*.rb')].sort.each { |file| require file }
|
9
|
+
Dir[File.join(__dir__, 'fingerprinter', 'utilities/*.rb')].sort.each { |file| require file }
|
10
|
+
|
11
|
+
def self.http_client
|
12
|
+
@http_client ||= HttpClient.new
|
13
|
+
end
|
14
|
+
|
15
|
+
# Technologies : Groups the different subcategories of technologies
|
16
|
+
class Technologies
|
17
|
+
Dir[File.join(__dir__, 'fingerprinter', 'technologies', '**/*.rb')].sort.each { |file| require file }
|
18
|
+
|
19
|
+
attr_accessor :results
|
20
|
+
attr_reader :http_client
|
21
|
+
|
22
|
+
def initialize(options = {})
|
23
|
+
ScanOptions.build(options)
|
24
|
+
@results ||= Concurrent::Hash.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.kb
|
28
|
+
@kb ||= {
|
29
|
+
inspected: Concurrent::Array.new
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def run(urls)
|
34
|
+
urls.each do |url|
|
35
|
+
response = get_response(url)
|
36
|
+
next unless response
|
37
|
+
|
38
|
+
url = Utilities::Urls.up_to_port(response.effective_url)
|
39
|
+
|
40
|
+
responses = response.redirections
|
41
|
+
responses << response
|
42
|
+
|
43
|
+
responses.each do |response|
|
44
|
+
doc = Utilities::Parser.doc(response.body)
|
45
|
+
|
46
|
+
results[url] = Concurrent::Array.new
|
47
|
+
data = { response:, doc:, url: }
|
48
|
+
Technologies.subclasses.each { |technology| results[url] << technology.run(data) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
results.transform_values! { |v| v.compact.uniq }
|
53
|
+
results
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def get_response(url)
|
59
|
+
response = Fingerprinter.http_client.get(url, { follow_location: true })[url]
|
60
|
+
return if response&.code&.zero?
|
61
|
+
return response if same_scope?(url, response)
|
62
|
+
|
63
|
+
Fingerprinter.http_client.get(url, { follow_location: false })[url]
|
64
|
+
end
|
65
|
+
|
66
|
+
def same_scope?(url, response)
|
67
|
+
url = "https://#{url}" unless url.start_with?('http')
|
68
|
+
|
69
|
+
Utilities::Urls.uri_parse(url)&.host == Utilities::Urls.uri_parse(response&.effective_url)&.host
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fingerprinter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joshua MARTINELLE
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-06-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: typhoeus
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: nokogiri
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: concurrent-ruby
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- contact@jomar.fr
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- lib/fingerprinter.rb
|
63
|
+
- lib/fingerprinter/configs/scan_options.rb
|
64
|
+
- lib/fingerprinter/core/detector.rb
|
65
|
+
- lib/fingerprinter/core/http_client.rb
|
66
|
+
- lib/fingerprinter/technologies/cms/magento.rb
|
67
|
+
- lib/fingerprinter/technologies/cms/wordpress.rb
|
68
|
+
- lib/fingerprinter/technologies/softwares/apache_ofbiz.rb
|
69
|
+
- lib/fingerprinter/technologies/softwares/f5_next_central_manager.rb
|
70
|
+
- lib/fingerprinter/technologies/softwares/nexus_repository.rb
|
71
|
+
- lib/fingerprinter/technologies/softwares/tinyproxy.rb
|
72
|
+
- lib/fingerprinter/utilities/kb.rb
|
73
|
+
- lib/fingerprinter/utilities/parser.rb
|
74
|
+
- lib/fingerprinter/utilities/urls.rb
|
75
|
+
homepage:
|
76
|
+
licenses: []
|
77
|
+
metadata: {}
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: 2.7.1
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
requirements: []
|
93
|
+
rubygems_version: 3.4.19
|
94
|
+
signing_key:
|
95
|
+
specification_version: 4
|
96
|
+
summary: Lorem Ipsum
|
97
|
+
test_files: []
|