arachni 1.0.2 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -0
- data/README.md +1 -9
- data/bin/arachni_script +1 -1
- data/components/checks/active/xss_dom_script_context.rb +1 -1
- data/components/checks/active/xss_event.rb +1 -1
- data/components/checks/active/xss_script_context.rb +1 -1
- data/components/plugins/autologin.rb +2 -2
- data/components/plugins/content_types.rb +4 -5
- data/components/plugins/cookie_collector.rb +6 -3
- data/components/plugins/uncommon_headers.rb +6 -2
- data/lib/arachni/browser.rb +26 -2
- data/lib/arachni/browser/element_locator.rb +9 -2
- data/lib/arachni/browser/javascript.rb +6 -0
- data/lib/arachni/browser/javascript/scripts/dom_monitor.js +39 -0
- data/lib/arachni/browser_cluster.rb +11 -25
- data/lib/arachni/element/capabilities/analyzable/differential.rb +4 -0
- data/lib/arachni/element/capabilities/analyzable/timeout.rb +4 -0
- data/lib/arachni/element/capabilities/auditable/dom.rb +1 -0
- data/lib/arachni/element/capabilities/mutable.rb +0 -9
- data/lib/arachni/element/capabilities/with_auditor/output.rb +2 -0
- data/lib/arachni/element/cookie.rb +9 -4
- data/lib/arachni/element/form.rb +6 -6
- data/lib/arachni/element/header.rb +1 -1
- data/lib/arachni/framework.rb +1 -0
- data/lib/arachni/http/client.rb +1 -0
- data/lib/arachni/option_groups.rb +3 -0
- data/lib/arachni/option_groups/paths.rb +63 -6
- data/lib/arachni/option_groups/snapshot.rb +4 -0
- data/lib/arachni/session.rb +73 -17
- data/lib/arachni/state/audit.rb +2 -0
- data/lib/version +1 -1
- data/spec/arachni/browser/javascript_spec.rb +20 -0
- data/spec/arachni/browser_spec.rb +51 -0
- data/spec/arachni/element/cookie_spec.rb +22 -1
- data/spec/arachni/element/form_spec.rb +19 -9
- data/spec/arachni/framework_spec.rb +17 -0
- data/spec/arachni/option_groups/paths_spec.rb +109 -8
- data/spec/arachni/option_groups/snapshot_spec.rb +17 -0
- data/spec/arachni/session_spec.rb +54 -26
- data/spec/components/plugins/autologin_spec.rb +59 -0
- data/spec/spec_helper.rb +1 -3
- data/spec/support/factories/element/body.rb +3 -0
- data/spec/support/factories/element/generic_dom.rb +6 -0
- data/spec/support/factories/element/path.rb +3 -0
- data/spec/support/factories/element/server.rb +3 -0
- data/spec/support/factories/page/dom/transition.rb +21 -0
- data/spec/support/helpers/resets.rb +1 -0
- data/spec/support/servers/arachni/browser/javascript/dom_monitor.rb +15 -0
- data/ui/cli/framework.rb +5 -0
- data/ui/cli/framework/option_parser.rb +1 -1
- data/ui/cli/option_parser.rb +3 -0
- data/ui/cli/output.rb +45 -19
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe4bb5082b1faba59faf683145dd95841b0299a2
|
4
|
+
data.tar.gz: 83374892882e248fb3334b7651c7f166c99c3bc2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: def0427f488fd60f1aed48b0a38d2cb3e3deff96c3270305f27a5a7e2e9a00d01074716b492169ef6e1f45984ac84f89ef39e81b1d155791b73aa560c40f5e33
|
7
|
+
data.tar.gz: 0edb331de9533d6f5b9a63d3fdcb3f4a7f64697ce6f0f3a98f610a53d41629037d50c01cee7037d28afa9b2489edaa82fd97dee4311c9c4188f4336e78815600
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,62 @@
|
|
1
1
|
# ChangeLog
|
2
2
|
|
3
|
+
## 1.0.3 _(October 3, 2014)_
|
4
|
+
|
5
|
+
- Added overrides for system write directories in `config/write_paths.yml`.
|
6
|
+
- `OptionGroups`
|
7
|
+
- `Paths`
|
8
|
+
- Added `.config` -- Parsing `config/write_paths.yml`.
|
9
|
+
- `#logs` -- Can now be set via `.config`.
|
10
|
+
- `#snapshots` -- Can now be set via `.config`.
|
11
|
+
- `Snapshot`
|
12
|
+
- `#save_path` -- Can now be set via `Paths.config`.
|
13
|
+
- `UI::Output`
|
14
|
+
- Moved default error log under `OptionGroups::Paths.logs`.
|
15
|
+
- Optimized file descriptor handling.
|
16
|
+
- `UI::CLI`
|
17
|
+
- `OptionParser`
|
18
|
+
- Set default report location save-dir from `OptionGroups::Paths.config`.
|
19
|
+
- `Framework`
|
20
|
+
- Print the error-log location at the end of the scan if there were errors.
|
21
|
+
- `Framework`
|
22
|
+
- Use `OptionGroups::Scope#extend_paths` to seed the crawl.
|
23
|
+
- `Browser`
|
24
|
+
- `ElementLocator.supported_element_attributes_for`
|
25
|
+
- Fixed `nil`-error when dealing with unknown attributes.
|
26
|
+
- Added `:ignore_scope` option, allowing the browser to roam completely
|
27
|
+
unrestricted.
|
28
|
+
- Capped `setTimeout` waiting period to `OptionGroups::HTTP#request_timeout`.
|
29
|
+
- Fixed issue resulting in multiple cookies with the same name being sent
|
30
|
+
to the web application.
|
31
|
+
- Assigned unique custom IDs to DOM elements without ID attributes.
|
32
|
+
- `BrowserCluster`
|
33
|
+
- Spawn browsers in series instead of in parallel to make it easier on
|
34
|
+
low resource systems.
|
35
|
+
- `Session`
|
36
|
+
- Fallback to `Framework` DOM Level 1 handlers when no `Browser` is available.
|
37
|
+
- When `OptionGroups::Scope#dom_depth_limit` is 0 don't use the `Browser`.
|
38
|
+
- Configured its `Browser` with `:ignore_scope` to allow for SSO support.
|
39
|
+
- `#logged_in?` -- Follow redirections for login check HTTP request.
|
40
|
+
- `Element`
|
41
|
+
- `Cookie`
|
42
|
+
- Added `#data` -- Providing access to raw cookie data.
|
43
|
+
- `Capabilities`
|
44
|
+
- `Analyzable`
|
45
|
+
- `Differential` -- Forcibly disable `OptionGroups::Audit#cookies_extensively`.
|
46
|
+
- `Timeout` -- Forcibly disable `OptionGroups::Audit#cookies_extensively`.
|
47
|
+
- `Mutable`
|
48
|
+
- `#each_mutation` -- Removed obsolete method-switch with default inputs.
|
49
|
+
- Plugins
|
50
|
+
- `uncommon_headers`
|
51
|
+
- Added `keep-alive` and `content-disposition` in the common list.
|
52
|
+
- Ignore out-of-scope responses.
|
53
|
+
- `content_types`
|
54
|
+
- Ignore out-of-scope responses.
|
55
|
+
- `cookie_collector`
|
56
|
+
- `Set-Cookie` header is now always an `Array`.
|
57
|
+
- `autologin`
|
58
|
+
- Don't modify `OptionGroups::Session` (login-check) options if already set.
|
59
|
+
|
3
60
|
## 1.0.2 _(September 13, 2014)_
|
4
61
|
|
5
62
|
- `UI::Output` -- Updated null output interface with placeholder debugging methods.
|
data/README.md
CHANGED
@@ -1,17 +1,9 @@
|
|
1
|
-
**NOTICE**:
|
2
|
-
|
3
|
-
* Arachni's license has changed, please see the _LICENSE_ file before working
|
4
|
-
with the project.
|
5
|
-
* v1.0 is not backwards compatible with v0.4.
|
6
|
-
|
7
|
-
<hr/>
|
8
|
-
|
9
1
|
# Arachni - Web Application Security Scanner Framework
|
10
2
|
|
11
3
|
<table>
|
12
4
|
<tr>
|
13
5
|
<th>Version</th>
|
14
|
-
<td>1.0.
|
6
|
+
<td>1.0.3</td>
|
15
7
|
</tr>
|
16
8
|
<tr>
|
17
9
|
<th>Homepage</th>
|
data/bin/arachni_script
CHANGED
@@ -68,7 +68,7 @@ Injects JS taint code and checks to see if it gets executed as proof of vulnerab
|
|
68
68
|
version: '0.1',
|
69
69
|
|
70
70
|
issue: {
|
71
|
-
name: %q{DOM-based Cross-Site Scripting (XSS)},
|
71
|
+
name: %q{DOM-based Cross-Site Scripting (XSS) in script context},
|
72
72
|
description: %q{
|
73
73
|
Client-side scripts are used extensively by modern web applications.
|
74
74
|
They perform from simple functions (such as the formatting of text) up to full
|
@@ -94,7 +94,7 @@ class Arachni::Checks::XssEvent < Arachni::Check::Base
|
|
94
94
|
version: '0.1.5',
|
95
95
|
|
96
96
|
issue: {
|
97
|
-
name: %q{Cross-Site Scripting in event tag of HTML element},
|
97
|
+
name: %q{Cross-Site Scripting (XSS) in event tag of HTML element},
|
98
98
|
description: %q{
|
99
99
|
Client-side scripts are used extensively by modern web applications.
|
100
100
|
They perform from simple functions (such as the formatting of text) up to full
|
@@ -139,7 +139,7 @@ Injects JS taint code and check to see if it gets executed as proof of vulnerabi
|
|
139
139
|
version: '0.2',
|
140
140
|
|
141
141
|
issue: {
|
142
|
-
name: %q{Cross-Site Scripting in
|
142
|
+
name: %q{Cross-Site Scripting (XSS) in script context},
|
143
143
|
description: %q{
|
144
144
|
Client-side scripts are used extensively by modern web applications.
|
145
145
|
They perform from simple functions (such as the formatting of text) up to full
|
@@ -48,8 +48,8 @@ class Arachni::Plugins::AutoLogin < Arachni::Plugin::Base
|
|
48
48
|
return
|
49
49
|
end
|
50
50
|
|
51
|
-
framework.options.session.check_url
|
52
|
-
framework.options.session.check_pattern
|
51
|
+
framework.options.session.check_url ||= response.url
|
52
|
+
framework.options.session.check_pattern ||= @verifier
|
53
53
|
|
54
54
|
if !session.logged_in?
|
55
55
|
register_results(
|
@@ -9,7 +9,6 @@
|
|
9
9
|
# Logs content-types of all server responses.
|
10
10
|
#
|
11
11
|
# @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
|
12
|
-
# @version 0.1.6
|
13
12
|
class Arachni::Plugins::ContentTypes < Arachni::Plugin::Base
|
14
13
|
|
15
14
|
is_distributable
|
@@ -29,7 +28,7 @@ class Arachni::Plugins::ContentTypes < Arachni::Plugin::Base
|
|
29
28
|
end
|
30
29
|
|
31
30
|
def run
|
32
|
-
|
31
|
+
http.on_complete do |response|
|
33
32
|
next if skip?( response )
|
34
33
|
|
35
34
|
type = response.headers.content_type
|
@@ -47,8 +46,8 @@ class Arachni::Plugins::ContentTypes < Arachni::Plugin::Base
|
|
47
46
|
end
|
48
47
|
|
49
48
|
def skip?( response )
|
50
|
-
logged?( response ) ||
|
51
|
-
!log?( response )
|
49
|
+
response.scope.out? || logged?( response ) ||
|
50
|
+
response.headers.content_type.to_s.empty? || !log?( response )
|
52
51
|
end
|
53
52
|
|
54
53
|
def log?( response )
|
@@ -97,7 +96,7 @@ It can help you categorize and identify publicly available file-types which in
|
|
97
96
|
turn can help you identify accidentally leaked files.
|
98
97
|
},
|
99
98
|
author: 'Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>',
|
100
|
-
version: '0.1.
|
99
|
+
version: '0.1.7',
|
101
100
|
options: [
|
102
101
|
Options::String.new( :exclude,
|
103
102
|
description: 'Exclude content-types that match this regular expression.',
|
@@ -9,7 +9,7 @@
|
|
9
9
|
# Simple cookie collector
|
10
10
|
#
|
11
11
|
# @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
|
12
|
-
# @version 0.2
|
12
|
+
# @version 0.2.1
|
13
13
|
class Arachni::Plugins::CookieCollector < Arachni::Plugin::Base
|
14
14
|
|
15
15
|
is_distributable
|
@@ -42,9 +42,12 @@ class Arachni::Plugins::CookieCollector < Arachni::Plugin::Base
|
|
42
42
|
response_hash.delete( :body )
|
43
43
|
response_hash.delete( :headers_string )
|
44
44
|
|
45
|
+
r_h = response_hash.my_stringify_keys
|
46
|
+
r_h['headers']['Set-Cookie'] = [r_h['headers']['Set-Cookie']].flatten.compact
|
47
|
+
|
45
48
|
@cookies << {
|
46
49
|
'time' => Time.now.to_s,
|
47
|
-
'response' =>
|
50
|
+
'response' => r_h,
|
48
51
|
'cookies' => cookies
|
49
52
|
}
|
50
53
|
end
|
@@ -81,7 +84,7 @@ thousands of results leading to a huge report, highly increased memory
|
|
81
84
|
consumption and CPU usage.
|
82
85
|
},
|
83
86
|
author: 'Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>',
|
84
|
-
version: '0.2',
|
87
|
+
version: '0.2.1',
|
85
88
|
options: [
|
86
89
|
Options::String.new( :filter,
|
87
90
|
description: 'Pattern to use to determine which cookies to ' +
|
@@ -32,7 +32,9 @@ class Arachni::Plugins::UncommonHeaders < Arachni::Plugin::Base
|
|
32
32
|
'proxy-authenticate',
|
33
33
|
'set-cookie',
|
34
34
|
'trailer',
|
35
|
-
'transfer-encoding'
|
35
|
+
'transfer-encoding',
|
36
|
+
'keep-alive',
|
37
|
+
'content-disposition'
|
36
38
|
])
|
37
39
|
|
38
40
|
def prepare
|
@@ -52,6 +54,8 @@ class Arachni::Plugins::UncommonHeaders < Arachni::Plugin::Base
|
|
52
54
|
|
53
55
|
def run
|
54
56
|
http.on_complete do |response|
|
57
|
+
next if response.scope.out?
|
58
|
+
|
55
59
|
headers = response.headers.
|
56
60
|
select { |name, _| !COMMON.include?( name.to_s.downcase ) }
|
57
61
|
next if headers.empty?
|
@@ -85,7 +89,7 @@ class Arachni::Plugins::UncommonHeaders < Arachni::Plugin::Base
|
|
85
89
|
name: 'Uncommon headers',
|
86
90
|
description: %q{Intercepts HTTP responses and logs uncommon headers.},
|
87
91
|
author: 'Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>',
|
88
|
-
version: '0.1.
|
92
|
+
version: '0.1.3'
|
89
93
|
}
|
90
94
|
end
|
91
95
|
|
data/lib/arachni/browser.rb
CHANGED
@@ -126,6 +126,8 @@ class Browser
|
|
126
126
|
super()
|
127
127
|
@options = options.dup
|
128
128
|
|
129
|
+
@ignore_scope = options[:ignore_scope]
|
130
|
+
|
129
131
|
@width = options[:width] || 1600
|
130
132
|
@height = options[:height] || 1200
|
131
133
|
|
@@ -310,6 +312,8 @@ class Browser
|
|
310
312
|
wait_for_timers
|
311
313
|
|
312
314
|
wait_for_pending_requests
|
315
|
+
|
316
|
+
javascript.set_element_ids
|
313
317
|
end
|
314
318
|
|
315
319
|
if @add_request_transitions
|
@@ -828,7 +832,12 @@ class Browser
|
|
828
832
|
def wait_for_timers
|
829
833
|
delay = load_delay
|
830
834
|
return if !delay
|
831
|
-
|
835
|
+
|
836
|
+
sleep [Options.http.request_timeout, delay].min / 1000.0
|
837
|
+
end
|
838
|
+
|
839
|
+
def skip_path?( path )
|
840
|
+
enforce_scope? && super( path )
|
832
841
|
end
|
833
842
|
|
834
843
|
def response
|
@@ -998,6 +1007,13 @@ class Browser
|
|
998
1007
|
kill_process
|
999
1008
|
end
|
1000
1009
|
|
1010
|
+
# Something went really bad, the browser couldn't be spawned even
|
1011
|
+
# after our valiant efforts.
|
1012
|
+
#
|
1013
|
+
# Bail out for now and count on the BrowserCluster to retry to boot
|
1014
|
+
# another process ass needed.
|
1015
|
+
return if !@process
|
1016
|
+
|
1001
1017
|
begin
|
1002
1018
|
@pid = @process.pid
|
1003
1019
|
# Not supported on JRuby on MS Windows.
|
@@ -1077,6 +1093,8 @@ class Browser
|
|
1077
1093
|
|
1078
1094
|
set_cookies = {}
|
1079
1095
|
HTTP::Client.cookie_jar.for_url( url ).each do |cookie|
|
1096
|
+
cookie = cookie.dup
|
1097
|
+
cookie.data.delete :domain
|
1080
1098
|
set_cookies[cookie.name] = cookie
|
1081
1099
|
end
|
1082
1100
|
cookies.each do |name, value|
|
@@ -1181,7 +1199,7 @@ class Browser
|
|
1181
1199
|
transition.complete
|
1182
1200
|
end
|
1183
1201
|
|
1184
|
-
return if response.scope.out?
|
1202
|
+
return if enforce_scope? && response.scope.out?
|
1185
1203
|
|
1186
1204
|
intercept response
|
1187
1205
|
save_response response
|
@@ -1203,6 +1221,8 @@ class Browser
|
|
1203
1221
|
end
|
1204
1222
|
|
1205
1223
|
def ignore_request?( request )
|
1224
|
+
return if !enforce_scope?
|
1225
|
+
|
1206
1226
|
# Only allow CSS and JS resources to be loaded from out-of-scope domains.
|
1207
1227
|
!['css', 'js'].include?( request.parsed_url.resource_extension ) &&
|
1208
1228
|
request.scope.out? || request.scope.redundant?
|
@@ -1304,6 +1324,10 @@ class Browser
|
|
1304
1324
|
end
|
1305
1325
|
end
|
1306
1326
|
|
1327
|
+
def enforce_scope?
|
1328
|
+
!@ignore_scope
|
1329
|
+
end
|
1330
|
+
|
1307
1331
|
def normalize_watir_url( url )
|
1308
1332
|
normalize_url( ::URI.encode( url, ';' ) ).gsub( '%3B', '%253B' )
|
1309
1333
|
end
|
@@ -127,8 +127,15 @@ class ElementLocator
|
|
127
127
|
# List of attributes supported by Watir.
|
128
128
|
def self.supported_element_attributes_for( tag_name )
|
129
129
|
@supported_element_attributes_for ||= {}
|
130
|
-
|
131
|
-
|
130
|
+
|
131
|
+
tag_name = tag_name.to_sym
|
132
|
+
|
133
|
+
if (klass = Watir.tag_to_class[tag_name])
|
134
|
+
@supported_element_attributes_for[tag_name] ||=
|
135
|
+
Set.new( klass.attribute_list )
|
136
|
+
else
|
137
|
+
@supported_element_attributes_for[tag_name] ||= Set.new
|
138
|
+
end
|
132
139
|
end
|
133
140
|
|
134
141
|
end
|
@@ -228,6 +228,12 @@ class Javascript
|
|
228
228
|
taint_tracer.flush_data_flow_sinks
|
229
229
|
end
|
230
230
|
|
231
|
+
# Sets a custom ID attribute to elements with events but without a proper ID.
|
232
|
+
def set_element_ids
|
233
|
+
return '' if !supported?
|
234
|
+
dom_monitor.setElementIds
|
235
|
+
end
|
236
|
+
|
231
237
|
# @return [String]
|
232
238
|
# Digest of the current DOM tree (i.e. node names and their attributes
|
233
239
|
# without text-nodes).
|
@@ -142,5 +142,44 @@ var _tokenDOMMonitor = _tokenDOMMonitor || {
|
|
142
142
|
registerEvent: function ( element, event, handler ) {
|
143
143
|
if( !('events' in element) ) element['events'] = [];
|
144
144
|
element['events'].push( [event, handler] );
|
145
|
+
},
|
146
|
+
|
147
|
+
// Sets a unique enough custom ID attribute to elements that lack proper IDs.
|
148
|
+
// This gets called externally (by the Browser) once the page is settled.
|
149
|
+
setElementIds: function() {
|
150
|
+
var elements = document.getElementsByTagName("*");
|
151
|
+
var length = elements.length;
|
152
|
+
|
153
|
+
for( var i = 0; i < length; i++ ) {
|
154
|
+
var element = elements[i];
|
155
|
+
|
156
|
+
// Window and others don't have attributes.
|
157
|
+
if( typeof( element.getAttribute ) !== 'function' ||
|
158
|
+
typeof( element.setAttribute) !== 'function' ) continue;
|
159
|
+
|
160
|
+
// If the element has an ID we're cool, move on.
|
161
|
+
if( element.getAttribute('id') ) continue;
|
162
|
+
|
163
|
+
// Skip invisible elements.
|
164
|
+
if( element.offsetWidth <= 0 && element.offsetHeight <= 0 ) continue;
|
165
|
+
|
166
|
+
// We don't care about elements without events.
|
167
|
+
if( !element.events || element.events.length == 0 ) continue;
|
168
|
+
|
169
|
+
element.setAttribute( 'data-arachni-id', _tokenDOMMonitor.hashCode( element.innerHTML ) );
|
170
|
+
}
|
171
|
+
},
|
172
|
+
|
173
|
+
hashCode: function( str ) {
|
174
|
+
var hash = 0;
|
175
|
+
if( str.length == 0 ) return hash;
|
176
|
+
|
177
|
+
for( var i = 0; i < str.length; i++ ) {
|
178
|
+
var char = str.charCodeAt( i );
|
179
|
+
hash = ((hash << 5) - hash) + char;
|
180
|
+
hash = hash & hash; // Convert to 32bit integer
|
181
|
+
}
|
182
|
+
|
183
|
+
return hash;
|
145
184
|
}
|
146
185
|
};
|
@@ -384,32 +384,18 @@ class BrowserCluster
|
|
384
384
|
def initialize_workers
|
385
385
|
print_status "Initializing #{pool_size} browsers..."
|
386
386
|
|
387
|
-
# Calculate maximum HTTP connection concurrency for each worker based on
|
388
|
-
# the HTTP request concurrency setting of the framework.
|
389
|
-
#
|
390
|
-
# Ideally, we'd throttle the collective connections of all browsers
|
391
|
-
# for optimal concurrency, but that would require all browsers sharing
|
392
|
-
# the same proxy which would make things **really** dirty and complicated
|
393
|
-
# so let's avoid that for as long as possible.
|
394
|
-
#concurrency = [(Options.http.request_concurrency / pool_size).to_i, 1].max
|
395
|
-
|
396
387
|
@workers = []
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
end
|
409
|
-
end
|
410
|
-
|
411
|
-
pool_size.times do
|
412
|
-
@workers << workers.pop.tap { |b| @consumed_pids << b.pid }
|
388
|
+
pool_size.times do |i|
|
389
|
+
worker = Worker.new(
|
390
|
+
javascript_token: @javascript_token,
|
391
|
+
master: self,
|
392
|
+
width: Options.browser_cluster.screen_width,
|
393
|
+
height: Options.browser_cluster.screen_height
|
394
|
+
)
|
395
|
+
@workers << worker
|
396
|
+
@consumed_pids << worker.pid
|
397
|
+
|
398
|
+
print_status "Spawned ##{i+1} with PID #{worker.pid}."
|
413
399
|
end
|
414
400
|
@consumed_pids.compact!
|
415
401
|
|