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