recog-intrigue 2.3.7
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 +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
|