arachni 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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