recog 0.02 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/.rspec +2 -1
  4. data/.travis.yml +5 -0
  5. data/.yardopts +1 -0
  6. data/Gemfile +3 -1
  7. data/README.md +12 -12
  8. data/Rakefile +22 -0
  9. data/bin/recog_verify.rb +1 -1
  10. data/features/match.feature +2 -2
  11. data/features/verify.feature +10 -7
  12. data/features/xml/no_tests.xml +0 -50
  13. data/features/xml/successful_tests.xml +7 -22
  14. data/features/xml/tests_with_failures.xml +10 -0
  15. data/features/xml/tests_with_warnings.xml +7 -0
  16. data/lib/recog/db.rb +26 -10
  17. data/lib/recog/db_manager.rb +1 -1
  18. data/lib/recog/fingerprint.rb +118 -34
  19. data/lib/recog/fingerprint/regexp_factory.rb +39 -0
  20. data/lib/recog/fingerprint/test.rb +13 -0
  21. data/lib/recog/matcher.rb +3 -3
  22. data/lib/recog/nizer.rb +16 -23
  23. data/lib/recog/verifier.rb +10 -25
  24. data/lib/recog/verifier_factory.rb +1 -1
  25. data/lib/recog/verify_reporter.rb +1 -1
  26. data/lib/recog/version.rb +1 -1
  27. data/recog.gemspec +12 -3
  28. data/spec/data/test_fingerprints.xml +12 -0
  29. data/spec/lib/fingerprint_self_test_spec.rb +8 -4
  30. data/spec/lib/{db_spec.rb → recog/db_spec.rb} +19 -7
  31. data/spec/lib/recog/fingerprint/regexp_factory.rb +61 -0
  32. data/spec/lib/recog/fingerprint_spec.rb +5 -0
  33. data/spec/lib/{formatter_spec.rb → recog/formatter_spec.rb} +1 -1
  34. data/spec/lib/{match_reporter_spec.rb → recog/match_reporter_spec.rb} +10 -9
  35. data/spec/lib/{nizer_spec.rb → recog/nizer_spec.rb} +5 -5
  36. data/spec/lib/{verify_reporter_spec.rb → recog/verify_reporter_spec.rb} +8 -7
  37. data/spec/spec_helper.rb +82 -0
  38. data/xml/apache_os.xml +48 -2
  39. data/xml/http_servers.xml +38 -6
  40. data/xml/ntp_banners.xml +4 -3
  41. data/xml/smb_native_os.xml +32 -32
  42. data/xml/smtp_expn.xml +1 -0
  43. data/xml/smtp_help.xml +2 -1
  44. data/xml/snmp_sysdescr.xml +164 -24
  45. data/xml/ssh_banners.xml +7 -3
  46. metadata +56 -8
  47. data/Gemfile.lock +0 -42
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2221758dde9a54ad057043aad3cad77ec8c8be7b
4
- data.tar.gz: b2b077c53b397ebb0f32f88b38524e23ee8bae03
3
+ metadata.gz: 4177857ea00bc92010990ba3dd3d16a9df376773
4
+ data.tar.gz: 0e75995b2330ce2f6425c4c3b465bdee53644188
5
5
  SHA512:
6
- metadata.gz: 268d78f87daefe24734c09a665dec59f35ce26a3bdf1255669329c3ce1668e7fbbd8a93a59d47ee0d294bfdade4e6f505f022e3c2b2f9cb2798231753804da07
7
- data.tar.gz: 8b953534f3954862bec8dbc987cd3d13dc92df8d65848517139dc12a4cddf575dfefdca91351428766a6c755817e549bd4735b85de41ea8924d1ef6f9b2f339f
6
+ metadata.gz: 36c37a2bf118bd25a395d477d6d5b29a80e145fd2aaf09f33b522ef6678b79daaf86783293f5d06d06a63ba068aa05ca4425e77308cc6362b3ade01f41b38d03
7
+ data.tar.gz: 59fd715fe36dee81b72b98dfe2e2426681e67fd2d651df56feeb8918f3d215eb91a13fad2ee1ed8813fac6947061eb328e8b5314b0364c4cef187c72ab0937f1
data/.gitignore CHANGED
@@ -1,3 +1,9 @@
1
+ .yardoc
2
+ coverage/
3
+ doc/
4
+
5
+ /Gemfile.lock
6
+
1
7
  # ignore rvm files
2
8
  .ruby-version
3
9
  .ruby-gemset
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --color
2
- --format documentation
2
+ --warnings
3
+ --require spec_helper
data/.travis.yml CHANGED
@@ -1,5 +1,10 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 2.1.2
3
4
  - 2.0.0
4
5
  - 1.9.3
6
+ - jruby
7
+ matrix:
8
+ allow_failures:
9
+ - rvm: jruby
5
10
  script: bundle exec rspec spec
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown
data/Gemfile CHANGED
@@ -1,9 +1,11 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ gemspec
4
+
3
5
  gem 'nokogiri'
4
6
 
5
7
  group :test do
6
- gem 'rspec', '~> 2.14.1'
8
+ gem 'rspec', '>= 2.99'
7
9
  gem 'cucumber', '~> 1.3.8'
8
10
  gem 'aruba', '~> 0.5.3'
9
11
  end
data/README.md CHANGED
@@ -3,7 +3,7 @@ Recog: A Recognition Framework
3
3
 
4
4
  Recog is a framework for identifying products, services, operating systems, and hardware by matching fingerprints against data returned from various network probes. Recog makes it simply to extract useful information from web server banners, snmp system description fields, and a whole lot more. Recog is open source, please see the [LICENSE](https://github.com/recog/LICENSE) file for more information.
5
5
 
6
- [![Build Status](https://travis-ci.org/rapid7/recog.png)](https://travis-ci.org/rapid7/recog) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/rapid7/recog)
6
+ [![Build Status](https://travis-ci.org/rapid7/recog.png)](https://travis-ci.org/rapid7/recog)
7
7
  ==
8
8
 
9
9
  ## Installation
@@ -16,18 +16,18 @@ Recog consists of both XML fingerprint files and an assortment of code, mostly i
16
16
 
17
17
  ## Maturity
18
18
 
19
- Please note that while the XML fingerprints themselves are quite stable and well-tested, the Ruby codebase in Recog is still fairly new and subject to change quickly. Please contact us (research[at]rapid7.com) before leveraging the Recog code within any production projects.
19
+ Please note that while the XML fingerprints themselves are quite stable and well-tested, the Ruby codebase in Recog is still fairly new and subject to change quickly. Please contact us (research[at]rapid7.com) before leveraging the Recog code within any production projects.
20
20
 
21
21
  ## Fingerprints
22
22
 
23
- The fingerprints within Recog are stored in XML files, each of which is designed to match a specific protocol response string or field. For example, the file [ssh_banners.xml](https://github.com/recog/xml/ssh_banners.xml) can determine the os, vendor, and sometimes hardware product by matching the initial SSH daemon banner string.
23
+ The fingerprints within Recog are stored in XML files, each of which is designed to match a specific protocol response string or field. For example, the file [ssh_banners.xml](https://github.com/rapid7/recog/blob/master/xml/ssh_banners.xml) can determine the os, vendor, and sometimes hardware product by matching the initial SSH daemon banner string.
24
24
 
25
25
  A fingerprint file consists of an XML document like the following:
26
26
 
27
27
  01: <?xml version="1.0"?>
28
- 02:
28
+ 02:
29
29
  03: <fingerprints matches="ssh.banner">
30
- 04:
30
+ 04:
31
31
  05: <fingerprint pattern="^RomSShell_([\d\.]+)$">
32
32
  06: <description>Allegro RomSShell SSH</description>
33
33
  07: <example>RomSShell_4.62</example>
@@ -35,25 +35,25 @@ A fingerprint file consists of an XML document like the following:
35
35
  09: <param pos="0" name="service.product" value="RomSShell"/>
36
36
  10: <param pos="1" name="service.version"/>
37
37
  11: </fingerprint>
38
- 12:
38
+ 12:
39
39
  13: </fingerprints>
40
40
 
41
- The first line should always consist of the XML version declaration. The first element should always be a <fingerpints/> block with a `matches` attribute indicating what this fingerprint file is supposed to match. The `matches` attribute is normally in the form of protocol.field.
41
+ The first line should always consist of the XML version declaration. The first element should always be a <fingerpints/> block with a `matches` attribute indicating what this fingerprint file is supposed to match. The `matches` attribute is normally in the form of protocol.field.
42
42
 
43
- Inside of the <fingerprints/> element there should be one or more <fingerprint/> elements. Every fingerprint should contain a `pattern` attribute, which contains the regular expression to be used against the match key.
43
+ Inside of the <fingerprints/> element there should be one or more <fingerprint/> elements. Every fingerprint should contain a `pattern` attribute, which contains the regular expression to be used against the match key.
44
44
 
45
- Inside of the fingerprint, a <description/> element should contain a human-readable string describing this fingerprint.
45
+ Inside of the fingerprint, a <description/> element should contain a human-readable string describing this fingerprint.
46
46
 
47
47
  The <example/> element should contain a successful match for the fingerprint's `pattern`. Multiple <example/> elements are preferred, as these elements are used for the built-in regression testing suite.
48
48
 
49
49
  the <param/> elements contain a `pos` attribute, which indicates what capture field from the `pattern` should be extracted, or `0` for a static string. The `name` attribute is the key that will be reported in the case of a successful match and the `value` will either be a static string for `pos` values of `0` or missing and taken from the captured field.
50
50
 
51
51
  Once a fingerprint has been added, the <examples/> entries can be tested by executing `bin/recog_verify.rb` against the fingerprint file:
52
-
52
+
53
53
  $ bin/recog_verify.rb xml/ssh_banners.xml
54
-
54
+
55
55
  Matches can be tested on the command-line in a similar fashion:
56
-
56
+
57
57
  $ echo 'OpenSSH_6.6p1 Ubuntu-2ubuntu1' | bin/recog_match.rb xml/ssh_banners.xml -
58
58
  MATCH: {"service.version"=>"6.6p1", "openssh.comment"=>"Ubuntu-2ubuntu1", "service.vendor"=>"OpenBSD", "service.family"=>"OpenSSH", "service.product"=>"OpenSSH", "data"=>"OpenSSH_6.6p1 Ubuntu-2ubuntu1"}
59
59
 
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new do |t|
5
+ t.pattern = "spec/**/*_spec.rb"
6
+ end
7
+
8
+ require 'yard'
9
+ require 'yard/rake/yardoc_task'
10
+ YARD::Rake::YardocTask.new do |t|
11
+ t.files = ['lib/**/*.rb', '-', 'README.md']
12
+ end
13
+
14
+ require 'cucumber'
15
+ require 'cucumber/rake/task'
16
+
17
+ Cucumber::Rake::Task.new(:features) do |t|
18
+ t.cucumber_opts = "features --format pretty"
19
+ end
20
+
21
+ task :default => [ :spec, :features, :yard ]
22
+
data/bin/recog_verify.rb CHANGED
@@ -14,7 +14,7 @@ option_parser = OptionParser.new do |opts|
14
14
  opts.separator ""
15
15
  opts.separator "Options"
16
16
 
17
- opts.on("-f", "--format FORMATTER",
17
+ opts.on("-f", "--format FORMATTER",
18
18
  "Choose a formatter.",
19
19
  " [s]ummary (default - failure/warning msgs and summary)",
20
20
  " [d]etail (fingerprint name with tests and expanded summary)") do |format|
@@ -1,6 +1,6 @@
1
1
  Feature: Match
2
2
  Scenario: Finds matches
3
- When I run `match.rb matching_banners_fingerprints.xml banners.xml`
3
+ When I run `recog_match.rb matching_banners_fingerprints.xml banners.xml`
4
4
  Then it should pass with:
5
5
  """
6
6
  MATCH: {"pureftpd.config"=>"[privsep] [TLS] ", "service.family"=>"Pure-FTPd", "service.product"=>"Pure-FTPd", "data"=>"---------- Welcome to Pure-FTPd [privsep] [TLS] ----------"}
@@ -8,7 +8,7 @@ Feature: Match
8
8
  """
9
9
 
10
10
  Scenario: Fails at finding matches
11
- When I run `match.rb failing_banners_fingerprints.xml banners.xml`
11
+ When I run `recog_match.rb failing_banners_fingerprints.xml banners.xml`
12
12
  Then it should pass with:
13
13
  """
14
14
  FAIL: ---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
@@ -1,31 +1,34 @@
1
1
  Feature: Verify
2
2
  Scenario: No tests
3
- When I run `verify.rb no_tests.xml`
3
+ When I run `recog_verify.rb no_tests.xml`
4
4
  Then it should pass with:
5
5
  """
6
6
  SUMMARY: Test completed with 0 successful, 0 warnings, and 0 failures
7
7
  """
8
8
 
9
9
  Scenario: Successful tests
10
- When I run `verify.rb successful_tests.xml`
10
+ When I run `recog_verify.rb successful_tests.xml`
11
11
  Then it should pass with:
12
12
  """
13
- SUMMARY: Test completed with 2 successful, 0 warnings, and 0 failures
13
+ SUMMARY: Test completed with 4 successful, 0 warnings, and 0 failures
14
14
  """
15
15
 
16
16
  Scenario: Tests with warnings
17
- When I run `verify.rb tests_with_warnings.xml`
17
+ When I run `recog_verify.rb tests_with_warnings.xml`
18
18
  Then it should pass with:
19
19
  """
20
- WARN: 'Pure-FTPd' failed to match \"---------- Welcome to Pure-FTPd ----------\" key 'pureftpd.config'' with (?-mix:^-{10} Welcome to Pure-FTPd (.*)-{10}$)'
20
+ WARN: 'Pure-FTPd' has no test cases
21
21
  SUMMARY: Test completed with 1 successful, 1 warnings, and 0 failures
22
22
  """
23
23
 
24
24
  Scenario: Tests with failures
25
- When I run `verify.rb tests_with_failures.xml`
25
+ When I run `recog_verify.rb tests_with_failures.xml`
26
26
  Then it should pass with:
27
27
  """
28
28
  FAIL: 'foo test' failed to match "bar" with (?-mix:^foo$)'
29
29
  FAIL: '' failed to match "This almost matches" with (?-mix:^This matches$)'
30
- SUMMARY: Test completed with 0 successful, 0 warnings, and 2 failures
30
+ FAIL: 'bar test' failed to find expected capture group os.version '5.0'
31
+ SUMMARY: Test completed with 0 successful, 0 warnings, and 3 failures
31
32
  """
33
+
34
+
@@ -1,53 +1,3 @@
1
1
  <?xml version="1.0"?>
2
- <!--
3
- SMTP response lines to the EHLO command are matched against these patterns
4
- (1 line at a time) to fingerprint SMTP servers.
5
-
6
- See comment at the top of smtp_banners.xml for additional info.
7
- -->
8
-
9
2
  <fingerprints>
10
- <fingerprint pattern="^500[ -]Syntax error, command &quot;XXXX&quot; unrecognized$">
11
- <description>
12
- Cisco PIX changes the command letters to 'X' before passing
13
- them to the real SMTP server.
14
- </description>
15
- <param pos="0" name="service.vendor" value="Cisco"/>
16
- <param pos="0" name="service.family" value="PIX"/>
17
- <param pos="0" name="service.product" value="PIX"/>
18
- </fingerprint>
19
-
20
- <!--
21
- Don't try to infer a fingerprint from XEXCH50, because if we do, it might overwrite
22
- a very precise MS IIS SMTP service or MS Exchange Server fingerprint found with the
23
- help of smtp_banners.xml. Instead, this case is handled specially by the Jess rule
24
- smtp-iis-xexch50-svc-fingerprint. -mrb
25
-
26
- <fingerprint pattern="^250[ -] *XEXCH50.*$">
27
- <description>
28
- Microsoft Exchange/IIS server
29
- </description>
30
- <param pos="0" name="service.vendor" value="Microsoft"/>
31
- <param pos="0" name="service.family" value="IIS"/>
32
- <param pos="0" name="service.product" value="IIS"/>
33
- <param pos="0" name="os.vendor" value="Microsoft"/>
34
- <param pos="0" name="os.family" value="Windows"/>
35
- <param pos="0" name="os.device" value="General"/>
36
- <param pos="0" name="os.product" value="Windows"/>
37
- </fingerprint>
38
- -->
39
-
40
- <fingerprint pattern="^221[ -]See ya in cyberspace$">
41
- <description>
42
- 221 See ya in cyberspace
43
- </description>
44
- <param pos="0" name="service.vendor" value="Alt-N"/>
45
- <param pos="0" name="service.family" value="MDaemon"/>
46
- <param pos="0" name="service.product" value="MDaemon"/>
47
- <param pos="0" name="os.vendor" value="Microsoft"/>
48
- <param pos="0" name="os.family" value="Windows"/>
49
- <param pos="0" name="os.device" value="General"/>
50
- <param pos="0" name="os.product" value="Windows"/>
51
- <param pos="0" name="os.arch" value="x86"/>
52
- </fingerprint>
53
3
  </fingerprints>
@@ -7,27 +7,12 @@
7
7
  <param pos="0" name="os.product" value="IOS"/>
8
8
  <param pos="1" name="os.version"/>
9
9
  </fingerprint>
10
- <fingerprint pattern="^Microsoft Exchange Server 2007 IMAP4 service ready$">
11
- <!-- Microsoft Exchange Server 2007 IMAP4 service ready
12
- -->
13
- <description>Microsoft Exchange Server 2007</description>
14
- <param pos="0" name="service.vendor" value="Microsoft"/>
15
- <param pos="0" name="service.family" value="Exchange Server"/>
16
- <param pos="0" name="service.product" value="Exchange 2007 Server"/>
17
- <param pos="0" name="os.vendor" value="Microsoft"/>
18
- <param pos="0" name="os.device" value="General"/>
19
- <param pos="0" name="os.family" value="Windows"/>
20
- <param pos="0" name="os.product" value="Windows"/>
21
- </fingerprint>
22
- <fingerprint pattern="^The Microsoft Exchange IMAP4 service is ready\.?$">
23
- <example>The Microsoft Exchange IMAP4 service is ready.</example>
24
- <description>Microsoft Exchange Server</description>
25
- <param pos="0" name="service.vendor" value="Microsoft"/>
26
- <param pos="0" name="service.family" value="Exchange Server"/>
27
- <param pos="0" name="service.product" value="Exchange Server"/>
28
- <param pos="0" name="os.vendor" value="Microsoft"/>
29
- <param pos="0" name="os.device" value="General"/>
30
- <param pos="0" name="os.family" value="Windows"/>
31
- <param pos="0" name="os.product" value="Windows"/>
10
+ <fingerprint pattern="^bar ([\d.]+)$">
11
+ <description>bar test</description>
12
+ <example os.version="1.0" >bar 1.0</example>
13
+ <example os.version="2.0" >bar 2.0</example>
14
+ <example os.version="2.1" >bar 2.1</example>
15
+ <param pos="1" name="os.version" />
16
+ <param pos="0" name="os.name" value="Bar" />
32
17
  </fingerprint>
33
18
  </fingerprints>
@@ -2,9 +2,19 @@
2
2
  <fingerprints>
3
3
  <fingerprint pattern="^foo$">
4
4
  <description>foo test</description>
5
+ <!-- Fail: doesn't match -->
5
6
  <example>bar</example>
6
7
  </fingerprint>
7
8
  <fingerprint pattern="^This matches$">
9
+ <!-- Warn: no name -->
10
+ <!-- Fail: doesn't match -->
8
11
  <example>This almost matches</example>
9
12
  </fingerprint>
13
+ <fingerprint pattern="^bar ([\d.]+)$">
14
+ <description>bar test</description>
15
+ <!-- Fail: expected os.version doesn't match the capture group -->
16
+ <example os.version="5.0" >bar 1.0</example>
17
+ <param pos="1" name="os.version" />
18
+ <param pos="0" name="os.name" value="Bar" />
19
+ </fingerprint>
10
20
  </fingerprints>
@@ -7,4 +7,11 @@
7
7
  <param pos="0" name="service.family" value="Pure-FTPd"/>
8
8
  <param pos="0" name="service.product" value="Pure-FTPd"/>
9
9
  </fingerprint>
10
+ <fingerprint pattern="^-{10} Welcome to Pure-FTPd (.*)-{10}$">
11
+ <!-- should warn with no examples -->
12
+ <description>Pure-FTPd</description>
13
+ <param pos="1" name="pureftpd.config"/>
14
+ <param pos="0" name="service.family" value="Pure-FTPd"/>
15
+ <param pos="0" name="service.product" value="Pure-FTPd"/>
16
+ </fingerprint>
10
17
  </fingerprints>
data/lib/recog/db.rb CHANGED
@@ -1,38 +1,54 @@
1
1
  module Recog
2
+
3
+ # A collection of {Fingerprint fingerprints} for matching against a particular
4
+ # kind of fingerprintable data, e.g. an HTTP `Server` header
2
5
  class DB
3
6
  require 'nokogiri'
4
7
  require 'recog/fingerprint'
5
8
 
6
- attr_accessor :path, :fingerprints, :match_key
9
+ # @return [String]
10
+ attr_reader :path
11
+
12
+ # @return [Array<Fingerprint>] {Fingerprint} objects that can be matched
13
+ # against strings that make sense for the {#match_key}
14
+ attr_reader :fingerprints
15
+
16
+ # @return [String] Taken from the `fingerprints/matches` element, or
17
+ # defaults to the basename of {#path} without the `.xml` extension.
18
+ attr_reader :match_key
7
19
 
20
+ # @param path [String]
8
21
  def initialize(path)
9
- self.path = path
22
+ @match_key = nil
23
+ @path = path
24
+ @fingerprints = []
25
+
10
26
  parse_fingerprints
11
27
  end
12
28
 
29
+ # @return [void]
13
30
  def parse_fingerprints
14
- self.fingerprints = []
15
31
  xml = nil
16
-
32
+
17
33
  File.open(self.path, "rb") do |fd|
18
- xml = Nokogiri::XML( fd.read(fd.stat.size))
34
+ xml = Nokogiri::XML(fd.read(fd.stat.size))
19
35
  end
20
36
 
21
37
  xml.xpath("/fingerprints").each do |fbase|
22
38
  if fbase['matches']
23
- self.match_key = fbase['matches'].to_s
39
+ @match_key = fbase['matches'].to_s
24
40
  end
25
41
  end
26
42
 
27
- unless self.match_key
28
- self.match_key = File.basename(self.path).sub(/\.xml$/, '')
43
+ unless @match_key
44
+ @match_key = File.basename(self.path).sub(/\.xml$/, '')
29
45
  end
30
46
 
31
47
  xml.xpath("/fingerprints/fingerprint").each do |fprint|
32
- fingerprints << Fingerprint.new(fprint)
48
+ @fingerprints << Fingerprint.new(fprint)
33
49
  end
34
50
 
35
51
  xml = nil
36
52
  end
37
53
  end
38
- end
54
+ end
@@ -24,4 +24,4 @@ class DBManager
24
24
  end
25
25
 
26
26
  end
27
- end
27
+ end
@@ -1,60 +1,144 @@
1
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
2
5
  class Fingerprint
3
- attr_reader :name, :regex, :params, :tests
6
+ require 'recog/fingerprint/regexp_factory'
7
+ require 'recog/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
4
28
 
29
+ # @param xml [Nokogiri::XML::Element]
5
30
  def initialize(xml)
6
- @name = description(xml)
31
+ @name = parse_description(xml)
7
32
  @regex = create_regexp(xml)
8
- @params = parse_params(xml)
9
- @tests = examples(xml)
33
+ @params = {}
34
+ @tests = []
35
+
36
+ parse_examples(xml)
37
+ parse_params(xml)
10
38
  end
11
39
 
12
- private
40
+ # Attempt to match the given string.
41
+ #
42
+ # @param match_string [String]
43
+ # @return [Hash,nil] Keys will be host, service, and os attributes
44
+ def match(match_string)
45
+ match_data = @regex.match(match_string)
46
+ return if match_data.nil?
13
47
 
14
- def description(xml)
15
- element = xml.xpath('description')
16
- element.empty? ? '' : element.first.content
48
+ result = { 'matched' => @name }
49
+ @params.each_pair do |k,v|
50
+ if v[0] == 0
51
+ # A match offset of 0 means this param has a hardcoded value
52
+ result[k] = v[1]
53
+ else
54
+ result[k] = match_data[ v[0] ]
55
+ end
56
+ end
57
+ return result
58
+ end
59
+
60
+ # Ensure all the {#tests} actually match the fingerprint and return the
61
+ # expected capture groups.
62
+ #
63
+ # @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
64
+ # indicate whether a test worked
65
+ # @yieldparam message [String] A human-readable string explaining the
66
+ # `status`
67
+ def verify_tests(&block)
68
+ if tests.size == 0
69
+ yield :warn, "'#{@name}' has no test cases"
70
+ end
71
+
72
+ tests.each do |test|
73
+ result = match(test.content)
74
+ if result.nil?
75
+ yield :fail, "'#{@name}' failed to match #{test.content.inspect} with #{@regex}'"
76
+ next
77
+ end
78
+
79
+ message = test
80
+ status = :success
81
+ # Ensure that all the attributes as provided by the example were parsed
82
+ # out correctly and match the capture group values we expect.
83
+ test.attributes.each do |k, v|
84
+ if !result.has_key?(k) || result[k] != v
85
+ message = "'#{@name}' failed to find expected capture group #{k} '#{v}'"
86
+ status = :fail
87
+ break
88
+ end
89
+ end
90
+ yield status, message
91
+ end
17
92
  end
18
93
 
94
+ private
95
+
96
+ # @param xml [Nokogiri::XML::Element]
97
+ # @return [Regexp]
19
98
  def create_regexp(xml)
20
99
  pattern = xml['pattern']
21
100
  flags = xml['flags'].to_s.split(',')
22
101
  RegexpFactory.build(pattern, flags)
23
102
  end
24
103
 
25
- def parse_params(xml)
26
- {}.tap do |h|
27
- xml.xpath('param').each do |e|
28
- name = e['name']
29
- pos = e['pos'].to_i
30
- value = e['value'].to_s
31
- h[name] = [pos, value]
32
- end
33
- end
104
+ # @param xml [Nokogiri::XML::Element]
105
+ # @return [String] Contents of the source XML's `description` tag
106
+ def parse_description(xml)
107
+ element = xml.xpath('description')
108
+ element.empty? ? '' : element.first.content
34
109
  end
35
110
 
36
- def examples(xml)
37
- xml.xpath('example').collect(&:content)
38
- end
111
+ # @param xml [Nokogiri::XML::Element]
112
+ # @return [void]
113
+ def parse_examples(xml)
114
+ elements = xml.xpath('example')
39
115
 
40
- module RegexpFactory
41
- def self.build(pattern, flags)
42
- options = build_options(flags)
43
- Regexp.new(pattern, options)
116
+ elements.each do |elem|
117
+ # convert nokogiri Attributes into a hash of name => value
118
+ attrs = elem.attributes.values.reduce({}) { |a,e| a.merge(e.name => e.value) }
119
+ @tests << Test.new(elem.content, attrs)
44
120
  end
45
121
 
46
- def self.build_options(flags)
47
- rflags = Regexp::NOENCODING
48
- flags.each do |flag|
49
- case flag
50
- when 'REG_DOT_NEWLINE', 'REG_LINE_ANY_CRLF'
51
- rflags |= Regexp::MULTILINE
52
- when 'REG_ICASE'
53
- rflags |= Regexp::IGNORECASE
54
- end
122
+ nil
123
+ end
124
+
125
+ # @param xml [Nokogiri::XML::Element]
126
+ # @return [Hash<String,Array>] Keys are things like `"os.name"`, values are a two
127
+ # element Array. The first element is an index for the capture group that returns
128
+ # that thing. If the index is 0, the second element is a static value for
129
+ # that thing; otherwise it is undefined.
130
+ def parse_params(xml)
131
+ @params = {}.tap do |h|
132
+ xml.xpath('param').each do |param|
133
+ name = param['name']
134
+ pos = param['pos'].to_i
135
+ value = param['value'].to_s
136
+ h[name] = [pos, value]
55
137
  end
56
- rflags
57
138
  end
139
+
140
+ nil
58
141
  end
142
+
59
143
  end
60
144
  end