recog 3.1.1 → 3.1.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/Gemfile +6 -0
- data/Rakefile +7 -5
- data/lib/recog/db.rb +67 -68
- data/lib/recog/db_manager.rb +22 -21
- data/lib/recog/fingerprint/regexp_factory.rb +10 -13
- data/lib/recog/fingerprint/test.rb +9 -8
- data/lib/recog/fingerprint.rb +252 -262
- data/lib/recog/fingerprint_parse_error.rb +3 -1
- data/lib/recog/formatter.rb +41 -39
- data/lib/recog/match_reporter.rb +82 -83
- data/lib/recog/matcher.rb +37 -40
- data/lib/recog/matcher_factory.rb +7 -6
- data/lib/recog/nizer.rb +218 -224
- data/lib/recog/verifier.rb +30 -28
- data/lib/recog/verify_reporter.rb +69 -73
- data/lib/recog/version.rb +3 -1
- data/lib/recog.rb +2 -0
- data/recog/bin/recog_match +21 -20
- data/recog/xml/apache_modules.xml +2 -0
- data/recog/xml/dhcp_vendor_class.xml +1 -1
- data/recog/xml/favicons.xml +133 -1
- data/recog/xml/ftp_banners.xml +1 -1
- data/recog/xml/html_title.xml +140 -1
- data/recog/xml/http_cookies.xml +20 -2
- data/recog/xml/http_servers.xml +38 -17
- data/recog/xml/http_wwwauth.xml +17 -4
- data/recog/xml/mdns_device-info_txt.xml +49 -15
- data/recog/xml/sip_banners.xml +0 -2
- data/recog/xml/sip_user_agents.xml +1 -1
- data/recog/xml/snmp_sysdescr.xml +1 -2
- data/recog/xml/ssh_banners.xml +8 -0
- data/recog/xml/telnet_banners.xml +3 -2
- data/recog/xml/tls_jarm.xml +1 -1
- data/recog/xml/x11_banners.xml +1 -0
- data/recog/xml/x509_issuers.xml +1 -1
- data/recog/xml/x509_subjects.xml +0 -1
- data/recog.gemspec +14 -13
- data/spec/lib/recog/db_spec.rb +37 -36
- data/spec/lib/recog/fingerprint/regexp_factory_spec.rb +19 -20
- data/spec/lib/recog/fingerprint_spec.rb +44 -42
- data/spec/lib/recog/formatter_spec.rb +20 -18
- data/spec/lib/recog/match_reporter_spec.rb +35 -30
- data/spec/lib/recog/nizer_spec.rb +85 -101
- data/spec/lib/recog/verify_reporter_spec.rb +45 -44
- data/spec/spec_helper.rb +2 -1
- data.tar.gz.sig +1 -3
- metadata +3 -3
- metadata.gz.sig +0 -0
data/lib/recog/nizer.rb
CHANGED
@@ -1,125 +1,127 @@
|
|
1
|
-
|
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.ip
|
12
|
-
host.mac
|
13
|
-
host.name
|
14
|
-
host.time
|
15
|
-
hw.device
|
16
|
-
hw.family
|
17
|
-
hw.serial_number
|
18
|
-
hw.product
|
19
|
-
hw.vendor
|
20
|
-
}
|
21
|
-
|
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
|
1
|
+
# frozen_string_literal: true
|
36
2
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
3
|
+
module Recog
|
4
|
+
class Nizer
|
5
|
+
# Default certainty ratings where none are specified in the fingerprint itself
|
6
|
+
DEFAULT_OS_CERTAINTY = 0.85 # Most frequent weights are 0.9, 1.0, and 0.5
|
7
|
+
DEFAULT_SERVICE_CERTAINTY = 0.85 # Most frequent weight is 0.85
|
8
|
+
|
9
|
+
# Non-weighted host attributes that can be extracted from fingerprint matches
|
10
|
+
HOST_ATTRIBUTES = %w[
|
11
|
+
host.domain
|
12
|
+
host.ip
|
13
|
+
host.mac
|
14
|
+
host.name
|
15
|
+
host.time
|
16
|
+
hw.device
|
17
|
+
hw.family
|
18
|
+
hw.serial_number
|
19
|
+
hw.product
|
20
|
+
hw.vendor
|
21
|
+
].freeze
|
42
22
|
|
43
|
-
#
|
44
|
-
# Destroy the current DBManager object
|
45
|
-
def self.unload_db
|
46
23
|
@@db_manager = nil
|
47
24
|
@@db_sorted = false
|
48
|
-
end
|
49
25
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
self.load_db
|
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
|
+
@@db_manager = if path
|
32
|
+
Recog::DBManager.new(path)
|
33
|
+
else
|
34
|
+
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
|
55
42
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
43
|
+
#
|
44
|
+
# Destroy the current DBManager object
|
45
|
+
def self.unload_db
|
46
|
+
@@db_manager = nil
|
47
|
+
@@db_sorted = false
|
60
48
|
end
|
61
|
-
end
|
62
49
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
filter = { match_key: match_key, multi_match: false }
|
76
|
-
matches = self.match_all_db(match_string, filter)
|
77
|
-
|
78
|
-
matches[0]
|
79
|
-
end
|
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
|
+
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
|
80
62
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
63
|
+
#
|
64
|
+
# 2016.11 - Rewritten to be wrapper around #match_db_all, functionality
|
65
|
+
# and results must remain unchanged.
|
66
|
+
#
|
67
|
+
# Locate a database that corresponds with the `match_key` and attempt to
|
68
|
+
# find a matching {Fingerprint fingerprint}, stopping at the first hit.
|
69
|
+
# Returns `nil` when no matching database or fingerprint is found.
|
70
|
+
#
|
71
|
+
# @param match_key [String] Fingerprint DB name, e.g. 'smb.native_os'
|
72
|
+
# @param match_string [String] String to match
|
73
|
+
# @return (see Fingerprint#match) or nil
|
74
|
+
def self.match(match_key, match_string)
|
75
|
+
filter = { match_key: match_key, multi_match: false }
|
76
|
+
matches = match_all_db(match_string, filter)
|
77
|
+
|
78
|
+
matches[0]
|
79
|
+
end
|
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
|
85
|
+
def self.multi_match(match_key, match_string)
|
86
|
+
filter = { match_key: match_key, multi_match: true }
|
87
|
+
match_all_db(match_string, filter)
|
88
|
+
end
|
89
|
+
|
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 to hold all fingerprint matches
|
114
|
+
|
115
|
+
load_db unless @@db_manager
|
116
|
+
|
117
|
+
@@db_manager.databases.each do |db|
|
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)
|
120
|
+
|
121
|
+
db.fingerprints.each do |fp|
|
122
|
+
m = fp.match(match_string)
|
123
|
+
next unless m
|
89
124
|
|
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
|
116
|
-
|
117
|
-
@@db_manager.databases.each do |db|
|
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)
|
120
|
-
db.fingerprints.each do |fp|
|
121
|
-
m = fp.match(match_string)
|
122
|
-
if m
|
123
125
|
# Filter on protocol after match since each individual fp
|
124
126
|
# can contain its own 'protocol' value that overrides the
|
125
127
|
# one set at the DB level.
|
@@ -127,140 +129,132 @@ class Nizer
|
|
127
129
|
return matches unless filters[:multi_match]
|
128
130
|
end
|
129
131
|
end
|
130
|
-
end
|
131
132
|
|
132
|
-
|
133
|
-
|
133
|
+
matches
|
134
|
+
end
|
134
135
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
136
|
+
#
|
137
|
+
# Consider an array of match outputs, choose the best result, taking into
|
138
|
+
# account the granularity of OS vs Version vs SP vs Language. Only consider
|
139
|
+
# fields relevant to the host (OS, name, mac address, etc).
|
140
|
+
#
|
141
|
+
def self.best_os_match(matches)
|
142
|
+
# The result hash we return to the caller
|
143
|
+
result = {}
|
144
|
+
|
145
|
+
# Certain attributes should be evaluated separately
|
146
|
+
host_attrs = {}
|
147
|
+
|
148
|
+
# Bucket matches into matched OS product names
|
149
|
+
os_products = {}
|
150
|
+
|
151
|
+
matches.each do |m|
|
152
|
+
# Count how many times each host attribute value is asserted
|
153
|
+
(HOST_ATTRIBUTES & m.keys).each do |ha|
|
154
|
+
host_attrs[ha] ||= {}
|
155
|
+
host_attrs[ha][m[ha]] ||= 0
|
156
|
+
host_attrs[ha][m[ha]] += 1
|
157
|
+
end
|
141
158
|
|
142
|
-
|
143
|
-
result = {}
|
159
|
+
next unless m.key?('os.product')
|
144
160
|
|
145
|
-
|
146
|
-
|
161
|
+
# Group matches by OS product and normalize certainty
|
162
|
+
cm = m.dup
|
163
|
+
cm['os.certainty'] = (m['os.certainty'] || DEFAULT_OS_CERTAINTY).to_f
|
164
|
+
os_products[cm['os.product']] ||= []
|
165
|
+
os_products[cm['os.product']] << cm
|
166
|
+
end
|
147
167
|
|
148
|
-
|
149
|
-
|
168
|
+
#
|
169
|
+
# Select the best host attribute value by highest frequency
|
170
|
+
#
|
171
|
+
host_attrs.each_key do |hk|
|
172
|
+
ranked_attr = host_attrs[hk].keys.sort do |a, b|
|
173
|
+
host_attrs[hk][b] <=> host_attrs[hk][a]
|
174
|
+
end
|
175
|
+
result[hk] = ranked_attr.first
|
176
|
+
end
|
150
177
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
178
|
+
# Unable to guess the OS without OS matches
|
179
|
+
return result unless os_products.keys.length > 0
|
180
|
+
|
181
|
+
#
|
182
|
+
# Select the best operating system name by combined certainty of all
|
183
|
+
# matches within an os.product group. Multiple weak matches can
|
184
|
+
# outweigh a single strong match by design.
|
185
|
+
#
|
186
|
+
ranked_os = os_products.keys.sort do |a, b|
|
187
|
+
os_products[b].map { |r| r['os.certainty'] }.inject(:+) <=>
|
188
|
+
os_products[a].map { |r| r['os.certainty'] }.inject(:+)
|
157
189
|
end
|
158
190
|
|
159
|
-
|
191
|
+
# Within the best match group, try to fill in missing attributes
|
192
|
+
os_name = ranked_os.first
|
160
193
|
|
161
|
-
#
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
os_products[ cm['os.product'] ] << cm
|
166
|
-
end
|
194
|
+
# Find the best match within the winning group
|
195
|
+
ranked_os_matches = os_products[os_name].sort do |a, b|
|
196
|
+
b['os.certainty'] <=> a['os.certainty']
|
197
|
+
end
|
167
198
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
host_attrs[hk][b] <=> host_attrs[hk][a]
|
199
|
+
# Fill in missing result values in descending order of best match
|
200
|
+
ranked_os_matches.each do |rm|
|
201
|
+
rm.each_pair do |k, v|
|
202
|
+
result[k] ||= v
|
203
|
+
end
|
174
204
|
end
|
175
|
-
result[hk] = ranked_attr.first
|
176
|
-
end
|
177
205
|
|
178
|
-
|
179
|
-
unless os_products.keys.length > 0
|
180
|
-
return result
|
206
|
+
result
|
181
207
|
end
|
182
208
|
|
183
209
|
#
|
184
|
-
#
|
185
|
-
#
|
186
|
-
#
|
210
|
+
# Consider an array of match outputs, choose the best result, taking into
|
211
|
+
# account the granularity of service. Only consider fields relevant to the
|
212
|
+
# service.
|
187
213
|
#
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
# Fill in missing result values in descending order of best match
|
202
|
-
ranked_os_matches.each do |rm|
|
203
|
-
rm.each_pair do |k,v|
|
204
|
-
result[k] ||= v
|
214
|
+
def self.best_service_match(matches)
|
215
|
+
# The result hash we return to the caller
|
216
|
+
result = {}
|
217
|
+
|
218
|
+
# Bucket matches into matched service product names
|
219
|
+
service_products = {}
|
220
|
+
|
221
|
+
matches.select { |m| m.key?('service.product') }.each do |m|
|
222
|
+
# Group matches by product and normalize certainty
|
223
|
+
cm = m.dup
|
224
|
+
cm['service.certainty'] = (m['service.certainty'] || DEFAULT_SERVICE_CERTAINTY).to_f
|
225
|
+
service_products[cm['service.product']] ||= []
|
226
|
+
service_products[cm['service.product']] << cm
|
205
227
|
end
|
206
|
-
end
|
207
|
-
|
208
|
-
result
|
209
|
-
end
|
210
228
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
matches.select{ |m| m.has_key?('service.product') }.each do |m|
|
225
|
-
# Group matches by product and normalize certainty
|
226
|
-
cm = m.dup
|
227
|
-
cm['service.certainty'] = ( m['service.certainty'] || DEFAULT_SERVICE_CERTAINTY ).to_f
|
228
|
-
service_products[ cm['service.product'] ] ||= []
|
229
|
-
service_products[ cm['service.product'] ] << cm
|
230
|
-
end
|
231
|
-
|
232
|
-
# Unable to guess the service without service matches
|
233
|
-
unless service_products.keys.length > 0
|
234
|
-
return result
|
235
|
-
end
|
236
|
-
|
237
|
-
#
|
238
|
-
# Select the best service name by combined certainty of all matches
|
239
|
-
# within an service.product group. Multiple weak matches can
|
240
|
-
# outweigh a single strong match by design.
|
241
|
-
#
|
242
|
-
ranked_service = service_products.keys.sort do |a,b|
|
243
|
-
service_products[b].map{ |r| r['service.certainty'] }.inject(:+) <=>
|
244
|
-
service_products[a].map{ |r| r['service.certainty'] }.inject(:+)
|
245
|
-
end
|
229
|
+
# Unable to guess the service without service matches
|
230
|
+
return result unless service_products.keys.length > 0
|
231
|
+
|
232
|
+
#
|
233
|
+
# Select the best service name by combined certainty of all matches
|
234
|
+
# within an service.product group. Multiple weak matches can
|
235
|
+
# outweigh a single strong match by design.
|
236
|
+
#
|
237
|
+
ranked_service = service_products.keys.sort do |a, b|
|
238
|
+
service_products[b].map { |r| r['service.certainty'] }.inject(:+) <=>
|
239
|
+
service_products[a].map { |r| r['service.certainty'] }.inject(:+)
|
240
|
+
end
|
246
241
|
|
247
|
-
|
248
|
-
|
242
|
+
# Within the best match group, try to fill in missing attributes
|
243
|
+
service_name = ranked_service.first
|
249
244
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
245
|
+
# Find the best match within the winning group
|
246
|
+
ranked_service_matches = service_products[service_name].sort do |a, b|
|
247
|
+
b['service.certainty'] <=> a['service.certainty']
|
248
|
+
end
|
254
249
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
250
|
+
# Fill in missing service values in descending order of best match
|
251
|
+
ranked_service_matches.each do |rm|
|
252
|
+
rm.keys.select { |k| k.index('service.') == 0 }.each do |k|
|
253
|
+
result[k] ||= rm[k]
|
254
|
+
end
|
259
255
|
end
|
260
|
-
end
|
261
256
|
|
262
|
-
|
257
|
+
result
|
258
|
+
end
|
263
259
|
end
|
264
|
-
|
265
|
-
end
|
266
260
|
end
|
data/lib/recog/verifier.rb
CHANGED
@@ -1,39 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Recog
|
2
|
-
class Verifier
|
3
|
-
|
4
|
+
class Verifier
|
5
|
+
attr_reader :db, :reporter
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
def initialize(db, reporter)
|
8
|
+
@db = db
|
9
|
+
@reporter = reporter
|
10
|
+
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
def verify
|
13
|
+
reporter.report(db.fingerprints.count) do
|
14
|
+
db.fingerprints.each do |fp|
|
15
|
+
reporter.print_name fp
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
fp.verify_params do |status, message|
|
18
|
+
case status
|
19
|
+
when :warn
|
20
|
+
reporter.warning message, fp.line
|
21
|
+
when :fail
|
22
|
+
reporter.failure message, fp.line
|
23
|
+
when :success
|
24
|
+
reporter.success(message)
|
25
|
+
end
|
23
26
|
end
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
fp.verify_tests do |status, message|
|
28
|
+
case status
|
29
|
+
when :warn
|
30
|
+
reporter.warning message, fp.line
|
31
|
+
when :fail
|
32
|
+
reporter.failure message, fp.line
|
33
|
+
when :success
|
34
|
+
reporter.success(message)
|
35
|
+
end
|
33
36
|
end
|
34
37
|
end
|
35
38
|
end
|
36
39
|
end
|
37
40
|
end
|
38
41
|
end
|
39
|
-
end
|