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,31 @@
1
+ module Recog
2
+ class DBManager
3
+ require 'nokogiri'
4
+ require_relative 'db'
5
+
6
+ attr_accessor :path, :databases
7
+
8
+ DefaultDatabasePath = File.expand_path( File.join( File.dirname(__FILE__), "..", "..", "xml") )
9
+
10
+ def initialize(path = DefaultDatabasePath)
11
+ self.path = path
12
+ reload
13
+ end
14
+
15
+ def load_databases
16
+ if File.directory?(self.path)
17
+ Dir[self.path + "/*.xml"].each do |dbxml|
18
+ self.databases << DB.new(dbxml)
19
+ end
20
+ else
21
+ self.databases << DB.new(self.path)
22
+ end
23
+ end
24
+
25
+ def reload
26
+ self.databases = []
27
+ load_databases
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,280 @@
1
+ module Recog
2
+
3
+ # A fingerprint that can be {#match matched} against a particular kind of
4
+ # fingerprintable data, e.g. an HTTP `Server` header
5
+ class Fingerprint
6
+ require_relative 'fingerprint/regexp_factory'
7
+ require_relative 'fingerprint/test'
8
+
9
+ # A human readable name describing this fingerprint
10
+ # @return (see #parse_description)
11
+ attr_reader :name
12
+
13
+ # Regular expression pulled from the {DB} xml file.
14
+ #
15
+ # @see #create_regexp
16
+ # @return [Regexp] the Regexp to try when calling {#match}
17
+ attr_reader :regex
18
+
19
+ # Collection of indexes for capture groups created by {#match}
20
+ #
21
+ # @return (see #parse_params)
22
+ attr_reader :params
23
+
24
+ # Collection of example strings that should {#match} our {#regex}
25
+ #
26
+ # @return (see #parse_examples)
27
+ attr_reader :tests
28
+
29
+ # @param xml [Nokogiri::XML::Element]
30
+ # @param match_key [String] See Recog::DB
31
+ # @param protocol [String] Protocol such as ftp, mssql, http, etc.
32
+ def initialize(xml, match_key=nil, protocol=nil)
33
+ @match_key = match_key
34
+ @protocol = protocol
35
+ @name = parse_description(xml)
36
+ @regex = create_regexp(xml)
37
+ @params = {}
38
+ @tests = []
39
+
40
+ @protocol.downcase! if @protocol
41
+ parse_examples(xml)
42
+ parse_params(xml)
43
+ end
44
+
45
+ def output_diag_data(message, data, exception)
46
+ STDERR.puts message
47
+ STDERR.puts exception.inspect
48
+ STDERR.puts "Length: #{data.length}"
49
+ STDERR.puts "Encoding: #{data.encoding}"
50
+ STDERR.puts "Problematic data:\n#{data}"
51
+ STDERR.puts "Raw bytes:\n#{data.pretty_inspect}\n"
52
+ end
53
+
54
+ # Attempt to match the given string.
55
+ #
56
+ # @param match_string [String]
57
+ # @return [Hash,nil] Keys will be host, service, and os attributes
58
+ def match(match_string)
59
+ # match_string.force_encoding('BINARY') if match_string
60
+ begin
61
+ match_data = @regex.match(match_string)
62
+ rescue Encoding::CompatibilityError => e
63
+ begin
64
+ # Replace invalid UTF-8 characters with spaces, just as DAP does.
65
+ encoded_str = match_string.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => '')
66
+ match_data = @regex.match(encoded_str)
67
+ rescue Exception => e
68
+ output_diag_data('Exception while re-encoding match_string to UTF-8', match_string, e)
69
+ end
70
+ rescue Exception => e
71
+ output_diag_data('Exception while running regex against match_string', match_string, e)
72
+ end
73
+ return if match_data.nil?
74
+
75
+ result = { 'matched' => @name }
76
+ replacements = {}
77
+ @params.each_pair do |k,v|
78
+ pos = v[0]
79
+ if pos == 0
80
+ # A match offset of 0 means this param has a hardcoded value
81
+ result[k] = v[1]
82
+ # if this value uses interpolation, note it for handling later
83
+ v[1].scan(/\{([^\s{}]+)\}/).flatten.each do |replacement|
84
+ replacements[k] ||= Set[]
85
+ replacements[k] << replacement
86
+ end
87
+ else
88
+ # A match offset other than 0 means the value should come from
89
+ # the corresponding match result index
90
+ result[k] = match_data[ pos ]
91
+ end
92
+ end
93
+
94
+ # Use the protocol specified in the XML database if there isn't one
95
+ # provided as part of this fingerprint.
96
+ if @protocol
97
+ unless result['service.protocol']
98
+ result['service.protocol'] = @protocol
99
+ end
100
+ end
101
+
102
+ result['fingerprint_db'] = @match_key if @match_key
103
+
104
+ # for everything identified as using interpolation, do so
105
+ replacements.each_pair do |replacement_k, replacement_vs|
106
+ replacement_vs.each do |replacement|
107
+ if result[replacement]
108
+ result[replacement_k] = result[replacement_k].gsub(/\{#{replacement}\}/, result[replacement])
109
+ else
110
+ # if the value uses an interpolated value that does not exist, in general this could be
111
+ # very bad, but over time we have allowed the use of regexes with
112
+ # optional captures that are then used for parts of the asserted
113
+ # fingerprints. This is frequently done for optional version
114
+ # strings. If the key in question is cpe23 and the interpolated
115
+ # value we are trying to replace is version related, use the CPE
116
+ # standard of '-' for the version, otherwise raise and exception as
117
+ # this code currently does not handle interpolation of undefined
118
+ # values in other cases.
119
+ if replacement_k =~ /\.cpe23$/ and replacement =~ /\.version$/
120
+ result[replacement_k] = result[replacement_k].gsub(/\{#{replacement}\}/, '-')
121
+ else
122
+ raise "Invalid use of nil interpolated non-version value #{replacement} in non-cpe23 fingerprint param #{replacement_k}"
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ return result
129
+ end
130
+
131
+ # Ensure all the {#params} are valid
132
+ #
133
+ # @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
134
+ # indicate whether a param is valid
135
+ # @yieldparam message [String] A human-readable string explaining the
136
+ # `status`
137
+ def verify_params(&block)
138
+ return if params.empty?
139
+ params.each do |param_name, pos_value|
140
+ pos, value = pos_value
141
+ if pos > 0 && !value.to_s.empty?
142
+ yield :fail, "'#{@name}'s #{param_name} is a non-zero pos but specifies a value of '#{value}'"
143
+ elsif pos == 0 && value.to_s.empty?
144
+ yield :fail, "'#{@name}'s #{param_name} is not a capture (pos=0) but doesn't specify a value"
145
+ end
146
+ end
147
+ end
148
+
149
+ # Ensure all the {#tests} actually match the fingerprint and return the
150
+ # expected capture groups.
151
+ #
152
+ # @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
153
+ # indicate whether a test worked
154
+ # @yieldparam message [String] A human-readable string explaining the
155
+ # `status`
156
+ def verify_tests(&block)
157
+
158
+ # look for the presence of test cases
159
+ if tests.size == 0
160
+ yield :warn, "'#{@name}' has no test cases"
161
+ end
162
+
163
+ # make sure each test case passes
164
+ tests.each do |test|
165
+ result = match(test.content)
166
+ if result.nil?
167
+ yield :fail, "'#{@name}' failed to match #{test.content.inspect} with #{@regex}'"
168
+ next
169
+ end
170
+
171
+ message = test
172
+ status = :success
173
+ # Ensure that all the attributes as provided by the example were parsed
174
+ # out correctly and match the capture group values we expect.
175
+ test.attributes.each do |k, v|
176
+ next if k == '_encoding'
177
+ if !result.has_key?(k) || result[k] != v
178
+ message = "'#{@name}' failed to find expected capture group #{k} '#{v}'. Result was #{result[k]}"
179
+ status = :fail
180
+ break
181
+ end
182
+ end
183
+ yield status, message
184
+ end
185
+
186
+ # make sure there are capture groups for all params that use them
187
+ verify_tests_have_capture_groups(&block)
188
+ end
189
+
190
+ # For fingerprints that specify parameters that are defined by
191
+ # capture groups, ensure that each parameter has at least one test
192
+ # that defines an attribute to test for the correct capture of that
193
+ # parameter.
194
+ #
195
+ # @yieldparam status [Symbol] One of `:warn`, `:fail`, or `:success` to
196
+ # indicate whether a test worked
197
+ # @yieldparam message [String] A human-readable string explaining the
198
+ # `status`
199
+ def verify_tests_have_capture_groups(&block)
200
+ capture_group_used = {}
201
+ if !params.empty?
202
+ # get a list of parameters that are defined by capture groups
203
+ params.each do |param_name, pos_value|
204
+ pos, value = pos_value
205
+ if pos > 0 && value.to_s.empty?
206
+ capture_group_used[param_name] = false
207
+ end
208
+ end
209
+ end
210
+
211
+ # match up the fingerprint parameters with test attributes
212
+ tests.each do |test|
213
+ test.attributes.each do |k,v|
214
+ if capture_group_used.has_key?(k)
215
+ capture_group_used[k] = true
216
+ end
217
+ end
218
+ end
219
+
220
+ # alert on untested parameters
221
+ capture_group_used.each do |param_name, param_used|
222
+ if !param_used
223
+ message = "'#{@name}' is missing an example that checks for parameter '#{param_name}' " +
224
+ "messsage which is derived from a capture group"
225
+ yield :warn, message
226
+ end
227
+ end
228
+ end
229
+
230
+ private
231
+
232
+ # @param xml [Nokogiri::XML::Element]
233
+ # @return [Regexp]
234
+ def create_regexp(xml)
235
+ pattern = xml['pattern']
236
+ flags = xml['flags'].to_s.split(',')
237
+ RegexpFactory.build(pattern, flags)
238
+ end
239
+
240
+ # @param xml [Nokogiri::XML::Element]
241
+ # @return [String] Contents of the source XML's `description` tag
242
+ def parse_description(xml)
243
+ element = xml.xpath('description')
244
+ element.empty? ? '' : element.first.content.to_s.gsub(/\s+/, ' ').strip
245
+ end
246
+
247
+ # @param xml [Nokogiri::XML::Element]
248
+ # @return [void]
249
+ def parse_examples(xml)
250
+ elements = xml.xpath('example')
251
+
252
+ elements.each do |elem|
253
+ # convert nokogiri Attributes into a hash of name => value
254
+ attrs = elem.attributes.values.reduce({}) { |a,e| a.merge(e.name => e.value) }
255
+ @tests << Test.new(elem.content, attrs)
256
+ end
257
+
258
+ nil
259
+ end
260
+
261
+ # @param xml [Nokogiri::XML::Element]
262
+ # @return [Hash<String,Array>] Keys are things like `"os.name"`, values are a two
263
+ # element Array. The first element is an index for the capture group that returns
264
+ # that thing. If the index is 0, the second element is a static value for
265
+ # that thing; otherwise it is undefined.
266
+ def parse_params(xml)
267
+ @params = {}.tap do |h|
268
+ xml.xpath('param').each do |param|
269
+ name = param['name']
270
+ pos = param['pos'].to_i
271
+ value = param['value'].to_s
272
+ h[name] = [pos, value]
273
+ end
274
+ end
275
+
276
+ nil
277
+ end
278
+
279
+ end
280
+ end
@@ -0,0 +1,56 @@
1
+
2
+ module Recog
3
+ class Fingerprint
4
+
5
+ #
6
+ # @example
7
+ # r = RegexpFactory.build("^Apache[ -]Coyote/(\d\.\d)$", "REG_ICASE")
8
+ # r.match("Apache-Coyote/1.1")
9
+ #
10
+ module RegexpFactory
11
+
12
+ # Currently, only options relating to case insensitivity and
13
+ # multiline/newline are supported. Because Recog's data is used by tools
14
+ # written in different languages like Ruby and Java, we currently support
15
+ # specifying them in a variety of ways. This map controls how they can
16
+ # be specified.
17
+ #
18
+ # TODO: consider supporting only a simpler variant and require that tools
19
+ # that use Recog data translate accordingly
20
+ FLAG_MAP = {
21
+ # multiline variations
22
+ 'REG_DOT_NEWLINE' => Regexp::MULTILINE,
23
+ 'REG_LINE_ANY_CRLF' => Regexp::MULTILINE,
24
+ 'REG_MULTILINE' => Regexp::MULTILINE,
25
+ # case variations
26
+ 'REG_ICASE' => Regexp::IGNORECASE,
27
+ 'IGNORECASE' => Regexp::IGNORECASE
28
+ }
29
+
30
+ DEFAULT_FLAGS = 0
31
+
32
+ # @return [Regexp]
33
+ def self.build(pattern, flags)
34
+ options = build_options(flags)
35
+ Regexp.new(pattern, options)
36
+ end
37
+
38
+ # Convert string flag names as used in Recog XML into a Fixnum suitable for
39
+ # passing as the `options` parameter to `Regexp.new`
40
+ #
41
+ # @see FLAG_MAP
42
+ # @param flags [Array<String>]
43
+ # @return [Fixnum] Flags for creating a regular expression object
44
+ def self.build_options(flags)
45
+ unsupported_flags = flags.select { |flag| !FLAG_MAP.key?(flag) }
46
+ unless unsupported_flags.empty?
47
+ fail "Unsupported regular expression flags found: #{unsupported_flags.join(',')}. Must be one of: #{FLAG_MAP.keys.join(',')}"
48
+ end
49
+ flags.reduce(DEFAULT_FLAGS) do |sum, flag|
50
+ sum |= (FLAG_MAP[flag] || 0)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
@@ -0,0 +1,18 @@
1
+
2
+ class Recog::Fingerprint::Test
3
+ attr_accessor :content
4
+ attr_accessor :attributes
5
+ def initialize(content, attributes=[])
6
+ @attributes = attributes
7
+
8
+ if @attributes['_encoding'] && @attributes['_encoding'] == 'base64'
9
+ @content = content.to_s.unpack('m*').first
10
+ else
11
+ @content = content
12
+ end
13
+ end
14
+
15
+ def to_s
16
+ content
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ module Recog
2
+ class Formatter
3
+ COLORS = {
4
+ :red => 31,
5
+ :yellow => 33,
6
+ :green => 32,
7
+ :white => 15
8
+ }
9
+
10
+ attr_reader :options, :output
11
+
12
+ def initialize(options, output)
13
+ @options = options
14
+ @output = output || StringIO.new
15
+ end
16
+
17
+ def status_message(text)
18
+ output.puts color(text, :white)
19
+ end
20
+
21
+ def success_message(text)
22
+ output.puts color(text, :green)
23
+ end
24
+
25
+ def warning_message(text)
26
+ output.puts color(text, :yellow)
27
+ end
28
+
29
+ def failure_message(text)
30
+ output.puts color(text, :red)
31
+ end
32
+
33
+ private
34
+
35
+ def color_enabled?
36
+ options.color
37
+ end
38
+
39
+ def color(text, color_code)
40
+ color_enabled? ? colorize(text, color_code) : text
41
+ end
42
+
43
+ def colorize(text, color_code)
44
+ "\e[#{color_code_for(color_code)}m#{text}\e[0m"
45
+ end
46
+
47
+ def color_code_for(code)
48
+ COLORS.fetch(code) { COLORS.fetch(:white) }
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,77 @@
1
+ module Recog
2
+ class MatchReporter
3
+ attr_reader :formatter
4
+ attr_reader :line_count, :match_count, :fail_count
5
+
6
+ def initialize(options, formatter)
7
+ @options = options
8
+ @formatter = formatter
9
+ reset_counts
10
+ end
11
+
12
+ def report
13
+ reset_counts
14
+ yield self
15
+ summarize unless @options.quiet
16
+ end
17
+
18
+ def stop?
19
+ return false unless @options.fail_fast
20
+ @fail_count >= @options.stop_after
21
+ end
22
+
23
+ def increment_line_count
24
+ @line_count += 1
25
+ end
26
+
27
+ def match(text)
28
+ @match_count += 1
29
+ formatter.success_message(text)
30
+ end
31
+
32
+ def failure(text)
33
+ @fail_count += 1
34
+ formatter.failure_message(text)
35
+ end
36
+
37
+ def print_summary
38
+ colorize_summary(summary_line)
39
+ end
40
+
41
+ private
42
+
43
+ def reset_counts
44
+ @line_count = @match_count = @fail_count = 0
45
+ end
46
+
47
+ def detail?
48
+ @options.detail
49
+ end
50
+
51
+ def summarize
52
+ if detail?
53
+ print_lines_processed
54
+ print_summary
55
+ end
56
+ end
57
+
58
+ def print_lines_processed
59
+ formatter.status_message("\nProcessed #{line_count} lines")
60
+ end
61
+
62
+ def summary_line
63
+ summary = "SUMMARY: "
64
+ summary << "#{match_count} matches"
65
+ summary << " and #{fail_count} failures"
66
+ summary
67
+ end
68
+
69
+ def colorize_summary(summary)
70
+ if @fail_count > 0
71
+ formatter.failure_message(summary)
72
+ else
73
+ formatter.success_message(summary)
74
+ end
75
+ end
76
+ end
77
+ end