stratagem 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest +99 -0
- data/Rakefile +17 -0
- data/bin/stratagem +10 -0
- data/init.rb +2 -0
- data/lib/bootstrap.rb +31 -0
- data/lib/stratagem/authentication.rb +64 -0
- data/lib/stratagem/auto_mock/aquifer.rb +86 -0
- data/lib/stratagem/auto_mock/factory.rb +213 -0
- data/lib/stratagem/auto_mock/value_generator.rb +174 -0
- data/lib/stratagem/auto_mock.rb +6 -0
- data/lib/stratagem/blocker.rb +16 -0
- data/lib/stratagem/client.rb +32 -0
- data/lib/stratagem/command.rb +13 -0
- data/lib/stratagem/commands/analyze.rb +22 -0
- data/lib/stratagem/commands/base.rb +11 -0
- data/lib/stratagem/commands/devel_crawl.rb +27 -0
- data/lib/stratagem/commands/devel_mock.rb +10 -0
- data/lib/stratagem/commands.rb +7 -0
- data/lib/stratagem/crawler/authentication.rb +109 -0
- data/lib/stratagem/crawler/form.rb +101 -0
- data/lib/stratagem/crawler/html_utils.rb +92 -0
- data/lib/stratagem/crawler/session.rb +296 -0
- data/lib/stratagem/crawler/site_model.rb +138 -0
- data/lib/stratagem/crawler/trace_utils.rb +10 -0
- data/lib/stratagem/crawler.rb +9 -0
- data/lib/stratagem/extensions/class.rb +9 -0
- data/lib/stratagem/extensions/hash.rb +16 -0
- data/lib/stratagem/extensions/module.rb +11 -0
- data/lib/stratagem/extensions/object.rb +15 -0
- data/lib/stratagem/extensions/red_parse.rb +86 -0
- data/lib/stratagem/extensions/string.rb +20 -0
- data/lib/stratagem/extensions.rb +6 -0
- data/lib/stratagem/framework_extensions/controllers/action_controller.rb +10 -0
- data/lib/stratagem/framework_extensions/controllers/action_mailer.rb +12 -0
- data/lib/stratagem/framework_extensions/controllers.rb +5 -0
- data/lib/stratagem/framework_extensions/models/adapters/active_model/detect.rb +7 -0
- data/lib/stratagem/framework_extensions/models/adapters/active_model/extensions.rb +35 -0
- data/lib/stratagem/framework_extensions/models/adapters/active_model/metadata.rb +103 -0
- data/lib/stratagem/framework_extensions/models/adapters/active_model/tracing.rb +50 -0
- data/lib/stratagem/framework_extensions/models/adapters/authlogic/detect.rb +11 -0
- data/lib/stratagem/framework_extensions/models/adapters/authlogic/extensions.rb +10 -0
- data/lib/stratagem/framework_extensions/models/adapters/authlogic/metadata.rb +30 -0
- data/lib/stratagem/framework_extensions/models/adapters/authlogic/tracing.rb +4 -0
- data/lib/stratagem/framework_extensions/models/adapters/common/authentication_metadata.rb +21 -0
- data/lib/stratagem/framework_extensions/models/adapters/restful_authentication/detect.rb +13 -0
- data/lib/stratagem/framework_extensions/models/adapters/restful_authentication/extensions.rb +19 -0
- data/lib/stratagem/framework_extensions/models/adapters/restful_authentication/metadata.rb +30 -0
- data/lib/stratagem/framework_extensions/models/adapters/restful_authentication/tracing.rb +4 -0
- data/lib/stratagem/framework_extensions/models/annotations.rb +79 -0
- data/lib/stratagem/framework_extensions/models/detect.rb +7 -0
- data/lib/stratagem/framework_extensions/models/metadata.rb +85 -0
- data/lib/stratagem/framework_extensions/models/mocking.rb +23 -0
- data/lib/stratagem/framework_extensions/models/tracing.rb +71 -0
- data/lib/stratagem/framework_extensions/models.rb +21 -0
- data/lib/stratagem/framework_extensions/rails.rb +8 -0
- data/lib/stratagem/framework_extensions.rb +6 -0
- data/lib/stratagem/interface/browser.rb +37 -0
- data/lib/stratagem/interface/public/images/backgrounds/content.png +0 -0
- data/lib/stratagem/interface/public/images/backgrounds/shadow.png +0 -0
- data/lib/stratagem/interface/public/javascripts/jquery-1.4.2.min.js +154 -0
- data/lib/stratagem/interface/public/javascripts/stratagem.js +27 -0
- data/lib/stratagem/interface/public/javascripts/stratagem_debug.js +53 -0
- data/lib/stratagem/interface/public/stylesheets/960.css +1 -0
- data/lib/stratagem/interface/public/stylesheets/reset.css +10 -0
- data/lib/stratagem/interface/public/stylesheets/stratagem.css +20 -0
- data/lib/stratagem/interface/public/stylesheets/stratagem_debug.css +20 -0
- data/lib/stratagem/interface/views/debug.haml +43 -0
- data/lib/stratagem/interface/views/index.haml +35 -0
- data/lib/stratagem/labs/auto_mock.rb +7 -0
- data/lib/stratagem/labs/crawler.rb +0 -0
- data/lib/stratagem/logger.rb +46 -0
- data/lib/stratagem/model/application.rb +157 -0
- data/lib/stratagem/model/components/base.rb +55 -0
- data/lib/stratagem/model/components/controller.rb +118 -0
- data/lib/stratagem/model/components/model.rb +170 -0
- data/lib/stratagem/model/components/reference.rb +30 -0
- data/lib/stratagem/model/components/route.rb +53 -0
- data/lib/stratagem/model/components/static_file.rb +18 -0
- data/lib/stratagem/model/components/view.rb +186 -0
- data/lib/stratagem/model/parse_util.rb +61 -0
- data/lib/stratagem/model.rb +12 -0
- data/lib/stratagem/model_builder.rb +146 -0
- data/lib/stratagem/recipes/deploy.rb +30 -0
- data/lib/stratagem/scan/checks/capistrano/secure_deploy.rb +43 -0
- data/lib/stratagem/scan/checks/email_address.rb +15 -0
- data/lib/stratagem/scan/checks/error_pages.rb +25 -0
- data/lib/stratagem/scan/checks/filter_parameter_logging.rb +6 -0
- data/lib/stratagem/scan/checks/mongo_mapper/base.rb +19 -0
- data/lib/stratagem/scan/checks/mongo_mapper/foreign_keys_exposed.rb +32 -0
- data/lib/stratagem/scan/checks/routes.rb +16 -0
- data/lib/stratagem/scan/checks/ssl/secure_login_page.rb +19 -0
- data/lib/stratagem/scan/checks/ssl/secure_login_submit.rb +18 -0
- data/lib/stratagem/scan/result.rb +45 -0
- data/lib/stratagem/scan.rb +19 -0
- data/lib/stratagem/scanner.rb +32 -0
- data/lib/stratagem/site_crawler.rb +47 -0
- data/lib/stratagem/snapshot.rb +33 -0
- data/lib/stratagem.rb +77 -0
- data/lib/tasks/_old_stratagem.rake +99 -0
- data/stratagem.gemspec +56 -0
- metadata +380 -0
@@ -0,0 +1,174 @@
|
|
1
|
+
module Stratagem::AutoMock
|
2
|
+
module ValueGenerator
|
3
|
+
ADDRESSES = [
|
4
|
+
{:street => "1600 Pennsylvania Avenue, NW", :city => "Washington", :state => "DC", :zip_code => '20500', :phone => "(202) 395-2020", :fax => "(202) 395-2020"},
|
5
|
+
{:street => "525 8th Ave", :city => "New York", :state => "NY", :zip_code => '10018', :phone => "(212) 869-6427", :fax => "(202) 395-2020" }
|
6
|
+
]
|
7
|
+
USERS = [
|
8
|
+
{:email => "wchurchill@stratagemapp.com", :first => "winston", :last => "churchill", :login => "wchurchill#{rand 1000}", :password => 'P7D8y5fycaaaa00pa' },
|
9
|
+
{:email => "bwalsh@stratagemapp.com", :first => "brenda", :last => "walsh", :login => "bwalsh#{rand 1000}", :password => '@.04@49\PFtB00a0d' },
|
10
|
+
{:email => "hiro@stratagemapp.com", :first => "hiro", :last => "protagonist", :login => "bigtimehiro#{rand 1000}", :password => '4Kq651p0A534p7T01klj' }
|
11
|
+
]
|
12
|
+
GENERIC_NAMES = [
|
13
|
+
'supercalifragilisticexpialidocious',
|
14
|
+
'hippopotomonstrosesquipedalian',
|
15
|
+
'honorificabilitudinitatibus',
|
16
|
+
'otorhinolaryngological',
|
17
|
+
'tom'
|
18
|
+
]
|
19
|
+
GENERIC_URLS = [
|
20
|
+
'http://www.thewebsiteisdown.com',
|
21
|
+
'http://www.failblog.org',
|
22
|
+
'http://www.xkcd.org'
|
23
|
+
]
|
24
|
+
|
25
|
+
NUMERIC_TYPES = [
|
26
|
+
:decimal, :float, :integer
|
27
|
+
]
|
28
|
+
|
29
|
+
def generate_value(attribute_name, attribute_type)
|
30
|
+
@address ||= ADDRESSES[rand ADDRESSES.size]
|
31
|
+
@user ||= USERS[rand USERS.size]
|
32
|
+
value = case attribute_type
|
33
|
+
when :string
|
34
|
+
generate_string(attribute_name)
|
35
|
+
when :integer
|
36
|
+
generate_int(attribute_name)
|
37
|
+
when :datetime
|
38
|
+
generate_datetime(attribute_name)
|
39
|
+
when :date
|
40
|
+
generate_datetime(attribute_name)
|
41
|
+
when :text
|
42
|
+
generate_text(attribute_name)
|
43
|
+
when :boolean
|
44
|
+
generate_boolean(attribute_name)
|
45
|
+
when :decimal
|
46
|
+
generate_float(attribute_name)
|
47
|
+
when :float
|
48
|
+
generate_float(attribute_name)
|
49
|
+
when :symbol
|
50
|
+
generate_string(attribute_name).to_sym
|
51
|
+
else
|
52
|
+
raise Stratagem::AutoMock::UnsupportedColumnTypeError.new("Attribute #{attribute_name} of type #{attribute_type} is not supported")
|
53
|
+
end
|
54
|
+
value = nil if (rand(20) == 1) && !NUMERIC_TYPES.include?(attribute_type)
|
55
|
+
value
|
56
|
+
end
|
57
|
+
|
58
|
+
def generate_string(attribute_name)
|
59
|
+
attribute_name = attribute_name.to_s
|
60
|
+
if (attribute_name.stratagem_contains_token?('email') || attribute_name.stratagem_contains_token?('e-mail'))
|
61
|
+
(random_noise(4,false) || '') + @user[:email]
|
62
|
+
elsif (attribute_name.stratagem_contains_token?('login') || attribute_name.stratagem_contains_token?('username') || attribute_name.stratagem_contains_token?('user_name'))
|
63
|
+
@user[:login] + (random_noise(4, false) || '')
|
64
|
+
elsif (attribute_name.stratagem_contains_token?('first') && attribute_name.stratagem_contains_token?('name'))
|
65
|
+
(random_noise(2, false) || '') + @user[:first]
|
66
|
+
elsif (attribute_name.stratagem_contains_token?('last') && attribute_name.stratagem_contains_token?('name'))
|
67
|
+
(random_noise(2, false) || '') + @user[:last]
|
68
|
+
elsif (attribute_name.stratagem_contains_token?('password'))
|
69
|
+
@user[:password]
|
70
|
+
elsif (attribute_name.stratagem_contains_token?('zip_code') || attribute_name.stratagem_contains_token?('zipcode'))
|
71
|
+
@address[:zip_code]
|
72
|
+
elsif (attribute_name.stratagem_contains_token?('phone'))
|
73
|
+
@address[:phone]
|
74
|
+
elsif (attribute_name.stratagem_contains_token?('fax'))
|
75
|
+
@address[:fax]
|
76
|
+
elsif (attribute_name.stratagem_contains_token?('address'))
|
77
|
+
@address[:street]
|
78
|
+
elsif (attribute_name.stratagem_contains_token?('city'))
|
79
|
+
@address[:city]
|
80
|
+
elsif (attribute_name.stratagem_contains_token?('state'))
|
81
|
+
@address[:state]
|
82
|
+
elsif (attribute_name.stratagem_contains_token?('name'))
|
83
|
+
value = GENERIC_NAMES[rand GENERIC_NAMES.size]
|
84
|
+
noise = random_noise(4)
|
85
|
+
value += noise unless value.nil? || noise.nil?
|
86
|
+
elsif (attribute_name.stratagem_contains_token?('url'))
|
87
|
+
GENERIC_URLS[rand GENERIC_URLS.size]
|
88
|
+
elsif (attribute_name.stratagem_contains_token?('ip'))
|
89
|
+
"#{rand(156)+100}.#{rand(156)+100}.#{rand(156)+100}.#{rand(156)+100}"
|
90
|
+
else
|
91
|
+
random_noise
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def generate_float(attribute_name)
|
96
|
+
if (attribute_name.to_s.stratagem_contains_token?('price'))
|
97
|
+
((rand + rand(10)) * 100).round.to_f / 100
|
98
|
+
else
|
99
|
+
rand
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def generate_boolean(attribute_name)
|
104
|
+
case rand(4)
|
105
|
+
when 0
|
106
|
+
false
|
107
|
+
when 1
|
108
|
+
true
|
109
|
+
when 2
|
110
|
+
'0'
|
111
|
+
when 3
|
112
|
+
'1'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def generate_int(attribute_name)
|
117
|
+
rand(1000)+1
|
118
|
+
end
|
119
|
+
|
120
|
+
def generate_datetime(attribute_name)
|
121
|
+
Time.now+rand(10.days)
|
122
|
+
end
|
123
|
+
|
124
|
+
def generate_text(attribute_name)
|
125
|
+
random_noise(200)
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def random_noise(size=nil, use_non_alpha_numeric=true)
|
131
|
+
size ||= rand(20)
|
132
|
+
options = {
|
133
|
+
:ssn => [
|
134
|
+
Proc.new { "#{rand(800)+100}-#{rand(80)+10}-#{rand(9000)+1000}" },
|
135
|
+
Proc.new { "#{rand(800)+100}#{rand(80)+10}#{rand(9000)+1000}" },
|
136
|
+
Proc.new { "#{rand(800)+100} #{rand(80)+10} #{rand(9000)+1000}" }
|
137
|
+
],
|
138
|
+
:phone => [
|
139
|
+
Proc.new { "#{rand(800)+100}-#{rand(800)+100}-#{rand(9000)+1000}" },
|
140
|
+
Proc.new { "(#{rand(800)+100}) #{rand(800)+100}-#{rand(9000)+1000}" },
|
141
|
+
Proc.new { "(#{rand(800)+100})#{rand(800)+100}-#{rand(9000)+1000}" },
|
142
|
+
],
|
143
|
+
:zip_code => [
|
144
|
+
Proc.new { "#{rand(90000)+10000}" },
|
145
|
+
Proc.new { "#{rand(90000)+10000}-#{rand(900)+100}" },
|
146
|
+
Proc.new { "#{rand(90000)+10000} #{rand(900)+100}" }
|
147
|
+
],
|
148
|
+
:credit_card => [
|
149
|
+
Proc.new { "#{rand(900)+100} #{rand(900)+100} #{rand(900)+100} #{rand(900)+100}" },
|
150
|
+
Proc.new { "#{rand(900)+100}-#{rand(900)+100}-#{rand(900)+100}-#{rand(900)+100}" },
|
151
|
+
Proc.new { "#{rand(900)+100}-#{rand(900)+100}-#{rand(900)+100}-#{rand(900)+100}" }
|
152
|
+
],
|
153
|
+
:date => [
|
154
|
+
Proc.new { "#{rand(9)+1}/#{rand(90)+10}" },
|
155
|
+
Proc.new { "#{rand(9)+1}/#{rand(100)+1990}" }
|
156
|
+
],
|
157
|
+
:random => [
|
158
|
+
Proc.new {
|
159
|
+
c = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ0123456789-_@. '
|
160
|
+
s = ''
|
161
|
+
size.times { |i| s << c[rand(c.length)] }
|
162
|
+
s
|
163
|
+
}
|
164
|
+
],
|
165
|
+
:nil => [Proc.new { nil }]
|
166
|
+
}
|
167
|
+
procs = options.values[rand(options.values.size)]
|
168
|
+
proc = procs[rand(procs.size)]
|
169
|
+
result = proc.call
|
170
|
+
result.gsub!(/[^A-Za-z0-9]/, '') unless use_non_alpha_numeric || result.nil?
|
171
|
+
result
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Stratagem
|
4
|
+
class Client
|
5
|
+
def initialize(authentication)
|
6
|
+
@authentication = authentication
|
7
|
+
end
|
8
|
+
|
9
|
+
def send(snapshot)
|
10
|
+
Stratagem.logger.debug "Sending report to server"
|
11
|
+
url = URI.parse("#{@authentication.base_url}/snapshots")
|
12
|
+
req = Net::HTTP::Post.new(url.path)
|
13
|
+
req.set_form_data({
|
14
|
+
'api_key' => @authentication.credentials[:token],
|
15
|
+
'project_id' => @authentication.credentials[:project],
|
16
|
+
'timestamp' => snapshot.timestamp.to_i,
|
17
|
+
'model' => snapshot.model.export.to_json
|
18
|
+
}, ';')
|
19
|
+
res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }
|
20
|
+
puts "response:"
|
21
|
+
case res
|
22
|
+
when Net::HTTPSuccess, Net::HTTPRedirection
|
23
|
+
puts "Visit #{@authentication.base_url} for your security posture."
|
24
|
+
# OK
|
25
|
+
else
|
26
|
+
res.error!
|
27
|
+
end
|
28
|
+
|
29
|
+
Stratagem.logger.phase('complete')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Stratagem::Command
|
2
|
+
class Analyze < Base
|
3
|
+
def run
|
4
|
+
require './config/environment'
|
5
|
+
require 'stratagem/interface/browser'
|
6
|
+
|
7
|
+
authentication = Stratagem::Authentication.instance
|
8
|
+
|
9
|
+
while !Sinatra::Application.running
|
10
|
+
puts "Waiting for Sinatra to launch"
|
11
|
+
sleep 0.5
|
12
|
+
end
|
13
|
+
|
14
|
+
puts "Launching your web browser"
|
15
|
+
Launchy::Browser.run(authentication.url)
|
16
|
+
Stratagem.wait_for_completion
|
17
|
+
|
18
|
+
log "" # ensure logger sends final message
|
19
|
+
puts "analysis complete. exiting."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Stratagem::Command
|
2
|
+
class DevelCrawl < Base
|
3
|
+
include Stratagem::Crawler::Session
|
4
|
+
|
5
|
+
def run
|
6
|
+
require './config/environment'
|
7
|
+
|
8
|
+
crawler_session do
|
9
|
+
phase(:unauthenticated)
|
10
|
+
crawl
|
11
|
+
display
|
12
|
+
authenticated = authenticate(true)
|
13
|
+
|
14
|
+
if (authenticated)
|
15
|
+
phase(:authenticated)
|
16
|
+
crawl
|
17
|
+
display
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
puts "SSL? #{authentication.ssl}"
|
22
|
+
puts "AUTHENTICATED? #{authentication.success}"
|
23
|
+
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Stratagem::Crawler
|
2
|
+
class AuthenticationData
|
3
|
+
attr_accessor :success, :login_page, :form, :response_page, :ssl
|
4
|
+
end
|
5
|
+
|
6
|
+
|
7
|
+
module Authentication
|
8
|
+
include Stratagem::Crawler::TraceUtils
|
9
|
+
|
10
|
+
def authentication
|
11
|
+
@authentication_data ||= AuthenticationData.new()
|
12
|
+
end
|
13
|
+
|
14
|
+
def authenticate
|
15
|
+
page, form = find_login_form
|
16
|
+
if (page && form)
|
17
|
+
authentication.login_page = page
|
18
|
+
login(form)
|
19
|
+
form.submit {|action,params|
|
20
|
+
post(action, params)
|
21
|
+
response
|
22
|
+
}
|
23
|
+
|
24
|
+
route = application_model.routes.recognize(response.request.path, :post)
|
25
|
+
page = site_model.add(route, response) {|response| }
|
26
|
+
authentication.response_page = page
|
27
|
+
|
28
|
+
begin
|
29
|
+
authentication.success = authentication.response_page.login_form.nil?
|
30
|
+
rescue
|
31
|
+
puts $!.message
|
32
|
+
puts $!.backtrace
|
33
|
+
end
|
34
|
+
puts "authenticated? #{authentication.success}"
|
35
|
+
else
|
36
|
+
puts "Authentication Error: Unable to locate sign in form"
|
37
|
+
end
|
38
|
+
|
39
|
+
if (response)
|
40
|
+
authentication.ssl = response.request.ssl?
|
41
|
+
authentication.success
|
42
|
+
else
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_login_form
|
48
|
+
site_model.pages.sort {|a,b| b.inbound_edges(:redirect).size <=> a.inbound_edges(:redirect).size }.each do |page|
|
49
|
+
puts "Testing page #{page.url} for sign in form"
|
50
|
+
page.reload {|url| get url; response }
|
51
|
+
form = page.login_form
|
52
|
+
return [page, form] if (form)
|
53
|
+
end
|
54
|
+
[]
|
55
|
+
end
|
56
|
+
|
57
|
+
def login(form)
|
58
|
+
attr_names = form.inputs.map {|input| input.guess_attribute.to_sym }
|
59
|
+
model = guess_login_model(attr_names)
|
60
|
+
|
61
|
+
if model
|
62
|
+
record = Stratagem::AutoMock::Aquifer.instance.random_instance(model.klass)
|
63
|
+
|
64
|
+
if (record)
|
65
|
+
puts "populating login form"
|
66
|
+
populate_login_form(model, form, record)
|
67
|
+
else
|
68
|
+
log "ERROR: Unable to find suitable model to populate authentication form with."
|
69
|
+
end
|
70
|
+
else
|
71
|
+
raise "Unable to infer model from inputs"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def guess_login_model(attr_names)
|
76
|
+
selections = application_model.models.select {|model|
|
77
|
+
intersect = (model.model_attributes.keys & attr_names)
|
78
|
+
intersect.size > 0
|
79
|
+
}.sort {|a,b|
|
80
|
+
a_intersect = (a.model_attributes.keys & attr_names)
|
81
|
+
b_intersect = (b.model_attributes.keys & attr_names)
|
82
|
+
b_intersect.size <=> a_intersect.size
|
83
|
+
}
|
84
|
+
puts "selecting model #{selections.first.klass.name} for authentication"
|
85
|
+
selections.first
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def populate_login_form(model, form, record)
|
90
|
+
form.inputs.each do |input|
|
91
|
+
attribute_name = input.guess_attribute.to_sym
|
92
|
+
attribute_value = record.stratagem.read_mock_attribute(attribute_name)
|
93
|
+
|
94
|
+
puts "authentication field: #{attribute_name} -> #{attribute_value}"
|
95
|
+
|
96
|
+
if (input.kind_of? Stratagem::Crawler::Toggle)
|
97
|
+
input.check
|
98
|
+
elsif (record.stratagem.mock_attributes.keys.include?(attribute_name))
|
99
|
+
input.value = record.stratagem.read_mock_attribute(attribute_name) unless input.hidden?
|
100
|
+
else
|
101
|
+
puts record.stratagem.mock_attributes.inspect
|
102
|
+
puts "ERROR: Cannot find attribute #{attribute_name} in model #{record.class.name}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
form.generate_parameters
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# Primarily used to fill out login forms rather than trying to fudge the before_filters
|
2
|
+
module Stratagem::Crawler
|
3
|
+
class Form
|
4
|
+
attr_accessor :action, :method, :fields, :buttons
|
5
|
+
attr_reader :inputs, :buttons
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@inputs = []
|
9
|
+
@buttons = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def << (input)
|
13
|
+
if (input.kind_of?(Button))
|
14
|
+
@buttons << input
|
15
|
+
else
|
16
|
+
@inputs << input
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def password?
|
21
|
+
!(inputs.find {|i| i.type == 'password' }.nil?)
|
22
|
+
end
|
23
|
+
|
24
|
+
def submit(&block)
|
25
|
+
response = block.call(action, generate_parameters)
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate_parameters
|
29
|
+
params = {}
|
30
|
+
inputs.each do |input|
|
31
|
+
params[input.name] = input.value
|
32
|
+
end
|
33
|
+
params
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Input
|
38
|
+
attr_accessor :name, :id
|
39
|
+
attr_reader :type, :value
|
40
|
+
|
41
|
+
def type=(type)
|
42
|
+
@type = type.downcase.strip
|
43
|
+
end
|
44
|
+
|
45
|
+
def value=(value)
|
46
|
+
if (value.kind_of?(String))
|
47
|
+
@value = value.nil? ? nil : value.strip
|
48
|
+
else
|
49
|
+
@value = value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def guess_attribute
|
54
|
+
if (name =~ /.*\[(.*)\]/)
|
55
|
+
$1.to_sym
|
56
|
+
else
|
57
|
+
name.to_sym
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def guess_model
|
62
|
+
return $1.camelize if (name =~ /(.*)?\[/)
|
63
|
+
return nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def hidden?
|
67
|
+
self.type == 'hidden'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Button < Input
|
72
|
+
end
|
73
|
+
|
74
|
+
class Toggle < Input
|
75
|
+
def check
|
76
|
+
@value = '1'
|
77
|
+
end
|
78
|
+
|
79
|
+
def uncheck
|
80
|
+
@value = '0'
|
81
|
+
end
|
82
|
+
|
83
|
+
def checked?
|
84
|
+
@value == '1'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Select < Input
|
89
|
+
def initialize
|
90
|
+
@options = []
|
91
|
+
end
|
92
|
+
|
93
|
+
def choose(value)
|
94
|
+
@value = value
|
95
|
+
end
|
96
|
+
|
97
|
+
def <<(value)
|
98
|
+
@options << value
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Stratagem::Crawler
|
2
|
+
class CrawlError < StratagemError; attr_accessor :html, :route; end
|
3
|
+
class FormParseError < CrawlError; end
|
4
|
+
|
5
|
+
module HtmlUtils
|
6
|
+
INPUT_TEXT = ['text', 'password', 'hidden']
|
7
|
+
INPUT_BUTTON = ['button', 'submit', 'reset', 'image', 'src']
|
8
|
+
INPUT_TOGGLE = ['checkbox']
|
9
|
+
|
10
|
+
def find_login_form(document)
|
11
|
+
possibilities = parse_forms(document).select {|form|
|
12
|
+
# this maps to the form action, not the controller action
|
13
|
+
form.action =~ /log[-]*in/ ||
|
14
|
+
form.action =~ /sign[-]*in/ ||
|
15
|
+
!form.inputs.find {|input| input.type == 'password' }.nil?
|
16
|
+
}.sort {|a,b| a.inputs.size <=> b.inputs.size }
|
17
|
+
possibilities.first
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_forms(document)
|
21
|
+
document.xpath('//form').map do |form_tag|
|
22
|
+
form = Form.new()
|
23
|
+
form.action = form_load_attribute(form_tag, 'action')
|
24
|
+
form.method = form_load_attribute(form_tag, 'method', false) || 'post'
|
25
|
+
(form_tag/'input').each do |input_tag|
|
26
|
+
form_add_input(form, input_tag)
|
27
|
+
end
|
28
|
+
(form_tag/'textarea').each do |input_tag|
|
29
|
+
form_add_input(form, input_tag)
|
30
|
+
end
|
31
|
+
(form_tag/'select').each do |select_tag|
|
32
|
+
form_add_select(form, select_tag)
|
33
|
+
end
|
34
|
+
|
35
|
+
form
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def form_add_select(form, select_tag)
|
42
|
+
input = Select.new()
|
43
|
+
input.id = form_load_attribute(select_tag, 'id', false)
|
44
|
+
input.name = form_load_attribute(select_tag, 'name')
|
45
|
+
|
46
|
+
(select_tag/'option').each do |option_tag|
|
47
|
+
value = option_tag.attributes['value']
|
48
|
+
value = (value ? value.value : option_tag.inner_html).strip
|
49
|
+
input << value unless value.empty?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def form_add_input(form, input_tag)
|
54
|
+
type = form_load_attribute(input_tag, 'type', false)
|
55
|
+
|
56
|
+
input = nil
|
57
|
+
|
58
|
+
if ((type.nil?) || INPUT_TEXT.include?(type))
|
59
|
+
if (input_tag.attributes['src'])
|
60
|
+
input = Button.new()
|
61
|
+
else
|
62
|
+
input = Input.new()
|
63
|
+
end
|
64
|
+
elsif (INPUT_BUTTON.include?(type))
|
65
|
+
input = Button.new()
|
66
|
+
elsif (INPUT_TOGGLE.include?(type))
|
67
|
+
input = Toggle.new()
|
68
|
+
else
|
69
|
+
raise FormParseError.new("Unsupported <input> type: '#{type}'", :html => input_tag.to_html)
|
70
|
+
end
|
71
|
+
|
72
|
+
input.id = form_load_attribute(input_tag, 'id', false)
|
73
|
+
input.type = form_load_attribute(input_tag, 'type', false) || 'text'
|
74
|
+
input.name = form_load_attribute(input_tag, 'name', false)
|
75
|
+
input.value = form_load_attribute(input_tag, 'value', false)
|
76
|
+
|
77
|
+
form << input
|
78
|
+
end
|
79
|
+
|
80
|
+
def form_load_attribute(node, attribute_name, raise_error = true)
|
81
|
+
# determine action
|
82
|
+
attr = node.attributes[attribute_name]
|
83
|
+
value = nil
|
84
|
+
if (attr)
|
85
|
+
value = attr.value.strip.downcase
|
86
|
+
elsif (raise_error)
|
87
|
+
raise FormParseError.new("#{attribute_name} attribute not found in tag - #{node.to_html}")
|
88
|
+
end
|
89
|
+
value
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|