recog 2.0.24 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/features/data/matching_banners_fingerprints.xml +2 -1
  4. data/features/data/multiple_banners_fingerprints.xml +2 -0
  5. data/features/match.feature +6 -6
  6. data/lib/recog/db.rb +33 -11
  7. data/lib/recog/db_manager.rb +6 -2
  8. data/lib/recog/fingerprint.rb +39 -2
  9. data/lib/recog/nizer.rb +93 -20
  10. data/lib/recog/version.rb +1 -1
  11. data/spec/lib/fingerprint_self_test_spec.rb +7 -0
  12. data/spec/lib/recog/nizer_spec.rb +165 -3
  13. data/xml/apache_os.xml +1 -1
  14. data/xml/architecture.xml +1 -1
  15. data/xml/fingerprints.xsd +91 -0
  16. data/xml/ftp_banners.xml +456 -74
  17. data/xml/h323_callresp.xml +1 -1
  18. data/xml/hp_pjl_id.xml +4 -1
  19. data/xml/http_cookies.xml +1 -1
  20. data/xml/http_servers.xml +1 -1
  21. data/xml/http_wwwauth.xml +1 -1
  22. data/xml/imap_banners.xml +1 -1
  23. data/xml/ldap_searchresult.xml +1 -1
  24. data/xml/mdns_device-info_txt.xml +1 -1
  25. data/xml/mdns_workstation_txt.xml +1 -1
  26. data/xml/mysql_banners.xml +1 -1
  27. data/xml/mysql_error.xml +1 -1
  28. data/xml/nntp_banners.xml +1 -1
  29. data/xml/ntp_banners.xml +1 -1
  30. data/xml/operating_system.xml +1 -1
  31. data/xml/pop_banners.xml +1 -1
  32. data/xml/rsh_resp.xml +1 -1
  33. data/xml/sip_banners.xml +1 -1
  34. data/xml/sip_user_agents.xml +1 -1
  35. data/xml/smb_native_lm.xml +1 -1
  36. data/xml/smb_native_os.xml +1 -1
  37. data/xml/smtp_banners.xml +5 -1
  38. data/xml/smtp_debug.xml +4 -1
  39. data/xml/smtp_ehlo.xml +4 -1
  40. data/xml/smtp_expn.xml +4 -1
  41. data/xml/smtp_help.xml +4 -1
  42. data/xml/smtp_mailfrom.xml +1 -1
  43. data/xml/smtp_noop.xml +4 -1
  44. data/xml/smtp_quit.xml +4 -1
  45. data/xml/smtp_rcptto.xml +1 -1
  46. data/xml/smtp_rset.xml +4 -1
  47. data/xml/smtp_turn.xml +4 -1
  48. data/xml/smtp_vrfy.xml +4 -1
  49. data/xml/snmp_sysdescr.xml +1 -1
  50. data/xml/snmp_sysobjid.xml +1 -1
  51. data/xml/ssh_banners.xml +1 -1
  52. data/xml/upnp_banners.xml +1 -1
  53. metadata +4 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6bcaa01bf7e7d6d81d2a46b792d905011a541b19
4
- data.tar.gz: 8593c9b71ff2b349a1f46693e87c6218d383bd59
3
+ metadata.gz: 11aff7826c38e379d719a70b39406f4d861f8d3f
4
+ data.tar.gz: 98f54e902871cb73ddcffbb7e0f72be5b21bf143
5
5
  SHA512:
6
- metadata.gz: 177feea5c499a7c29a802f5b2244edc8144fb7ddca69248e455a95e7e4bea211afb2ea7b6b50558cc89aa7deeb2a01068d126c6c55f4b5e270b71e8ed88d13ff
7
- data.tar.gz: fdb3c3908a3f9168911b4e559fb56d90c2956b63ba0cfea49015d0d9fa90fcd3b82219271fc61ddffb70a2bc6e4e3a5a7c289054d08624b57a1d8b0bce5cc71a
6
+ metadata.gz: 398dc88dfda3030d5d0e8da3c0774fc95ec99c2506f9f15ba528ef5ec930591f02f43d313ec5ed06cb4b7bb28798ef71bf53a539a6b50b8158a7f0b69877e0ed
7
+ data.tar.gz: 19bf9ddccb4031ade9bf5d94feb4516df400816eaf81a894b24716e06ef7e5d5a2400bd0424d55875b38451fcad83edb7f99f7b00ac1e38fa059447682f17d0a
data/README.md CHANGED
@@ -75,7 +75,7 @@ Matches can be tested on the command-line in a similar fashion:
75
75
 
76
76
  ```
77
77
  $ echo 'OpenSSH_6.6p1 Ubuntu-2ubuntu1' | bin/recog_match xml/ssh_banners.xml -
78
- MATCH: {"service.version"=>"6.6p1", "openssh.comment"=>"Ubuntu-2ubuntu1", "service.vendor"=>"OpenBSD", "service.family"=>"OpenSSH", "service.product"=>"OpenSSH", "data"=>"OpenSSH_6.6p1 Ubuntu-2ubuntu1"}
78
+ MATCH: {"matched"=>"OpenSSH running on Ubuntu 14.04", "service.version"=>"6.6p1", "openssh.comment"=>"Ubuntu-2ubuntu1", "service.vendor"=>"OpenBSD", "service.family"=>"OpenSSH", "service.product"=>"OpenSSH", "os.vendor"=>"Ubuntu", "os.device"=>"General", "os.family"=>"Linux", "os.product"=>"Linux", "os.version"=>"14.04", "service.protocol"=>"ssh", "fingerprint_db"=>"ssh.banner", "data"=>"OpenSSH_6.6p1 Ubuntu-2ubuntu1"}
79
79
  ```
80
80
 
81
81
  ### Best Practices
@@ -1,5 +1,5 @@
1
1
  <?xml version="1.0"?>
2
- <fingerprints>
2
+ <fingerprints protocol="ftp" database_type="service">
3
3
  <fingerprint pattern="^-{10} Welcome to Pure-FTPd (.*)-{10}$">
4
4
  <example>---------- Welcome to Pure-FTPd ----------</example>
5
5
  <description>Pure-FTPd
@@ -8,6 +8,7 @@
8
8
  <param pos="1" name="pureftpd.config"/>
9
9
  <param pos="0" name="service.family" value="Pure-FTPd"/>
10
10
  <param pos="0" name="service.product" value="Pure-FTPd"/>
11
+ <param pos="0" name="service.protocol" value="ftp"/>
11
12
  </fingerprint>
12
13
  <fingerprint pattern="^(\S+) FTP Server \(SunOS (\S+)\) ready\.?$" flags="REG_ICASE">
13
14
  <description>SunOS/Solaris</description>
@@ -16,10 +16,12 @@
16
16
  <param pos="1" name="pureftpd.config"/>
17
17
  <param pos="0" name="service.family" value="Pure-FTPd"/>
18
18
  <param pos="0" name="service.product" value="Pure-FTPd"/>
19
+ <param pos="0" name="service.protocol" value="ftp"/>
19
20
  </fingerprint>
20
21
  <fingerprint pattern="^(\S+) FTP Server \(SunOS (\S+)\) ready\.?$" flags="REG_ICASE">
21
22
  <description>SunOS/Solaris</description>
22
23
  <example>example.com FTP server (SunOS 5.7) ready.</example>
24
+ <param pos="0" name="service.protocol" value="ftp"/>
23
25
  <param pos="0" name="os.vendor" value="Sun"/>
24
26
  <param pos="0" name="os.family" value="Solaris"/>
25
27
  <param pos="0" name="os.product" value="Solaris"/>
@@ -3,8 +3,8 @@ Feature: Match
3
3
  When I run `recog_match matching_banners_fingerprints.xml sample_banner.txt`
4
4
  Then it should pass with:
5
5
  """
6
- MATCH: {"matched"=>"Pure-FTPd Config data can be zero or more of: [privsep] [TLS]", "pureftpd.config"=>"[privsep] [TLS] ", "service.family"=>"Pure-FTPd", "service.product"=>"Pure-FTPd", "data"=>"---------- Welcome to Pure-FTPd [privsep] [TLS] ----------"}
7
- MATCH: {"matched"=>"SunOS/Solaris", "os.vendor"=>"Sun", "os.family"=>"Solaris", "os.product"=>"Solaris", "os.device"=>"General", "host.name"=>"polaris", "os.version"=>"5.8", "data"=>"polaris FTP server (SunOS 5.8) ready."}
6
+ MATCH: {"matched"=>"Pure-FTPd Config data can be zero or more of: [privsep] [TLS]", "pureftpd.config"=>"[privsep] [TLS] ", "service.family"=>"Pure-FTPd", "service.product"=>"Pure-FTPd", "service.protocol"=>"ftp", "fingerprint_db"=>"matching_banners_fingerprints", "data"=>"---------- Welcome to Pure-FTPd [privsep] [TLS] ----------"}
7
+ MATCH: {"matched"=>"SunOS/Solaris", "os.vendor"=>"Sun", "os.family"=>"Solaris", "os.product"=>"Solaris", "os.device"=>"General", "host.name"=>"polaris", "os.version"=>"5.8", "service.protocol"=>"ftp", "fingerprint_db"=>"matching_banners_fingerprints", "data"=>"polaris FTP server (SunOS 5.8) ready."}
8
8
  """
9
9
 
10
10
  Scenario: Fails at finding matches
@@ -19,14 +19,14 @@ Feature: Match
19
19
  When I run `recog_match multiple_banners_fingerprints.xml sample_banner.txt --multi-match`
20
20
  Then it should pass with:
21
21
  """
22
- MATCHES: {"matched"=>"Generic FTP, Checks for the existence of the word FTP in the line", "data"=>"---------- Welcome to Pure-FTPd [privsep] [TLS] ----------"},{"matched"=>"Pure-FTPd Config data can be zero or more of: [privsep] [TLS]", "pureftpd.config"=>"[privsep] [TLS] ", "service.family"=>"Pure-FTPd", "service.product"=>"Pure-FTPd", "data"=>"---------- Welcome to Pure-FTPd [privsep] [TLS] ----------"}
23
- MATCHES: {"matched"=>"Generic FTP, Checks for the existence of the word FTP in the line", "data"=>"polaris FTP server (SunOS 5.8) ready."},{"matched"=>"SunOS/Solaris", "os.vendor"=>"Sun", "os.family"=>"Solaris", "os.product"=>"Solaris", "os.device"=>"General", "host.name"=>"polaris", "os.version"=>"5.8", "data"=>"polaris FTP server (SunOS 5.8) ready."}
22
+ MATCHES: {"matched"=>"Generic FTP, Checks for the existence of the word FTP in the line", "service.protocol"=>"", "fingerprint_db"=>"multiple_banners_fingerprints", "data"=>"---------- Welcome to Pure-FTPd [privsep] [TLS] ----------"},{"matched"=>"Pure-FTPd Config data can be zero or more of: [privsep] [TLS]", "pureftpd.config"=>"[privsep] [TLS] ", "service.family"=>"Pure-FTPd", "service.product"=>"Pure-FTPd", "service.protocol"=>"ftp", "fingerprint_db"=>"multiple_banners_fingerprints", "data"=>"---------- Welcome to Pure-FTPd [privsep] [TLS] ----------"}
23
+ MATCHES: {"matched"=>"Generic FTP, Checks for the existence of the word FTP in the line", "service.protocol"=>"", "fingerprint_db"=>"multiple_banners_fingerprints", "data"=>"polaris FTP server (SunOS 5.8) ready."},{"matched"=>"SunOS/Solaris", "service.protocol"=>"ftp", "os.vendor"=>"Sun", "os.family"=>"Solaris", "os.product"=>"Solaris", "os.device"=>"General", "host.name"=>"polaris", "os.version"=>"5.8", "fingerprint_db"=>"multiple_banners_fingerprints", "data"=>"polaris FTP server (SunOS 5.8) ready."}
24
24
  """
25
25
 
26
26
  Scenario: Finds first matches using no-multi-match flag
27
27
  When I run `recog_match multiple_banners_fingerprints.xml sample_banner.txt --no-multi-match`
28
28
  Then it should pass with:
29
29
  """
30
- MATCH: {"matched"=>"Generic FTP, Checks for the existence of the word FTP in the line", "data"=>"---------- Welcome to Pure-FTPd [privsep] [TLS] ----------"}
31
- MATCH: {"matched"=>"Generic FTP, Checks for the existence of the word FTP in the line", "data"=>"polaris FTP server (SunOS 5.8) ready."}
30
+ MATCH: {"matched"=>"Generic FTP, Checks for the existence of the word FTP in the line", "service.protocol"=>"", "fingerprint_db"=>"multiple_banners_fingerprints", "data"=>"---------- Welcome to Pure-FTPd [privsep] [TLS] ----------"}
31
+ MATCH: {"matched"=>"Generic FTP, Checks for the existence of the word FTP in the line", "service.protocol"=>"", "fingerprint_db"=>"multiple_banners_fingerprints", "data"=>"polaris FTP server (SunOS 5.8) ready."}
32
32
  """
@@ -13,13 +13,34 @@ class DB
13
13
  # against strings that make sense for the {#match_key}
14
14
  attr_reader :fingerprints
15
15
 
16
- # @return [String] Taken from the `fingerprints/matches` element, or
16
+ # @return [String] Taken from the `fingerprints/matches` attribute, or
17
17
  # defaults to the basename of {#path} without the `.xml` extension.
18
18
  attr_reader :match_key
19
19
 
20
+ # @return [String] Taken from the `fingerprints/protocol` attribute, or
21
+ # defaults to an empty string
22
+ attr_reader :protocol
23
+
24
+ # @return [String] Taken from the `fingerprints/database_type` attribute
25
+ # defaults to an empty string
26
+ attr_reader :database_type
27
+
28
+ # @return [Float] Taken from the `fingerprints/preference` attribute,
29
+ # defaults to 0.10. Used when ordering databases, highest numbers
30
+ # are given priority and are processed first.
31
+ attr_reader :preference
32
+
33
+ # Default Fingerprint database preference when it isn't specified in file
34
+ # Do not use a value below 0.10 so as to allow users to specify lower
35
+ # values in their own custom XML that will always run last.
36
+ DEFAULT_FP_PREFERENCE = 0.10
37
+
20
38
  # @param path [String]
21
39
  def initialize(path)
22
40
  @match_key = nil
41
+ @protocol = ''
42
+ @database_type = ''
43
+ @preference = DEFAULT_FP_PREFERENCE.to_f
23
44
  @path = path
24
45
  @fingerprints = []
25
46
 
@@ -30,24 +51,25 @@ class DB
30
51
  def parse_fingerprints
31
52
  xml = nil
32
53
 
33
- File.open(self.path, "rb") do |fd|
54
+ File.open(self.path, 'rb') do |fd|
34
55
  xml = Nokogiri::XML(fd.read(fd.stat.size))
35
56
  end
36
57
 
37
58
  raise "#{self.path} is invalid XML: #{xml.errors.join(',')}" unless xml.errors.empty?
38
59
 
39
- xml.xpath("/fingerprints").each do |fbase|
40
- if fbase['matches']
41
- @match_key = fbase['matches'].to_s
42
- end
43
- end
60
+ xml.xpath('/fingerprints').each do |fbase|
61
+
62
+ @match_key = fbase['matches'].to_s if fbase['matches']
63
+ @protocol = fbase['protocol'].to_s if fbase['protocol']
64
+ @database_type = fbase['database_type'].to_s if fbase['database_type']
65
+ @preference = fbase['preference'].to_f if fbase['preference']
44
66
 
45
- unless @match_key
46
- @match_key = File.basename(self.path).sub(/\.xml$/, '')
47
67
  end
48
68
 
49
- xml.xpath("/fingerprints/fingerprint").each do |fprint|
50
- @fingerprints << Fingerprint.new(fprint)
69
+ @match_key = File.basename(self.path).sub(/\.xml$/, '') unless @match_key
70
+
71
+ xml.xpath('/fingerprints/fingerprint').each do |fprint|
72
+ @fingerprints << Fingerprint.new(fprint, @match_key, @protocol)
51
73
  end
52
74
 
53
75
  xml = nil
@@ -13,8 +13,12 @@ class DBManager
13
13
  end
14
14
 
15
15
  def load_databases
16
- Dir[self.path + "/*.xml"].each do |dbxml|
17
- self.databases << DB.new(dbxml)
16
+ if File.directory?(self.path)
17
+ Dir[self.path + "/*.xml"].each do |dbxml|
18
+ self.databases << DB.new(dbxml)
19
+ end
20
+ else
21
+ self.databases << DB.new(self.path)
18
22
  end
19
23
  end
20
24
 
@@ -27,23 +27,49 @@ class Fingerprint
27
27
  attr_reader :tests
28
28
 
29
29
  # @param xml [Nokogiri::XML::Element]
30
- def initialize(xml)
30
+ # @param match_key [String] See Recog::DB
31
+ # @param protocol [String] Protocol such as ftp, mssql, http, etc.
32
+ def initialize(xml, match_key=nil, protocol=nil)
33
+ @match_key = match_key
34
+ @protocol = protocol
31
35
  @name = parse_description(xml)
32
36
  @regex = create_regexp(xml)
33
37
  @params = {}
34
38
  @tests = []
35
39
 
40
+ @protocol.downcase! if @protocol
36
41
  parse_examples(xml)
37
42
  parse_params(xml)
38
43
  end
39
44
 
45
+ def output_diag_data(message, data, exception)
46
+ STDERR.puts message
47
+ STDERR.puts exception.inspect
48
+ STDERR.puts "Length: #{data.length}"
49
+ STDERR.puts "Encoding: #{data.encoding}"
50
+ STDERR.puts "Problematic data:\n#{data}"
51
+ STDERR.puts "Raw bytes:\n#{data.pretty_inspect}\n"
52
+ end
53
+
40
54
  # Attempt to match the given string.
41
55
  #
42
56
  # @param match_string [String]
43
57
  # @return [Hash,nil] Keys will be host, service, and os attributes
44
58
  def match(match_string)
45
59
  # match_string.force_encoding('BINARY') if match_string
46
- match_data = @regex.match(match_string)
60
+ begin
61
+ match_data = @regex.match(match_string)
62
+ rescue Encoding::CompatibilityError => e
63
+ begin
64
+ # Replace invalid UTF-8 characters with spaces, just as DAP does.
65
+ encoded_str = match_string.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => '')
66
+ match_data = @regex.match(encoded_str)
67
+ rescue Exception => e
68
+ output_diag_data('Exception while re-encoding match_string to UTF-8', match_string, e)
69
+ end
70
+ rescue Exception => e
71
+ output_diag_data('Exception while running regex against match_string', match_string, e)
72
+ end
47
73
  return if match_data.nil?
48
74
 
49
75
  result = { 'matched' => @name }
@@ -58,6 +84,17 @@ class Fingerprint
58
84
  result[k] = match_data[ pos ]
59
85
  end
60
86
  end
87
+
88
+ # Use the protocol specified in the XML database if there isn't one
89
+ # provided as part of this fingerprint.
90
+ if @protocol
91
+ unless result['service.protocol']
92
+ result['service.protocol'] = @protocol
93
+ end
94
+ end
95
+
96
+ result['fingerprint_db'] = @match_key if @match_key
97
+
61
98
  return result
62
99
  end
63
100
 
@@ -20,43 +20,116 @@ class Nizer
20
20
  }
21
21
 
22
22
  @@db_manager = nil
23
+ @@db_sorted = false
24
+
25
+
26
+ #
27
+ # Load fingerprints from a specific file or directory
28
+ # This will not preserve any fingerprints that have already been loaded
29
+ # @param path [String] Path to file or directory of XML fingerprints
30
+ def self.load_db(path = nil)
31
+ if path
32
+ @@db_manager = Recog::DBManager.new(path)
33
+ else
34
+ @@db_manager = Recog::DBManager.new
35
+ end
36
+
37
+ # Sort the databases, no behavior or result change for those calling
38
+ # Nizer.match or Nizer.multi_match as they have a single DB
39
+ @@db_manager.databases.sort! { |a, b| b.preference <=> a.preference }
40
+ @@db_sorted = true
41
+ end
42
+
43
+ #
44
+ # Destroy the current DBManager object
45
+ def self.unload_db
46
+ @@db_manager = nil
47
+ @@db_sorted = false
48
+ end
23
49
 
50
+ #
51
+ # Display the fingerprint databases in the order in which they will be used
52
+ # to match banners. This is useful for fingerprint tuning and debugging.
53
+ def self.display_db_order
54
+ self.load_db unless @@db_manager
55
+
56
+ puts format('%s %-22s %-8s %s', 'Preference', 'Database', 'Type', 'Protocol')
57
+ @@db_manager.databases.each do |db|
58
+ puts format('%10.3f %-22s %-8s %s', db.preference, db.match_key,
59
+ db.database_type, db.protocol)
60
+ end
61
+ end
62
+
63
+ #
64
+ # 2016.11 - Rewritten to be wrapper around #match_db_all, functionality
65
+ # and results must remain unchanged.
24
66
  #
25
67
  # Locate a database that corresponds with the `match_key` and attempt to
26
- # find a matching {Fingerprint fingerprint}, stopping at the first hit. Returns `nil`
27
- # when no matching database or fingerprint is found.
68
+ # find a matching {Fingerprint fingerprint}, stopping at the first hit.
69
+ # Returns `nil` when no matching database or fingerprint is found.
28
70
  #
29
71
  # @param match_key [String] Fingerprint DB name, e.g. 'smb.native_os'
30
- # @return (see Fingerprint#match)
72
+ # @param match_string [String] String to match
73
+ # @return (see Fingerprint#match) or nil
31
74
  def self.match(match_key, match_string)
32
- match_string = match_string.to_s.unpack("C*").pack("C*")
33
- @@db_manager ||= Recog::DBManager.new
34
- @@db_manager.databases.each do |db|
35
- next unless db.match_key == match_key
36
- db.fingerprints.each do |fprint|
37
- m = fprint.match(match_string)
38
- return m if m
39
- end
40
- end
41
- nil
75
+ filter = { match_key: match_key, multi_match: false }
76
+ matches = self.match_all_db(match_string, filter)
77
+
78
+ matches[0]
42
79
  end
43
80
 
81
+ #
82
+ # @param match_key [String] Fingerprint DB name, e.g. 'smb.native_os'
83
+ # @param match_string [String] String to match
84
+ # @return [Array] Array of Fingerprint#match or empty array
44
85
  def self.multi_match(match_key, match_string)
45
- match_string = match_string.to_s.unpack("C*").pack("C*")
46
- @@db_manager ||= Recog::DBManager.new
86
+ filter = { match_key: match_key, multi_match: true }
87
+ self.match_all_db(match_string, filter)
88
+ end
47
89
 
48
- matches = Array.new #array to hold all fingerprint matches
90
+ #
91
+ # Search all fingerprint dbs and attempt to find matching
92
+ # {Fingerprint fingerprint}s. It will return the first match found
93
+ # unless the :multi_match option is used to request all matches.
94
+ # Returns an array of all matching fingerprints or an empty array.
95
+ #
96
+ # @param match_string [String] Service banner to match
97
+ # @param [Hash] filters This hash contains filters used to limit the
98
+ # results to just those from specific types of fingerprints.
99
+ # The values that these filters match come from the 'fingerprints' top
100
+ # level element in the fingerprint DB XML or, in the case of 'protocol',
101
+ # this value can be overridden at the individual fingerprint level by
102
+ # setting a value for 'service.protocol'
103
+ #
104
+ # With the exception of 'match_key', the filters below match the
105
+ # 'fingerprints' attributes with the same name.
106
+ # @option filters [String] :match_key Value from XML 'matches' or file name
107
+ # @option filters [String] :database_type fprint db type: service, util.os, etc.
108
+ # @option filters [String] :protocol Protocol (ftp, smtp, etc.)
109
+ # @option filters [Boolean] :multi_match Return all matches instead of first
110
+ # @return [Array] Array of Fingerprint#match or empty array
111
+ def self.match_all_db(match_string, filters = {})
112
+ match_string = match_string.to_s.unpack('C*').pack('C*')
113
+ matches = Array.new # array to hold all fingerprint matches
114
+
115
+ self.load_db unless @@db_manager
49
116
 
50
117
  @@db_manager.databases.each do |db|
51
- next unless db.match_key == match_key
52
-
118
+ next if filters[:match_key] && !filters[:match_key].eql?(db.match_key)
119
+ next if filters[:database_type] && !filters[:database_type].eql?(db.database_type)
53
120
  db.fingerprints.each do |fp|
54
121
  m = fp.match(match_string)
55
- matches.push(m) if m
122
+ if m
123
+ # Filter on protocol after match since each individual fp
124
+ # can contain its own 'protocol' value that overrides the
125
+ # one set at the DB level.
126
+ matches.push(m) unless filters[:protocol] && !filters[:protocol].eql?(m['service.protocol'])
127
+ return matches unless filters[:multi_match]
128
+ end
56
129
  end
57
130
  end
58
131
 
59
- return matches
132
+ matches
60
133
  end
61
134
 
62
135
  #
@@ -1,3 +1,3 @@
1
1
  module Recog
2
- VERSION = '2.0.24'
2
+ VERSION = '2.1.0'
3
3
  end
@@ -21,6 +21,13 @@ describe Recog::DB do
21
21
  expect(db.match_key).not_to be_empty
22
22
  end
23
23
 
24
+ it "has valid 'preference' value" do
25
+ # Reserve values below 0.10 and above 0.90 for users
26
+ # See xml/fingerprints.xsd
27
+ expect(db.preference.class).to be ::Float
28
+ expect(db.preference).to be_between(0.10, 0.90)
29
+ end
30
+
24
31
  db.fingerprints.each_index do |i|
25
32
  fp = db.fingerprints[i]
26
33
 
@@ -1,6 +1,12 @@
1
1
  require 'recog'
2
2
  require 'yaml'
3
3
 
4
+
5
+ VALID_FILTER = {match_key: 'smb.native_os', protocol: 'smb', database_type: 'util.os'}
6
+ NOMATCH_MATCH_KEY = {match_key: 'no_such_987', protocol: 'smb', database_type: 'util.os'}
7
+ NOMATCH_PROTO = {match_key: 'smb.native_os', protocol: 'no_such_987', database_type: 'util.os'}
8
+ NOMATCH_TYPE = {match_key: 'smb.native_os', protocol: 'smb', database_type: 'no_such_987'}
9
+
4
10
  describe Recog::Nizer do
5
11
  subject { described_class }
6
12
 
@@ -24,6 +30,15 @@ describe Recog::Nizer do
24
30
  end
25
31
  end
26
32
 
33
+ let(:nomatch_result) { subject.match('smb.native_os', 'no_such_987_76tgklh') }
34
+ it "returns a nil when data cannot be matched" do
35
+ expect(nomatch_result).to be_nil
36
+ end
37
+
38
+ let(:invalid_db_result) { subject.match('no_such_987', data) }
39
+ it "returns a nil when match_key search doesn't match" do
40
+ expect(invalid_db_result).to be_nil
41
+ end
27
42
  end
28
43
  end
29
44
 
@@ -36,6 +51,83 @@ describe Recog::Nizer do
36
51
  end
37
52
  end
38
53
 
54
+ describe ".match_all_db" do
55
+ File.readlines(File.expand_path(File.join('spec', 'data', 'smb_native_os.txt'))).each do |line|
56
+ data = line.strip
57
+ context "with smb_native_os:#{data}" do
58
+ let(:match_all_result) { subject.match_all_db(data, VALID_FILTER) }
59
+
60
+ it "returns an array" do
61
+ expect(match_all_result.class).to eq(::Array)
62
+ end
63
+
64
+ it "returns a successful match" do
65
+ expect(match_all_result[0]['matched']).to match(/^[A-Z]/)
66
+ end
67
+
68
+ it "correctly matches service or os" do
69
+ if data =~ /^Windows/
70
+ expect(match_all_result[0]['os.product']).to match(/^Windows/)
71
+ end
72
+ end
73
+
74
+ it "correctly matches protocol" do
75
+ expect(match_all_result[0]['service.protocol']).to eq('smb')
76
+ end
77
+
78
+ let(:no_filter_result) { subject.match_all_db(data) }
79
+ it "returns an array when searching without a filter" do
80
+ expect(no_filter_result.class).to eq(::Array)
81
+ end
82
+
83
+ it "returns a successful match when searching without a filter" do
84
+ expect(no_filter_result[0]['matched']).to match(/^[A-Z]/)
85
+ end
86
+
87
+ it "correctly matches service or os when searching without a filter" do
88
+ if data =~ /^Windows/
89
+ expect(no_filter_result[0]['os.product']).to match(/^Windows/)
90
+ end
91
+ end
92
+
93
+ let(:nomatch_db_result) { subject.match_all_db(data, NOMATCH_MATCH_KEY) }
94
+ it "returns an array when match_key search doesn't match" do
95
+ expect(nomatch_db_result.class).to eq(::Array)
96
+ end
97
+ it "returns an empty array when match_key search doesn't match" do
98
+ expect(nomatch_db_result).to be_empty
99
+ end
100
+
101
+ let(:nomatch_proto_result) { subject.match_all_db(data, NOMATCH_PROTO) }
102
+ it "returns an array when protocol search doesn't match" do
103
+ expect(nomatch_proto_result.class).to eq(::Array)
104
+ end
105
+ it "returns an empty array when protocol search doesn't match" do
106
+ expect(nomatch_proto_result).to be_empty
107
+ end
108
+
109
+ let(:nomatch_type_result) { subject.match_all_db(data, NOMATCH_TYPE) }
110
+ it "returns an array when database_type search doesn't match" do
111
+ expect(nomatch_type_result.class).to eq(::Array)
112
+ end
113
+ it "returns an empty array when database_type search doesn't match" do
114
+ expect(nomatch_proto_result).to be_empty
115
+ end
116
+ end
117
+ end
118
+
119
+ line = 'non-existent'
120
+ context "with non-existent match" do
121
+ let(:match_result) {subject.match_all_db(line) }
122
+ it "returns an array" do
123
+ expect(match_result.class).to eq(::Array)
124
+ end
125
+ it "returns an empty array" do
126
+ expect(match_result).to be_empty
127
+ end
128
+ end
129
+ end
130
+
39
131
  describe ".multi_match" do
40
132
  File.readlines(File.expand_path(File.join('spec', 'data', 'smb_native_os.txt'))).each do |line|
41
133
  data = line.strip
@@ -58,8 +150,46 @@ describe Recog::Nizer do
58
150
  end
59
151
  end
60
152
  end
153
+
154
+ let(:invalid_db_result) { subject.multi_match('no_such_987', data) }
155
+ it "returns an array when passed an invalid match_key" do
156
+ expect(invalid_db_result.class).to eq(::Array)
157
+ end
158
+
159
+ it "returns an empty array when passed an invalid match_key" do
160
+ expect(invalid_db_result).to be_empty
161
+ end
162
+ end
163
+
164
+ end
165
+
166
+ data = 'Windows Server 2012 R2 Standard 9600'
167
+ context "with {data}" do
168
+ let(:match_results) {subject.multi_match('smb.native_os', data) }
169
+
170
+ it "returns an array" do
171
+ expect(match_results.class).to eq(::Array)
172
+ end
173
+
174
+ it "returns at least two successful matches" do
175
+ expect(match_results.size).to be > 1
176
+ end
177
+
178
+ it "correctly matches os.product for all matches" do
179
+ match_results do |mr|
180
+ if data =~ /^Windows/
181
+ expect(mr['os.product']).to match(/^Windows/)
182
+ end
183
+ end
61
184
  end
62
185
 
186
+ it "correctly matches protocol for all matches" do
187
+ match_results do |mr|
188
+ if data =~ /^Windows/
189
+ expect(mr['service.protocol']).to eq('smb')
190
+ end
191
+ end
192
+ end
63
193
  end
64
194
 
65
195
  line = 'non-existent'
@@ -77,7 +207,6 @@ describe Recog::Nizer do
77
207
  end
78
208
 
79
209
  describe ".best_os_match" do
80
-
81
210
  # Demonstrates how this method picks up additional attributes from other members of the winning
82
211
  # os.product match group and applies them to the result.
83
212
  matches1 = YAML.load(File.read(File.expand_path(File.join('spec', 'data', 'best_os_match_1.yml'))))
@@ -134,8 +263,7 @@ describe Recog::Nizer do
134
263
 
135
264
  end
136
265
 
137
- describe ".best_service_match" do
138
-
266
+ describe ".best_service_match" do
139
267
  # Demonstrates how this method picks up additional attributes from other members of the winning
140
268
  # service.product match group and applies them to the result.
141
269
  matches1 = YAML.load(File.read(File.expand_path(File.join('spec', 'data', 'best_service_match_1.yml'))))
@@ -165,4 +293,38 @@ describe Recog::Nizer do
165
293
 
166
294
  end
167
295
 
296
+
297
+ describe '.load_db' do
298
+ file_path = File.expand_path(File.join('spec', 'data', 'test_fingerprints.xml'))
299
+ context "with #{file_path}" do
300
+ let(:fp_db) { subject.load_db(file_path) }
301
+ it "loads without error" do
302
+ expect(fp_db).to be true
303
+ subject.unload_db()
304
+ end
305
+ end
306
+
307
+ context "with no path specified" do
308
+ let(:fp_db) { subject.load_db }
309
+ it "loads without error" do
310
+ expect(fp_db).to be true
311
+ subject.unload_db()
312
+ end
313
+ end
314
+
315
+ context "with empty file path" do
316
+ it "raises an error" do
317
+ expect { subject.load_db('') }.to raise_error(Errno::ENOENT)
318
+ subject.unload_db()
319
+ end
320
+ end
321
+
322
+ context "with invalid file path" do
323
+ it "raises an error" do
324
+ expect { subject.load_db('no_such_987_file_path') }.to raise_error(Errno::ENOENT)
325
+ subject.unload_db()
326
+ end
327
+ end
328
+ end
329
+
168
330
  end