recog-intrigue 2.3.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
  4. data/.github/ISSUE_TEMPLATE/fingerprint_request.md +27 -0
  5. data/.github/PULL_REQUEST_TEMPLATE +24 -0
  6. data/.gitignore +14 -0
  7. data/.rbenv-gemset +1 -0
  8. data/.rspec +3 -0
  9. data/.ruby-gemset +1 -0
  10. data/.ruby-version +1 -0
  11. data/.travis.yml +25 -0
  12. data/.yardopts +1 -0
  13. data/CONTRIBUTING.md +171 -0
  14. data/COPYING +23 -0
  15. data/Gemfile +10 -0
  16. data/LICENSE +7 -0
  17. data/README.md +85 -0
  18. data/Rakefile +22 -0
  19. data/bin/recog_export +81 -0
  20. data/bin/recog_match +55 -0
  21. data/bin/recog_standardize +118 -0
  22. data/bin/recog_verify +64 -0
  23. data/cpe-remap.yaml +134 -0
  24. data/features/data/failing_banners_fingerprints.xml +20 -0
  25. data/features/data/matching_banners_fingerprints.xml +23 -0
  26. data/features/data/multiple_banners_fingerprints.xml +32 -0
  27. data/features/data/no_tests.xml +3 -0
  28. data/features/data/sample_banner.txt +2 -0
  29. data/features/data/successful_tests.xml +18 -0
  30. data/features/data/tests_with_failures.xml +20 -0
  31. data/features/data/tests_with_warnings.xml +17 -0
  32. data/features/match.feature +36 -0
  33. data/features/support/aruba.rb +3 -0
  34. data/features/support/env.rb +6 -0
  35. data/features/verify.feature +48 -0
  36. data/identifiers/README.md +47 -0
  37. data/identifiers/os_architecture.txt +20 -0
  38. data/identifiers/os_device.txt +52 -0
  39. data/identifiers/os_family.txt +160 -0
  40. data/identifiers/os_product.txt +199 -0
  41. data/identifiers/service_family.txt +185 -0
  42. data/identifiers/service_product.txt +255 -0
  43. data/identifiers/software_class.txt +26 -0
  44. data/identifiers/software_family.txt +91 -0
  45. data/identifiers/software_product.txt +333 -0
  46. data/identifiers/vendor.txt +405 -0
  47. data/lib/recog.rb +4 -0
  48. data/lib/recog/db.rb +78 -0
  49. data/lib/recog/db_manager.rb +31 -0
  50. data/lib/recog/fingerprint.rb +280 -0
  51. data/lib/recog/fingerprint/regexp_factory.rb +56 -0
  52. data/lib/recog/fingerprint/test.rb +18 -0
  53. data/lib/recog/formatter.rb +51 -0
  54. data/lib/recog/match_reporter.rb +77 -0
  55. data/lib/recog/matcher.rb +94 -0
  56. data/lib/recog/matcher_factory.rb +14 -0
  57. data/lib/recog/nizer.rb +347 -0
  58. data/lib/recog/verifier.rb +39 -0
  59. data/lib/recog/verifier_factory.rb +13 -0
  60. data/lib/recog/verify_reporter.rb +86 -0
  61. data/lib/recog/version.rb +3 -0
  62. data/misc/convert_mysql_err +61 -0
  63. data/misc/order.xsl +17 -0
  64. data/recog-intrigue.gemspec +45 -0
  65. data/requirements.txt +2 -0
  66. data/spec/data/best_os_match_1.yml +17 -0
  67. data/spec/data/best_os_match_2.yml +17 -0
  68. data/spec/data/best_service_match_1.yml +17 -0
  69. data/spec/data/smb_native_os.txt +25 -0
  70. data/spec/data/test_fingerprints.xml +36 -0
  71. data/spec/data/verification_fingerprints.xml +86 -0
  72. data/spec/data/whitespaced_fingerprint.xml +5 -0
  73. data/spec/lib/fingerprint_self_test_spec.rb +174 -0
  74. data/spec/lib/recog/db_spec.rb +98 -0
  75. data/spec/lib/recog/fingerprint/regexp_factory_spec.rb +73 -0
  76. data/spec/lib/recog/fingerprint_spec.rb +112 -0
  77. data/spec/lib/recog/formatter_spec.rb +69 -0
  78. data/spec/lib/recog/match_reporter_spec.rb +91 -0
  79. data/spec/lib/recog/nizer_spec.rb +330 -0
  80. data/spec/lib/recog/verify_reporter_spec.rb +113 -0
  81. data/spec/spec_helper.rb +82 -0
  82. data/update_cpes.py +186 -0
  83. data/xml/apache_modules.xml +1911 -0
  84. data/xml/apache_os.xml +273 -0
  85. data/xml/architecture.xml +36 -0
  86. data/xml/dns_versionbind.xml +761 -0
  87. data/xml/fingerprints.xsd +128 -0
  88. data/xml/ftp_banners.xml +1553 -0
  89. data/xml/h323_callresp.xml +603 -0
  90. data/xml/hp_pjl_id.xml +358 -0
  91. data/xml/html_title.xml +1630 -0
  92. data/xml/http_cookies.xml +411 -0
  93. data/xml/http_servers.xml +3195 -0
  94. data/xml/http_wwwauth.xml +595 -0
  95. data/xml/imap_banners.xml +245 -0
  96. data/xml/ldap_searchresult.xml +711 -0
  97. data/xml/mdns_device-info_txt.xml +1796 -0
  98. data/xml/mdns_workstation_txt.xml +15 -0
  99. data/xml/mysql_banners.xml +1649 -0
  100. data/xml/mysql_error.xml +871 -0
  101. data/xml/nntp_banners.xml +82 -0
  102. data/xml/ntp_banners.xml +1223 -0
  103. data/xml/operating_system.xml +629 -0
  104. data/xml/pop_banners.xml +499 -0
  105. data/xml/rsh_resp.xml +76 -0
  106. data/xml/rtsp_servers.xml +76 -0
  107. data/xml/sip_banners.xml +359 -0
  108. data/xml/sip_user_agents.xml +221 -0
  109. data/xml/smb_native_lm.xml +62 -0
  110. data/xml/smb_native_os.xml +662 -0
  111. data/xml/smtp_banners.xml +1690 -0
  112. data/xml/smtp_debug.xml +39 -0
  113. data/xml/smtp_ehlo.xml +49 -0
  114. data/xml/smtp_expn.xml +82 -0
  115. data/xml/smtp_help.xml +157 -0
  116. data/xml/smtp_mailfrom.xml +20 -0
  117. data/xml/smtp_noop.xml +44 -0
  118. data/xml/smtp_quit.xml +29 -0
  119. data/xml/smtp_rcptto.xml +25 -0
  120. data/xml/smtp_rset.xml +26 -0
  121. data/xml/smtp_turn.xml +26 -0
  122. data/xml/smtp_vrfy.xml +89 -0
  123. data/xml/snmp_sysdescr.xml +6507 -0
  124. data/xml/snmp_sysobjid.xml +430 -0
  125. data/xml/ssh_banners.xml +1968 -0
  126. data/xml/telnet_banners.xml +1595 -0
  127. data/xml/x11_banners.xml +232 -0
  128. data/xml/x509_issuers.xml +134 -0
  129. data/xml/x509_subjects.xml +1268 -0
  130. metadata +304 -0
@@ -0,0 +1,5 @@
1
+ <fingerprint pattern=".*this doesn't matter.*">
2
+ <description> I love
3
+ whitespace!
4
+ </description>
5
+ </fingerprint>
@@ -0,0 +1,174 @@
1
+ require 'recog/db'
2
+ require 'regexp_parser'
3
+ require 'nokogiri'
4
+
5
+ describe Recog::DB do
6
+ let(:schema) { Nokogiri::XML::Schema(open(File.expand_path(File.join(%w(xml fingerprints.xsd))))) }
7
+ Dir[File.expand_path File.join('xml', '*.xml')].each do |xml_file_name|
8
+
9
+ describe "##{File.basename(xml_file_name)}" do
10
+
11
+ it "is valid XML" do
12
+ doc = Nokogiri::XML(open(xml_file_name))
13
+ errors = schema.validate(doc)
14
+ expect(errors).to be_empty, "#{xml_file_name} is invalid recog XML -- #{errors.inspect}"
15
+ end
16
+
17
+ db = Recog::DB.new(xml_file_name)
18
+
19
+ it "has a match key" do
20
+ expect(db.match_key).not_to be_nil
21
+ expect(db.match_key).not_to be_empty
22
+ end
23
+
24
+ it "has valid 'preference' value" do
25
+ # Reserve values below 0.10 and above 0.90 for users
26
+ # See xml/fingerprints.xsd
27
+ expect(db.preference.class).to be ::Float
28
+ expect(db.preference).to be_between(0.10, 0.90)
29
+ end
30
+
31
+ fp_descriptions = []
32
+ db.fingerprints.each_index do |i|
33
+ fp = db.fingerprints[i]
34
+
35
+ it "doesn't have a duplicate description" do
36
+ if fp_descriptions.include?(fp.name)
37
+ fail "'#{fp.name}'s description is not unique"
38
+ else
39
+ fp_descriptions << fp.name
40
+ end
41
+ end
42
+
43
+ context "#{fp.name}" do
44
+ param_names = []
45
+ it "has consistent os.device and hw.device" do
46
+ if fp.params['os.device'] && fp.params['hw.device'] && (fp.params['os.device'] != fp.params['hw.device'])
47
+ fail "#{fp.name} has both hw.device and os.device but with differing values"
48
+ end
49
+ end
50
+ fp.params.each do |param_name, pos_value|
51
+ pos, value = pos_value
52
+ it "has valid looking fingerprint parameter names" do
53
+ unless param_name =~ /^(?:cookie|[^\.]+\..*)$/
54
+ fail "'#{param_name}' is invalid"
55
+ end
56
+ end
57
+
58
+ it "doesn't have param values for capture params" do
59
+ if pos > 0 && !value.to_s.empty?
60
+ fail "'#{fp.name}'s #{param_name} is a non-zero pos but specifies a value of '#{value}'"
61
+ end
62
+ end
63
+
64
+ it "has parameter values other than General, Server or Unknown, which are not helpful" do
65
+ if pos == 0 && value =~ /^(?i:general|server|unknown)$/
66
+ fail "'#{param_name}' has general/server/unknown value '#{value}'"
67
+ end
68
+ end
69
+
70
+ it "doesn't omit values for non-capture params" do
71
+ if pos == 0 && value.to_s.empty?
72
+ fail "'#{fp.name}'s #{param_name} is not a capture (pos=0) but doesn't specify a value"
73
+ end
74
+ end
75
+
76
+ it "doesn't have duplicate params" do
77
+ if param_names.include?(param_name)
78
+ fail "'#{fp.name}'s has duplicate #{param_name}"
79
+ else
80
+ param_names << param_name
81
+ end
82
+ end
83
+
84
+ it "uses interpolation correctly" do
85
+ if pos == 0 && /\{(?<interpolated>[^\s{}]+)\}/ =~ value
86
+ unless fp.params.key?(interpolated)
87
+ fail "'#{fp.name}' uses interpolated value '#{interpolated}' that does not exist"
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ context "#{fp.regex}" do
95
+
96
+ it "has a valid looking name" do
97
+ expect(fp.name).not_to be_nil
98
+ expect(fp.name).not_to be_empty
99
+ end
100
+
101
+ it "has a regex" do
102
+ expect(fp.regex).not_to be_nil
103
+ expect(fp.regex.class).to be ::Regexp
104
+ end
105
+
106
+ it 'uses capturing regular expressions properly' do
107
+ # the list of index-based captures that the fingerprint is expecting
108
+ expected_capture_positions = fp.params.values.map(&:first).map(&:to_i).select { |position| position > 0 }
109
+ if fp.params.empty? && expected_capture_positions.size > 0
110
+ fail "Non-asserting fingerprint with regex #{fp.regex} captures #{expected_capture_positions.size} time(s); 0 are needed"
111
+ else
112
+ # parse the regex and count the number of captures
113
+ actual_capture_positions = []
114
+ capture_number = 1
115
+ Regexp::Scanner.scan(fp.regex).each do |token_parts|
116
+ if token_parts.first == :group && ![:close, :passive, :options, :options_switch].include?(token_parts[1])
117
+ actual_capture_positions << capture_number
118
+ capture_number += 1
119
+ end
120
+ end
121
+ # compare the captures actually performed to those being used and ensure that they contain
122
+ # the same elements regardless of order, preventing, over-, under- and other forms of mis-capturing.
123
+ actual_capture_positions = actual_capture_positions.sort.uniq
124
+ expected_capture_positions = expected_capture_positions.sort.uniq
125
+ expect(actual_capture_positions).to eq(expected_capture_positions),
126
+ "Regex has #{actual_capture_positions.size} capture groups, but the fingerprint expected #{expected_capture_positions.size} extractions."
127
+ end
128
+ end
129
+
130
+ # Not yet enforced
131
+ # it "has test cases" do
132
+ # expect(fp.tests.length).not_to equal(0)
133
+ # end
134
+
135
+ it "Has a reasonable number (<= 20) of test cases" do
136
+ expect(fp.tests.length).to be <= 20
137
+ end
138
+
139
+ fp_examples = []
140
+ fp.tests.each do |example|
141
+ it "doesn't have a duplicate examples" do
142
+ if fp_examples.include?(example.content)
143
+ fail "'#{fp.name}' has duplicate example '#{example.content}'"
144
+ else
145
+ fp_examples << example.content
146
+ end
147
+ end
148
+ it "Example '#{example.content}' matches this regex" do
149
+ match = fp.match(example.content)
150
+ expect(match).to_not be_nil, 'Regex did not match'
151
+ # test any extractions specified in the example
152
+ example.attributes.each_pair do |k,v|
153
+ next if k == '_encoding'
154
+ expect(match[k]).to eq(v), "Regex didn't extract expected value for fingerprint attribute #{k} -- got #{match[k]} instead of #{v}"
155
+ end
156
+ end
157
+
158
+ it "Example '#{example.content}' matches this regex first" do
159
+ db.fingerprints.slice(0, i).each_index do |previous_i|
160
+ prev_fp = db.fingerprints[previous_i]
161
+ prev_fp.tests.each do |prev_example|
162
+ match = prev_fp.match(example.content)
163
+ expect(match).to be_nil, "Matched regex ##{previous_i} (#{db.fingerprints[previous_i].regex}) rather than ##{i} (#{db.fingerprints[i].regex})"
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ end
170
+ end
171
+
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,98 @@
1
+ require 'recog/db'
2
+
3
+ describe Recog::DB do
4
+ let(:xml_file) { File.expand_path File.join('spec', 'data', 'test_fingerprints.xml') }
5
+ subject { Recog::DB.new(xml_file) }
6
+
7
+ describe "#fingerprints" do
8
+ subject(:fingerprints) { described_class.new(xml_file).fingerprints }
9
+
10
+ it { is_expected.to be_a(Enumerable) }
11
+
12
+ context "with only a pattern" do
13
+ subject(:entry) { described_class.new(xml_file).fingerprints[0] }
14
+
15
+ it "has a blank name with no description" do
16
+ expect(entry.name).to be_empty
17
+ end
18
+
19
+ it "has a pattern" do
20
+ expect(entry.regex.source).to eq(".*\\(iSeries\\).*")
21
+ end
22
+
23
+ it "has no params" do
24
+ expect(entry.params).to be_empty
25
+ end
26
+
27
+ it "has no tests" do
28
+ expect(entry.tests).to be_empty
29
+ end
30
+ end
31
+
32
+ context "with params" do
33
+ subject(:entry) { described_class.new(xml_file).fingerprints[1] }
34
+
35
+ it "has a name" do
36
+ expect(entry.name).to eq('PalmOS')
37
+ end
38
+
39
+ it "has a pattern" do
40
+ expect(entry.regex.source).to eq(".*\\(PalmOS\\).*")
41
+ end
42
+
43
+ it "has params" do
44
+ expect(entry.params).to eq({"os.vendor"=>[1, "Palm"], "os.device"=>[2, "General"]})
45
+ end
46
+
47
+ it "has no tests" do
48
+ expect(entry.tests).to be_empty
49
+ end
50
+ end
51
+
52
+ context "with pattern flags" do
53
+ subject(:entry) { described_class.new(xml_file).fingerprints[2] }
54
+
55
+ it "has a name and only uses the first value" do
56
+ expect(entry.name).to eq('HP Designjet printer')
57
+ end
58
+
59
+ it 'creates a Regexp with expected flags' do
60
+ expect(entry.regex).to be_a(Regexp)
61
+ expect(entry.regex.options).to eq(Recog::Fingerprint::RegexpFactory::DEFAULT_FLAGS | Regexp::IGNORECASE)
62
+ end
63
+
64
+ it "has a pattern" do
65
+ expect(entry.regex).to be_a(Regexp)
66
+ expect(entry.regex.source).to eq("(designjet \\S+)")
67
+ end
68
+
69
+ it "has params" do
70
+ expect(entry.params).to eq({"service.vendor"=>[0, "HP"]})
71
+ end
72
+
73
+ it "has no tests" do
74
+ expect(entry.tests).to be_empty
75
+ end
76
+ end
77
+
78
+ context "with test" do
79
+ subject(:entry) { described_class.new(xml_file).fingerprints[3] }
80
+
81
+ it "has a name" do
82
+ expect(entry.name).to eq('HP JetDirect Printer')
83
+ end
84
+
85
+ it "has a pattern" do
86
+ expect(entry.regex.source).to eq("laserjet (.*)(?: series)?")
87
+ end
88
+
89
+ it "has params" do
90
+ expect(entry.params).to eq({"service.vendor"=>[0, "HP"]})
91
+ end
92
+
93
+ it "has no tests" do
94
+ expect(entry.tests.map(&:content)).to match_array(["HP LaserJet 4100 Series", "HP LaserJet 2200"])
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,73 @@
1
+
2
+ require 'recog/fingerprint/regexp_factory'
3
+
4
+ describe Recog::Fingerprint::RegexpFactory do
5
+
6
+ describe 'FLAG_MAP' do
7
+ subject { described_class::FLAG_MAP }
8
+
9
+ it "should have the right number of flags" do
10
+ expect(subject.size).to be 5
11
+ end
12
+ end
13
+
14
+ describe '.build' do
15
+ subject { described_class.build(pattern, options) }
16
+
17
+ let(:pattern) { 'Apache/(\d+)' }
18
+ let(:options) { [ 'REG_ICASE' ] }
19
+
20
+ it { is_expected.to be_a(Regexp) }
21
+ it { is_expected.to match('Apache/2') }
22
+
23
+ end
24
+
25
+ describe '.build_options' do
26
+ subject { described_class.build_options(flags) }
27
+
28
+ let(:flags) { [ ] }
29
+ it { is_expected.to be_a(Integer) }
30
+
31
+ context 'without any explicit flags' do
32
+ let(:flags) { [ ] }
33
+ specify "sets default flags" do
34
+ expect(subject).to be Recog::Fingerprint::RegexpFactory::DEFAULT_FLAGS
35
+ end
36
+ end
37
+
38
+ context 'with REG_ICASE' do
39
+ let(:flags) { [ 'REG_ICASE' ] }
40
+ specify "sets IGNORECASE" do
41
+ expect(subject).to be (Recog::Fingerprint::RegexpFactory::DEFAULT_FLAGS | Regexp::IGNORECASE)
42
+ end
43
+ end
44
+
45
+ context 'with REG_DOT_NEWLINE' do
46
+ let(:flags) { [ 'REG_DOT_NEWLINE' ] }
47
+ specify "sets MULTILINE" do
48
+ expect(subject).to be (Recog::Fingerprint::RegexpFactory::DEFAULT_FLAGS | Regexp::MULTILINE)
49
+ end
50
+ end
51
+
52
+ context 'with REG_LINE_ANY_CRLF' do
53
+ let(:flags) { [ 'REG_LINE_ANY_CRLF' ] }
54
+ specify "sets MULTILINE" do
55
+ expect(subject).to be (Recog::Fingerprint::RegexpFactory::DEFAULT_FLAGS | Regexp::MULTILINE)
56
+ end
57
+ end
58
+
59
+ context 'with multiple flags' do
60
+ let(:flags) { [ 'REG_LINE_ANY_CRLF', 'REG_ICASE' ] }
61
+ specify "sets correct flags" do
62
+ expect(subject).to be (Recog::Fingerprint::RegexpFactory::DEFAULT_FLAGS | Regexp::MULTILINE | Regexp::IGNORECASE)
63
+ end
64
+ end
65
+
66
+ context 'with invalid flags' do
67
+ let(:flags) { %w(SYN ACK FIN) } # oh, wrong flags!
68
+ specify 'raises and lists supported/unsupported flags' do
69
+ expect { subject }.to raise_error(/SYN,ACK,FIN. Must be one of: .+/)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,112 @@
1
+ require 'nokogiri'
2
+ require 'recog/fingerprint'
3
+
4
+ describe Recog::Fingerprint do
5
+ context "whitespace" do
6
+ let(:xml) do
7
+ path = File.expand_path(File.join('spec', 'data', 'whitespaced_fingerprint.xml'))
8
+ doc = Nokogiri::XML(IO.read(path))
9
+ doc.xpath("//fingerprint").first
10
+ end
11
+ subject { Recog::Fingerprint.new(xml) }
12
+
13
+ describe "#name" do
14
+ it "properly squashes whitespace" do
15
+ expect(subject.name).to eq('I love whitespace!')
16
+ end
17
+ end
18
+ end
19
+
20
+ describe "#verification" do
21
+ let(:xml_file) { File.expand_path(File.join('spec', 'data', 'verification_fingerprints.xml')) }
22
+ let(:doc) { Nokogiri::XML(IO.read(xml_file)) }
23
+
24
+ context "0 params" do
25
+ let(:entry) { described_class.new(doc.xpath("//fingerprints/fingerprint")[0]) }
26
+
27
+ it "does not yield if a fingerprint has 0 parameters" do
28
+ expect { |unused| entry.verify_tests_have_capture_groups(&unused) }.not_to yield_control
29
+ end
30
+ end
31
+
32
+ context "0 capture groups" do
33
+ let(:entry) { described_class.new(doc.xpath("//fingerprints/fingerprint")[1]) }
34
+
35
+ it "does not yield if a fingerprint has parameters, but 0 are defined by a capture group " do
36
+ expect { |unused| entry.verify_tests_have_capture_groups(&unused) }.not_to yield_control
37
+ end
38
+ end
39
+
40
+ context "0 examples" do
41
+ let(:entry) { described_class.new(doc.xpath("//fingerprints/fingerprint")[2]) }
42
+
43
+ it "does not yield if a fingerprint has 0 examples" do
44
+ expect { |unused| entry.verify_tests_have_capture_groups(&unused) }.not_to yield_control
45
+ end
46
+ end
47
+
48
+ context "1 capture group, 1 example" do
49
+
50
+ let(:entry) { described_class.new(doc.xpath("//fingerprints/fingerprint")[3]) }
51
+
52
+ it "does not yield when one capture group parameter is tested for in one example" do
53
+ expect { |unused| entry.verify_tests_have_capture_groups(&unused) }.not_to yield_control
54
+ end
55
+ end
56
+
57
+ context "2 capture groups, 1 example" do
58
+ let(:entry) { described_class.new(doc.xpath("//fingerprints/fingerprint")[4]) }
59
+
60
+ it "does not yield when two capture group parameters are tested for in one example" do
61
+ expect { |unused| entry.verify_tests_have_capture_groups(&unused) }.not_to yield_control
62
+ end
63
+ end
64
+
65
+ context "2 capture groups, 2 examples, 1 in each" do
66
+ let(:entry) { described_class.new(doc.xpath("//fingerprints/fingerprint")[5]) }
67
+
68
+ it "does not yield when two capture group parameters are tested for in two examples, one parameter in each" do
69
+ expect { |unused| entry.verify_tests_have_capture_groups(&unused) }.not_to yield_control
70
+ end
71
+ end
72
+
73
+ context "1 missing capture group, 1 example" do
74
+
75
+ let(:entry) { described_class.new(doc.xpath("//fingerprints/fingerprint")[6]) }
76
+
77
+ it "identifies when a parameter defined by a capture group is not included in one example" do
78
+ expect { |unused| entry.verify_tests_have_capture_groups(&unused) }.to yield_successive_args([:warn, String])
79
+ end
80
+ end
81
+
82
+ context "2 missing capture groups, 1 example" do
83
+ let(:entry) { described_class.new(doc.xpath("//fingerprints/fingerprint")[7]) }
84
+
85
+ it "identifies when two parameters defined by a capture groups are not included in one example" do
86
+ expect { |unused| entry.verify_tests_have_capture_groups(&unused) }.to yield_successive_args([:warn, String], [:warn, String])
87
+ end
88
+ end
89
+
90
+ context "1 missing capture group, 2 examples" do
91
+
92
+ let(:entry) { described_class.new(doc.xpath("//fingerprints/fingerprint")[8]) }
93
+
94
+ it "identifies when a parameter defined by a capture group is not included in one example" do
95
+ expect { |unused| entry.verify_tests_have_capture_groups(&unused) }.to yield_successive_args([:warn, String])
96
+ end
97
+ end
98
+
99
+ context "2 missing capture groups, 2 examples" do
100
+ let(:entry) { described_class.new(doc.xpath("//fingerprints/fingerprint")[9]) }
101
+
102
+ it "identifies when two parameters defined by a capture groups are not included in one example" do
103
+ expect { |unused| entry.verify_tests_have_capture_groups(&unused) }.to yield_successive_args([:warn, String], [:warn, String])
104
+ end
105
+ end
106
+
107
+ end
108
+
109
+ skip "value interpolation" do
110
+ # TODO
111
+ end
112
+ end