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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/Gemfile +6 -0
  4. data/Rakefile +7 -5
  5. data/lib/recog/db.rb +67 -68
  6. data/lib/recog/db_manager.rb +22 -21
  7. data/lib/recog/fingerprint/regexp_factory.rb +10 -13
  8. data/lib/recog/fingerprint/test.rb +9 -8
  9. data/lib/recog/fingerprint.rb +252 -262
  10. data/lib/recog/fingerprint_parse_error.rb +3 -1
  11. data/lib/recog/formatter.rb +41 -39
  12. data/lib/recog/match_reporter.rb +82 -83
  13. data/lib/recog/matcher.rb +37 -40
  14. data/lib/recog/matcher_factory.rb +7 -6
  15. data/lib/recog/nizer.rb +218 -224
  16. data/lib/recog/verifier.rb +30 -28
  17. data/lib/recog/verify_reporter.rb +69 -73
  18. data/lib/recog/version.rb +3 -1
  19. data/lib/recog.rb +2 -0
  20. data/recog/bin/recog_match +21 -20
  21. data/recog/xml/apache_modules.xml +2 -0
  22. data/recog/xml/dhcp_vendor_class.xml +1 -1
  23. data/recog/xml/favicons.xml +133 -1
  24. data/recog/xml/ftp_banners.xml +1 -1
  25. data/recog/xml/html_title.xml +140 -1
  26. data/recog/xml/http_cookies.xml +20 -2
  27. data/recog/xml/http_servers.xml +38 -17
  28. data/recog/xml/http_wwwauth.xml +17 -4
  29. data/recog/xml/mdns_device-info_txt.xml +49 -15
  30. data/recog/xml/sip_banners.xml +0 -2
  31. data/recog/xml/sip_user_agents.xml +1 -1
  32. data/recog/xml/snmp_sysdescr.xml +1 -2
  33. data/recog/xml/ssh_banners.xml +8 -0
  34. data/recog/xml/telnet_banners.xml +3 -2
  35. data/recog/xml/tls_jarm.xml +1 -1
  36. data/recog/xml/x11_banners.xml +1 -0
  37. data/recog/xml/x509_issuers.xml +1 -1
  38. data/recog/xml/x509_subjects.xml +0 -1
  39. data/recog.gemspec +14 -13
  40. data/spec/lib/recog/db_spec.rb +37 -36
  41. data/spec/lib/recog/fingerprint/regexp_factory_spec.rb +19 -20
  42. data/spec/lib/recog/fingerprint_spec.rb +44 -42
  43. data/spec/lib/recog/formatter_spec.rb +20 -18
  44. data/spec/lib/recog/match_reporter_spec.rb +35 -30
  45. data/spec/lib/recog/nizer_spec.rb +85 -101
  46. data/spec/lib/recog/verify_reporter_spec.rb +45 -44
  47. data/spec/spec_helper.rb +2 -1
  48. data.tar.gz.sig +1 -3
  49. metadata +3 -3
  50. metadata.gz.sig +0 -0
@@ -1,316 +1,306 @@
1
- module Recog
1
+ # frozen_string_literal: true
2
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 'set'
7
-
8
- require 'recog/fingerprint_parse_error'
9
- require 'recog/fingerprint/regexp_factory'
10
- require 'recog/fingerprint/test'
11
-
12
- # A human readable name describing this fingerprint
13
- # @return (see #parse_description)
14
- attr_reader :name
15
-
16
- # Regular expression pulled from the {DB} xml file.
17
- #
18
- # @see #create_regexp
19
- # @return [Regexp] the Regexp to try when calling {#match}
20
- attr_reader :regex
21
-
22
- # Collection of indexes for capture groups created by {#match}
23
- #
24
- # @return (see #parse_params)
25
- attr_reader :params
26
-
27
- # Collection of example strings that should {#match} our {#regex}
28
- #
29
- # @return (see #parse_examples)
30
- attr_reader :tests
31
-
32
- # The line number of the XML entity in the source file for this
33
- # fingerprint.
34
- #
35
- # @return [Integer] The line number of this entity.
36
- attr_reader :line
37
-
38
- # @param xml [Nokogiri::XML::Element]
39
- # @param match_key [String] See Recog::DB
40
- # @param protocol [String] Protocol such as ftp, mssql, http, etc.
41
- # @param example_path [String] Directory path for fingerprint example files
42
- def initialize(xml, match_key=nil, protocol=nil, example_path=nil)
43
- @match_key = match_key
44
- @protocol = protocol
45
- @name = parse_description(xml)
46
- @regex = create_regexp(xml)
47
- @line = xml.line
48
- @params = {}
49
- @tests = []
50
-
51
- @protocol.downcase! if @protocol
52
- parse_examples(xml, example_path)
53
- parse_params(xml)
54
- end
3
+ module Recog
4
+ # A fingerprint that can be {#match matched} against a particular kind of
5
+ # fingerprintable data, e.g. an HTTP `Server` header
6
+ class Fingerprint
7
+ require 'set'
8
+
9
+ require 'recog/fingerprint_parse_error'
10
+ require 'recog/fingerprint/regexp_factory'
11
+ require 'recog/fingerprint/test'
12
+
13
+ # A human readable name describing this fingerprint
14
+ # @return (see #parse_description)
15
+ attr_reader :name
16
+
17
+ # Regular expression pulled from the {DB} xml file.
18
+ #
19
+ # @see #create_regexp
20
+ # @return [Regexp] the Regexp to try when calling {#match}
21
+ attr_reader :regex
22
+
23
+ # Collection of indexes for capture groups created by {#match}
24
+ #
25
+ # @return (see #parse_params)
26
+ attr_reader :params
27
+
28
+ # Collection of example strings that should {#match} our {#regex}
29
+ #
30
+ # @return (see #parse_examples)
31
+ attr_reader :tests
32
+
33
+ # The line number of the XML entity in the source file for this
34
+ # fingerprint.
35
+ #
36
+ # @return [Integer] The line number of this entity.
37
+ attr_reader :line
38
+
39
+ # @param xml [Nokogiri::XML::Element]
40
+ # @param match_key [String] See Recog::DB
41
+ # @param protocol [String] Protocol such as ftp, mssql, http, etc.
42
+ # @param example_path [String] Directory path for fingerprint example files
43
+ def initialize(xml, match_key = nil, protocol = nil, example_path = nil)
44
+ @match_key = match_key
45
+ @protocol = protocol&.downcase
46
+ @name = parse_description(xml)
47
+ @regex = create_regexp(xml)
48
+ @line = xml.line
49
+ @params = {}
50
+ @tests = []
51
+
52
+ parse_examples(xml, example_path)
53
+ parse_params(xml)
54
+ end
55
55
 
56
- def output_diag_data(message, data, exception)
57
- STDERR.puts message
58
- STDERR.puts exception.inspect
59
- STDERR.puts "Length: #{data.length}"
60
- STDERR.puts "Encoding: #{data.encoding}"
61
- STDERR.puts "Problematic data:\n#{data}"
62
- STDERR.puts "Raw bytes:\n#{data.pretty_inspect}\n"
63
- end
56
+ def output_diag_data(message, data, exception)
57
+ $stderr.puts message
58
+ $stderr.puts exception.inspect
59
+ $stderr.puts "Length: #{data.length}"
60
+ $stderr.puts "Encoding: #{data.encoding}"
61
+ $stderr.puts "Problematic data:\n#{data}"
62
+ $stderr.puts "Raw bytes:\n#{data.pretty_inspect}\n"
63
+ end
64
64
 
65
- # Attempt to match the given string.
66
- #
67
- # @param match_string [String]
68
- # @return [Hash,nil] Keys will be host, service, and os attributes
69
- def match(match_string)
70
- # match_string.force_encoding('BINARY') if match_string
71
- begin
72
- match_data = @regex.match(match_string)
73
- rescue Encoding::CompatibilityError => e
65
+ # Attempt to match the given string.
66
+ #
67
+ # @param match_string [String]
68
+ # @return [Hash,nil] Keys will be host, service, and os attributes
69
+ def match(match_string)
70
+ # match_string.force_encoding('BINARY') if match_string
74
71
  begin
75
- # Replace invalid UTF-8 characters with spaces, just as DAP does.
76
- encoded_str = match_string.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => '')
77
- match_data = @regex.match(encoded_str)
72
+ match_data = @regex.match(match_string)
73
+ rescue Encoding::CompatibilityError
74
+ begin
75
+ # Replace invalid UTF-8 characters with spaces, just as DAP does.
76
+ encoded_str = match_string.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
77
+ match_data = @regex.match(encoded_str)
78
+ rescue Exception => e
79
+ output_diag_data('Exception while re-encoding match_string to UTF-8', match_string, e)
80
+ end
78
81
  rescue Exception => e
79
- output_diag_data('Exception while re-encoding match_string to UTF-8', match_string, e)
82
+ output_diag_data('Exception while running regex against match_string', match_string, e)
80
83
  end
81
- rescue Exception => e
82
- output_diag_data('Exception while running regex against match_string', match_string, e)
83
- end
84
- return if match_data.nil?
85
-
86
- result = { 'matched' => @name }
87
- replacements = {}
88
- @params.each_pair do |k,v|
89
- pos = v[0]
90
- if pos == 0
91
- # A match offset of 0 means this param has a hardcoded value
92
- result[k] = v[1]
93
- # if this value uses interpolation, note it for handling later
94
- v[1].scan(/\{([^\s{}]+)\}/).flatten.each do |replacement|
95
- replacements[k] ||= Set[]
96
- replacements[k] << replacement
84
+ return if match_data.nil?
85
+
86
+ result = { 'matched' => @name }
87
+ replacements = {}
88
+ @params.each_pair do |k, v|
89
+ pos = v[0]
90
+ if pos == 0
91
+ # A match offset of 0 means this param has a hardcoded value
92
+ result[k] = v[1]
93
+ # if this value uses interpolation, note it for handling later
94
+ v[1].scan(/\{([^\s{}]+)\}/).flatten.each do |replacement|
95
+ replacements[k] ||= Set[]
96
+ replacements[k] << replacement
97
+ end
98
+ else
99
+ # A match offset other than 0 means the value should come from
100
+ # the corresponding match result index
101
+ result[k] = match_data[pos]
97
102
  end
98
- else
99
- # A match offset other than 0 means the value should come from
100
- # the corresponding match result index
101
- result[k] = match_data[ pos ]
102
103
  end
103
- end
104
104
 
105
- # Use the protocol specified in the XML database if there isn't one
106
- # provided as part of this fingerprint.
107
- if @protocol
108
- unless result['service.protocol']
109
- result['service.protocol'] = @protocol
110
- end
111
- end
105
+ # Use the protocol specified in the XML database if there isn't one
106
+ # provided as part of this fingerprint.
107
+ result['service.protocol'] = @protocol if @protocol && !(result['service.protocol'])
112
108
 
113
- result['fingerprint_db'] = @match_key if @match_key
109
+ result['fingerprint_db'] = @match_key if @match_key
114
110
 
115
- # for everything identified as using interpolation, do so
116
- replacements.each_pair do |replacement_k, replacement_vs|
117
- replacement_vs.each do |replacement|
118
- if result[replacement]
119
- result[replacement_k] = result[replacement_k].gsub(/\{#{replacement}\}/, result[replacement])
120
- else
121
- # if the value uses an interpolated value that does not exist, in general this could be
122
- # very bad, but over time we have allowed the use of regexes with
123
- # optional captures that are then used for parts of the asserted
124
- # fingerprints. This is frequently done for optional version
125
- # strings. If the key in question is cpe23 and the interpolated
126
- # value we are trying to replace is version related, use the CPE
127
- # standard of '-' for the version, otherwise raise and exception as
128
- # this code currently does not handle interpolation of undefined
129
- # values in other cases.
130
- if replacement_k =~ /\.cpe23$/ and replacement =~ /\.version$/
131
- result[replacement_k] = result[replacement_k].gsub(/\{#{replacement}\}/, '-')
111
+ # for everything identified as using interpolation, do so
112
+ replacements.each_pair do |replacement_k, replacement_vs|
113
+ replacement_vs.each do |replacement|
114
+ if result[replacement]
115
+ result[replacement_k] = result[replacement_k].gsub(/\{#{replacement}\}/, result[replacement])
132
116
  else
133
- raise "Invalid use of nil interpolated non-version value #{replacement} in non-cpe23 fingerprint param #{replacement_k}"
117
+ # if the value uses an interpolated value that does not exist, in general this could be
118
+ # very bad, but over time we have allowed the use of regexes with
119
+ # optional captures that are then used for parts of the asserted
120
+ # fingerprints. This is frequently done for optional version
121
+ # strings. If the key in question is cpe23 and the interpolated
122
+ # value we are trying to replace is version related, use the CPE
123
+ # standard of '-' for the version, otherwise raise and exception as
124
+ # this code currently does not handle interpolation of undefined
125
+ # values in other cases.
126
+ raise "Invalid use of nil interpolated non-version value #{replacement} in non-cpe23 fingerprint param #{replacement_k}" unless replacement_k =~ (/\.cpe23$/) && replacement =~ (/\.version$/)
127
+
128
+ result[replacement_k] = result[replacement_k].gsub(/\{#{replacement}\}/, '-')
129
+
134
130
  end
135
131
  end
136
132
  end
137
- end
138
133
 
139
- # After performing interpolation, remove temporary keys from results
140
- result.each_pair do |k, _|
141
- if k.start_with?('_tmp.')
142
- result.delete(k)
134
+ # After performing interpolation, remove temporary keys from results
135
+ result.each_pair do |k, _|
136
+ result.delete(k) if k.start_with?('_tmp.')
143
137
  end
138
+
139
+ result
144
140
  end
145
141
 
146
- return result
147
- end
142
+ # Ensure all the {#params} are valid
143
+ #
144
+ # @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
145
+ # indicate whether a param is valid
146
+ # @yieldparam message [String] A human-readable string explaining the
147
+ # `status`
148
+ def verify_params
149
+ return if params.empty?
148
150
 
149
- # Ensure all the {#params} are valid
150
- #
151
- # @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
152
- # indicate whether a param is valid
153
- # @yieldparam message [String] A human-readable string explaining the
154
- # `status`
155
- def verify_params(&block)
156
- return if params.empty?
157
- params.each do |param_name, pos_value|
158
- pos, value = pos_value
159
- if pos > 0 && !value.to_s.empty?
160
- yield :fail, "'#{@name}'s #{param_name} is a non-zero pos but specifies a value of '#{value}'"
161
- elsif pos == 0 && value.to_s.empty?
162
- yield :fail, "'#{@name}'s #{param_name} is not a capture (pos=0) but doesn't specify a value"
151
+ params.each do |param_name, pos_value|
152
+ pos, value = pos_value
153
+ if pos > 0 && !value.to_s.empty?
154
+ yield :fail, "'#{@name}'s #{param_name} is a non-zero pos but specifies a value of '#{value}'"
155
+ elsif pos == 0 && value.to_s.empty?
156
+ yield :fail, "'#{@name}'s #{param_name} is not a capture (pos=0) but doesn't specify a value"
157
+ end
163
158
  end
164
159
  end
165
- end
166
160
 
167
- # Ensure all the {#tests} actually match the fingerprint and return the
168
- # expected capture groups.
169
- #
170
- # @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
171
- # indicate whether a test worked
172
- # @yieldparam message [String] A human-readable string explaining the
173
- # `status`
174
- def verify_tests(&block)
175
-
176
- # look for the presence of test cases
177
- if tests.size == 0
178
- yield :warn, "'#{@name}' has no test cases"
179
- return
180
- end
181
-
182
- # make sure each test case passes
183
- tests.each do |test|
184
- result = match(test.content)
185
- if result.nil?
186
- yield :fail, "'#{@name}' failed to match #{test.content.inspect} with #{@regex}'"
187
- next
161
+ # Ensure all the {#tests} actually match the fingerprint and return the
162
+ # expected capture groups.
163
+ #
164
+ # @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
165
+ # indicate whether a test worked
166
+ # @yieldparam message [String] A human-readable string explaining the
167
+ # `status`
168
+ def verify_tests(&block)
169
+ # look for the presence of test cases
170
+ if tests.size == 0
171
+ yield :warn, "'#{@name}' has no test cases"
172
+ return
188
173
  end
189
174
 
190
- message = test
191
- status = :success
192
- # Ensure that all the attributes as provided by the example were parsed
193
- # out correctly and match the capture group values we expect.
194
- test.attributes.each do |k, v|
195
- next if k == '_encoding'
196
- next if k == '_filename'
197
- if !result.has_key?(k) || result[k] != v
175
+ # make sure each test case passes
176
+ tests.each do |test|
177
+ result = match(test.content)
178
+ if result.nil?
179
+ yield :fail, "'#{@name}' failed to match #{test.content.inspect} with #{@regex}'"
180
+ next
181
+ end
182
+
183
+ message = test
184
+ status = :success
185
+ # Ensure that all the attributes as provided by the example were parsed
186
+ # out correctly and match the capture group values we expect.
187
+ test.attributes.each do |k, v|
188
+ next if k == '_encoding'
189
+ next if k == '_filename'
190
+
191
+ next unless !result.key?(k) || result[k] != v
192
+
198
193
  message = "'#{@name}' failed to find expected capture group #{k} '#{v}'. Result was #{result[k]}"
199
194
  status = :fail
200
195
  break
201
196
  end
197
+ yield status, message
202
198
  end
203
- yield status, message
204
- end
205
199
 
206
- # make sure there are capture groups for all params that use them
207
- verify_tests_have_capture_groups(&block)
208
- end
200
+ # make sure there are capture groups for all params that use them
201
+ verify_tests_have_capture_groups(&block)
202
+ end
209
203
 
210
- # For fingerprints that specify parameters that are defined by
211
- # capture groups, ensure that each parameter has at least one test
212
- # that defines an attribute to test for the correct capture of that
213
- # parameter.
214
- #
215
- # @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
216
- # indicate whether a test worked
217
- # @yieldparam message [String] A human-readable string explaining the
218
- # `status`
219
- def verify_tests_have_capture_groups(&block)
220
- capture_group_used = {}
221
- if !params.empty?
222
- # get a list of parameters that are defined by capture groups
223
- params.each do |param_name, pos_value|
224
- pos, value = pos_value
225
- if pos > 0 && value.to_s.empty?
226
- capture_group_used[param_name] = false
204
+ # For fingerprints that specify parameters that are defined by
205
+ # capture groups, ensure that each parameter has at least one test
206
+ # that defines an attribute to test for the correct capture of that
207
+ # parameter.
208
+ #
209
+ # @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
210
+ # indicate whether a test worked
211
+ # @yieldparam message [String] A human-readable string explaining the
212
+ # `status`
213
+ def verify_tests_have_capture_groups
214
+ capture_group_used = {}
215
+ unless params.empty?
216
+ # get a list of parameters that are defined by capture groups
217
+ params.each do |param_name, pos_value|
218
+ pos, value = pos_value
219
+ capture_group_used[param_name] = false if pos > 0 && value.to_s.empty?
227
220
  end
228
221
  end
229
- end
230
222
 
231
- # match up the fingerprint parameters with test attributes
232
- tests.each do |test|
233
- test.attributes.each do |k,v|
234
- if capture_group_used.has_key?(k)
235
- capture_group_used[k] = true
223
+ # match up the fingerprint parameters with test attributes
224
+ tests.each do |test|
225
+ test.attributes.each do |k, _v|
226
+ capture_group_used[k] = true if capture_group_used.key?(k)
236
227
  end
237
228
  end
238
- end
239
229
 
240
- # alert on untested parameters unless they are temporary
241
- capture_group_used.each do |param_name, param_used|
242
- if !param_used && !param_name.start_with?('_tmp.')
243
- message = "'#{@name}' is missing an example that checks for parameter '#{param_name}' " +
244
- "which is derived from a capture group"
230
+ # alert on untested parameters unless they are temporary
231
+ capture_group_used.each do |param_name, param_used|
232
+ next unless !param_used && !param_name.start_with?('_tmp.')
233
+
234
+ message = "'#{@name}' is missing an example that checks for parameter '#{param_name}' " \
235
+ 'which is derived from a capture group'
245
236
  yield :fail, message
246
237
  end
247
238
  end
248
- end
249
239
 
250
- private
240
+ private
251
241
 
252
- # @param xml [Nokogiri::XML::Element]
253
- # @return [Regexp]
254
- def create_regexp(xml)
255
- pattern = xml['pattern']
256
- flags = xml['flags'].to_s.split(',')
257
- RegexpFactory.build(pattern, flags)
258
- end
242
+ # @param xml [Nokogiri::XML::Element]
243
+ # @return [Regexp]
244
+ def create_regexp(xml)
245
+ pattern = xml['pattern']
246
+ flags = xml['flags'].to_s.split(',')
247
+ RegexpFactory.build(pattern, flags)
248
+ end
259
249
 
260
- # @param xml [Nokogiri::XML::Element]
261
- # @return [String] Contents of the source XML's `description` tag
262
- def parse_description(xml)
263
- element = xml.xpath('description')
264
- element.empty? ? '' : element.first.content.to_s.gsub(/\s+/, ' ').strip
265
- end
250
+ # @param xml [Nokogiri::XML::Element]
251
+ # @return [String] Contents of the source XML's `description` tag
252
+ def parse_description(xml)
253
+ element = xml.xpath('description')
254
+ element.empty? ? '' : element.first.content.to_s.gsub(/\s+/, ' ').strip
255
+ end
266
256
 
267
- # @param xml [Nokogiri::XML::Element]
268
- # @param example_path [String] Directory path for fingerprint example files
269
- # @return [void]
270
- def parse_examples(xml, example_path)
271
- elements = xml.xpath('example')
272
-
273
- elements.each do |elem|
274
- # convert nokogiri Attributes into a hash of name => value
275
- attrs = elem.attributes.values.reduce({}) { |a,e| a.merge(e.name => e.value) }
276
- if attrs["_filename"]
277
- contents = ""
278
- filename = attrs["_filename"]
279
- fn = File.expand_path(File.join(example_path, filename))
280
- unless fn.start_with?(File.expand_path(example_path) + File::Separator)
281
- raise FingerprintParseError.new("an example specifies an illegal file path '#{filename}'", line_number = @line)
282
- end
257
+ # @param xml [Nokogiri::XML::Element]
258
+ # @param example_path [String] Directory path for fingerprint example files
259
+ # @return [void]
260
+ def parse_examples(xml, example_path)
261
+ elements = xml.xpath('example')
262
+
263
+ elements.each do |elem|
264
+ # convert nokogiri Attributes into a hash of name => value
265
+ attrs = elem.attributes.values.reduce({}) { |a, e| a.merge(e.name => e.value) }
266
+ if attrs['_filename']
267
+ contents = ''
268
+ filename = attrs['_filename']
269
+ fn = File.expand_path(File.join(example_path, filename))
270
+ unless fn.start_with?(File.expand_path(example_path) + File::Separator)
271
+ raise FingerprintParseError.new("an example specifies an illegal file path '#{filename}'",
272
+ @line)
273
+ end
283
274
 
284
- File.open(fn, "rb") do |file|
285
- contents = file.read
286
- contents.force_encoding(Encoding::ASCII_8BIT)
275
+ File.open(fn, 'rb') do |file|
276
+ contents = file.read
277
+ contents.force_encoding(Encoding::ASCII_8BIT)
278
+ end
279
+ @tests << Test.new(contents, attrs)
280
+ else
281
+ @tests << Test.new(elem.content, attrs)
287
282
  end
288
- @tests << Test.new(contents, attrs)
289
- else
290
- @tests << Test.new(elem.content, attrs)
291
283
  end
292
- end
293
284
 
294
- nil
295
- end
285
+ nil
286
+ end
296
287
 
297
- # @param xml [Nokogiri::XML::Element]
298
- # @return [Hash<String,Array>] Keys are things like `"os.name"`, values are a two
299
- # element Array. The first element is an index for the capture group that returns
300
- # that thing. If the index is 0, the second element is a static value for
301
- # that thing; otherwise it is undefined.
302
- def parse_params(xml)
303
- @params = {}.tap do |h|
304
- xml.xpath('param').each do |param|
305
- name = param['name']
306
- pos = param['pos'].to_i
307
- value = param['value'].to_s
308
- h[name] = [pos, value]
288
+ # @param xml [Nokogiri::XML::Element]
289
+ # @return [Hash<String,Array>] Keys are things like `"os.name"`, values are a two
290
+ # element Array. The first element is an index for the capture group that returns
291
+ # that thing. If the index is 0, the second element is a static value for
292
+ # that thing; otherwise it is undefined.
293
+ def parse_params(xml)
294
+ @params = {}.tap do |h|
295
+ xml.xpath('param').each do |param|
296
+ name = param['name']
297
+ pos = param['pos'].to_i
298
+ value = param['value'].to_s
299
+ h[name] = [pos, value]
300
+ end
309
301
  end
310
- end
311
302
 
312
- nil
303
+ nil
304
+ end
313
305
  end
314
-
315
- end
316
306
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Recog
2
4
  class FingerprintParseError < StandardError
3
5
  attr_reader :line_number
4
6
 
5
- def initialize(msg, line_number=nil)
7
+ def initialize(msg, line_number = nil)
6
8
  @line_number = line_number
7
9
  super(msg)
8
10
  end