recog-intrigue 2.3.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
- data/.github/ISSUE_TEMPLATE/fingerprint_request.md +27 -0
- data/.github/PULL_REQUEST_TEMPLATE +24 -0
- data/.gitignore +14 -0
- data/.rbenv-gemset +1 -0
- data/.rspec +3 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +25 -0
- data/.yardopts +1 -0
- data/CONTRIBUTING.md +171 -0
- data/COPYING +23 -0
- data/Gemfile +10 -0
- data/LICENSE +7 -0
- data/README.md +85 -0
- data/Rakefile +22 -0
- data/bin/recog_export +81 -0
- data/bin/recog_match +55 -0
- data/bin/recog_standardize +118 -0
- data/bin/recog_verify +64 -0
- data/cpe-remap.yaml +134 -0
- data/features/data/failing_banners_fingerprints.xml +20 -0
- data/features/data/matching_banners_fingerprints.xml +23 -0
- data/features/data/multiple_banners_fingerprints.xml +32 -0
- data/features/data/no_tests.xml +3 -0
- data/features/data/sample_banner.txt +2 -0
- data/features/data/successful_tests.xml +18 -0
- data/features/data/tests_with_failures.xml +20 -0
- data/features/data/tests_with_warnings.xml +17 -0
- data/features/match.feature +36 -0
- data/features/support/aruba.rb +3 -0
- data/features/support/env.rb +6 -0
- data/features/verify.feature +48 -0
- data/identifiers/README.md +47 -0
- data/identifiers/os_architecture.txt +20 -0
- data/identifiers/os_device.txt +52 -0
- data/identifiers/os_family.txt +160 -0
- data/identifiers/os_product.txt +199 -0
- data/identifiers/service_family.txt +185 -0
- data/identifiers/service_product.txt +255 -0
- data/identifiers/software_class.txt +26 -0
- data/identifiers/software_family.txt +91 -0
- data/identifiers/software_product.txt +333 -0
- data/identifiers/vendor.txt +405 -0
- data/lib/recog.rb +4 -0
- data/lib/recog/db.rb +78 -0
- data/lib/recog/db_manager.rb +31 -0
- data/lib/recog/fingerprint.rb +280 -0
- data/lib/recog/fingerprint/regexp_factory.rb +56 -0
- data/lib/recog/fingerprint/test.rb +18 -0
- data/lib/recog/formatter.rb +51 -0
- data/lib/recog/match_reporter.rb +77 -0
- data/lib/recog/matcher.rb +94 -0
- data/lib/recog/matcher_factory.rb +14 -0
- data/lib/recog/nizer.rb +347 -0
- data/lib/recog/verifier.rb +39 -0
- data/lib/recog/verifier_factory.rb +13 -0
- data/lib/recog/verify_reporter.rb +86 -0
- data/lib/recog/version.rb +3 -0
- data/misc/convert_mysql_err +61 -0
- data/misc/order.xsl +17 -0
- data/recog-intrigue.gemspec +45 -0
- data/requirements.txt +2 -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 +25 -0
- data/spec/data/test_fingerprints.xml +36 -0
- data/spec/data/verification_fingerprints.xml +86 -0
- data/spec/data/whitespaced_fingerprint.xml +5 -0
- data/spec/lib/fingerprint_self_test_spec.rb +174 -0
- data/spec/lib/recog/db_spec.rb +98 -0
- data/spec/lib/recog/fingerprint/regexp_factory_spec.rb +73 -0
- data/spec/lib/recog/fingerprint_spec.rb +112 -0
- data/spec/lib/recog/formatter_spec.rb +69 -0
- data/spec/lib/recog/match_reporter_spec.rb +91 -0
- data/spec/lib/recog/nizer_spec.rb +330 -0
- data/spec/lib/recog/verify_reporter_spec.rb +113 -0
- data/spec/spec_helper.rb +82 -0
- data/update_cpes.py +186 -0
- data/xml/apache_modules.xml +1911 -0
- data/xml/apache_os.xml +273 -0
- data/xml/architecture.xml +36 -0
- data/xml/dns_versionbind.xml +761 -0
- data/xml/fingerprints.xsd +128 -0
- data/xml/ftp_banners.xml +1553 -0
- data/xml/h323_callresp.xml +603 -0
- data/xml/hp_pjl_id.xml +358 -0
- data/xml/html_title.xml +1630 -0
- data/xml/http_cookies.xml +411 -0
- data/xml/http_servers.xml +3195 -0
- data/xml/http_wwwauth.xml +595 -0
- data/xml/imap_banners.xml +245 -0
- data/xml/ldap_searchresult.xml +711 -0
- data/xml/mdns_device-info_txt.xml +1796 -0
- data/xml/mdns_workstation_txt.xml +15 -0
- data/xml/mysql_banners.xml +1649 -0
- data/xml/mysql_error.xml +871 -0
- data/xml/nntp_banners.xml +82 -0
- data/xml/ntp_banners.xml +1223 -0
- data/xml/operating_system.xml +629 -0
- data/xml/pop_banners.xml +499 -0
- data/xml/rsh_resp.xml +76 -0
- data/xml/rtsp_servers.xml +76 -0
- data/xml/sip_banners.xml +359 -0
- data/xml/sip_user_agents.xml +221 -0
- data/xml/smb_native_lm.xml +62 -0
- data/xml/smb_native_os.xml +662 -0
- data/xml/smtp_banners.xml +1690 -0
- data/xml/smtp_debug.xml +39 -0
- data/xml/smtp_ehlo.xml +49 -0
- data/xml/smtp_expn.xml +82 -0
- data/xml/smtp_help.xml +157 -0
- data/xml/smtp_mailfrom.xml +20 -0
- data/xml/smtp_noop.xml +44 -0
- data/xml/smtp_quit.xml +29 -0
- data/xml/smtp_rcptto.xml +25 -0
- data/xml/smtp_rset.xml +26 -0
- data/xml/smtp_turn.xml +26 -0
- data/xml/smtp_vrfy.xml +89 -0
- data/xml/snmp_sysdescr.xml +6507 -0
- data/xml/snmp_sysobjid.xml +430 -0
- data/xml/ssh_banners.xml +1968 -0
- data/xml/telnet_banners.xml +1595 -0
- data/xml/x11_banners.xml +232 -0
- data/xml/x509_issuers.xml +134 -0
- data/xml/x509_subjects.xml +1268 -0
- metadata +304 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
module Recog
|
2
|
+
class DBManager
|
3
|
+
require 'nokogiri'
|
4
|
+
require_relative '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
|
+
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)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def reload
|
26
|
+
self.databases = []
|
27
|
+
load_databases
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,280 @@
|
|
1
|
+
module Recog
|
2
|
+
|
3
|
+
# A fingerprint that can be {#match matched} against a particular kind of
|
4
|
+
# fingerprintable data, e.g. an HTTP `Server` header
|
5
|
+
class Fingerprint
|
6
|
+
require_relative 'fingerprint/regexp_factory'
|
7
|
+
require_relative 'fingerprint/test'
|
8
|
+
|
9
|
+
# A human readable name describing this fingerprint
|
10
|
+
# @return (see #parse_description)
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# Regular expression pulled from the {DB} xml file.
|
14
|
+
#
|
15
|
+
# @see #create_regexp
|
16
|
+
# @return [Regexp] the Regexp to try when calling {#match}
|
17
|
+
attr_reader :regex
|
18
|
+
|
19
|
+
# Collection of indexes for capture groups created by {#match}
|
20
|
+
#
|
21
|
+
# @return (see #parse_params)
|
22
|
+
attr_reader :params
|
23
|
+
|
24
|
+
# Collection of example strings that should {#match} our {#regex}
|
25
|
+
#
|
26
|
+
# @return (see #parse_examples)
|
27
|
+
attr_reader :tests
|
28
|
+
|
29
|
+
# @param xml [Nokogiri::XML::Element]
|
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
|
35
|
+
@name = parse_description(xml)
|
36
|
+
@regex = create_regexp(xml)
|
37
|
+
@params = {}
|
38
|
+
@tests = []
|
39
|
+
|
40
|
+
@protocol.downcase! if @protocol
|
41
|
+
parse_examples(xml)
|
42
|
+
parse_params(xml)
|
43
|
+
end
|
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
|
+
|
54
|
+
# Attempt to match the given string.
|
55
|
+
#
|
56
|
+
# @param match_string [String]
|
57
|
+
# @return [Hash,nil] Keys will be host, service, and os attributes
|
58
|
+
def match(match_string)
|
59
|
+
# match_string.force_encoding('BINARY') if 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
|
73
|
+
return if match_data.nil?
|
74
|
+
|
75
|
+
result = { 'matched' => @name }
|
76
|
+
replacements = {}
|
77
|
+
@params.each_pair do |k,v|
|
78
|
+
pos = v[0]
|
79
|
+
if pos == 0
|
80
|
+
# A match offset of 0 means this param has a hardcoded value
|
81
|
+
result[k] = v[1]
|
82
|
+
# if this value uses interpolation, note it for handling later
|
83
|
+
v[1].scan(/\{([^\s{}]+)\}/).flatten.each do |replacement|
|
84
|
+
replacements[k] ||= Set[]
|
85
|
+
replacements[k] << replacement
|
86
|
+
end
|
87
|
+
else
|
88
|
+
# A match offset other than 0 means the value should come from
|
89
|
+
# the corresponding match result index
|
90
|
+
result[k] = match_data[ pos ]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Use the protocol specified in the XML database if there isn't one
|
95
|
+
# provided as part of this fingerprint.
|
96
|
+
if @protocol
|
97
|
+
unless result['service.protocol']
|
98
|
+
result['service.protocol'] = @protocol
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
result['fingerprint_db'] = @match_key if @match_key
|
103
|
+
|
104
|
+
# for everything identified as using interpolation, do so
|
105
|
+
replacements.each_pair do |replacement_k, replacement_vs|
|
106
|
+
replacement_vs.each do |replacement|
|
107
|
+
if result[replacement]
|
108
|
+
result[replacement_k] = result[replacement_k].gsub(/\{#{replacement}\}/, result[replacement])
|
109
|
+
else
|
110
|
+
# if the value uses an interpolated value that does not exist, in general this could be
|
111
|
+
# very bad, but over time we have allowed the use of regexes with
|
112
|
+
# optional captures that are then used for parts of the asserted
|
113
|
+
# fingerprints. This is frequently done for optional version
|
114
|
+
# strings. If the key in question is cpe23 and the interpolated
|
115
|
+
# value we are trying to replace is version related, use the CPE
|
116
|
+
# standard of '-' for the version, otherwise raise and exception as
|
117
|
+
# this code currently does not handle interpolation of undefined
|
118
|
+
# values in other cases.
|
119
|
+
if replacement_k =~ /\.cpe23$/ and replacement =~ /\.version$/
|
120
|
+
result[replacement_k] = result[replacement_k].gsub(/\{#{replacement}\}/, '-')
|
121
|
+
else
|
122
|
+
raise "Invalid use of nil interpolated non-version value #{replacement} in non-cpe23 fingerprint param #{replacement_k}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
return result
|
129
|
+
end
|
130
|
+
|
131
|
+
# Ensure all the {#params} are valid
|
132
|
+
#
|
133
|
+
# @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
|
134
|
+
# indicate whether a param is valid
|
135
|
+
# @yieldparam message [String] A human-readable string explaining the
|
136
|
+
# `status`
|
137
|
+
def verify_params(&block)
|
138
|
+
return if params.empty?
|
139
|
+
params.each do |param_name, pos_value|
|
140
|
+
pos, value = pos_value
|
141
|
+
if pos > 0 && !value.to_s.empty?
|
142
|
+
yield :fail, "'#{@name}'s #{param_name} is a non-zero pos but specifies a value of '#{value}'"
|
143
|
+
elsif pos == 0 && value.to_s.empty?
|
144
|
+
yield :fail, "'#{@name}'s #{param_name} is not a capture (pos=0) but doesn't specify a value"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Ensure all the {#tests} actually match the fingerprint and return the
|
150
|
+
# expected capture groups.
|
151
|
+
#
|
152
|
+
# @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
|
153
|
+
# indicate whether a test worked
|
154
|
+
# @yieldparam message [String] A human-readable string explaining the
|
155
|
+
# `status`
|
156
|
+
def verify_tests(&block)
|
157
|
+
|
158
|
+
# look for the presence of test cases
|
159
|
+
if tests.size == 0
|
160
|
+
yield :warn, "'#{@name}' has no test cases"
|
161
|
+
end
|
162
|
+
|
163
|
+
# make sure each test case passes
|
164
|
+
tests.each do |test|
|
165
|
+
result = match(test.content)
|
166
|
+
if result.nil?
|
167
|
+
yield :fail, "'#{@name}' failed to match #{test.content.inspect} with #{@regex}'"
|
168
|
+
next
|
169
|
+
end
|
170
|
+
|
171
|
+
message = test
|
172
|
+
status = :success
|
173
|
+
# Ensure that all the attributes as provided by the example were parsed
|
174
|
+
# out correctly and match the capture group values we expect.
|
175
|
+
test.attributes.each do |k, v|
|
176
|
+
next if k == '_encoding'
|
177
|
+
if !result.has_key?(k) || result[k] != v
|
178
|
+
message = "'#{@name}' failed to find expected capture group #{k} '#{v}'. Result was #{result[k]}"
|
179
|
+
status = :fail
|
180
|
+
break
|
181
|
+
end
|
182
|
+
end
|
183
|
+
yield status, message
|
184
|
+
end
|
185
|
+
|
186
|
+
# make sure there are capture groups for all params that use them
|
187
|
+
verify_tests_have_capture_groups(&block)
|
188
|
+
end
|
189
|
+
|
190
|
+
# For fingerprints that specify parameters that are defined by
|
191
|
+
# capture groups, ensure that each parameter has at least one test
|
192
|
+
# that defines an attribute to test for the correct capture of that
|
193
|
+
# parameter.
|
194
|
+
#
|
195
|
+
# @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
|
196
|
+
# indicate whether a test worked
|
197
|
+
# @yieldparam message [String] A human-readable string explaining the
|
198
|
+
# `status`
|
199
|
+
def verify_tests_have_capture_groups(&block)
|
200
|
+
capture_group_used = {}
|
201
|
+
if !params.empty?
|
202
|
+
# get a list of parameters that are defined by capture groups
|
203
|
+
params.each do |param_name, pos_value|
|
204
|
+
pos, value = pos_value
|
205
|
+
if pos > 0 && value.to_s.empty?
|
206
|
+
capture_group_used[param_name] = false
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# match up the fingerprint parameters with test attributes
|
212
|
+
tests.each do |test|
|
213
|
+
test.attributes.each do |k,v|
|
214
|
+
if capture_group_used.has_key?(k)
|
215
|
+
capture_group_used[k] = true
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# alert on untested parameters
|
221
|
+
capture_group_used.each do |param_name, param_used|
|
222
|
+
if !param_used
|
223
|
+
message = "'#{@name}' is missing an example that checks for parameter '#{param_name}' " +
|
224
|
+
"messsage which is derived from a capture group"
|
225
|
+
yield :warn, message
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
private
|
231
|
+
|
232
|
+
# @param xml [Nokogiri::XML::Element]
|
233
|
+
# @return [Regexp]
|
234
|
+
def create_regexp(xml)
|
235
|
+
pattern = xml['pattern']
|
236
|
+
flags = xml['flags'].to_s.split(',')
|
237
|
+
RegexpFactory.build(pattern, flags)
|
238
|
+
end
|
239
|
+
|
240
|
+
# @param xml [Nokogiri::XML::Element]
|
241
|
+
# @return [String] Contents of the source XML's `description` tag
|
242
|
+
def parse_description(xml)
|
243
|
+
element = xml.xpath('description')
|
244
|
+
element.empty? ? '' : element.first.content.to_s.gsub(/\s+/, ' ').strip
|
245
|
+
end
|
246
|
+
|
247
|
+
# @param xml [Nokogiri::XML::Element]
|
248
|
+
# @return [void]
|
249
|
+
def parse_examples(xml)
|
250
|
+
elements = xml.xpath('example')
|
251
|
+
|
252
|
+
elements.each do |elem|
|
253
|
+
# convert nokogiri Attributes into a hash of name => value
|
254
|
+
attrs = elem.attributes.values.reduce({}) { |a,e| a.merge(e.name => e.value) }
|
255
|
+
@tests << Test.new(elem.content, attrs)
|
256
|
+
end
|
257
|
+
|
258
|
+
nil
|
259
|
+
end
|
260
|
+
|
261
|
+
# @param xml [Nokogiri::XML::Element]
|
262
|
+
# @return [Hash<String,Array>] Keys are things like `"os.name"`, values are a two
|
263
|
+
# element Array. The first element is an index for the capture group that returns
|
264
|
+
# that thing. If the index is 0, the second element is a static value for
|
265
|
+
# that thing; otherwise it is undefined.
|
266
|
+
def parse_params(xml)
|
267
|
+
@params = {}.tap do |h|
|
268
|
+
xml.xpath('param').each do |param|
|
269
|
+
name = param['name']
|
270
|
+
pos = param['pos'].to_i
|
271
|
+
value = param['value'].to_s
|
272
|
+
h[name] = [pos, value]
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
nil
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
|
2
|
+
module Recog
|
3
|
+
class Fingerprint
|
4
|
+
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# r = RegexpFactory.build("^Apache[ -]Coyote/(\d\.\d)$", "REG_ICASE")
|
8
|
+
# r.match("Apache-Coyote/1.1")
|
9
|
+
#
|
10
|
+
module RegexpFactory
|
11
|
+
|
12
|
+
# Currently, only options relating to case insensitivity and
|
13
|
+
# multiline/newline are supported. Because Recog's data is used by tools
|
14
|
+
# written in different languages like Ruby and Java, we currently support
|
15
|
+
# specifying them in a variety of ways. This map controls how they can
|
16
|
+
# be specified.
|
17
|
+
#
|
18
|
+
# TODO: consider supporting only a simpler variant and require that tools
|
19
|
+
# that use Recog data translate accordingly
|
20
|
+
FLAG_MAP = {
|
21
|
+
# multiline variations
|
22
|
+
'REG_DOT_NEWLINE' => Regexp::MULTILINE,
|
23
|
+
'REG_LINE_ANY_CRLF' => Regexp::MULTILINE,
|
24
|
+
'REG_MULTILINE' => Regexp::MULTILINE,
|
25
|
+
# case variations
|
26
|
+
'REG_ICASE' => Regexp::IGNORECASE,
|
27
|
+
'IGNORECASE' => Regexp::IGNORECASE
|
28
|
+
}
|
29
|
+
|
30
|
+
DEFAULT_FLAGS = 0
|
31
|
+
|
32
|
+
# @return [Regexp]
|
33
|
+
def self.build(pattern, flags)
|
34
|
+
options = build_options(flags)
|
35
|
+
Regexp.new(pattern, options)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Convert string flag names as used in Recog XML into a Fixnum suitable for
|
39
|
+
# passing as the `options` parameter to `Regexp.new`
|
40
|
+
#
|
41
|
+
# @see FLAG_MAP
|
42
|
+
# @param flags [Array<String>]
|
43
|
+
# @return [Fixnum] Flags for creating a regular expression object
|
44
|
+
def self.build_options(flags)
|
45
|
+
unsupported_flags = flags.select { |flag| !FLAG_MAP.key?(flag) }
|
46
|
+
unless unsupported_flags.empty?
|
47
|
+
fail "Unsupported regular expression flags found: #{unsupported_flags.join(',')}. Must be one of: #{FLAG_MAP.keys.join(',')}"
|
48
|
+
end
|
49
|
+
flags.reduce(DEFAULT_FLAGS) do |sum, flag|
|
50
|
+
sum |= (FLAG_MAP[flag] || 0)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
class Recog::Fingerprint::Test
|
3
|
+
attr_accessor :content
|
4
|
+
attr_accessor :attributes
|
5
|
+
def initialize(content, attributes=[])
|
6
|
+
@attributes = attributes
|
7
|
+
|
8
|
+
if @attributes['_encoding'] && @attributes['_encoding'] == 'base64'
|
9
|
+
@content = content.to_s.unpack('m*').first
|
10
|
+
else
|
11
|
+
@content = content
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
content
|
17
|
+
end
|
18
|
+
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 unless @options.quiet
|
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
|