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
@@ -0,0 +1,158 @@
1
+ =begin
2
+ Copyright 2010-2014 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
+
4
+ This file is part of the Arachni Framework project and is subject to
5
+ redistribution and commercial restrictions. Please see the Arachni Framework
6
+ web site for more information on licensing and terms of use.
7
+ =end
8
+
9
+ module Arachni
10
+ class Framework
11
+ module Parts
12
+
13
+ # Provides access to {Arachni::Data::Framework} and helpers.
14
+ #
15
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
16
+ module Data
17
+
18
+ # @return [Data::Framework]
19
+ def data
20
+ Arachni::Data.framework
21
+ end
22
+
23
+ # @param [Page] page
24
+ # Page to push to the page audit queue -- increases {#page_queue_total_size}
25
+ #
26
+ # @return [Bool]
27
+ # `true` if push was successful, `false` if the `page` matched any
28
+ # exclusion criteria or has already been seen.
29
+ def push_to_page_queue( page, force = false )
30
+ return false if !force && (!accepts_more_pages? || state.page_seen?( page ) ||
31
+ page.scope.out? || page.scope.redundant?)
32
+
33
+ # We want to update from the already loaded page cache (if there is one)
34
+ # as we have to store the page anyways (needs to go through Browser analysis)
35
+ # and it's not worth the resources to parse its elements.
36
+ #
37
+ # We're basically doing this to give the Browser and Trainer a better
38
+ # view of what elements have been seen, so that they won't feed us pages
39
+ # with elements that they think are new, but have been provided to us by
40
+ # some other component; however, it wouldn't be the end of the world if
41
+ # that were to happen.
42
+ ElementFilter.update_from_page_cache page
43
+
44
+ data.push_to_page_queue page
45
+ state.page_seen page
46
+
47
+ true
48
+ end
49
+
50
+ # @param [String] url
51
+ # URL to push to the audit queue -- increases {#url_queue_total_size}
52
+ #
53
+ # @return [Bool]
54
+ # `true` if push was successful, `false` if the `url` matched any
55
+ # exclusion criteria or has already been seen.
56
+ def push_to_url_queue( url, force = false )
57
+ return if !force && !accepts_more_pages?
58
+
59
+ url = to_absolute( url ) || url
60
+ if state.url_seen?( url ) || skip_path?( url ) || redundant_path?( url )
61
+ return false
62
+ end
63
+
64
+ data.push_to_url_queue url
65
+ state.url_seen url
66
+
67
+ true
68
+ end
69
+
70
+ # @return [Integer]
71
+ # Total number of pages added to the {#push_to_page_queue page audit queue}.
72
+ def page_queue_total_size
73
+ data.page_queue_total_size
74
+ end
75
+
76
+ # @return [Integer]
77
+ # Total number of URLs added to the {#push_to_url_queue URL audit queue}.
78
+ def url_queue_total_size
79
+ data.url_queue_total_size
80
+ end
81
+
82
+ # @return [Hash<String, Integer>]
83
+ # List of crawled URLs with their HTTP codes.
84
+ def sitemap
85
+ data.sitemap
86
+ end
87
+
88
+ private
89
+
90
+ def page_queue
91
+ data.page_queue
92
+ end
93
+
94
+ def url_queue
95
+ data.url_queue
96
+ end
97
+
98
+ def has_audit_workload?
99
+ !url_queue.empty? || !page_queue.empty?
100
+ end
101
+
102
+ def pop_page_from_url_queue( &block )
103
+ return if url_queue.empty?
104
+
105
+ grabbed_page = nil
106
+ Page.from_url( url_queue.pop, http: { update_cookies: true } ) do |page|
107
+ @retries[page.url.hash] ||= 0
108
+
109
+ if (location = page.response.headers.location)
110
+ [location].flatten.each do |l|
111
+ print_info "Scheduled #{page.code} redirection: #{page.url} => #{l}"
112
+ push_to_url_queue to_absolute( l, page.url )
113
+ end
114
+ end
115
+
116
+ if page.code != 0
117
+ grabbed_page = page
118
+ block.call grabbed_page if block_given?
119
+ next
120
+ end
121
+
122
+ if @retries[page.url.hash] >= AUDIT_PAGE_MAX_TRIES
123
+ @failures << page.url
124
+
125
+ print_error "Giving up trying to audit: #{page.url}"
126
+ print_error "Couldn't get a response after #{AUDIT_PAGE_MAX_TRIES} tries."
127
+ else
128
+ print_bad "Retrying for: #{page.url}"
129
+ @retries[page.url.hash] += 1
130
+ url_queue << page.url
131
+ end
132
+
133
+ grabbed_page = nil
134
+ block.call grabbed_page if block_given?
135
+ end
136
+ http.run if !block_given?
137
+ grabbed_page
138
+ end
139
+
140
+ # @return [Page]
141
+ def pop_page_from_queue
142
+ return if page_queue.empty?
143
+ page_queue.pop
144
+ end
145
+
146
+ def add_to_sitemap( page )
147
+ data.add_page_to_sitemap( page )
148
+ end
149
+
150
+ def push_paths_from_page( page )
151
+ page.paths.select { |path| push_to_url_queue( path ) }
152
+ end
153
+
154
+ end
155
+
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,34 @@
1
+ =begin
2
+ Copyright 2010-2014 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
+
4
+ This file is part of the Arachni Framework project and is subject to
5
+ redistribution and commercial restrictions. Please see the Arachni Framework
6
+ web site for more information on licensing and terms of use.
7
+ =end
8
+
9
+ module Arachni
10
+ class Framework
11
+ module Parts
12
+
13
+ # Provides access to {Arachni::Platform} helpers.
14
+ #
15
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
16
+ module Platform
17
+
18
+ # @return [Array<Hash>]
19
+ # Information about all available platforms.
20
+ def list_platforms
21
+ platforms = Arachni::Platform::Manager.new
22
+ platforms.valid.inject({}) do |h, platform|
23
+ type = Arachni::Platform::Manager::TYPES[platforms.find_type( platform )]
24
+ h[type] ||= {}
25
+ h[type][platform] = platforms.fullname( platform )
26
+ h
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,61 @@
1
+ =begin
2
+ Copyright 2010-2014 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
+
4
+ This file is part of the Arachni Framework project and is subject to
5
+ redistribution and commercial restrictions. Please see the Arachni Framework
6
+ web site for more information on licensing and terms of use.
7
+ =end
8
+
9
+ module Arachni
10
+ class Framework
11
+ module Parts
12
+
13
+ # Provides a {Arachni::Plugin::Manager} and related helpers.
14
+ #
15
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
16
+ module Plugin
17
+
18
+ # @return [Arachni::Plugin::Manager]
19
+ attr_reader :plugins
20
+
21
+ def initialize
22
+ super
23
+ @plugins = Arachni::Plugin::Manager.new( self )
24
+ end
25
+
26
+ # @return [Array<Hash>]
27
+ # Information about all available {Plugins}.
28
+ def list_plugins( patterns = nil )
29
+ loaded = @plugins.loaded
30
+
31
+ begin
32
+ @plugins.clear
33
+ @plugins.available.map do |plugin|
34
+ path = @plugins.name_to_path( plugin )
35
+ next if !list_plugin?( path, patterns )
36
+
37
+ @plugins[plugin].info.merge(
38
+ options: @plugins[plugin].info[:options] || [],
39
+ shortname: plugin,
40
+ path: path,
41
+ author: [@plugins[plugin].info[:author]].
42
+ flatten.map { |a| a.strip }
43
+ )
44
+ end.compact
45
+ ensure
46
+ @plugins.clear
47
+ @plugins.load loaded
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def list_plugin?( path, patterns = nil )
54
+ regexp_array_match( patterns, path )
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,128 @@
1
+ =begin
2
+ Copyright 2010-2014 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
+
4
+ This file is part of the Arachni Framework project and is subject to
5
+ redistribution and commercial restrictions. Please see the Arachni Framework
6
+ web site for more information on licensing and terms of use.
7
+ =end
8
+
9
+ module Arachni
10
+ class Framework
11
+ module Parts
12
+
13
+ # Provides a {Arachni::Report::Manager} and related helpers.
14
+ #
15
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
16
+ module Report
17
+
18
+ # @return [Arachni::Reporter::Manager]
19
+ attr_reader :reporters
20
+
21
+ def initialize
22
+ super
23
+
24
+ # Deep clone the redundancy rules to preserve their original counters
25
+ # for the reports.
26
+ @original_redundant_path_patterns =
27
+ options.scope.redundant_path_patterns.deep_clone
28
+
29
+ @reporters = Arachni::Reporter::Manager.new
30
+ end
31
+
32
+ # @return [Report]
33
+ # Scan results.
34
+ def report
35
+ opts = options.to_hash.deep_clone
36
+
37
+ # restore the original redundancy rules and their counters
38
+ opts[:scope][:redundant_path_patterns] = @original_redundant_path_patterns
39
+
40
+ Arachni::Report.new(
41
+ options: options,
42
+ sitemap: sitemap,
43
+ issues: Arachni::Data.issues.sort,
44
+ plugins: @plugins.results,
45
+ start_datetime: @start_datetime,
46
+ finish_datetime: @finish_datetime
47
+ )
48
+ end
49
+
50
+ # Runs a reporter component and returns the contents of the generated report.
51
+ #
52
+ # Only accepts reporters which support an `outfile` option.
53
+ #
54
+ # @param [String] name
55
+ # Name of the reporter component to run, as presented by {#list_reporters}'s
56
+ # `:shortname` key.
57
+ # @param [Report] external_report
58
+ # Report to use -- defaults to the local one.
59
+ #
60
+ # @return [String]
61
+ # Scan report.
62
+ #
63
+ # @raise [Component::Error::NotFound]
64
+ # If the given reporter name doesn't correspond to a valid reporter component.
65
+ #
66
+ # @raise [Component::Options::Error::Invalid]
67
+ # If the requested reporter doesn't format the scan results as a String.
68
+ def report_as( name, external_report = report )
69
+ if !@reporters.available.include?( name.to_s )
70
+ fail Component::Error::NotFound, "Reporter '#{name}' could not be found."
71
+ end
72
+
73
+ loaded = @reporters.loaded
74
+ begin
75
+ @reporters.clear
76
+
77
+ if !@reporters[name].has_outfile?
78
+ fail Component::Options::Error::Invalid,
79
+ "Reporter '#{name}' cannot format the audit results as a String."
80
+ end
81
+
82
+ outfile = "#{Dir.tmpdir}/#{generate_token}"
83
+ @reporters.run( name, external_report, outfile: outfile )
84
+
85
+ IO.binread( outfile )
86
+ ensure
87
+ File.delete( outfile ) if outfile
88
+ @reporters.clear
89
+ @reporters.load loaded
90
+ end
91
+ end
92
+
93
+ # @return [Array<Hash>]
94
+ # Information about all available {Reporters}.
95
+ def list_reporters( patterns = nil )
96
+ loaded = @reporters.loaded
97
+
98
+ begin
99
+ @reporters.clear
100
+ @reporters.available.map do |report|
101
+ path = @reporters.name_to_path( report )
102
+ next if !list_reporter?( path, patterns )
103
+
104
+ @reporters[report].info.merge(
105
+ options: @reporters[report].info[:options] || [],
106
+ shortname: report,
107
+ path: path,
108
+ author: [@reporters[report].info[:author]].
109
+ flatten.map { |a| a.strip }
110
+ )
111
+ end.compact
112
+ ensure
113
+ @reporters.clear
114
+ @reporters.load loaded
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def list_reporter?( path, patterns = nil )
121
+ regexp_array_match( patterns, path )
122
+ end
123
+
124
+ end
125
+
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,40 @@
1
+ =begin
2
+ Copyright 2010-2014 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
+
4
+ This file is part of the Arachni Framework project and is subject to
5
+ redistribution and commercial restrictions. Please see the Arachni Framework
6
+ web site for more information on licensing and terms of use.
7
+ =end
8
+
9
+ module Arachni
10
+ class Framework
11
+ module Parts
12
+
13
+ # Provides scope helpers.
14
+ #
15
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
16
+ module Scope
17
+
18
+ # @return [Bool]
19
+ # `true` if the {OptionGroups::Scope#page_limit} has been reached,
20
+ # `false` otherwise.
21
+ def page_limit_reached?
22
+ options.scope.page_limit_reached?( sitemap.size )
23
+ end
24
+
25
+ def crawl?
26
+ options.scope.crawl? && options.scope.restrict_paths.empty?
27
+ end
28
+
29
+ # @return [Bool]
30
+ # `true` if the framework can process more pages, `false` is scope limits
31
+ # have been reached.
32
+ def accepts_more_pages?
33
+ crawl? && !page_limit_reached?
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,457 @@
1
+ =begin
2
+ Copyright 2010-2014 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
+
4
+ This file is part of the Arachni Framework project and is subject to
5
+ redistribution and commercial restrictions. Please see the Arachni Framework
6
+ web site for more information on licensing and terms of use.
7
+ =end
8
+
9
+ module Arachni
10
+ class Framework
11
+ module Parts
12
+
13
+ # Provides access to {Arachni::State::Framework} and helpers.
14
+ #
15
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
16
+ module State
17
+
18
+ def self.included( base )
19
+ base.extend ClassMethods
20
+ end
21
+
22
+ module ClassMethods
23
+
24
+ # @param [String] afs
25
+ # Path to an `.afs.` (Arachni Framework Snapshot) file created by
26
+ # {#suspend}.
27
+ #
28
+ # @return [Framework]
29
+ # Restored instance.
30
+ def restore( afs, &block )
31
+ framework = new
32
+ framework.restore( afs )
33
+
34
+ if block_given?
35
+ begin
36
+ block.call framework
37
+ ensure
38
+ framework.clean_up
39
+ framework.reset
40
+ end
41
+ end
42
+
43
+ framework
44
+ end
45
+
46
+ # @note You should first reset {Arachni::Options}.
47
+ #
48
+ # Resets everything and allows the framework environment to be re-used.
49
+ def reset
50
+ Arachni::State.clear
51
+ Arachni::Data.clear
52
+
53
+ Arachni::Platform::Manager.reset
54
+ Arachni::Check::Auditor.reset
55
+ ElementFilter.reset
56
+ Element::Capabilities::Auditable.reset
57
+ Element::Capabilities::Analyzable.reset
58
+ Arachni::Check::Manager.reset
59
+ Arachni::Plugin::Manager.reset
60
+ Arachni::Reporter::Manager.reset
61
+ HTTP::Client.reset
62
+ end
63
+ end
64
+
65
+ def initialize
66
+ super
67
+
68
+ state.status = :ready
69
+ end
70
+
71
+ # @return [String]
72
+ # Provisioned {#suspend} dump file for this instance.
73
+ def snapshot_path
74
+ return @state_archive if @state_archive
75
+
76
+ default_filename =
77
+ "#{URI(options.url).host} #{Time.now.to_s.gsub( ':', '_' )} " <<
78
+ "#{generate_token}.afs"
79
+
80
+ location = options.snapshot.save_path
81
+
82
+ if !location
83
+ location = default_filename
84
+ elsif File.directory? location
85
+ location += "/#{default_filename}"
86
+ end
87
+
88
+ @state_archive ||= File.expand_path( location )
89
+ end
90
+
91
+ # Cleans up the framework; should be called after running the audit or
92
+ # after canceling a running scan.
93
+ #
94
+ # It stops the clock and waits for the plugins to finish up.
95
+ def clean_up( shutdown_browsers = true )
96
+ return if @cleaned_up
97
+ @cleaned_up = true
98
+
99
+ state.status = :cleanup
100
+
101
+ sitemap.merge!( browser_sitemap )
102
+
103
+ if shutdown_browsers
104
+ state.set_status_message :browser_cluster_shutdown
105
+ shutdown_browser_cluster
106
+ end
107
+
108
+ state.set_status_message :clearing_queues
109
+ page_queue.clear
110
+ url_queue.clear
111
+
112
+ @finish_datetime = Time.now
113
+ @start_datetime ||= Time.now
114
+
115
+ # Make sure this is disabled or it'll break reporter output.
116
+ disable_only_positives
117
+
118
+ state.running = false
119
+
120
+ state.set_status_message :waiting_for_plugins
121
+ @plugins.block
122
+
123
+ # Plugins may need the session right till the very end so save it for last.
124
+ @session.clean_up
125
+ @session = nil
126
+
127
+ true
128
+ end
129
+
130
+ # @note Prefer this from {.reset} if you already have an instance.
131
+ # @note You should first reset {Arachni::Options}.
132
+ #
133
+ # Resets everything and allows the framework to be re-used.
134
+ def reset
135
+ @cleaned_up = false
136
+ @browser_job = nil
137
+
138
+ @failures.clear
139
+ @retries.clear
140
+
141
+ # This needs to happen before resetting the other components so they
142
+ # will be able to put in their hooks.
143
+ self.class.reset
144
+
145
+ clear_observers
146
+ reset_trainer
147
+ reset_session
148
+
149
+ @checks.clear
150
+ @reporters.clear
151
+ @plugins.clear
152
+ end
153
+
154
+ # @return [State::Framework]
155
+ def state
156
+ Arachni::State.framework
157
+ end
158
+
159
+ # @param [String] afs
160
+ # Path to an `.afs.` (Arachni Framework Snapshot) file created by {#suspend}.
161
+ #
162
+ # @return [Framework]
163
+ # Restored instance.
164
+ def restore( afs )
165
+ Snapshot.load afs
166
+
167
+ browser_job_update_skip_states state.browser_skip_states
168
+
169
+ checks.load Options.checks
170
+ plugins.load Options.plugins.keys
171
+
172
+ nil
173
+ end
174
+
175
+ # @return [Array<String>]
176
+ # Messages providing more information about the current {#status} of
177
+ # the framework.
178
+ def status_messages
179
+ state.status_messages
180
+ end
181
+
182
+ # @return [Symbol]
183
+ # Status of the instance, possible values are (in order):
184
+ #
185
+ # * `:ready` -- {#initialize Initialised} and waiting for instructions.
186
+ # * `:preparing` -- Getting ready to start (i.e. initializing plugins etc.).
187
+ # * `:scanning` -- The instance is currently {#run auditing} the webapp.
188
+ # * `:pausing` -- The instance is being {#pause paused} (if applicable).
189
+ # * `:paused` -- The instance has been {#pause paused} (if applicable).
190
+ # * `:suspending` -- The instance is being {#suspend suspended} (if applicable).
191
+ # * `:suspended` -- The instance has being {#suspend suspended} (if applicable).
192
+ # * `:cleanup` -- The scan has completed and the instance is
193
+ # {Framework::Parts::State#clean_up cleaning up} after itself (i.e. waiting for
194
+ # plugins to finish etc.).
195
+ # * `:aborted` -- The scan has been {Framework::Parts::State#abort}, you can grab the
196
+ # report and shutdown.
197
+ # * `:done` -- The scan has completed, you can grab the report and shutdown.
198
+ def status
199
+ state.status
200
+ end
201
+
202
+ # @return [Bool]
203
+ # `true` if the framework is running, `false` otherwise. This is `true`
204
+ # even if the scan is {#paused?}.
205
+ def running?
206
+ state.running?
207
+ end
208
+
209
+ # @return [Bool]
210
+ # `true` if the system is scanning, `false` otherwise.
211
+ def scanning?
212
+ state.scanning?
213
+ end
214
+
215
+ # @return [Bool]
216
+ # `true` if the framework is paused, `false` otherwise.
217
+ def paused?
218
+ state.paused?
219
+ end
220
+
221
+ # @return [Bool]
222
+ # `true` if the framework has been instructed to pause (i.e. is in the
223
+ # process of being paused or has been paused), `false` otherwise.
224
+ def pause?
225
+ state.pause?
226
+ end
227
+
228
+ # @return [Bool]
229
+ # `true` if the framework is in the process of pausing, `false` otherwise.
230
+ def pausing?
231
+ state.pausing?
232
+ end
233
+
234
+ # @return (see Arachni::State::Framework#done?)
235
+ def done?
236
+ state.done?
237
+ end
238
+
239
+ # @note Each call from a unique caller is counted as a pause request
240
+ # and in order for the system to resume **all** pause callers need to
241
+ # {#resume} it.
242
+ #
243
+ # Pauses the framework on a best effort basis.
244
+ #
245
+ # @param [Bool] wait
246
+ # Wait until the system has been paused.
247
+ #
248
+ # @return [Integer]
249
+ # ID identifying this pause request.
250
+ def pause( wait = true )
251
+ id = generate_token.hash
252
+ state.pause id, wait
253
+ id
254
+ end
255
+
256
+ # @return [Bool]
257
+ # `true` if the framework {#run} has been aborted, `false` otherwise.
258
+ def aborted?
259
+ state.aborted?
260
+ end
261
+
262
+ # @return [Bool]
263
+ # `true` if the framework has been instructed to abort (i.e. is in the
264
+ # process of being aborted or has been aborted), `false` otherwise.
265
+ def abort?
266
+ state.abort?
267
+ end
268
+
269
+ # @return [Bool]
270
+ # `true` if the framework is in the process of aborting, `false` otherwise.
271
+ def aborting?
272
+ state.aborting?
273
+ end
274
+
275
+ # Aborts the framework {#run} on a best effort basis.
276
+ #
277
+ # @param [Bool] wait
278
+ # Wait until the system has been aborted.
279
+ def abort( wait = true )
280
+ state.abort wait
281
+ end
282
+
283
+ # @note Each call from a unique caller is counted as a pause request
284
+ # and in order for the system to resume **all** pause callers need to
285
+ # {#resume} it.
286
+ #
287
+ # Removes a {#pause} request for the current caller.
288
+ #
289
+ # @param [Integer] id
290
+ # ID of the {#pause} request.
291
+ def resume( id )
292
+ state.resume id
293
+ end
294
+
295
+ # Writes a {Snapshot.dump} to disk and aborts the scan.
296
+ #
297
+ # @param [Bool] wait
298
+ # Wait for the system to write it state to disk.
299
+ #
300
+ # @return [String,nil]
301
+ # Path to the state file `wait` is `true`, `nil` otherwise.
302
+ def suspend( wait = true )
303
+ state.suspend( wait )
304
+ return snapshot_path if wait
305
+ nil
306
+ end
307
+
308
+ # @return [Bool]
309
+ # `true` if the system is in the process of being suspended, `false`
310
+ # otherwise.
311
+ def suspend?
312
+ state.suspend?
313
+ end
314
+
315
+ # @return [Bool]
316
+ # `true` if the system has been suspended, `false` otherwise.
317
+ def suspended?
318
+ state.suspended?
319
+ end
320
+
321
+ # @private
322
+ def reset_trainer
323
+ @trainer = Trainer.new( self )
324
+ end
325
+
326
+ private
327
+
328
+ # @note Must be called before calling any audit methods.
329
+ #
330
+ # Prepares the framework for the audit.
331
+ #
332
+ # * Sets the status to `:preparing`.
333
+ # * Starts the clock.
334
+ # * Runs the plugins.
335
+ def prepare
336
+ state.status = :preparing
337
+ state.running = true
338
+ @start_datetime = Time.now
339
+
340
+ Snapshot.restored? ? @plugins.restore : @plugins.run
341
+ end
342
+
343
+ def reset_session
344
+ @session.clean_up if @session
345
+ @session = Session.new
346
+ end
347
+
348
+ # Small but (sometimes) important optimization:
349
+ #
350
+ # Keep track of page elements which have already been passed to checks,
351
+ # in order to filter them out and hopefully even avoid running checks
352
+ # against pages with no new elements.
353
+ #
354
+ # It's not like there were going to be redundant audits anyways, because
355
+ # each layer of the audit performs its own redundancy checks, but those
356
+ # redundancy checks can introduce significant latencies when dealing
357
+ # with pages with lots of elements.
358
+ def pre_audit_element_filter( page )
359
+ redundant_elements = {}
360
+ page.elements.each do |e|
361
+ next if !Options.audit.element?( e.type )
362
+ next if e.is_a?( Cookie ) || e.is_a?( Header )
363
+
364
+ new_element = false
365
+ redundant_elements[e.type] ||= []
366
+
367
+ if !state.element_checked?( e )
368
+ state.element_checked e
369
+ new_element = true
370
+ end
371
+
372
+ if e.respond_to?( :dom ) && e.dom
373
+ if !state.element_checked?( e.dom )
374
+ state.element_checked e.dom
375
+ new_element = true
376
+ end
377
+ end
378
+
379
+ next if new_element
380
+
381
+ redundant_elements[e.type] << e
382
+ end
383
+
384
+ # Remove redundant elements from the page cache, if there are thousands
385
+ # of them then just skipping them during the audit will introduce latency.
386
+ redundant_elements.each do |type, elements|
387
+ page.send( "#{type}s=", page.send( "#{type}s" ) - elements )
388
+ end
389
+
390
+ page
391
+ end
392
+
393
+ def handle_signals
394
+ wait_if_paused
395
+ abort_if_signaled
396
+ suspend_if_signaled
397
+ end
398
+
399
+ def wait_if_paused
400
+ state.paused if pause?
401
+ sleep 0.2 while pause? && !abort?
402
+ end
403
+
404
+ def abort_if_signaled
405
+ return if !abort?
406
+ clean_up
407
+ state.aborted
408
+ end
409
+
410
+ def suspend_if_signaled
411
+ return if !suspend?
412
+ suspend_to_disk
413
+ end
414
+
415
+ def suspend_to_disk
416
+ while wait_for_browser?
417
+ last_pending_jobs ||= 0
418
+ pending_jobs = browser_cluster.pending_job_counter
419
+
420
+ if pending_jobs != last_pending_jobs
421
+ state.set_status_message :waiting_for_browser_cluster_jobs, pending_jobs
422
+ print_info "Suspending: #{status_messages.first}"
423
+ end
424
+
425
+ last_pending_jobs = pending_jobs
426
+ sleep 0.1
427
+ end
428
+
429
+ # Make sure the component options are up to date with what's actually
430
+ # happening.
431
+ options.checks = checks.loaded
432
+ options.plugins = plugins.loaded.
433
+ inject({}) { |h, name| h[name.to_s] = Options.plugins[name.to_s] || {}; h }
434
+
435
+ if browser_job_skip_states
436
+ state.browser_skip_states.merge browser_job_skip_states
437
+ end
438
+
439
+ state.set_status_message :suspending_plugins
440
+ @plugins.suspend
441
+
442
+ state.set_status_message :saving_snapshot, snapshot_path
443
+ Snapshot.dump( snapshot_path )
444
+ state.clear_status_messages
445
+
446
+ clean_up
447
+
448
+ state.set_status_message :snapshot_location, snapshot_path
449
+ print_info status_messages.first
450
+ state.suspended
451
+ end
452
+
453
+ end
454
+
455
+ end
456
+ end
457
+ end