recog 3.1.1 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
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