arachni 1.0.5 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +9 -2
  4. data/components/checks/active/code_injection.rb +5 -5
  5. data/components/checks/active/code_injection_timing.rb +3 -3
  6. data/components/checks/active/no_sql_injection_differential.rb +3 -2
  7. data/components/checks/active/os_cmd_injection.rb +11 -5
  8. data/components/checks/active/os_cmd_injection_timing.rb +11 -4
  9. data/components/checks/active/path_traversal.rb +2 -2
  10. data/components/checks/active/sql_injection.rb +1 -1
  11. data/components/checks/active/sql_injection/patterns/mssql +1 -0
  12. data/components/checks/active/sql_injection_differential.rb +3 -2
  13. data/components/checks/active/unvalidated_redirect.rb +3 -3
  14. data/components/checks/passive/common_directories/directories.txt +2 -0
  15. data/components/checks/passive/common_files/filenames.txt +1 -0
  16. data/lib/arachni/browser.rb +17 -1
  17. data/lib/arachni/check/auditor.rb +5 -2
  18. data/lib/arachni/check/base.rb +30 -5
  19. data/lib/arachni/element/capabilities/analyzable/differential.rb +2 -5
  20. data/lib/arachni/element/capabilities/auditable.rb +3 -1
  21. data/lib/arachni/element/capabilities/with_dom.rb +1 -0
  22. data/lib/arachni/element/capabilities/with_node.rb +1 -1
  23. data/lib/arachni/element/cookie.rb +2 -2
  24. data/lib/arachni/element/form.rb +1 -1
  25. data/lib/arachni/element/header.rb +2 -2
  26. data/lib/arachni/element/link_template.rb +1 -1
  27. data/lib/arachni/framework.rb +21 -1144
  28. data/lib/arachni/framework/parts/audit.rb +282 -0
  29. data/lib/arachni/framework/parts/browser.rb +132 -0
  30. data/lib/arachni/framework/parts/check.rb +86 -0
  31. data/lib/arachni/framework/parts/data.rb +158 -0
  32. data/lib/arachni/framework/parts/platform.rb +34 -0
  33. data/lib/arachni/framework/parts/plugin.rb +61 -0
  34. data/lib/arachni/framework/parts/report.rb +128 -0
  35. data/lib/arachni/framework/parts/scope.rb +40 -0
  36. data/lib/arachni/framework/parts/state.rb +457 -0
  37. data/lib/arachni/http/client.rb +33 -30
  38. data/lib/arachni/http/request.rb +6 -2
  39. data/lib/arachni/issue.rb +55 -1
  40. data/lib/arachni/platform/manager.rb +25 -21
  41. data/lib/arachni/state/framework.rb +7 -1
  42. data/lib/arachni/utilities.rb +10 -0
  43. data/lib/version +1 -1
  44. data/spec/arachni/browser_spec.rb +13 -0
  45. data/spec/arachni/check/auditor_spec.rb +1 -0
  46. data/spec/arachni/check/base_spec.rb +80 -0
  47. data/spec/arachni/element/cookie_spec.rb +2 -2
  48. data/spec/arachni/framework/parts/audit_spec.rb +391 -0
  49. data/spec/arachni/framework/parts/browser_spec.rb +26 -0
  50. data/spec/arachni/framework/parts/check_spec.rb +24 -0
  51. data/spec/arachni/framework/parts/data_spec.rb +187 -0
  52. data/spec/arachni/framework/parts/platform_spec.rb +62 -0
  53. data/spec/arachni/framework/parts/plugin_spec.rb +41 -0
  54. data/spec/arachni/framework/parts/report_spec.rb +66 -0
  55. data/spec/arachni/framework/parts/scope_spec.rb +86 -0
  56. data/spec/arachni/framework/parts/state_spec.rb +528 -0
  57. data/spec/arachni/framework_spec.rb +17 -1344
  58. data/spec/arachni/http/client_spec.rb +12 -7
  59. data/spec/arachni/issue_spec.rb +35 -0
  60. data/spec/arachni/platform/manager_spec.rb +2 -3
  61. data/spec/arachni/state/framework_spec.rb +15 -0
  62. data/spec/components/checks/active/code_injection_timing_spec.rb +5 -5
  63. data/spec/components/checks/active/no_sql_injection_differential_spec.rb +4 -0
  64. data/spec/components/checks/active/os_cmd_injection_spec.rb +20 -7
  65. data/spec/components/checks/active/os_cmd_injection_timing_spec.rb +5 -5
  66. data/spec/components/checks/active/sql_injection_differential_spec.rb +4 -0
  67. data/spec/components/checks/active/sql_injection_spec.rb +2 -3
  68. data/spec/support/servers/arachni/browser.rb +31 -0
  69. data/spec/support/servers/checks/active/code_injection.rb +1 -1
  70. data/spec/support/servers/checks/active/no_sql_injection_differential.rb +36 -34
  71. data/spec/support/servers/checks/active/os_cmd_injection.rb +6 -12
  72. data/spec/support/servers/checks/active/os_cmd_injection_timing.rb +9 -4
  73. data/spec/support/servers/checks/active/sql_injection.rb +1 -1
  74. data/spec/support/servers/checks/active/sql_injection_differential.rb +37 -34
  75. data/spec/support/shared/element/capabilities/with_node.rb +25 -0
  76. data/spec/support/shared/framework.rb +26 -0
  77. data/ui/cli/output.rb +2 -0
  78. data/ui/cli/rpc/server/dispatcher/option_parser.rb +1 -1
  79. metadata +32 -4
  80. data/components/checks/active/sql_injection/patterns/coldfusion +0 -1
@@ -188,9 +188,7 @@ module Differential
188
188
  signatures[:controls][altered_hash].refine!(res.body) :
189
189
  Support::Signature.new(res.body)
190
190
 
191
- @data_gathering[:received_responses] += 1
192
-
193
- finalize_if_done( opts, signatures )
191
+ increase_received_responses( opts, signatures )
194
192
  end
195
193
  end
196
194
  end
@@ -260,8 +258,7 @@ module Differential
260
258
 
261
259
  signature_sieve( altered_hash, signatures, pair_hash )
262
260
 
263
- @data_gathering[:received_responses] += 1
264
- finalize_if_done( opts, signatures )
261
+ increase_received_responses( opts, signatures )
265
262
  end
266
263
  end
267
264
  end
@@ -245,7 +245,9 @@ module Auditable
245
245
  submit( options ) do |response|
246
246
  element = response.request.performer
247
247
  if !element.audit_options[:silent]
248
- print_status "Analyzing response ##{response.request.id}..."
248
+ print_status "Analyzing response ##{response.request.id} for " <<
249
+ "#{self.type} input '#{affected_input_name}'" <<
250
+ " pointing to: '#{audit_status_message_action}'"
249
251
  end
250
252
 
251
253
  exception_jail( false ){ block.call( response, element ) }
@@ -21,6 +21,7 @@ module WithDOM
21
21
  # @return [DOM]
22
22
  def dom
23
23
  @dom ||= self.class::DOM.new( parent: self )
24
+ rescue Inputtable::Error
24
25
  end
25
26
 
26
27
  def dup
@@ -22,7 +22,7 @@ module WithNode
22
22
  end
23
23
 
24
24
  def html=( s )
25
- @html = s.freeze
25
+ @html = (s ? s.recode.freeze : s)
26
26
  end
27
27
 
28
28
  # @return [Nokogiri::XML::Element]
@@ -466,10 +466,10 @@ class Cookie < Base
466
466
  #
467
467
  # @return [String]
468
468
  def encode( str, type = :value )
469
- reserved = "+;%\0\'\""
469
+ reserved = "+;%\0\'\"&"
470
470
  reserved << '=' if type == :name
471
471
 
472
- URI.encode( str, reserved ).recode.gsub( ' ', '+' )
472
+ URI.encode( str.to_s, reserved ).recode.gsub( ' ', '+' )
473
473
  end
474
474
 
475
475
  # Decodes a {String} encoded for the `Cookie` header field.
@@ -466,7 +466,7 @@ class Form < Base
466
466
  # @return [String]
467
467
  def encode( str )
468
468
  ::URI.encode(
469
- ::URI.encode( str, '+%' ).recode.gsub( ' ', '+' ),
469
+ ::URI.encode( str.to_s, '+%' ).recode.gsub( ' ', '+' ),
470
470
  ";&\\=\0"
471
471
  )
472
472
  end
@@ -75,11 +75,11 @@ class Header < Base
75
75
 
76
76
  class <<self
77
77
  def encode( header )
78
- ::URI.encode( header, "\r\n" )
78
+ ::URI.encode( header.to_s, "\r\n" )
79
79
  end
80
80
 
81
81
  def decode( header )
82
- ::URI.decode( header )
82
+ ::URI.decode( header.to_s )
83
83
  end
84
84
  end
85
85
 
@@ -232,7 +232,7 @@ class LinkTemplate < Base
232
232
  end
233
233
 
234
234
  def encode( string )
235
- URI.encode( URI.encode( URI.encode( string, ';' ) ), '/' )
235
+ URI.encode( URI.encode( URI.encode( string.to_s, ';' ) ), '/' )
236
236
  end
237
237
 
238
238
  def decode( *args )
@@ -41,121 +41,57 @@ require lib + 'session'
41
41
  require lib + 'trainer'
42
42
  require lib + 'browser_cluster'
43
43
 
44
- # The Framework class ties together all the systems.
44
+ Dir.glob( "#{lib}framework/parts/**/*.rb" ).each { |h| require h }
45
+
46
+ # The Framework class ties together all the subsystems.
45
47
  #
46
48
  # It's the brains of the operation, it bosses the rest of the subsystems around.
47
- # It runs the audit, loads checks and reports and runs them according to
48
- # user options.
49
+ # It loads checks, reports and plugins and runs them according to user options.
49
50
  #
50
51
  # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
51
52
  class Framework
52
53
  include UI::Output
53
-
54
54
  include Utilities
55
- include Support::Mixins::Observable
56
55
 
57
- # @!method on_page_audit( &block )
58
- advertise :on_page_audit
56
+ # How many times to request a page upon failure.
57
+ AUDIT_PAGE_MAX_TRIES = 5
59
58
 
60
- # @!method after_page_audit( &block )
61
- advertise :after_page_audit
59
+ include Parts::Scope
60
+ include Parts::Browser
61
+ include Parts::Report
62
+ include Parts::Plugin
63
+ include Parts::Check
64
+ include Parts::Platform
65
+ include Parts::Audit
66
+ include Parts::Data
67
+ include Parts::State
62
68
 
63
69
  # {Framework} error namespace.
64
70
  #
65
71
  # All {Framework} errors inherit from and live under it.
66
72
  #
67
- # When I say Framework I mean the {Framework} class, not the
68
- # entire Arachni Framework.
73
+ # When I say Framework I mean the {Framework} class, not the entire Arachni
74
+ # Framework.
69
75
  #
70
76
  # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
71
77
  class Error < Arachni::Error
72
78
  end
73
79
 
74
- # How many times to request a page upon failure.
75
- AUDIT_PAGE_MAX_TRIES = 5
76
-
77
80
  # @return [Options]
78
81
  # System options
79
82
  attr_reader :options
80
83
 
81
- # @return [Arachni::Reporter::Manager]
82
- attr_reader :reporters
83
-
84
- # @return [Arachni::Check::Manager]
85
- attr_reader :checks
86
-
87
- # @return [Arachni::Plugin::Manager]
88
- attr_reader :plugins
89
-
90
- # @return [Session]
91
- # Web application session manager.
92
- attr_reader :session
93
-
94
- # @return [Arachni::HTTP]
95
- attr_reader :http
96
-
97
- # @return [Trainer]
98
- attr_reader :trainer
99
-
100
- # @return [Array<String>]
101
- # Page URLs which elicited no response from the server and were not audited.
102
- # Not determined by HTTP status codes, we're talking network failures here.
103
- attr_reader :failures
104
-
105
- # @param [String] afs
106
- # Path to an `.afs.` (Arachni Framework Snapshot) file created by {#suspend}.
107
- #
108
- # @return [Framework]
109
- # Restored instance.
110
- def self.restore( afs, &block )
111
- framework = new
112
- framework.restore( afs )
113
-
114
- if block_given?
115
- begin
116
- block.call framework
117
- ensure
118
- framework.clean_up
119
- framework.reset
120
- end
121
- end
122
-
123
- framework
124
- end
125
-
126
84
  # @param [Options] options
127
85
  # @param [Block] block
128
86
  # Block to be passed a {Framework} instance which will then be {#reset}.
129
- def initialize( options = Arachni::Options.instance, &block )
130
- super()
131
-
87
+ def initialize( options = Options.instance, &block )
132
88
  Encoding.default_external = 'BINARY'
133
89
  Encoding.default_internal = 'BINARY'
134
90
 
135
91
  @options = options
136
92
 
137
- @checks = Check::Manager.new( self )
138
- @reporters = Reporter::Manager.new
139
- @plugins = Plugin::Manager.new( self )
140
-
141
- reset_session
142
- @http = HTTP::Client.instance
143
-
144
- reset_trainer
145
-
146
- # Deep clone the redundancy rules to preserve their original counters
147
- # for the reports.
148
- @orig_redundant = options.scope.redundant_path_patterns.deep_clone
149
-
150
- state.status = :ready
151
-
152
- @current_url = ''
153
-
154
- # Holds page URLs which returned no response.
155
- @failures = []
156
- @retries = {}
157
-
158
- @after_page_audit_blocks = []
93
+ # Initialize the Parts.
94
+ super()
159
95
 
160
96
  # Little helper to run a piece of code and reset the framework to be
161
97
  # ready to be reused.
@@ -169,46 +105,6 @@ class Framework
169
105
  end
170
106
  end
171
107
 
172
- # @return [Integer]
173
- # Total number of pages added to the {#push_to_page_queue page audit queue}.
174
- def page_queue_total_size
175
- data.page_queue_total_size
176
- end
177
-
178
- # @return [Integer]
179
- # Total number of URLs added to the {#push_to_url_queue URL audit queue}.
180
- def url_queue_total_size
181
- data.url_queue_total_size
182
- end
183
-
184
- # @return [Hash<String, Integer>]
185
- # List of crawled URLs with their HTTP codes.
186
- def sitemap
187
- data.sitemap
188
- end
189
-
190
- # @return [BrowserCluster, nil]
191
- # A lazy-loaded browser cluster or `nil` if
192
- # {OptionGroups::BrowserCluster#pool_size} or
193
- # {OptionGroups::Scope#dom_depth_limit} are 0 or not
194
- # {#host_has_browser?}.
195
- def browser_cluster
196
- return if options.browser_cluster.pool_size == 0 ||
197
- Options.scope.dom_depth_limit == 0 || !host_has_browser?
198
-
199
- # Initialization may take a while so since we lazy load this make sure
200
- # that only one thread gets to this code at a time.
201
- synchronize do
202
- if !@browser_cluster
203
- state.set_status_message :browser_cluster_startup
204
- end
205
-
206
- @browser_cluster ||= BrowserCluster.new
207
- state.clear_status_messages
208
- @browser_cluster
209
- end
210
- end
211
-
212
108
  # Starts the scan.
213
109
  #
214
110
  # @param [Block] block
@@ -222,10 +118,8 @@ class Framework
222
118
  # exit the reporters will still run with whatever results Arachni managed
223
119
  # to gather.
224
120
  exception_jail( false ){ audit }
225
- # print_with_statistics
226
121
 
227
- return if aborted?
228
- return if suspended?
122
+ return if aborted? || suspended?
229
123
 
230
124
  clean_up
231
125
  exception_jail( false ){ block.call } if block_given?
@@ -234,133 +128,6 @@ class Framework
234
128
  true
235
129
  end
236
130
 
237
- # @return [State::Framework]
238
- def state
239
- State.framework
240
- end
241
-
242
- # @return [Data::Framework]
243
- def data
244
- Data.framework
245
- end
246
-
247
- # @note Will update the {HTTP::Client#cookie_jar} with {Page#cookie_jar}.
248
- # @note It will audit just the given `page` and not any subsequent pages
249
- # discovered by the {Trainer} -- i.e. ignore any new elements that might
250
- # appear as a result.
251
- # @note It will pass the `page` to the {BrowserCluster} for analysis if the
252
- # {Page::Scope#dom_depth_limit_reached? DOM depth limit} has
253
- # not been reached and push resulting pages to {#push_to_page_queue} but
254
- # will not audit those pages either.
255
- #
256
- # @param [Page] page
257
- # Runs loaded checks against `page`
258
- def audit_page( page )
259
- return if !page
260
-
261
- if page.scope.out?
262
- print_info "Ignoring page due to exclusion criteria: #{page.dom.url}"
263
- return false
264
- end
265
-
266
- # Initialize the BrowserCluster.
267
- browser_cluster
268
-
269
- state.audited_page_count += 1
270
- add_to_sitemap( page )
271
- sitemap.merge!( browser_sitemap )
272
-
273
- print_line
274
- print_status "[HTTP: #{page.code}] #{page.dom.url}"
275
-
276
- if page.platforms.any?
277
- print_info "Identified as: #{page.platforms.to_a.join( ', ' )}"
278
- end
279
-
280
- if crawl?
281
- pushed = push_paths_from_page( page )
282
- print_info "Analysis resulted in #{pushed.size} usable paths."
283
- end
284
-
285
- if host_has_browser?
286
- print_info "DOM depth: #{page.dom.depth} (Limit: #{options.scope.dom_depth_limit})"
287
-
288
- if page.dom.transitions.any?
289
- print_info ' Transitions:'
290
- page.dom.print_transitions( method(:print_info), ' ' )
291
- end
292
- end
293
-
294
- # Aside from plugins and whatnot, the Trainer hooks here to update the
295
- # ElementFilter so that it'll know if new elements appear during the
296
- # audit, so it's a big deal.
297
- notify_on_page_audit( page )
298
-
299
- @current_url = page.dom.url.to_s
300
-
301
- http.update_cookies( page.cookie_jar )
302
- perform_browser_analysis( page )
303
-
304
- # Remove elements which have already passed through here.
305
- pre_audit_element_filter( page )
306
-
307
- # Run checks which **don't** benefit from fingerprinting first, so that
308
- # we can use the responses of their HTTP requests to fingerprint the
309
- # webapp platforms, so that the checks which **do** benefit from knowing
310
- # the remote platforms can run more efficiently.
311
- ran = false
312
- @checks.without_platforms.values.each do |check|
313
- ran = true if check_page( check, page )
314
- end
315
- harvest_http_responses if ran
316
- run_http = ran
317
-
318
- ran = false
319
- @checks.with_platforms.values.each do |check|
320
- ran = true if check_page( check, page )
321
- end
322
- harvest_http_responses if ran
323
- run_http ||= ran
324
-
325
- if Check::Auditor.has_timeout_candidates?
326
- print_line
327
- print_status "Verifying timeout-analysis candidates for: #{page.dom.url}"
328
- print_info '---------------------------------------'
329
- Check::Auditor.timeout_audit_run
330
- run_http = true
331
- end
332
-
333
- # Makes it easier on the GC.
334
- page.clear_cache
335
-
336
- notify_after_page_audit( page )
337
- run_http
338
- end
339
-
340
- # @return [Bool]
341
- # `true` if the environment has a browser, `false` otherwise.
342
- def host_has_browser?
343
- Browser.has_executable?
344
- end
345
-
346
- # @return [Bool]
347
- # `true` if the {OptionGroups::Scope#page_limit} has been reached,
348
- # `false` otherwise.
349
- def page_limit_reached?
350
- options.scope.page_limit_reached?( sitemap.size )
351
- end
352
-
353
- def crawl?
354
- options.scope.crawl? && options.scope.restrict_paths.empty?
355
- end
356
-
357
- # @return [Bool]
358
- # `true` if the framework can process more pages, `false` is scope limits
359
- # have been reached.
360
- def accepts_more_pages?
361
- crawl? && !page_limit_reached?
362
- end
363
-
364
131
  # @return [Hash]
365
132
  #
366
133
  # Framework statistics:
@@ -382,901 +149,11 @@ class Framework
382
149
  }
383
150
  end
384
151
 
385
- # @return [Array<String>]
386
- # Messages providing more information about the current {#status} of
387
- # the framework.
388
- def status_messages
389
- state.status_messages
390
- end
391
-
392
- # @param [Page] page
393
- # Page to push to the page audit queue -- increases {#page_queue_total_size}
394
- #
395
- # @return [Bool]
396
- # `true` if push was successful, `false` if the `page` matched any
397
- # exclusion criteria or has already been seen.
398
- def push_to_page_queue( page, force = false )
399
- return false if !force && (!accepts_more_pages? || state.page_seen?( page ) ||
400
- page.scope.out? || page.scope.redundant?)
401
-
402
- # We want to update from the already loaded page cache (if there is one)
403
- # as we have to store the page anyways (needs to go through Browser analysis)
404
- # and it's not worth the resources to parse its elements.
405
- #
406
- # We're basically doing this to give the Browser and Trainer a better
407
- # view of what elements have been seen, so that they won't feed us pages
408
- # with elements that they think are new, but have been provided to us by
409
- # some other component; however, it wouldn't be the end of the world if
410
- # that were to happen.
411
- ElementFilter.update_from_page_cache page
412
-
413
- data.push_to_page_queue page
414
- state.page_seen page
415
-
416
- true
417
- end
418
-
419
- # @param [String] url
420
- # URL to push to the audit queue -- increases {#url_queue_total_size}
421
- #
422
- # @return [Bool]
423
- # `true` if push was successful, `false` if the `url` matched any
424
- # exclusion criteria or has already been seen.
425
- def push_to_url_queue( url, force = false )
426
- return if !force && !accepts_more_pages?
427
-
428
- url = to_absolute( url ) || url
429
- if state.url_seen?( url ) || skip_path?( url ) || redundant_path?( url )
430
- return false
431
- end
432
-
433
- data.push_to_url_queue url
434
- state.url_seen url
435
-
436
- true
437
- end
438
-
439
- # @return [Report]
440
- # Scan results.
441
- def report
442
- opts = options.to_hash.deep_clone
443
-
444
- # restore the original redundancy rules and their counters
445
- opts[:scope][:redundant_path_patterns] = @orig_redundant
446
-
447
- Report.new(
448
- options: options,
449
- sitemap: sitemap,
450
- issues: Data.issues.sort,
451
- plugins: @plugins.results,
452
- start_datetime: @start_datetime,
453
- finish_datetime: @finish_datetime
454
- )
455
- end
456
-
457
- # Runs a reporter component and returns the contents of the generated report.
458
- #
459
- # Only accepts reporters which support an `outfile` option.
460
- #
461
- # @param [String] name
462
- # Name of the reporter component to run, as presented by {#list_reporters}'s
463
- # `:shortname` key.
464
- # @param [Report] external_report
465
- # Report to use -- defaults to the local one.
466
- #
467
- # @return [String]
468
- # Scan report.
469
- #
470
- # @raise [Component::Error::NotFound]
471
- # If the given reporter name doesn't correspond to a valid reporter component.
472
- #
473
- # @raise [Component::Options::Error::Invalid]
474
- # If the requested reporter doesn't format the scan results as a String.
475
- def report_as( name, external_report = report )
476
- if !@reporters.available.include?( name.to_s )
477
- fail Component::Error::NotFound, "Reporter '#{name}' could not be found."
478
- end
479
-
480
- loaded = @reporters.loaded
481
- begin
482
- @reporters.clear
483
-
484
- if !@reporters[name].has_outfile?
485
- fail Component::Options::Error::Invalid,
486
- "Reporter '#{name}' cannot format the audit results as a String."
487
- end
488
-
489
- outfile = "#{Dir.tmpdir}/#{generate_token}"
490
- @reporters.run( name, external_report, outfile: outfile )
491
-
492
- IO.binread( outfile )
493
- ensure
494
- File.delete( outfile ) if outfile
495
- @reporters.clear
496
- @reporters.load loaded
497
- end
498
- end
499
-
500
- # @return [Array<Hash>]
501
- # Information about all available {Checks}.
502
- def list_checks( patterns = nil )
503
- loaded = @checks.loaded
504
-
505
- begin
506
- @checks.clear
507
- @checks.available.map do |name|
508
- path = @checks.name_to_path( name )
509
- next if !list_check?( path, patterns )
510
-
511
- @checks[name].info.merge(
512
- shortname: name,
513
- author: [@checks[name].info[:author]].
514
- flatten.map { |a| a.strip },
515
- path: path.strip,
516
- platforms: @checks[name].platforms,
517
- elements: @checks[name].elements
518
- )
519
- end.compact
520
- ensure
521
- @checks.clear
522
- @checks.load loaded
523
- end
524
- end
525
-
526
- # @return [Array<Hash>]
527
- # Information about all available {Reporters}.
528
- def list_reporters( patterns = nil )
529
- loaded = @reporters.loaded
530
-
531
- begin
532
- @reporters.clear
533
- @reporters.available.map do |report|
534
- path = @reporters.name_to_path( report )
535
- next if !list_reporter?( path, patterns )
536
-
537
- @reporters[report].info.merge(
538
- options: @reporters[report].info[:options] || [],
539
- shortname: report,
540
- path: path,
541
- author: [@reporters[report].info[:author]].
542
- flatten.map { |a| a.strip }
543
- )
544
- end.compact
545
- ensure
546
- @reporters.clear
547
- @reporters.load loaded
548
- end
549
- end
550
-
551
- # @return [Array<Hash>]
552
- # Information about all available {Plugins}.
553
- def list_plugins( patterns = nil )
554
- loaded = @plugins.loaded
555
-
556
- begin
557
- @plugins.clear
558
- @plugins.available.map do |plugin|
559
- path = @plugins.name_to_path( plugin )
560
- next if !list_plugin?( path, patterns )
561
-
562
- @plugins[plugin].info.merge(
563
- options: @plugins[plugin].info[:options] || [],
564
- shortname: plugin,
565
- path: path,
566
- author: [@plugins[plugin].info[:author]].
567
- flatten.map { |a| a.strip }
568
- )
569
- end.compact
570
- ensure
571
- @plugins.clear
572
- @plugins.load loaded
573
- end
574
- end
575
-
576
- # @return [Array<Hash>]
577
- # Information about all available platforms.
578
- def list_platforms
579
- platforms = Platform::Manager.new
580
- platforms.valid.inject({}) do |h, platform|
581
- type = Platform::Manager::TYPES[platforms.find_type( platform )]
582
- h[type] ||= {}
583
- h[type][platform] = platforms.fullname( platform )
584
- h
585
- end
586
- end
587
-
588
- # @return [Symbol]
589
- # Status of the instance, possible values are (in order):
590
- #
591
- # * `:ready` -- {#initialize Initialised} and waiting for instructions.
592
- # * `:preparing` -- Getting ready to start (i.e. initializing plugins etc.).
593
- # * `:scanning` -- The instance is currently {#run auditing} the webapp.
594
- # * `:pausing` -- The instance is being {#pause paused} (if applicable).
595
- # * `:paused` -- The instance has been {#pause paused} (if applicable).
596
- # * `:suspending` -- The instance is being {#suspend suspended} (if applicable).
597
- # * `:suspended` -- The instance has being {#suspend suspended} (if applicable).
598
- # * `:cleanup` -- The scan has completed and the instance is
599
- # {Framework#clean_up cleaning up} after itself (i.e. waiting for
600
- # plugins to finish etc.).
601
- # * `:aborted` -- The scan has been {Framework#abort}, you can grab the
602
- # report and shutdown.
603
- # * `:done` -- The scan has completed, you can grab the report and shutdown.
604
- def status
605
- state.status
606
- end
607
-
608
- # @return [Bool]
609
- # `true` if the framework is running, `false` otherwise. This is `true`
610
- # even if the scan is {#paused?}.
611
- def running?
612
- state.running?
613
- end
614
-
615
- # @return [Bool]
616
- # `true` if the system is scanning, `false` otherwise.
617
- def scanning?
618
- state.scanning?
619
- end
620
-
621
- # @return [Bool]
622
- # `true` if the framework is paused, `false` otherwise.
623
- def paused?
624
- state.paused?
625
- end
626
-
627
- # @return [Bool]
628
- # `true` if the framework has been instructed to pause (i.e. is in the
629
- # process of being paused or has been paused), `false` otherwise.
630
- def pause?
631
- state.pause?
632
- end
633
-
634
- # @return [Bool]
635
- # `true` if the framework is in the process of pausing, `false` otherwise.
636
- def pausing?
637
- state.pausing?
638
- end
639
-
640
- # @note Each call from a unique caller is counted as a pause request
641
- # and in order for the system to resume **all** pause callers need to
642
- # {#resume} it.
643
- #
644
- # Pauses the framework on a best effort basis.
645
- #
646
- # @param [Bool] wait
647
- # Wait until the system has been paused.
648
- #
649
- # @return [Integer]
650
- # ID identifying this pause request.
651
- def pause( wait = true )
652
- id = generate_token.hash
653
- state.pause id, wait
654
- id
655
- end
656
-
657
- # @return [Bool]
658
- # `true` if the framework {#run} has been aborted, `false` otherwise.
659
- def aborted?
660
- state.aborted?
661
- end
662
-
663
- # @return [Bool]
664
- # `true` if the framework has been instructed to abort (i.e. is in the
665
- # process of being aborted or has been aborted), `false` otherwise.
666
- def abort?
667
- state.abort?
668
- end
669
-
670
- # @return [Bool]
671
- # `true` if the framework is in the process of aborting, `false` otherwise.
672
- def aborting?
673
- state.aborting?
674
- end
675
-
676
- # Aborts the framework {#run} on a best effort basis.
677
- #
678
- # @param [Bool] wait
679
- # Wait until the system has been aborted.
680
- def abort( wait = true )
681
- state.abort wait
682
- end
683
-
684
- # @note Each call from a unique caller is counted as a pause request
685
- # and in order for the system to resume **all** pause callers need to
686
- # {#resume} it.
687
- #
688
- # Removes a {#pause} request for the current caller.
689
- #
690
- # @param [Integer] id
691
- # ID of the {#pause} request.
692
- def resume( id )
693
- state.resume id
694
- end
695
-
696
- # Writes a {Snapshot.dump} to disk and aborts the scan.
697
- #
698
- # @param [Bool] wait
699
- # Wait for the system to write it state to disk.
700
- #
701
- # @return [String,nil]
702
- # Path to the state file `wait` is `true`, `nil` otherwise.
703
- def suspend( wait = true )
704
- state.suspend( wait )
705
- return snapshot_path if wait
706
- nil
707
- end
708
-
709
- # @return [Bool]
710
- # `true` if the system is in the process of being suspended, `false`
711
- # otherwise.
712
- def suspend?
713
- state.suspend?
714
- end
715
-
716
- # @return [Bool]
717
- # `true` if the system has been suspended, `false` otherwise.
718
- def suspended?
719
- state.suspended?
720
- end
721
-
722
- # @return [String]
723
- # Provisioned {#suspend} dump file for this instance.
724
- def snapshot_path
725
- return @state_archive if @state_archive
726
-
727
- default_filename =
728
- "#{URI(options.url).host} #{Time.now.to_s.gsub( ':', '_' )} #{generate_token}.afs"
729
-
730
- location = options.snapshot.save_path
731
-
732
- if !location
733
- location = default_filename
734
- elsif File.directory? location
735
- location += "/#{default_filename}"
736
- end
737
-
738
- @state_archive ||= File.expand_path( location )
739
- end
740
-
741
- # @param [String] afs
742
- # Path to an `.afs.` (Arachni Framework Snapshot) file created by {#suspend}.
743
- #
744
- # @return [Framework]
745
- # Restored instance.
746
- def restore( afs )
747
- Snapshot.load afs
748
-
749
- browser_job_update_skip_states state.browser_skip_states
750
-
751
- checks.load Options.checks
752
- plugins.load Options.plugins.keys
753
-
754
- nil
755
- end
756
-
757
- def wait_for_browser?
758
- @browser_cluster && !browser_cluster.done?
759
- end
760
-
761
- # Cleans up the framework; should be called after running the audit or
762
- # after canceling a running scan.
763
- #
764
- # It stops the clock and waits for the plugins to finish up.
765
- def clean_up( shutdown_browsers = true )
766
- return if @cleaned_up
767
- @cleaned_up = true
768
-
769
- state.status = :cleanup
770
-
771
- sitemap.merge!( browser_sitemap )
772
-
773
- if shutdown_browsers
774
- state.set_status_message :browser_cluster_shutdown
775
- shutdown_browser_cluster
776
- end
777
-
778
- state.set_status_message :clearing_queues
779
- page_queue.clear
780
- url_queue.clear
781
-
782
- @finish_datetime = Time.now
783
- @start_datetime ||= Time.now
784
-
785
- # Make sure this is disabled or it'll break reporter output.
786
- disable_only_positives
787
-
788
- state.running = false
789
-
790
- state.set_status_message :waiting_for_plugins
791
- @plugins.block
792
-
793
- # Plugins may need the session right till the very end so save it for last.
794
- @session.clean_up
795
- @session = nil
796
-
797
- true
798
- end
799
-
800
- def browser_job_skip_states
801
- return if !@browser_cluster
802
- browser_cluster.skip_states( browser_job.id )
803
- end
804
-
805
152
  # @return [String]
806
153
  # Returns the version of the framework.
807
154
  def version
808
155
  Arachni::VERSION
809
156
  end
810
157
 
811
- # @note Prefer this from {.reset} if you already have an instance.
812
- # @note You should first reset {Arachni::Options}.
813
- #
814
- # Resets everything and allows the framework to be re-used.
815
- def reset
816
- @cleaned_up = false
817
- @browser_job = nil
818
-
819
- @failures.clear
820
- @retries.clear
821
-
822
- # This needs to happen before resetting the other components so they
823
- # will be able to put in their hooks.
824
- self.class.reset
825
-
826
- clear_observers
827
- reset_trainer
828
- reset_session
829
- @checks.clear
830
- @reporters.clear
831
- @plugins.clear
832
- end
833
-
834
- # @note You should first reset {Arachni::Options}.
835
- #
836
- # Resets everything and allows the framework environment to be re-used.
837
- def self.reset
838
- State.clear
839
- Data.clear
840
-
841
- Platform::Manager.reset
842
- Check::Auditor.reset
843
- ElementFilter.reset
844
- Element::Capabilities::Auditable.reset
845
- Element::Capabilities::Analyzable.reset
846
- Check::Manager.reset
847
- Plugin::Manager.reset
848
- Reporter::Manager.reset
849
- HTTP::Client.reset
850
- end
851
-
852
- # @private
853
- def reset_trainer
854
- @trainer = Trainer.new( self )
855
- end
856
-
857
- private
858
-
859
- def shutdown_browser_cluster
860
- return if !@browser_cluster
861
-
862
- browser_cluster.shutdown
863
-
864
- @browser_cluster = nil
865
- @browser_job = nil
866
- end
867
-
868
- def push_paths_from_page( page )
869
- page.paths.select { |path| push_to_url_queue( path ) }
870
- end
871
-
872
- def browser_sitemap
873
- return {} if !@browser_cluster
874
- browser_cluster.sitemap
875
- end
876
-
877
- def browser_job_update_skip_states( states )
878
- return if states.empty?
879
- browser_cluster.update_skip_states browser_job.id, states
880
- end
881
-
882
- def reset_session
883
- @session.clean_up if @session
884
- @session = Session.new
885
- end
886
-
887
- def abort_if_signaled
888
- return if !abort?
889
- clean_up
890
- state.aborted
891
- end
892
-
893
- def suspend_if_signaled
894
- return if !suspend?
895
- suspend_to_disk
896
- end
897
-
898
- def suspend_to_disk
899
- while wait_for_browser?
900
- last_pending_jobs ||= 0
901
- pending_jobs = browser_cluster.pending_job_counter
902
-
903
- if pending_jobs != last_pending_jobs
904
- state.set_status_message :waiting_for_browser_cluster_jobs, pending_jobs
905
- print_info "Suspending: #{status_messages.first}"
906
- end
907
-
908
- last_pending_jobs = pending_jobs
909
- sleep 0.1
910
- end
911
-
912
- # Make sure the component options are up to date with what's actually
913
- # happening.
914
- options.checks = checks.loaded
915
- options.plugins = plugins.loaded.
916
- inject({}) { |h, name| h[name.to_s] = Options.plugins[name.to_s] || {}; h }
917
-
918
- if browser_job_skip_states
919
- state.browser_skip_states.merge browser_job_skip_states
920
- end
921
-
922
- state.set_status_message :suspending_plugins
923
- @plugins.suspend
924
-
925
- state.set_status_message :saving_snapshot, snapshot_path
926
- Snapshot.dump( snapshot_path )
927
- state.clear_status_messages
928
-
929
- clean_up
930
-
931
- state.set_status_message :snapshot_location, snapshot_path
932
- print_info status_messages.first
933
- state.suspended
934
- end
935
-
936
- def handle_signals
937
- wait_if_paused
938
- abort_if_signaled
939
- suspend_if_signaled
940
- end
941
-
942
- def wait_if_paused
943
- state.paused if pause?
944
- sleep 0.2 while pause? && !abort?
945
- end
946
-
947
- # @note Must be called before calling any audit methods.
948
- #
949
- # Prepares the framework for the audit.
950
- #
951
- # * Sets the status to `:preparing`.
952
- # * Starts the clock.
953
- # * Runs the plugins.
954
- def prepare
955
- state.status = :preparing
956
- state.running = true
957
- @start_datetime = Time.now
958
-
959
- Snapshot.restored? ? @plugins.restore : @plugins.run
960
- end
961
-
962
- def handle_browser_page( page )
963
- synchronize do
964
- return if !push_to_page_queue page
965
-
966
- pushed_paths = nil
967
- if crawl?
968
- pushed_paths = push_paths_from_page( page ).size
969
- end
970
-
971
- print_status "Got new page from the browser-cluster: #{page.dom.url}"
972
- print_info "DOM depth: #{page.dom.depth} (Limit: #{options.scope.dom_depth_limit})"
973
-
974
- if page.dom.transitions.any?
975
- print_info ' Transitions:'
976
- page.dom.print_transitions( method(:print_info), ' ' )
977
- end
978
-
979
- if pushed_paths
980
- print_info " -- Analysis resulted in #{pushed_paths} usable paths."
981
- end
982
- end
983
- end
984
-
985
- # Passes the `page` to {BrowserCluster#queue} and then pushes
986
- # the resulting pages to {#push_to_page_queue}.
987
- #
988
- # @param [Page] page
989
- # Page to analyze.
990
- def perform_browser_analysis( page )
991
- return if !browser_cluster || !accepts_more_pages? ||
992
- Options.scope.dom_depth_limit.to_i < page.dom.depth + 1 ||
993
- !page.has_script?
994
-
995
- browser_cluster.queue( browser_job.forward( resource: page ) ) do |response|
996
- handle_browser_page response.page
997
- end
998
-
999
- true
1000
- end
1001
-
1002
- def browser_job
1003
- # We'll recycle the same job since all of them will have the same
1004
- # callback. This will force the BrowserCluster to use the same block
1005
- # for all queued jobs.
1006
- #
1007
- # Also, this job should never end so that all analysis operations
1008
- # share the same state.
1009
- @browser_job ||= BrowserCluster::Jobs::ResourceExploration.new(
1010
- never_ending: true
1011
- )
1012
- end
1013
-
1014
- # Performs the audit.
1015
- def audit
1016
- handle_signals
1017
- return if aborted?
1018
-
1019
- state.status = :scanning if !pausing?
1020
-
1021
- push_to_url_queue( options.url )
1022
- options.scope.extend_paths.each { |url| push_to_url_queue( url ) }
1023
- options.scope.restrict_paths.each { |url| push_to_url_queue( url, true ) }
1024
-
1025
- # Initialize the BrowserCluster.
1026
- browser_cluster
1027
-
1028
- # Keep auditing until there are no more resources in the queues and the
1029
- # browsers have stopped spinning.
1030
- loop do
1031
- show_workload_msg = true
1032
- while !has_audit_workload? && wait_for_browser?
1033
- if show_workload_msg
1034
- print_line
1035
- print_status 'Workload exhausted, waiting for new pages' <<
1036
- ' from the browser-cluster...'
1037
- end
1038
- show_workload_msg = false
1039
-
1040
- last_pending_jobs ||= 0
1041
- pending_jobs = browser_cluster.pending_job_counter
1042
- if pending_jobs != last_pending_jobs
1043
- browser_cluster.print_info "Pending jobs: #{pending_jobs}"
1044
- end
1045
- last_pending_jobs = pending_jobs
1046
-
1047
- sleep 0.1
1048
- end
1049
-
1050
- audit_queues
1051
-
1052
- break if page_limit_reached?
1053
- break if !has_audit_workload? && !wait_for_browser?
1054
- end
1055
- end
1056
-
1057
- def has_audit_workload?
1058
- !url_queue.empty? || !page_queue.empty?
1059
- end
1060
-
1061
- def page_queue
1062
- data.page_queue
1063
- end
1064
-
1065
- def url_queue
1066
- data.url_queue
1067
- end
1068
-
1069
- # Audits the {Data::Framework.url_queue URL} and {Data::Framework.page_queue Page}
1070
- # queues while maintaining a valid session with the webapp if we've got
1071
- # login capabilities.
1072
- def audit_queues
1073
- return if @audit_queues_done == false || !has_audit_workload? ||
1074
- page_limit_reached?
1075
-
1076
- @audit_queues_done = false
1077
-
1078
- # If for some reason we've got pages in the page queue this early,
1079
- # consume them and get it over with.
1080
- audit_page_queue
1081
-
1082
- next_page = nil
1083
- while !suspended? && !page_limit_reached? &&
1084
- (page = next_page || pop_page_from_url_queue)
1085
-
1086
- # Helps us schedule the next page to be grabbed along with the audit
1087
- # requests for the current page to avoid blocking.
1088
- next_page = nil
1089
- next_page_call = proc do
1090
- pop_page_from_url_queue { |p| next_page = p }
1091
- end
1092
-
1093
- # If we can login capabilities make sure that our session is valid
1094
- # before grabbing and auditing the next page.
1095
- if session.can_login?
1096
- # Schedule the login check to happen along with the audit requests
1097
- # to prevent blocking and grab the next page as well.
1098
- session.logged_in? do |bool|
1099
- next next_page_call.call if bool
1100
-
1101
- session.login
1102
- next_page_call
1103
- end
1104
- else
1105
- next_page_call.call
1106
- end
1107
-
1108
- # We're counting on piggybacking the next page retrieval with the
1109
- # page audit, however if there wasn't an audit we need to force an
1110
- # HTTP run.
1111
- audit_page( page ) or http.run
1112
-
1113
- if next_page && suspend?
1114
- data.page_queue << next_page
1115
- end
1116
-
1117
- handle_signals
1118
-
1119
- # Consume pages somehow triggered by the audit and pushed by the
1120
- # trainer or plugins or whatever.
1121
- audit_page_queue
1122
- end
1123
-
1124
- audit_page_queue
1125
-
1126
- @audit_queues_done = true
1127
- true
1128
- end
1129
-
1130
- def pop_page_from_url_queue( &block )
1131
- return if url_queue.empty?
1132
-
1133
- grabbed_page = nil
1134
- Page.from_url( url_queue.pop, http: { update_cookies: true } ) do |page|
1135
- @retries[page.url.hash] ||= 0
1136
-
1137
- if (location = page.response.headers['Location'])
1138
- print_info "Scheduled #{page.code} redirection: #{page.url} => #{location}"
1139
- push_to_url_queue to_absolute( location, page.url )
1140
- end
1141
-
1142
- if page.code != 0
1143
- grabbed_page = page
1144
- block.call grabbed_page if block_given?
1145
- next
1146
- end
1147
-
1148
- if @retries[page.url.hash] >= AUDIT_PAGE_MAX_TRIES
1149
- @failures << page.url
1150
-
1151
- print_error "Giving up trying to audit: #{page.url}"
1152
- print_error "Couldn't get a response after #{AUDIT_PAGE_MAX_TRIES} tries."
1153
- else
1154
- print_bad "Retrying for: #{page.url}"
1155
- @retries[page.url.hash] += 1
1156
- url_queue << page.url
1157
- end
1158
-
1159
- grabbed_page = nil
1160
- block.call grabbed_page if block_given?
1161
- end
1162
- http.run if !block_given?
1163
- grabbed_page
1164
- end
1165
-
1166
- # Audits the page queue.
1167
- #
1168
- # @see #pop_page_from_queue
1169
- def audit_page_queue
1170
- while !suspended? && !page_limit_reached? && (page = pop_page_from_queue)
1171
- audit_page( page )
1172
- handle_signals
1173
- end
1174
- end
1175
-
1176
- # @return [Page]
1177
- def pop_page_from_queue
1178
- return if page_queue.empty?
1179
- page_queue.pop
1180
- end
1181
-
1182
- def harvest_http_responses
1183
- print_status 'Harvesting HTTP responses...'
1184
- print_info 'Depending on server responsiveness and network' <<
1185
- ' conditions this may take a while.'
1186
-
1187
- # Run all the queued HTTP requests and harvest the responses.
1188
- http.run
1189
-
1190
- # Needed for some HTTP callbacks.
1191
- http.run
1192
- end
1193
-
1194
- # Passes a page to the check and runs it.
1195
- # It also handles any exceptions thrown by the check at runtime.
1196
- #
1197
- # @param [Check::Base] check
1198
- # Check to run.
1199
- # @param [Page] page
1200
- def check_page( check, page )
1201
- begin
1202
- @checks.run_one( check, page )
1203
- rescue => e
1204
- print_error "Error in #{check.to_s}: #{e.to_s}"
1205
- print_error_backtrace e
1206
- false
1207
- end
1208
- end
1209
-
1210
- # Small but (sometimes) important optimization:
1211
- #
1212
- # Keep track of page elements which have already been passed to checks,
1213
- # in order to filter them out and hopefully even avoid running checks
1214
- # against pages with no new elements.
1215
- #
1216
- # It's not like there were going to be redundant audits anyways, because
1217
- # each layer of the audit performs its own redundancy checks, but those
1218
- # redundancy checks can introduce significant latencies when dealing
1219
- # with pages with lots of elements.
1220
- def pre_audit_element_filter( page )
1221
- redundant_elements = {}
1222
- page.elements.each do |e|
1223
- next if !Options.audit.element?( e.type )
1224
- next if e.is_a?( Cookie ) || e.is_a?( Header )
1225
-
1226
- new_element = false
1227
- redundant_elements[e.type] ||= []
1228
-
1229
- if !state.element_checked?( e )
1230
- state.element_checked e
1231
- new_element = true
1232
- end
1233
-
1234
- if e.respond_to?( :dom ) && e.dom
1235
- if !state.element_checked?( e.dom )
1236
- state.element_checked e.dom
1237
- new_element = true
1238
- end
1239
- end
1240
-
1241
- next if new_element
1242
-
1243
- redundant_elements[e.type] << e
1244
- end
1245
-
1246
- # Remove redundant elements from the page cache, if there are thousands
1247
- # of them then just skipping them during the audit will introduce latency.
1248
- redundant_elements.each do |type, elements|
1249
- page.send( "#{type}s=", page.send( "#{type}s" ) - elements )
1250
- end
1251
-
1252
- page
1253
- end
1254
-
1255
- def add_to_sitemap( page )
1256
- data.add_page_to_sitemap( page )
1257
- end
1258
-
1259
- def list_reporter?( path, patterns = nil )
1260
- regexp_array_match( patterns, path )
1261
- end
1262
-
1263
- def list_check?( path, patterns = nil )
1264
- regexp_array_match( patterns, path )
1265
- end
1266
-
1267
- def list_plugin?( path, patterns = nil )
1268
- regexp_array_match( patterns, path )
1269
- end
1270
-
1271
- def regexp_array_match( regexps, str )
1272
- regexps = [regexps].flatten.compact.
1273
- map { |s| s.is_a?( Regexp ) ? s : Regexp.new( s.to_s ) }
1274
- return true if regexps.empty?
1275
-
1276
- cnt = 0
1277
- regexps.each { |filter| cnt += 1 if str =~ filter }
1278
- cnt == regexps.size
1279
- end
1280
-
1281
158
  end
1282
159
  end