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
@@ -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