Skip to content

Commit e5b9059

Browse files
committed
Merge branch 'master' into feature/MSP-11616/db-schema-fail
MSP-11616
2 parents 4e2277c + a521d46 commit e5b9059

File tree

33 files changed

+1362
-148
lines changed

33 files changed

+1362
-148
lines changed

Rakefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ require 'metasploit/framework/spec/untested_payloads'
1010
Metasploit::Framework::Require.optionally_active_record_railtie
1111

1212
Metasploit::Framework::Application.load_tasks
13+
Metasploit::Framework::Spec::Constants.define_task
1314
Metasploit::Framework::Spec::UntestedPayloads.define_task

db/schema.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
#
1212
# It's strongly recommended to check this file into your version control system.
1313

14-
ActiveRecord::Schema.define(:version => 20140905031549) do
14+
ActiveRecord::Schema.define(:version => 20140922170030) do
1515

1616
create_table "api_keys", :force => true do |t|
1717
t.text "token"
@@ -272,6 +272,7 @@
272272
t.string "username", :null => false
273273
t.datetime "created_at", :null => false
274274
t.datetime "updated_at", :null => false
275+
t.string "type", :null => false
275276
end
276277

277278
add_index "metasploit_credential_publics", ["username"], :name => "index_metasploit_credential_publics_on_username", :unique => true

lib/metasploit/framework.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ module Metasploit
3232
# works in compatible manner with activerecord's rake tasks and other
3333
# railties.
3434
module Framework
35+
extend ActiveSupport::Autoload
36+
37+
autoload :Spec
38+
3539
# Returns the root of the metasploit-framework project. Use in place of
3640
# `Rails.root`.
3741
#

lib/metasploit/framework/spec.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module Metasploit::Framework::Spec
2+
extend ActiveSupport::Autoload
3+
4+
autoload :Constants
5+
end
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
require 'msf/core/modules'
2+
3+
# Monitor constants created by module loading to ensure that the loads in one example don't interfere with the
4+
# assertions in another example.
5+
module Metasploit::Framework::Spec::Constants
6+
extend ActiveSupport::Autoload
7+
8+
autoload :Each
9+
autoload :Suite
10+
11+
#
12+
# CONSTANTS
13+
#
14+
15+
# Regex parsing loaded module constants
16+
LOADED_MODULE_CHILD_CONSTANT_REGEXP = /^Mod(?<unpacked_full_name>[0-9a-f]+)$/
17+
# The parent namespace child_constant_name that can have children added when loading modules.
18+
PARENT_CONSTANT = Msf::Modules
19+
# Constant names under {PARENT_CONSTANT} that can persist between specs because they are part of the loader library
20+
# and not dynamically loaded code
21+
PERSISTENT_CHILD_CONSTANT_NAMES = %w{
22+
Error
23+
Loader
24+
MetasploitClassCompatibilityError
25+
Namespace
26+
VersionCompatibilityError
27+
}.map(&:to_sym)
28+
29+
# Cleans child constants from {PARENT_CONSTANT}.
30+
#
31+
# @return [true] if there were leaked constants that were cleaned.
32+
# @return [false] if there were no leaked constants.
33+
# @see each
34+
def self.clean
35+
count = each do |child_name|
36+
PARENT_CONSTANT.send(:remove_const, child_name)
37+
end
38+
39+
count != 0
40+
end
41+
42+
# Adds actions to `spec` task so that `rake spec` fails if any of the following:
43+
#
44+
# # `log/leaked-constants.log` exists after printing out the leaked constants.
45+
# # {Each.configured!} is unnecessary in `spec/spec_helper.rb` and should be removed.
46+
#
47+
# @return [void]
48+
def self.define_task
49+
Suite.define_task
50+
# After Suite as Suite will kill for leaks before Each say it cleaned no leaks in case there are leaks in an
51+
# `after(:all)` that {Each} won't catch in its `after(:each)` checks.
52+
Each.define_task
53+
end
54+
55+
# Yields each child_constant_name under {PARENT_CONSTANT}.
56+
#
57+
# @yield [child_name]
58+
# @yieldparam child_name [Symbol] name of child_constant_name relative to {PARENT_CONSTANT}.
59+
# @yieldreturn [void]
60+
# @return [Integer] count
61+
def self.each
62+
inherit = false
63+
count = 0
64+
65+
child_constant_names = PARENT_CONSTANT.constants(inherit)
66+
67+
child_constant_names.each do |child_constant_name|
68+
unless PERSISTENT_CHILD_CONSTANT_NAMES.include? child_constant_name
69+
count += 1
70+
yield child_constant_name
71+
end
72+
end
73+
74+
count
75+
end
76+
77+
# The module full name for `child_constant_name`
78+
#
79+
# @param child_constant_name [String] the name of a child constant_name under {PARENT_CONSTANT}.
80+
# @return [String] full module name used to load `child_constant_name`.
81+
# @return [nil] if `child_constant_name` does not correspond to a loaded module.
82+
def self.full_name(child_constant_name)
83+
full_name = nil
84+
85+
match = LOADED_MODULE_CHILD_CONSTANT_REGEXP.match(child_constant_name)
86+
87+
if match
88+
potential_full_name = [match[:unpacked_full_name]].pack('H*')
89+
90+
module_type, _reference_name = potential_full_name.split('/', 2)
91+
92+
if Msf::MODULE_TYPES.include? module_type
93+
full_name = potential_full_name
94+
end
95+
end
96+
97+
full_name
98+
end
99+
end
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# @note This should only temporarily be used in `spec/spec_helper.rb` when
2+
# `Metasploit::Framework::Spec::Constants::Suite.configure!` detects a leak. Permanently having
3+
# `Metasploit::Framework::Spec::Constants::Each.configure!` can lead to false positives when modules are purposely
4+
# loaded in a `before(:all)` and cleaned up in a `after(:all)`.
5+
#
6+
# Fails example if it leaks module loading constants.
7+
module Metasploit::Framework::Spec::Constants::Each
8+
#
9+
# CONSTANTS
10+
#
11+
12+
LOG_PATHNAME = Pathname.new('log/metasploit/framework/spec/constants/each.log')
13+
14+
#
15+
# Module Methods
16+
#
17+
18+
class << self
19+
attr_accessor :leaks_cleaned
20+
end
21+
22+
# Is {Metasploit::Framework::Spec::Constants::Each.configure!} still necessary or should it be removed?
23+
#
24+
# @return [true] if {configure!}'s `before(:each)` cleaned up leaked constants
25+
# @return [false] otherwise
26+
def self.leaks_cleaned?
27+
!!@leaks_cleaned
28+
end
29+
30+
# Configures after(:each) callback for RSpe to fail example if leaked constants.
31+
#
32+
# @return [void]
33+
def self.configure!
34+
unless @configured
35+
RSpec.configure do |config|
36+
config.before(:each) do |example|
37+
leaks_cleaned = Metasploit::Framework::Spec::Constants.clean
38+
39+
if leaks_cleaned
40+
$stderr.puts "Cleaned leaked constants before #{example.metadata.full_description}"
41+
end
42+
43+
# clean so that leaks from earlier example aren't attributed to this example
44+
Metasploit::Framework::Spec::Constants::Each.leaks_cleaned ||= leaks_cleaned
45+
end
46+
47+
config.after(:each) do |example|
48+
child_names = Metasploit::Framework::Spec::Constants.to_enum(:each).to_a
49+
50+
if child_names.length > 0
51+
lines = ['Leaked constants:']
52+
53+
child_names.sort.each do |child_name|
54+
lines << " #{child_name}"
55+
end
56+
57+
lines << ''
58+
lines << "Add `include_context 'Metasploit::Framework::Spec::Constants cleaner'` to clean up constants from #{example.metadata.full_description}"
59+
60+
message = lines.join("\n")
61+
62+
# use caller metadata so that Jump to Source in the Rubymine RSpec running jumps to the example instead of
63+
# here
64+
fail RuntimeError, message, example.metadata[:caller]
65+
end
66+
end
67+
68+
config.after(:suite) do
69+
if Metasploit::Framework::Spec::Constants::Each.leaks_cleaned?
70+
if LOG_PATHNAME.exist?
71+
LOG_PATHNAME.delete
72+
end
73+
else
74+
LOG_PATHNAME.open('w') { |f|
75+
f.puts "No leaks were cleaned by `Metasploit::Framework::Spec::Constants::Each.configured!`. Remove " \
76+
"it from `spec/spec_helper.rb` so it does not interfere with contexts that persist loaded " \
77+
"modules for entire context and clean up modules in `after(:all)`"
78+
}
79+
end
80+
end
81+
end
82+
83+
@configured = true
84+
end
85+
end
86+
87+
# Whether {configure!} was called
88+
#
89+
# @return [Boolean]
90+
def self.configured?
91+
!!@configured
92+
end
93+
94+
# Adds action to `spec` task so that `rake spec` fails if {configured!} is unnecessary in `spec/spec_helper.rb` and
95+
# should be removed
96+
#
97+
# @return [void]
98+
def self.define_task
99+
Rake::Task.define_task('metasploit:framework:spec:constant:each:clean') do
100+
if LOG_PATHNAME.exist?
101+
LOG_PATHNAME.delete
102+
end
103+
end
104+
105+
Rake::Task.define_task(spec: 'metasploit:framework:spec:constant:each:clean')
106+
107+
Rake::Task.define_task(:spec) do
108+
if LOG_PATHNAME.exist?
109+
LOG_PATHNAME.open { |f|
110+
f.each_line do |line|
111+
$stderr.write line
112+
end
113+
}
114+
115+
exit(1)
116+
end
117+
end
118+
end
119+
end
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Logs if constants created by module loading are left over after suite has completed.
2+
module Metasploit::Framework::Spec::Constants::Suite
3+
#
4+
# CONSTANTS
5+
#
6+
7+
LOGS_PATHNAME = Pathname.new('log/metasploit/framework/spec/constants/suite')
8+
9+
# Logs leaked constants to {LOG_PATHNAME} and prints `message` to stderr.
10+
#
11+
# @param hook (see log_pathname)
12+
# @param message [String] additional message printed to stderr when there is at least one leaked constant.
13+
# @return [void]
14+
def self.log_leaked_constants(hook, message)
15+
count = 0
16+
hook_log_pathname = log_pathname(hook)
17+
hook_log_pathname.parent.mkpath
18+
19+
hook_log_pathname.open('w') do |f|
20+
count = Metasploit::Framework::Spec::Constants.each do |child_name|
21+
f.puts child_name
22+
end
23+
end
24+
25+
if count > 0
26+
$stderr.puts "#{count} #{'constant'.pluralize(count)} leaked under " \
27+
"#{Metasploit::Framework::Spec::Constants::PARENT_CONSTANT}. #{message} See #{hook_log_pathname} " \
28+
"for details."
29+
else
30+
hook_log_pathname.delete
31+
end
32+
end
33+
34+
# Configures after(:suite) callback for RSpec to check for leaked constants.
35+
def self.configure!
36+
unless @configured
37+
RSpec.configure do |config|
38+
config.before(:suite) do
39+
Metasploit::Framework::Spec::Constants::Suite.log_leaked_constants(
40+
:before,
41+
'Modules are being loaded outside callbacks before suite starts.'
42+
)
43+
end
44+
45+
config.after(:suite) do
46+
Metasploit::Framework::Spec::Constants::Suite.log_leaked_constants(
47+
:after,
48+
'Modules are being loaded inside callbacks or examples during suite run.'
49+
)
50+
end
51+
end
52+
53+
@configured = true
54+
end
55+
end
56+
57+
# Adds action to `spec` task so that `rake spec` fails if `log/leaked-constants.log` exists after printing out the
58+
# leaked constants.
59+
#
60+
# @return [void]
61+
def self.define_task
62+
Rake::Task.define_task(:spec) do
63+
leaked_before = Metasploit::Framework::Spec::Constants::Suite.print_leaked_constants(:before)
64+
leaked_after = Metasploit::Framework::Spec::Constants::Suite.print_leaked_constants(:after)
65+
66+
# leaks after suite can be be cleaned up by {Metasploit::Framework::Spec::Constants::Each.configure!}, but
67+
# leaks before suite require user intervention to find the leaks since it's a programming error in how the specs
68+
# are written where Modules are being loaded in the context scope.
69+
if leaked_after
70+
$stderr.puts
71+
$stderr.puts "Add `Metasploit::Framework::Spec::Constants::Each.configure!` to `spec/spec_helper.rb` " \
72+
"**NOTE: `Metasploit::Framework::Spec::Constants::Each` may report false leaks if `after(:all)` " \
73+
"is used to clean up constants instead of `after(:each)`**"
74+
end
75+
76+
if leaked_before || leaked_after
77+
exit 1
78+
end
79+
end
80+
end
81+
82+
# @param hook [:after, :before] Whether the log is recording leaked constants `:before` the suite runs or `:after` the
83+
# suite runs.
84+
def self.log_pathname(hook)
85+
LOGS_PATHNAME.join("#{hook}.log")
86+
end
87+
88+
# Prints logged leaked constants to stderr.
89+
#
90+
# @param hook [:after, :before] Whether the log is recording leaked constants `:before` the suite runs or `:after` the
91+
# suite runs.
92+
# @return [true] if leaks printed
93+
# @return [false] otherwise
94+
def self.print_leaked_constants(hook)
95+
hook_log_pathname = log_pathname(hook)
96+
97+
leaks = false
98+
99+
if hook_log_pathname.exist?
100+
leaks = true
101+
$stderr.puts "Leaked constants detected under #{Metasploit::Framework::Spec::Constants::PARENT_CONSTANT} #{hook} suite:"
102+
103+
hook_log_pathname.open do |f|
104+
f.each_line do |line|
105+
constant_name = line.strip
106+
full_name = Metasploit::Framework::Spec::Constants.full_name(constant_name)
107+
108+
if full_name
109+
formatted_full_name = " # #{full_name}"
110+
end
111+
112+
$stderr.puts " #{constant_name}#{formatted_full_name}"
113+
end
114+
end
115+
end
116+
117+
leaks
118+
end
119+
end

0 commit comments

Comments
 (0)