arachni 1.0.5 → 1.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +9 -2
- data/components/checks/active/code_injection.rb +5 -5
- data/components/checks/active/code_injection_timing.rb +3 -3
- data/components/checks/active/no_sql_injection_differential.rb +3 -2
- data/components/checks/active/os_cmd_injection.rb +11 -5
- data/components/checks/active/os_cmd_injection_timing.rb +11 -4
- data/components/checks/active/path_traversal.rb +2 -2
- data/components/checks/active/sql_injection.rb +1 -1
- data/components/checks/active/sql_injection/patterns/mssql +1 -0
- data/components/checks/active/sql_injection_differential.rb +3 -2
- data/components/checks/active/unvalidated_redirect.rb +3 -3
- data/components/checks/passive/common_directories/directories.txt +2 -0
- data/components/checks/passive/common_files/filenames.txt +1 -0
- data/lib/arachni/browser.rb +17 -1
- data/lib/arachni/check/auditor.rb +5 -2
- data/lib/arachni/check/base.rb +30 -5
- data/lib/arachni/element/capabilities/analyzable/differential.rb +2 -5
- data/lib/arachni/element/capabilities/auditable.rb +3 -1
- data/lib/arachni/element/capabilities/with_dom.rb +1 -0
- data/lib/arachni/element/capabilities/with_node.rb +1 -1
- data/lib/arachni/element/cookie.rb +2 -2
- data/lib/arachni/element/form.rb +1 -1
- data/lib/arachni/element/header.rb +2 -2
- data/lib/arachni/element/link_template.rb +1 -1
- data/lib/arachni/framework.rb +21 -1144
- data/lib/arachni/framework/parts/audit.rb +282 -0
- data/lib/arachni/framework/parts/browser.rb +132 -0
- data/lib/arachni/framework/parts/check.rb +86 -0
- data/lib/arachni/framework/parts/data.rb +158 -0
- data/lib/arachni/framework/parts/platform.rb +34 -0
- data/lib/arachni/framework/parts/plugin.rb +61 -0
- data/lib/arachni/framework/parts/report.rb +128 -0
- data/lib/arachni/framework/parts/scope.rb +40 -0
- data/lib/arachni/framework/parts/state.rb +457 -0
- data/lib/arachni/http/client.rb +33 -30
- data/lib/arachni/http/request.rb +6 -2
- data/lib/arachni/issue.rb +55 -1
- data/lib/arachni/platform/manager.rb +25 -21
- data/lib/arachni/state/framework.rb +7 -1
- data/lib/arachni/utilities.rb +10 -0
- data/lib/version +1 -1
- data/spec/arachni/browser_spec.rb +13 -0
- data/spec/arachni/check/auditor_spec.rb +1 -0
- data/spec/arachni/check/base_spec.rb +80 -0
- data/spec/arachni/element/cookie_spec.rb +2 -2
- data/spec/arachni/framework/parts/audit_spec.rb +391 -0
- data/spec/arachni/framework/parts/browser_spec.rb +26 -0
- data/spec/arachni/framework/parts/check_spec.rb +24 -0
- data/spec/arachni/framework/parts/data_spec.rb +187 -0
- data/spec/arachni/framework/parts/platform_spec.rb +62 -0
- data/spec/arachni/framework/parts/plugin_spec.rb +41 -0
- data/spec/arachni/framework/parts/report_spec.rb +66 -0
- data/spec/arachni/framework/parts/scope_spec.rb +86 -0
- data/spec/arachni/framework/parts/state_spec.rb +528 -0
- data/spec/arachni/framework_spec.rb +17 -1344
- data/spec/arachni/http/client_spec.rb +12 -7
- data/spec/arachni/issue_spec.rb +35 -0
- data/spec/arachni/platform/manager_spec.rb +2 -3
- data/spec/arachni/state/framework_spec.rb +15 -0
- data/spec/components/checks/active/code_injection_timing_spec.rb +5 -5
- data/spec/components/checks/active/no_sql_injection_differential_spec.rb +4 -0
- data/spec/components/checks/active/os_cmd_injection_spec.rb +20 -7
- data/spec/components/checks/active/os_cmd_injection_timing_spec.rb +5 -5
- data/spec/components/checks/active/sql_injection_differential_spec.rb +4 -0
- data/spec/components/checks/active/sql_injection_spec.rb +2 -3
- data/spec/support/servers/arachni/browser.rb +31 -0
- data/spec/support/servers/checks/active/code_injection.rb +1 -1
- data/spec/support/servers/checks/active/no_sql_injection_differential.rb +36 -34
- data/spec/support/servers/checks/active/os_cmd_injection.rb +6 -12
- data/spec/support/servers/checks/active/os_cmd_injection_timing.rb +9 -4
- data/spec/support/servers/checks/active/sql_injection.rb +1 -1
- data/spec/support/servers/checks/active/sql_injection_differential.rb +37 -34
- data/spec/support/shared/element/capabilities/with_node.rb +25 -0
- data/spec/support/shared/framework.rb +26 -0
- data/ui/cli/output.rb +2 -0
- data/ui/cli/rpc/server/dispatcher/option_parser.rb +1 -1
- metadata +32 -4
- 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
|