tarantula-rails3 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/CHANGELOG +49 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +161 -0
  4. data/Rakefile +83 -0
  5. data/VERSION.yml +4 -0
  6. data/examples/example_helper.rb +57 -0
  7. data/examples/relevance/core_extensions/ellipsize_example.rb +19 -0
  8. data/examples/relevance/core_extensions/file_example.rb +8 -0
  9. data/examples/relevance/core_extensions/response_example.rb +29 -0
  10. data/examples/relevance/core_extensions/test_case_example.rb +20 -0
  11. data/examples/relevance/tarantula/attack_handler_example.rb +29 -0
  12. data/examples/relevance/tarantula/basic_attack_example.rb +12 -0
  13. data/examples/relevance/tarantula/crawler_example.rb +375 -0
  14. data/examples/relevance/tarantula/form_example.rb +50 -0
  15. data/examples/relevance/tarantula/form_submission_example.rb +171 -0
  16. data/examples/relevance/tarantula/html_document_handler_example.rb +43 -0
  17. data/examples/relevance/tarantula/html_report_helper_example.rb +46 -0
  18. data/examples/relevance/tarantula/html_reporter_example.rb +82 -0
  19. data/examples/relevance/tarantula/invalid_html_handler_example.rb +33 -0
  20. data/examples/relevance/tarantula/io_reporter_example.rb +11 -0
  21. data/examples/relevance/tarantula/link_example.rb +84 -0
  22. data/examples/relevance/tarantula/log_grabber_example.rb +26 -0
  23. data/examples/relevance/tarantula/rails_integration_proxy_example.rb +88 -0
  24. data/examples/relevance/tarantula/result_example.rb +85 -0
  25. data/examples/relevance/tarantula/tidy_handler_example.rb +58 -0
  26. data/examples/relevance/tarantula/transform_example.rb +20 -0
  27. data/examples/relevance/tarantula_example.rb +23 -0
  28. data/laf/images/header_bg.jpg +0 -0
  29. data/laf/images/logo.png +0 -0
  30. data/laf/images/tagline.png +0 -0
  31. data/laf/javascripts/jquery-1.2.3.js +3408 -0
  32. data/laf/javascripts/jquery-ui-tabs.js +890 -0
  33. data/laf/javascripts/jquery.tablesorter.js +861 -0
  34. data/laf/javascripts/tarantula.js +10 -0
  35. data/laf/stylesheets/tarantula.css +346 -0
  36. data/lib/relevance/core_extensions/ellipsize.rb +34 -0
  37. data/lib/relevance/core_extensions/file.rb +9 -0
  38. data/lib/relevance/core_extensions/metaclass.rb +78 -0
  39. data/lib/relevance/core_extensions/response.rb +9 -0
  40. data/lib/relevance/core_extensions/string_chars_fix.rb +11 -0
  41. data/lib/relevance/core_extensions/test_case.rb +19 -0
  42. data/lib/relevance/tarantula.rb +58 -0
  43. data/lib/relevance/tarantula/attack.rb +18 -0
  44. data/lib/relevance/tarantula/attack_handler.rb +37 -0
  45. data/lib/relevance/tarantula/basic_attack.rb +40 -0
  46. data/lib/relevance/tarantula/crawler.rb +254 -0
  47. data/lib/relevance/tarantula/detail.html.erb +81 -0
  48. data/lib/relevance/tarantula/form.rb +23 -0
  49. data/lib/relevance/tarantula/form_submission.rb +88 -0
  50. data/lib/relevance/tarantula/html_document_handler.rb +36 -0
  51. data/lib/relevance/tarantula/html_report_helper.rb +39 -0
  52. data/lib/relevance/tarantula/html_reporter.rb +105 -0
  53. data/lib/relevance/tarantula/index.html.erb +37 -0
  54. data/lib/relevance/tarantula/invalid_html_handler.rb +18 -0
  55. data/lib/relevance/tarantula/io_reporter.rb +34 -0
  56. data/lib/relevance/tarantula/link.rb +94 -0
  57. data/lib/relevance/tarantula/log_grabber.rb +16 -0
  58. data/lib/relevance/tarantula/rails_integration_proxy.rb +68 -0
  59. data/lib/relevance/tarantula/recording.rb +12 -0
  60. data/lib/relevance/tarantula/response.rb +13 -0
  61. data/lib/relevance/tarantula/result.rb +77 -0
  62. data/lib/relevance/tarantula/test_report.html.erb +32 -0
  63. data/lib/relevance/tarantula/tidy_handler.rb +32 -0
  64. data/lib/relevance/tarantula/transform.rb +17 -0
  65. data/lib/relevance/tasks/tarantula_tasks.rake +42 -0
  66. data/lib/tarantula-rails3.rb +9 -0
  67. data/template/tarantula_test.rb +22 -0
  68. metadata +164 -0
@@ -0,0 +1,9 @@
1
+ # dynamically mixed in to response objects
2
+ module Relevance::CoreExtensions::Response
3
+ def html?
4
+ # some versions of Rails integration tests don't set content type
5
+ # so we are treating nil as html. A better fix would be welcome here.
6
+ ((content_type =~ %r{^text/html}) != nil) || content_type == nil
7
+ end
8
+ end
9
+
@@ -0,0 +1,11 @@
1
+ if RUBY_VERSION == "1.8.7" # fix interaction between Ruby 187 and Rails 202, so we can at least run the test suite on that combination
2
+ unless '1.9'.respond_to?(:force_encoding)
3
+ String.class_eval do
4
+ begin
5
+ remove_method :chars
6
+ rescue NameError
7
+ # OK
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ require 'action_dispatch/testing/integration'
2
+
3
+ module Relevance::CoreExtensions::TestCaseExtensions
4
+
5
+ def tarantula_crawl(integration_test, options = {})
6
+ url = options[:url] || "/"
7
+ t = tarantula_crawler(integration_test, options)
8
+ t.crawl url
9
+ end
10
+
11
+ def tarantula_crawler(integration_test, options = {})
12
+ Relevance::Tarantula::RailsIntegrationProxy.rails_integration_test(integration_test, options)
13
+ end
14
+
15
+ end
16
+
17
+ if defined? ActionController::IntegrationTest
18
+ ActionController::IntegrationTest.class_eval { include Relevance::CoreExtensions::TestCaseExtensions }
19
+ end
@@ -0,0 +1,58 @@
1
+ TARANTULA_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "../.."))
2
+
3
+ require 'forwardable'
4
+ require 'erb'
5
+ require 'active_support'
6
+ require 'action_controller'
7
+ # bringing in xss-shield requires a bunch of other dependencies
8
+ # still not certain about this, if it ruins your world please let me know
9
+ #xss_shield_path = File.join(TARANTULA_ROOT, %w{vendor xss-shield})
10
+ #$: << File.join(xss_shield_path, "lib")
11
+ #require File.join(xss_shield_path, "init")
12
+
13
+ require 'htmlentities'
14
+
15
+ module Relevance; end
16
+ module Relevance; module CoreExtensions; end; end
17
+ module Relevance
18
+ module Tarantula
19
+ def tarantula_home
20
+ File.expand_path(File.join(File.dirname(__FILE__), "../.."))
21
+ end
22
+ def log(msg)
23
+ puts msg if verbose
24
+ end
25
+ def rails_root
26
+ ::Rails.root.to_s
27
+ end
28
+ def verbose
29
+ ENV["VERBOSE"]
30
+ end
31
+ end
32
+ end
33
+
34
+ require File.expand_path(File.join(File.dirname(__FILE__), "core_extensions", "test_case"))
35
+ require File.expand_path(File.join(File.dirname(__FILE__), "core_extensions", "ellipsize"))
36
+ require File.expand_path(File.join(File.dirname(__FILE__), "core_extensions", "file"))
37
+ require File.expand_path(File.join(File.dirname(__FILE__), "core_extensions", "response"))
38
+ require File.expand_path(File.join(File.dirname(__FILE__), "core_extensions", "metaclass"))
39
+ require File.expand_path(File.join(File.dirname(__FILE__), "core_extensions", "string_chars_fix"))
40
+
41
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "html_reporter"))
42
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "html_report_helper"))
43
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "io_reporter"))
44
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "recording"))
45
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "response"))
46
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "result"))
47
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "log_grabber"))
48
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "invalid_html_handler"))
49
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "transform"))
50
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "crawler"))
51
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "basic_attack"))
52
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "form"))
53
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "form_submission"))
54
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "attack"))
55
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "attack_handler"))
56
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "link"))
57
+
58
+ require File.expand_path(File.join(File.dirname(__FILE__), "tarantula", "tidy_handler")) if ENV['TIDY_PATH']
@@ -0,0 +1,18 @@
1
+ class Relevance::Tarantula::Attack
2
+ HASHABLE_ATTRS = [:name, :input, :output, :description]
3
+ attr_accessor *HASHABLE_ATTRS
4
+ def initialize(hash)
5
+ hash.each do |k,v|
6
+ raise ArgumentError, k unless HASHABLE_ATTRS.member?(k)
7
+ self.instance_variable_set("@#{k}", v)
8
+ end
9
+ end
10
+ def ==(other)
11
+ Relevance::Tarantula::Attack === other && HASHABLE_ATTRS.all? { |attr| send(attr) == other.send(attr)}
12
+ end
13
+ def input(input_field=nil)
14
+ @input
15
+ end
16
+ end
17
+
18
+
@@ -0,0 +1,37 @@
1
+ require 'hpricot'
2
+
3
+ class Relevance::Tarantula::AttackHandler
4
+ include ERB::Util
5
+
6
+ def attacks
7
+ Relevance::Tarantula::FormSubmission.attacks.select(&:output)
8
+ end
9
+
10
+ def handle(result)
11
+ return unless attacks.size > 0
12
+ regexp = '(' + attacks.map {|a| Regexp.escape a.output}.join('|') + ')'
13
+ response = result.response
14
+ return unless response.html?
15
+ if n = (response.body =~ /#{regexp}/)
16
+ error_result = result.dup
17
+ error_result.success = false
18
+ error_result.description = "XSS error found, match was: #{h($1)}"
19
+ error_result.data = <<-STR
20
+ ########################################################################
21
+ # Text around unescaped string: #{$1}
22
+ ########################################################################
23
+ #{response.body[[0, n - 200].max , 400]}
24
+
25
+
26
+
27
+
28
+
29
+ ########################################################################
30
+ # Attack information:
31
+ ########################################################################
32
+ #{attacks.select {|a| a.output == $1}[0].to_yaml}
33
+ STR
34
+ error_result
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ class Relevance::Tarantula::BasicAttack
2
+ ATTRS = [:name, :output, :description]
3
+
4
+ attr_reader *ATTRS
5
+
6
+ def initialize
7
+ @name = "Tarantula Basic Fuzzer"
8
+ @output = nil
9
+ @description = "Supplies purely random but simplistically generated form input."
10
+ end
11
+
12
+ def ==(other)
13
+ Relevance::Tarantula::BasicAttack === other && ATTRS.all? { |attr| send(attr) == other.send(attr)}
14
+ end
15
+
16
+ def input(input_field)
17
+ case input_field['name']
18
+ when /amount/ then random_int
19
+ when /_id$/ then random_whole_number
20
+ when /uploaded_data/ then nil
21
+ when nil then input['value']
22
+ else
23
+ random_int
24
+ end
25
+ end
26
+
27
+ def big_number
28
+ 10000 # arbitrary
29
+ end
30
+
31
+ def random_int
32
+ rand(big_number) - (big_number/2)
33
+ end
34
+
35
+ def random_whole_number
36
+ rand(big_number)
37
+ end
38
+ end
39
+
40
+
@@ -0,0 +1,254 @@
1
+ require 'active_record'
2
+ require 'active_record/base'
3
+ require File.expand_path(File.join(File.dirname(__FILE__), "rails_integration_proxy"))
4
+ require File.expand_path(File.join(File.dirname(__FILE__), "html_document_handler.rb"))
5
+
6
+ class Relevance::Tarantula::Crawler
7
+ extend Forwardable
8
+ include Relevance::Tarantula
9
+
10
+ class CrawlTimeout < RuntimeError; end
11
+
12
+ attr_accessor :proxy, :handlers, :skip_uri_patterns, :log_grabber,
13
+ :reporters, :crawl_queue, :links_queued,
14
+ :form_signatures_queued, :max_url_length, :response_code_handler,
15
+ :times_to_crawl, :fuzzers, :test_name, :crawl_timeout
16
+ attr_reader :transform_url_patterns, :referrers, :failures, :successes, :crawl_start_times, :crawl_end_times
17
+
18
+ def initialize
19
+ @max_url_length = 1024
20
+ @successes = []
21
+ @failures = []
22
+ @handlers = [@response_code_handler = Result]
23
+ @links_queued = Set.new
24
+ @form_signatures_queued = Set.new
25
+ @crawl_queue = []
26
+ @crawl_start_times, @crawl_end_times = [], []
27
+ @crawl_timeout = 20.minutes
28
+ @referrers = {}
29
+ @skip_uri_patterns = [
30
+ /^javascript/,
31
+ /^mailto/,
32
+ /^http/,
33
+ ]
34
+ self.transform_url_patterns = [
35
+ [/#.*$/, '']
36
+ ]
37
+ @reporters = [Relevance::Tarantula::IOReporter.new($stderr)]
38
+ @decoder = HTMLEntities.new
39
+ @times_to_crawl = 1
40
+ @fuzzers = [Relevance::Tarantula::FormSubmission]
41
+
42
+ @stdout_tty = $stdout.tty?
43
+ end
44
+
45
+ def method_missing(meth, *args)
46
+ super unless Result::ALLOW_NNN_FOR =~ meth.to_s
47
+ @response_code_handler.send(meth, *args)
48
+ end
49
+
50
+ def transform_url_patterns=(patterns)
51
+ @transform_url_patterns = patterns.map do |pattern|
52
+ Array === pattern ? Relevance::Tarantula::Transform.new(*pattern) : pattern
53
+ end
54
+ end
55
+
56
+ def crawl(url = "/")
57
+ orig_links_queued = @links_queued.dup
58
+ orig_form_signatures_queued = @form_signatures_queued.dup
59
+ orig_crawl_queue = @crawl_queue.dup
60
+ @times_to_crawl.times do |num|
61
+ queue_link url
62
+
63
+ begin
64
+ do_crawl num
65
+ rescue CrawlTimeout => e
66
+ puts
67
+ puts e.message
68
+ end
69
+
70
+ puts "#{(num+1).ordinalize} crawl" if @times_to_crawl > 1
71
+
72
+ if num + 1 < @times_to_crawl
73
+ @links_queued = orig_links_queued
74
+ @form_signatures_queued = orig_form_signatures_queued
75
+ @crawl_queue = orig_crawl_queue
76
+ @referrers = {}
77
+ end
78
+ end
79
+ rescue Interrupt
80
+ $stderr.puts "CTRL-C"
81
+ ensure
82
+ report_results
83
+ end
84
+
85
+ def finished?
86
+ @crawl_queue.empty?
87
+ end
88
+
89
+ def do_crawl(number)
90
+ while (!finished?)
91
+ @crawl_start_times << Time.now
92
+ crawl_the_queue(number)
93
+ @crawl_end_times << Time.now
94
+ end
95
+ end
96
+
97
+ def crawl_the_queue(number = 0)
98
+ while (request = @crawl_queue.pop)
99
+ request.crawl
100
+ blip(number)
101
+ end
102
+ end
103
+
104
+ def save_result(result)
105
+ reporters.each do |reporter|
106
+ reporter.report(result)
107
+ end
108
+ end
109
+
110
+ def handle_link_results(link, result)
111
+ handlers.each do |h|
112
+ begin
113
+ save_result h.handle(result)
114
+ rescue Exception => e
115
+ log "error handling #{link} #{e.message}"
116
+ # TODO: pass to results
117
+ end
118
+ end
119
+ end
120
+
121
+ def follow(method, url, data=nil)
122
+ proxy.send(method, url, data)
123
+ end
124
+
125
+ def submit(method, action, data)
126
+ proxy.send(method, action, data)
127
+ end
128
+
129
+ def elasped_time_for_pass(num)
130
+ Time.now - crawl_start_times[num]
131
+ end
132
+
133
+ def grab_log!
134
+ @log_grabber && @log_grabber.grab!
135
+ end
136
+
137
+ def make_result(options)
138
+ defaults = {
139
+ :log => grab_log!,
140
+ :test_name => test_name
141
+ }
142
+ Result.new(defaults.merge(options)).freeze
143
+ end
144
+
145
+ def handle_form_results(form, response)
146
+ handlers.each do |h|
147
+ save_result h.handle(Result.new(:method => form.method,
148
+ :url => form.action,
149
+ :response => response,
150
+ :log => grab_log!,
151
+ :referrer => form.action,
152
+ :data => form.data.inspect,
153
+ :test_name => test_name).freeze)
154
+ end
155
+ end
156
+
157
+ def should_skip_url?(url)
158
+ return true if url.blank?
159
+ if @skip_uri_patterns.any? {|pattern| pattern =~ url}
160
+ log "Skipping #{url}"
161
+ return true
162
+ end
163
+ if url.length > max_url_length
164
+ log "Skipping long url #{url}"
165
+ return true
166
+ end
167
+ end
168
+
169
+ def should_skip_link?(link)
170
+ should_skip_url?(link.href) || @links_queued.member?(link)
171
+ end
172
+
173
+ def should_skip_form_submission?(fs)
174
+ should_skip_url?(fs.action) || @form_signatures_queued.member?(fs.signature)
175
+ end
176
+
177
+ def transform_url(url)
178
+ return unless url
179
+ url = @decoder.decode(url)
180
+ @transform_url_patterns.each do |pattern|
181
+ url = pattern[url]
182
+ end
183
+ url
184
+ end
185
+
186
+ def queue_link(dest, referrer = nil)
187
+ dest = Link.new(dest, self, referrer)
188
+ return if should_skip_link?(dest)
189
+ @crawl_queue << dest
190
+ @links_queued << dest
191
+ dest
192
+ end
193
+
194
+ def queue_form(form, referrer = nil)
195
+ fuzzers.each do |fuzzer|
196
+ fuzzer.mutate(Form.new(form, self, referrer)).each do |fs|
197
+ # fs = fuzzer.new(Form.new(form, self, referrer))
198
+ fs.action = transform_url(fs.action)
199
+ return if should_skip_form_submission?(fs)
200
+ @referrers[fs.action] = referrer if referrer
201
+ @crawl_queue << fs
202
+ @form_signatures_queued << fs.signature
203
+ end
204
+ end
205
+ end
206
+
207
+ def report_dir
208
+ File.join(rails_root, "tmp", "tarantula")
209
+ end
210
+
211
+ def generate_reports
212
+ errors = []
213
+ reporters.each do |reporter|
214
+ begin
215
+ reporter.finish_report(test_name)
216
+ rescue RuntimeError => e
217
+ errors << e
218
+ end
219
+ end
220
+ unless errors.empty?
221
+ raise errors.map(&:message).join("\n")
222
+ end
223
+ end
224
+
225
+ def report_results
226
+ puts "Crawled #{total_links_count} links and forms."
227
+ generate_reports
228
+ end
229
+
230
+ def total_links_count
231
+ @links_queued.size + @form_signatures_queued.size
232
+ end
233
+
234
+ def links_remaining_count
235
+ @crawl_queue.size
236
+ end
237
+
238
+ def links_completed_count
239
+ total_links_count - links_remaining_count
240
+ end
241
+
242
+ def blip(number = 0)
243
+ unless verbose
244
+ print "\r #{links_completed_count} of #{total_links_count} links completed " if @stdout_tty
245
+ timeout_if_too_long(number)
246
+ end
247
+ end
248
+
249
+ def timeout_if_too_long(number = 0)
250
+ if elasped_time_for_pass(number) > crawl_timeout
251
+ raise CrawlTimeout, "Exceeded crawl timeout of #{crawl_timeout} seconds - skipping to the next crawl..."
252
+ end
253
+ end
254
+ end