recog 0.01
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.
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +23 -0
- data/README.md +63 -0
- data/bin/recog_export.rb +81 -0
- data/bin/recog_match.rb +51 -0
- data/bin/recog_verify.rb +45 -0
- data/features/match.feature +16 -0
- data/features/support/env.rb +5 -0
- data/features/verify.feature +31 -0
- data/features/xml/banners.xml +2 -0
- data/features/xml/failing_banners_fingerprints.xml +20 -0
- data/features/xml/matching_banners_fingerprints.xml +22 -0
- data/features/xml/no_tests.xml +53 -0
- data/features/xml/successful_tests.xml +33 -0
- data/features/xml/tests_with_failures.xml +10 -0
- data/features/xml/tests_with_warnings.xml +10 -0
- data/lib/recog.rb +3 -0
- data/lib/recog/db.rb +38 -0
- data/lib/recog/db_manager.rb +27 -0
- data/lib/recog/fingerprint.rb +60 -0
- data/lib/recog/formatter.rb +51 -0
- data/lib/recog/match_reporter.rb +77 -0
- data/lib/recog/matcher.rb +60 -0
- data/lib/recog/matcher_factory.rb +14 -0
- data/lib/recog/nizer.rb +263 -0
- data/lib/recog/verifier.rb +46 -0
- data/lib/recog/verifier_factory.rb +13 -0
- data/lib/recog/verify_reporter.rb +85 -0
- data/lib/recog/version.rb +3 -0
- data/recog.gemspec +34 -0
- data/spec/data/best_os_match_1.yml +17 -0
- data/spec/data/best_os_match_2.yml +17 -0
- data/spec/data/best_service_match_1.yml +17 -0
- data/spec/data/smb_native_os.txt +31 -0
- data/spec/data/test_fingerprints.xml +24 -0
- data/spec/lib/db_spec.rb +89 -0
- data/spec/lib/formatter_spec.rb +69 -0
- data/spec/lib/match_reporter_spec.rb +90 -0
- data/spec/lib/nizer_spec.rb +124 -0
- data/spec/lib/verify_reporter_spec.rb +112 -0
- data/xml/apache_os.xml +295 -0
- data/xml/architecture.xml +45 -0
- data/xml/ftp_banners.xml +808 -0
- data/xml/h323_callresp.xml +701 -0
- data/xml/hp_pjl_id.xml +435 -0
- data/xml/http_cookies.xml +379 -0
- data/xml/http_servers.xml +3326 -0
- data/xml/http_wwwauth.xml +412 -0
- data/xml/imap_banners.xml +267 -0
- data/xml/nntp_banners.xml +51 -0
- data/xml/ntp_banners.xml +538 -0
- data/xml/pop_banners.xml +452 -0
- data/xml/rsh_resp.xml +90 -0
- data/xml/sip_banners.xml +14 -0
- data/xml/smb_native_os.xml +385 -0
- data/xml/smtp_banners.xml +1738 -0
- data/xml/smtp_debug.xml +45 -0
- data/xml/smtp_ehlo.xml +53 -0
- data/xml/smtp_expn.xml +95 -0
- data/xml/smtp_help.xml +212 -0
- data/xml/smtp_mailfrom.xml +24 -0
- data/xml/smtp_noop.xml +45 -0
- data/xml/smtp_quit.xml +31 -0
- data/xml/smtp_rcptto.xml +33 -0
- data/xml/smtp_rset.xml +23 -0
- data/xml/smtp_turn.xml +23 -0
- data/xml/smtp_vrfy.xml +109 -0
- data/xml/snmp_sysdescr.xml +8008 -0
- data/xml/snmp_sysobjid.xml +284 -0
- data/xml/ssh_banners.xml +790 -0
- data/xml/upnp_banners.xml +590 -0
- metadata +190 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
<fingerprints>
|
3
|
+
<fingerprint pattern="^Cisco-SIPGateway/IOS-([\d\.x]+)$">
|
4
|
+
<description>Cisco SIPGateway</description>
|
5
|
+
<example>Cisco-SIPGateway/IOS-12.x</example>
|
6
|
+
<param pos="0" name="os.vendor" value="Cisco"/>
|
7
|
+
<param pos="0" name="os.product" value="IOS"/>
|
8
|
+
<param pos="1" name="os.version"/>
|
9
|
+
</fingerprint>
|
10
|
+
<fingerprint pattern="^Microsoft Exchange Server 2007 IMAP4 service ready$">
|
11
|
+
<!-- Microsoft Exchange Server 2007 IMAP4 service ready
|
12
|
+
-->
|
13
|
+
<description>Microsoft Exchange Server 2007</description>
|
14
|
+
<param pos="0" name="service.vendor" value="Microsoft"/>
|
15
|
+
<param pos="0" name="service.family" value="Exchange Server"/>
|
16
|
+
<param pos="0" name="service.product" value="Exchange 2007 Server"/>
|
17
|
+
<param pos="0" name="os.vendor" value="Microsoft"/>
|
18
|
+
<param pos="0" name="os.device" value="General"/>
|
19
|
+
<param pos="0" name="os.family" value="Windows"/>
|
20
|
+
<param pos="0" name="os.product" value="Windows"/>
|
21
|
+
</fingerprint>
|
22
|
+
<fingerprint pattern="^The Microsoft Exchange IMAP4 service is ready\.?$">
|
23
|
+
<example>The Microsoft Exchange IMAP4 service is ready.</example>
|
24
|
+
<description>Microsoft Exchange Server</description>
|
25
|
+
<param pos="0" name="service.vendor" value="Microsoft"/>
|
26
|
+
<param pos="0" name="service.family" value="Exchange Server"/>
|
27
|
+
<param pos="0" name="service.product" value="Exchange Server"/>
|
28
|
+
<param pos="0" name="os.vendor" value="Microsoft"/>
|
29
|
+
<param pos="0" name="os.device" value="General"/>
|
30
|
+
<param pos="0" name="os.family" value="Windows"/>
|
31
|
+
<param pos="0" name="os.product" value="Windows"/>
|
32
|
+
</fingerprint>
|
33
|
+
</fingerprints>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
<fingerprints>
|
3
|
+
<fingerprint pattern="^foo$">
|
4
|
+
<description>foo test</description>
|
5
|
+
<example>bar</example>
|
6
|
+
</fingerprint>
|
7
|
+
<fingerprint pattern="^This matches$">
|
8
|
+
<example>This almost matches</example>
|
9
|
+
</fingerprint>
|
10
|
+
</fingerprints>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
<fingerprints>
|
3
|
+
<fingerprint pattern="^-{10} Welcome to Pure-FTPd (.*)-{10}$">
|
4
|
+
<example>---------- Welcome to Pure-FTPd ----------</example>
|
5
|
+
<description>Pure-FTPd</description>
|
6
|
+
<param pos="1" name="pureftpd.config"/>
|
7
|
+
<param pos="0" name="service.family" value="Pure-FTPd"/>
|
8
|
+
<param pos="0" name="service.product" value="Pure-FTPd"/>
|
9
|
+
</fingerprint>
|
10
|
+
</fingerprints>
|
data/lib/recog.rb
ADDED
data/lib/recog/db.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module Recog
|
2
|
+
class DB
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'recog/fingerprint'
|
5
|
+
|
6
|
+
attr_accessor :path, :fingerprints, :match_key
|
7
|
+
|
8
|
+
def initialize(path)
|
9
|
+
self.path = path
|
10
|
+
parse_fingerprints
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse_fingerprints
|
14
|
+
self.fingerprints = []
|
15
|
+
xml = nil
|
16
|
+
|
17
|
+
File.open(self.path, "rb") do |fd|
|
18
|
+
xml = Nokogiri::XML( fd.read(fd.stat.size))
|
19
|
+
end
|
20
|
+
|
21
|
+
xml.xpath("/fingerprints").each do |fbase|
|
22
|
+
if fbase['matches']
|
23
|
+
self.match_key = fbase['matches'].to_s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
unless self.match_key
|
28
|
+
self.match_key = File.basename(self.path).sub(/\.xml$/, '')
|
29
|
+
end
|
30
|
+
|
31
|
+
xml.xpath("/fingerprints/fingerprint").each do |fprint|
|
32
|
+
fingerprints << Fingerprint.new(fprint)
|
33
|
+
end
|
34
|
+
|
35
|
+
xml = nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Recog
|
2
|
+
class DBManager
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'recog/db'
|
5
|
+
|
6
|
+
attr_accessor :path, :databases
|
7
|
+
|
8
|
+
DefaultDatabasePath = File.expand_path( File.join( File.dirname(__FILE__), "..", "..", "xml") )
|
9
|
+
|
10
|
+
def initialize(path = DefaultDatabasePath)
|
11
|
+
self.path = path
|
12
|
+
reload
|
13
|
+
end
|
14
|
+
|
15
|
+
def load_databases
|
16
|
+
Dir[self.path + "/*.xml"].each do |dbxml|
|
17
|
+
self.databases << DB.new(dbxml)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def reload
|
22
|
+
self.databases = []
|
23
|
+
load_databases
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Recog
|
2
|
+
class Fingerprint
|
3
|
+
attr_reader :name, :regex, :params, :tests
|
4
|
+
|
5
|
+
def initialize(xml)
|
6
|
+
@name = description(xml)
|
7
|
+
@regex = create_regexp(xml)
|
8
|
+
@params = parse_params(xml)
|
9
|
+
@tests = examples(xml)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def description(xml)
|
15
|
+
element = xml.xpath('description')
|
16
|
+
element.empty? ? '' : element.first.content
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_regexp(xml)
|
20
|
+
pattern = xml['pattern']
|
21
|
+
flags = xml['flags'].to_s.split(',')
|
22
|
+
RegexpFactory.build(pattern, flags)
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_params(xml)
|
26
|
+
{}.tap do |h|
|
27
|
+
xml.xpath('param').each do |e|
|
28
|
+
name = e['name']
|
29
|
+
pos = e['pos'].to_i
|
30
|
+
value = e['value'].to_s
|
31
|
+
h[name] = [pos, value]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def examples(xml)
|
37
|
+
xml.xpath('example').collect(&:content)
|
38
|
+
end
|
39
|
+
|
40
|
+
module RegexpFactory
|
41
|
+
def self.build(pattern, flags)
|
42
|
+
options = build_options(flags)
|
43
|
+
Regexp.new(pattern, options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.build_options(flags)
|
47
|
+
rflags = Regexp::NOENCODING
|
48
|
+
flags.each do |flag|
|
49
|
+
case flag
|
50
|
+
when 'REG_DOT_NEWLINE', 'REG_LINE_ANY_CRLF'
|
51
|
+
rflags |= Regexp::MULTILINE
|
52
|
+
when 'REG_ICASE'
|
53
|
+
rflags |= Regexp::IGNORECASE
|
54
|
+
end
|
55
|
+
end
|
56
|
+
rflags
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Recog
|
2
|
+
class Formatter
|
3
|
+
COLORS = {
|
4
|
+
:red => 31,
|
5
|
+
:yellow => 33,
|
6
|
+
:green => 32,
|
7
|
+
:white => 15
|
8
|
+
}
|
9
|
+
|
10
|
+
attr_reader :options, :output
|
11
|
+
|
12
|
+
def initialize(options, output)
|
13
|
+
@options = options
|
14
|
+
@output = output || StringIO.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def status_message(text)
|
18
|
+
output.puts color(text, :white)
|
19
|
+
end
|
20
|
+
|
21
|
+
def success_message(text)
|
22
|
+
output.puts color(text, :green)
|
23
|
+
end
|
24
|
+
|
25
|
+
def warning_message(text)
|
26
|
+
output.puts color(text, :yellow)
|
27
|
+
end
|
28
|
+
|
29
|
+
def failure_message(text)
|
30
|
+
output.puts color(text, :red)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def color_enabled?
|
36
|
+
options.color
|
37
|
+
end
|
38
|
+
|
39
|
+
def color(text, color_code)
|
40
|
+
color_enabled? ? colorize(text, color_code) : text
|
41
|
+
end
|
42
|
+
|
43
|
+
def colorize(text, color_code)
|
44
|
+
"\e[#{color_code_for(color_code)}m#{text}\e[0m"
|
45
|
+
end
|
46
|
+
|
47
|
+
def color_code_for(code)
|
48
|
+
COLORS.fetch(code) { COLORS.fetch(:white) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Recog
|
2
|
+
class MatchReporter
|
3
|
+
attr_reader :formatter
|
4
|
+
attr_reader :line_count, :match_count, :fail_count
|
5
|
+
|
6
|
+
def initialize(options, formatter)
|
7
|
+
@options = options
|
8
|
+
@formatter = formatter
|
9
|
+
reset_counts
|
10
|
+
end
|
11
|
+
|
12
|
+
def report
|
13
|
+
reset_counts
|
14
|
+
yield self
|
15
|
+
summarize
|
16
|
+
end
|
17
|
+
|
18
|
+
def stop?
|
19
|
+
return false unless @options.fail_fast
|
20
|
+
@fail_count >= @options.stop_after
|
21
|
+
end
|
22
|
+
|
23
|
+
def increment_line_count
|
24
|
+
@line_count += 1
|
25
|
+
end
|
26
|
+
|
27
|
+
def match(text)
|
28
|
+
@match_count += 1
|
29
|
+
formatter.success_message(text)
|
30
|
+
end
|
31
|
+
|
32
|
+
def failure(text)
|
33
|
+
@fail_count += 1
|
34
|
+
formatter.failure_message(text)
|
35
|
+
end
|
36
|
+
|
37
|
+
def print_summary
|
38
|
+
colorize_summary(summary_line)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def reset_counts
|
44
|
+
@line_count = @match_count = @fail_count = 0
|
45
|
+
end
|
46
|
+
|
47
|
+
def detail?
|
48
|
+
@options.detail
|
49
|
+
end
|
50
|
+
|
51
|
+
def summarize
|
52
|
+
if detail?
|
53
|
+
print_lines_processed
|
54
|
+
print_summary
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def print_lines_processed
|
59
|
+
formatter.status_message("\nProcessed #{line_count} lines")
|
60
|
+
end
|
61
|
+
|
62
|
+
def summary_line
|
63
|
+
summary = "SUMMARY: "
|
64
|
+
summary << "#{match_count} matches"
|
65
|
+
summary << " and #{fail_count} failures"
|
66
|
+
summary
|
67
|
+
end
|
68
|
+
|
69
|
+
def colorize_summary(summary)
|
70
|
+
if @fail_count > 0
|
71
|
+
formatter.failure_message(summary)
|
72
|
+
else
|
73
|
+
formatter.success_message(summary)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Recog
|
2
|
+
class Matcher
|
3
|
+
attr_reader :fingerprints, :reporter
|
4
|
+
|
5
|
+
def initialize(fingerprints, reporter)
|
6
|
+
@fingerprints = fingerprints
|
7
|
+
@reporter = reporter
|
8
|
+
end
|
9
|
+
|
10
|
+
def match_banners(banners_file)
|
11
|
+
reporter.report do
|
12
|
+
|
13
|
+
fd = $stdin
|
14
|
+
file_source = false
|
15
|
+
|
16
|
+
if banners_file and banners_file != "-"
|
17
|
+
fd = File.open(banners_file, "rb")
|
18
|
+
file_source = true
|
19
|
+
end
|
20
|
+
|
21
|
+
fd.each_line do |line|
|
22
|
+
reporter.increment_line_count
|
23
|
+
|
24
|
+
line = line.to_s.unpack("C*").pack("C*").strip.gsub(/\\[rn]/, '')
|
25
|
+
found = nil
|
26
|
+
fingerprints.each do |fp|
|
27
|
+
m = line.match(fp.regex)
|
28
|
+
if m
|
29
|
+
found = [fp, m]
|
30
|
+
break
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if found
|
35
|
+
info = { }
|
36
|
+
fp, m = found
|
37
|
+
fp.params.each_pair do |k,v|
|
38
|
+
if v[0] == 0
|
39
|
+
info[k] = v[1]
|
40
|
+
else
|
41
|
+
info[k] = m[ v[0] ]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
info['data'] = line
|
45
|
+
reporter.match "MATCH: #{info.inspect}"
|
46
|
+
else
|
47
|
+
reporter.failure "FAIL: #{line}"
|
48
|
+
end
|
49
|
+
|
50
|
+
if reporter.stop?
|
51
|
+
break
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
fd.close if file_source
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
require 'recog/matcher'
|
3
|
+
require 'recog/formatter'
|
4
|
+
require 'recog/match_reporter'
|
5
|
+
|
6
|
+
module Recog
|
7
|
+
module MatcherFactory
|
8
|
+
def self.build(options)
|
9
|
+
formatter = Formatter.new(options, $stdout)
|
10
|
+
reporter = MatchReporter.new(options, formatter)
|
11
|
+
Matcher.new(options.fingerprints, reporter)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/recog/nizer.rb
ADDED
@@ -0,0 +1,263 @@
|
|
1
|
+
module Recog
|
2
|
+
class Nizer
|
3
|
+
|
4
|
+
# Default certainty ratings where none are specified in the fingerprint itself
|
5
|
+
DEFAULT_OS_CERTAINTY = 0.85 # Most frequent weights are 0.9, 1.0, and 0.5
|
6
|
+
DEFAULT_SERVICE_CERTAINTY = 0.85 # Most frequent weight is 0.85
|
7
|
+
|
8
|
+
# Non-weighted host attributes that can be extracted from fingerprint matches
|
9
|
+
HOST_ATTRIBUTES = %W{
|
10
|
+
host.domain
|
11
|
+
host.id
|
12
|
+
host.ip
|
13
|
+
host.mac
|
14
|
+
host.name
|
15
|
+
host.time
|
16
|
+
hw.device
|
17
|
+
hw.family
|
18
|
+
hw.product
|
19
|
+
hw.vendor
|
20
|
+
}
|
21
|
+
|
22
|
+
@@db_manager = nil
|
23
|
+
|
24
|
+
#
|
25
|
+
# Locate a database that corresponds with the match_key and attempt to
|
26
|
+
# find a matching fingerprinting, stopping a the first hit. Returns
|
27
|
+
# nil when no matching database or fingerprint is found.
|
28
|
+
#
|
29
|
+
def self.match(match_key, match_string)
|
30
|
+
match_string = match_string.to_s.unpack("C*").pack("C*")
|
31
|
+
@@db_manager ||= Recog::DBManager.new
|
32
|
+
@@db_manager.databases.each do |db|
|
33
|
+
next unless db.match_key == match_key
|
34
|
+
db.fingerprints.each do |fprint|
|
35
|
+
m = fprint.regex.match(match_string)
|
36
|
+
next unless m
|
37
|
+
result = { 'matched' => fprint.name }
|
38
|
+
fprint.params.each_pair do |k,v|
|
39
|
+
if v[0] == 0
|
40
|
+
result[k] = v[1]
|
41
|
+
else
|
42
|
+
result[k] = m[ v[0] ]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
return result
|
46
|
+
end
|
47
|
+
end
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Consider an array of match outputs, choose the best result, taking into
|
53
|
+
# account the granularity of OS vs Version vs SP vs Language. Only consider
|
54
|
+
# fields relevant to the host (OS, name, mac address, etc).
|
55
|
+
#
|
56
|
+
def self.best_os_match(matches)
|
57
|
+
|
58
|
+
# The result hash we return to the caller
|
59
|
+
result = {}
|
60
|
+
|
61
|
+
# Certain attributes should be evaluated separately
|
62
|
+
host_attrs = {}
|
63
|
+
|
64
|
+
# Bucket matches into matched OS product names
|
65
|
+
os_products = {}
|
66
|
+
|
67
|
+
matches.each do |m|
|
68
|
+
# Count how many times each host attribute value is asserted
|
69
|
+
(HOST_ATTRIBUTES & m.keys).each do |ha|
|
70
|
+
host_attrs[ha] ||= {}
|
71
|
+
host_attrs[ha][m[ha]] ||= 0
|
72
|
+
host_attrs[ha][m[ha]] += 1
|
73
|
+
end
|
74
|
+
|
75
|
+
next unless m.has_key?('os.product')
|
76
|
+
|
77
|
+
# Group matches by OS product and normalize certainty
|
78
|
+
cm = m.dup
|
79
|
+
cm['os.certainty'] = ( m['os.certainty'] || DEFAULT_OS_CERTAINTY ).to_f
|
80
|
+
os_products[ cm['os.product'] ] ||= []
|
81
|
+
os_products[ cm['os.product'] ] << cm
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# Select the best host attribute value by highest frequency
|
86
|
+
#
|
87
|
+
host_attrs.keys.each do |hk|
|
88
|
+
ranked_attr = host_attrs[hk].keys.sort do |a,b|
|
89
|
+
host_attrs[hk][b] <=> host_attrs[hk][a]
|
90
|
+
end
|
91
|
+
result[hk] = ranked_attr.first
|
92
|
+
end
|
93
|
+
|
94
|
+
# Unable to guess the OS without OS matches
|
95
|
+
unless os_products.keys.length > 0
|
96
|
+
return result
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Select the best operating system name by combined certainty of all
|
101
|
+
# matches within an os.product group. Multiple weak matches can
|
102
|
+
# outweigh a single strong match by design.
|
103
|
+
#
|
104
|
+
ranked_os = os_products.keys.sort do |a,b|
|
105
|
+
os_products[b].map{ |r| r['os.certainty'] }.inject(:+) <=>
|
106
|
+
os_products[a].map{ |r| r['os.certainty'] }.inject(:+)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Within the best match group, try to fill in missing attributes
|
110
|
+
os_name = ranked_os.first
|
111
|
+
|
112
|
+
# Find the best match within the winning group
|
113
|
+
ranked_os_matches = os_products[os_name].sort do |a,b|
|
114
|
+
b['os.certainty'] <=> a['os.certainty']
|
115
|
+
end
|
116
|
+
|
117
|
+
# Fill in missing result values in descending order of best match
|
118
|
+
ranked_os_matches.each do |rm|
|
119
|
+
rm.each_pair do |k,v|
|
120
|
+
result[k] ||= v
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
result
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# Consider an array of match outputs, choose the best result, taking into
|
129
|
+
# account the granularity of service. Only consider fields relevant to the
|
130
|
+
# service.
|
131
|
+
#
|
132
|
+
def self.best_service_match(matches)
|
133
|
+
|
134
|
+
# The result hash we return to the caller
|
135
|
+
result = {}
|
136
|
+
|
137
|
+
# Bucket matches into matched service product names
|
138
|
+
service_products = {}
|
139
|
+
|
140
|
+
matches.select{ |m| m.has_key?('service.product') }.each do |m|
|
141
|
+
# Group matches by product and normalize certainty
|
142
|
+
cm = m.dup
|
143
|
+
cm['service.certainty'] = ( m['service.certainty'] || DEFAULT_SERVICE_CERTAINTY ).to_f
|
144
|
+
service_products[ cm['service.product'] ] ||= []
|
145
|
+
service_products[ cm['service.product'] ] << cm
|
146
|
+
end
|
147
|
+
|
148
|
+
# Unable to guess the service without service matches
|
149
|
+
unless service_products.keys.length > 0
|
150
|
+
return result
|
151
|
+
end
|
152
|
+
|
153
|
+
#
|
154
|
+
# Select the best service name by combined certainty of all matches
|
155
|
+
# within an service.product group. Multiple weak matches can
|
156
|
+
# outweigh a single strong match by design.
|
157
|
+
#
|
158
|
+
ranked_service = service_products.keys.sort do |a,b|
|
159
|
+
service_products[b].map{ |r| r['service.certainty'] }.inject(:+) <=>
|
160
|
+
service_products[a].map{ |r| r['service.certainty'] }.inject(:+)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Within the best match group, try to fill in missing attributes
|
164
|
+
service_name = ranked_service.first
|
165
|
+
|
166
|
+
# Find the best match within the winning group
|
167
|
+
ranked_service_matches = service_products[service_name].sort do |a,b|
|
168
|
+
b['service.certainty'] <=> a['service.certainty']
|
169
|
+
end
|
170
|
+
|
171
|
+
# Fill in missing service values in descending order of best match
|
172
|
+
ranked_service_matches.each do |rm|
|
173
|
+
rm.keys.select{ |k| k.index('service.') == 0 }.each do |k|
|
174
|
+
result[k] ||= rm[k]
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
result
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
=begin
|
185
|
+
|
186
|
+
Current key names:
|
187
|
+
|
188
|
+
apache.info
|
189
|
+
apache.variant
|
190
|
+
apache.variant.version
|
191
|
+
cookie
|
192
|
+
host.domain
|
193
|
+
host.id
|
194
|
+
host.ip
|
195
|
+
host.mac
|
196
|
+
host.name
|
197
|
+
host.time
|
198
|
+
hw.device
|
199
|
+
hw.family
|
200
|
+
hw.product
|
201
|
+
hw.vendor
|
202
|
+
imail.eval
|
203
|
+
jetty.info
|
204
|
+
junction.cookie
|
205
|
+
junction.name
|
206
|
+
linux.kernel.version
|
207
|
+
loadbalancer.poolname
|
208
|
+
mdaemon.unregistered
|
209
|
+
mercur.os.info
|
210
|
+
metainfo.version
|
211
|
+
metainfo.version.version
|
212
|
+
ms.nttp.version
|
213
|
+
notes.build.version
|
214
|
+
notes.intl
|
215
|
+
ntmail.id
|
216
|
+
openssh.comment
|
217
|
+
openssh.cvepatch
|
218
|
+
os.arch
|
219
|
+
os.build
|
220
|
+
os.certainty
|
221
|
+
os.device
|
222
|
+
os.edition
|
223
|
+
os.family
|
224
|
+
os.product
|
225
|
+
os.vendor
|
226
|
+
os.version
|
227
|
+
os.version.version
|
228
|
+
os.version.version.version
|
229
|
+
postfix.os.info
|
230
|
+
postoffice.build
|
231
|
+
postoffice.id
|
232
|
+
proftpd.server.name
|
233
|
+
pureftpd.config
|
234
|
+
qpopper.version
|
235
|
+
sendmail.config.version
|
236
|
+
sendmail.hpux.phne.version
|
237
|
+
sendmail.vendor.version
|
238
|
+
service.certainty
|
239
|
+
service.component.family
|
240
|
+
service.component.product
|
241
|
+
service.component.vendor
|
242
|
+
service.component.version
|
243
|
+
service.family
|
244
|
+
service.product
|
245
|
+
service.vendor
|
246
|
+
service.version
|
247
|
+
service.version.version
|
248
|
+
service.version.version.version
|
249
|
+
service.version.version.version.version
|
250
|
+
service.version.version.version.version.version
|
251
|
+
siemens.model
|
252
|
+
snmp.fpmib.oid.1
|
253
|
+
snmp.fpmib.oid.2
|
254
|
+
system.time
|
255
|
+
system.time.format
|
256
|
+
system.time.micros
|
257
|
+
system.time.millis
|
258
|
+
thttpd.mx-patch
|
259
|
+
timeout
|
260
|
+
tomcat.info
|
261
|
+
zmailer.ident
|
262
|
+
|
263
|
+
=end
|