diff --git a/.gitignore b/.gitignore index fa85fdbfbf..5651548b7f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ .yardoc # Mac OS X files .DS_Store +# simplecov coverage data +coverage data/meterpreter/ext_server_pivot.dll data/meterpreter/ext_server_pivot.x64.dll doc diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..8c8562baa0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: ruby +rvm: +# - '1.8.7' + - '1.9.3' diff --git a/Gemfile b/Gemfile index 825f532450..77d2123dae 100755 --- a/Gemfile +++ b/Gemfile @@ -24,4 +24,7 @@ end group :test do # testing framework gem 'rspec' + # code coverage for tests + # any version newer than 0.5.4 gives an Encoding error when trying to read the source files. + gem 'simplecov', '0.5.4', :require => false end diff --git a/Gemfile.lock b/Gemfile.lock index 10894e9139..a1fddbd358 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -45,6 +45,10 @@ GEM rspec-expectations (2.11.3) diff-lcs (~> 1.1.3) rspec-mocks (2.11.3) + simplecov (0.5.4) + multi_json (~> 1.0.3) + simplecov-html (~> 0.5.3) + simplecov-html (0.5.3) slop (3.3.3) tzinfo (0.3.33) yard (0.8.2.1) @@ -60,4 +64,5 @@ DEPENDENCIES rake redcarpet rspec + simplecov (= 0.5.4) yard diff --git a/README.md b/README.md index 4b8c0c1b15..c24db4ad92 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -Metasploit [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/rapid7/metasploit-framework) +Metasploit [![Build Status](https://travis-ci.org/rapid7/metasploit-framework.png)](https://travis-ci.org/rapid7/metasploit-framework) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/rapid7/metasploit-framework) == The Metasploit Framework is released under a BSD-style license. See COPYING for more details. diff --git a/lib/msf/core/module_manager/loading.rb b/lib/msf/core/module_manager/loading.rb index 6b344537f9..0137542c0e 100644 --- a/lib/msf/core/module_manager/loading.rb +++ b/lib/msf/core/module_manager/loading.rb @@ -109,4 +109,4 @@ module Msf::ModuleManager::Loading count_by_type end -end \ No newline at end of file +end diff --git a/lib/msf/core/modules/error.rb b/lib/msf/core/modules/error.rb new file mode 100644 index 0000000000..aa5c2d5e40 --- /dev/null +++ b/lib/msf/core/modules/error.rb @@ -0,0 +1,38 @@ +# Base error class for all error under {Msf::Modules} +class Msf::Modules::Error < StandardError + def initialize(attributes={}) + @module_path = attributes[:module_path] + @module_reference_name = attributes[:module_reference_name] + + message_parts = [] + message_parts << "Failed to load module" + + if module_reference_name or module_path + clause_parts = [] + + if module_reference_name + clause_parts << module_reference_name + end + + if module_path + clause_parts << "from #{module_path}" + end + + clause = clause_parts.join(' ') + message_parts << "(#{clause})" + end + + causal_message = attributes[:causal_message] + + if causal_message + message_parts << "due to #{causal_message}" + end + + message = message_parts.join(' ') + + super(message) + end + + attr_reader :module_reference_name + attr_reader :module_path +end \ No newline at end of file diff --git a/lib/msf/core/modules/loader/base.rb b/lib/msf/core/modules/loader/base.rb index a7c304b6b0..651e39e412 100644 --- a/lib/msf/core/modules/loader/base.rb +++ b/lib/msf/core/modules/loader/base.rb @@ -3,6 +3,7 @@ # require 'msf/core/modules/loader' require 'msf/core/modules/namespace' +require 'msf/core/modules/metasploit_class_compatibility_error' require 'msf/core/modules/version_compatibility_error' # Responsible for loading modules for {Msf::ModuleManager}. @@ -117,12 +118,17 @@ class Msf::Modules::Loader::Base metasploit_class = nil + module_content = read_module_content(parent_path, type, module_reference_name) + + if module_content.empty? + # read_module_content is responsible for calling {#load_error}, so just return here. + return false + end + loaded = namespace_module_transaction(type + "/" + module_reference_name, :reload => reload) { |namespace_module| # set the parent_path so that the module can be reloaded with #load_module namespace_module.parent_path = parent_path - module_content = read_module_content(parent_path, type, module_reference_name) - begin namespace_module.module_eval_with_lexical_scope(module_content, module_path) # handle interrupts as pass-throughs unlike other Exceptions so users can bail with Ctrl+C @@ -133,45 +139,33 @@ class Msf::Modules::Loader::Base begin namespace_module.version_compatible!(module_path, module_reference_name) rescue Msf::Modules::VersionCompatibilityError => version_compatibility_error - error_message = "Failed to load module (#{module_path}) due to error and #{version_compatibility_error}" + load_error(module_path, version_compatibility_error) else - error_message = "#{error.class} #{error}" + load_error(module_path, error) end - # record the error message without the backtrace for the console - module_manager.module_load_error_by_path[module_path] = error_message - - error_message_with_backtrace = "#{error_message}:\n#{error.backtrace.join("\n")}" - elog(error_message_with_backtrace) - return false end begin namespace_module.version_compatible!(module_path, module_reference_name) rescue Msf::Modules::VersionCompatibilityError => version_compatibility_error - error_message = version_compatibility_error.to_s - - elog(error_message) - module_manager.module_load_error_by_path[module_path] = error_message + load_error(module_path, version_compatibility_error) return false end - metasploit_class = namespace_module.metasploit_class + begin + metasploit_class = namespace_module.metasploit_class!(module_path, module_reference_name) + rescue Msf::Modules::MetasploitClassCompatibilityError => error + load_error(module_path, error) - unless metasploit_class - error_message = "Missing Metasploit class constant" - - elog(error_message) - module_manager.module_load_error_by_path[module_path] = error_message - - return false + return false end unless usable?(metasploit_class) ilog( - "Skipping module #{module_reference_name} under #{parent_path} because is_usable returned false.", + "Skipping module (#{module_reference_name} from #{module_path}) because is_usable returned false.", 'core', LEV_1 ) @@ -409,6 +403,29 @@ class Msf::Modules::Loader::Base raise ::NotImplementedError end + # Records the load error to {Msf::ModuleManager::Loading#module_load_error_by_path} and the log. + # + # @param [String] module_path Path to the module as returned by {#module_path}. + # @param [Exception, #class, #to_s, #backtrace] error the error that cause the module not to load. + # @return [void] + # + # @see #module_path + def load_error(module_path, error) + # module_load_error_by_path does not get the backtrace because the value is echoed to the msfconsole where + # backtraces should not appear. + module_manager.module_load_error_by_path[module_path] = "#{error.class} #{error}" + + log_lines = [] + log_lines << "#{module_path} failed to load due to the following error:" + log_lines << error.class.to_s + log_lines << error.to_s + log_lines << "Call stack:" + log_lines += error.backtrace + + log_message = log_lines.join("\n") + elog(log_message) + end + # @return [Msf::ModuleManager] The module manager for which this loader is loading modules. attr_reader :module_manager diff --git a/lib/msf/core/modules/loader/directory.rb b/lib/msf/core/modules/loader/directory.rb index a549fdb188..420cbb109e 100644 --- a/lib/msf/core/modules/loader/directory.rb +++ b/lib/msf/core/modules/loader/directory.rb @@ -75,13 +75,17 @@ class Msf::Modules::Loader::Directory < Msf::Modules::Loader::Base module_content = '' - # force to read in binary mode so Pro modules won't be truncated on Windows - File.open(full_path, 'rb') do |f| - # Pass the size of the file as it leads to faster reads due to fewer buffer resizes. Greatest effect on Windows. - # @see http://www.ruby-forum.com/topic/209005 - # @see https://github.com/ruby/ruby/blob/ruby_1_8_7/io.c#L1205 - # @see https://github.com/ruby/ruby/blob/ruby_1_9_3/io.c#L2038 - module_content = f.read(f.stat.size) + begin + # force to read in binary mode so Pro modules won't be truncated on Windows + File.open(full_path, 'rb') do |f| + # Pass the size of the file as it leads to faster reads due to fewer buffer resizes. Greatest effect on Windows. + # @see http://www.ruby-forum.com/topic/209005 + # @see https://github.com/ruby/ruby/blob/ruby_1_8_7/io.c#L1205 + # @see https://github.com/ruby/ruby/blob/ruby_1_9_3/io.c#L2038 + module_content = f.read(f.stat.size) + end + rescue Errno::ENOENT => error + load_error(full_path, error) end module_content diff --git a/lib/msf/core/modules/metasploit_class_compatibility_error.rb b/lib/msf/core/modules/metasploit_class_compatibility_error.rb new file mode 100644 index 0000000000..e5d8193556 --- /dev/null +++ b/lib/msf/core/modules/metasploit_class_compatibility_error.rb @@ -0,0 +1,13 @@ +require 'msf/core/modules/error' + +# Error raised by {Msf::Modules::Namespace#metasploit_class!} if it cannot the namespace_module does not have a constant +# with {Msf::Framework::Major} or lower as a number after 'Metasploit', which indicates a compatible Msf::Module. +class Msf::Modules::MetasploitClassCompatibilityError < Msf::Modules::Error + def initialize(attributes={}) + super_attributes = { + :causal_message => 'Missing compatible Metasploit class constant', + }.merge(attributes) + + super(super_attributes) + end +end \ No newline at end of file diff --git a/lib/msf/core/modules/namespace.rb b/lib/msf/core/modules/namespace.rb index 0d5a62a0a0..7841952464 100644 --- a/lib/msf/core/modules/namespace.rb +++ b/lib/msf/core/modules/namespace.rb @@ -10,8 +10,6 @@ module Msf::Modules::Namespace # @return [nil] if such as class is not defined. def metasploit_class metasploit_class = nil - # don't search ancestors for the metasploit_class - #inherit = false ::Msf::Framework::Major.downto(1) do |major| # Since we really only care about the deepest namespace, we don't @@ -29,6 +27,19 @@ module Msf::Modules::Namespace metasploit_class end + def metasploit_class!(module_path, module_reference_name) + metasploit_class = self.metasploit_class + + unless metasploit_class + raise Msf::Modules::MetasploitClassCompatibilityError.new( + :module_path => module_path, + :module_reference_name => module_reference_name + ) + end + + metasploit_class + end + # Raises an error unless {Msf::Framework::VersionCore} and {Msf::Framework::VersionAPI} meet the minimum required # versions defined in RequiredVersions in the module content. # diff --git a/lib/msf/core/modules/version_compatibility_error.rb b/lib/msf/core/modules/version_compatibility_error.rb index 00c5341046..638f6d361d 100644 --- a/lib/msf/core/modules/version_compatibility_error.rb +++ b/lib/msf/core/modules/version_compatibility_error.rb @@ -1,20 +1,43 @@ +require 'msf/core/modules/error' + # Error raised by {Msf::Modules::Namespace#version_compatible!} on {Msf::Modules::Loader::Base#create_namespace_module} # if the API or Core version does not meet the minimum requirements defined in the RequiredVersions constant in the # {Msf::Modules::Loader::Base#read_module_content module content}. -class Msf::Modules::VersionCompatibilityError < StandardError +class Msf::Modules::VersionCompatibilityError < Msf::Modules::Error # @param [Hash{Symbol => Float}] attributes # @option attributes [Float] :minimum_api_version The minimum {Msf::Framework::VersionAPI} as defined in # RequiredVersions. # @option attributes [Float] :minimum_core_version The minimum {Msf::Framework::VersionCore} as defined in # RequiredVersions. def initialize(attributes={}) - @module_path = attributes[:module_path] - @module_reference_name = attributes[:module_reference_name] @minimum_api_version = attributes[:minimum_api_version] @minimum_core_version = attributes[:minimum_core_version] - super("Failed to reload module (#{module_reference_name} from #{module_path}) due to version check " \ - "(requires API:#{minimum_api_version} Core:#{minimum_core_version})") + message_parts = [] + message_parts << 'version check' + + if minimum_api_version or minimum_core_version + clause_parts = [] + + if minimum_api_version + clause_parts << "API >= #{minimum_api_version}" + end + + if minimum_core_version + clause_parts << "Core >= #{minimum_core_version}" + end + + clause = clause_parts.join(' and ') + message_parts << "(requires #{clause})" + end + + causal_message = message_parts.join(' ') + + super_attributes = { + :causal_message => causal_message + }.merge(attributes) + + super(super_attributes) end # @return [Float] The minimum value of {Msf::Framework::VersionAPI} for the module to be compatible. diff --git a/spec/lib/msf/core/modules/error_spec.rb b/spec/lib/msf/core/modules/error_spec.rb new file mode 100644 index 0000000000..144f485573 --- /dev/null +++ b/spec/lib/msf/core/modules/error_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe Msf::Modules::Error do + context 'instance methods' do + context '#initialize' do + include_context 'Msf::Modules::Error attributes' + + context 'with :causal_message' do + subject do + described_class.new(:causal_message => causal_message) + end + + it 'should include causal_message in error' do + subject.to_s.should == "Failed to load module due to #{causal_message}" + end + end + + context 'with :causal_message and :module_path' do + subject do + described_class.new( + :causal_message => causal_message, + :module_path => module_path + ) + end + + it 'should include causal_message and module_path in error' do + subject.to_s.should == "Failed to load module (from #{module_path}) due to #{causal_message}" + end + end + + context 'with :causal_message and :module_reference_name' do + subject do + described_class.new( + :causal_message => causal_message, + :module_reference_name => module_reference_name + ) + end + + it 'should include causal_message and module_reference_name in error' do + subject.to_s.should == "Failed to load module (#{module_reference_name}) due to #{causal_message}" + end + end + + context 'with :causal_message, :module_path, and :module_reference_nam' do + subject do + described_class.new( + :causal_message => causal_message, + :module_path => module_path, + :module_reference_name => module_reference_name + ) + end + + it 'should include causal_message, module_path, and module_reference_name in error' do + subject.to_s.should == "Failed to load module (#{module_reference_name} from #{module_path}) due to #{causal_message}" + end + end + + context 'with :module_path' do + subject do + described_class.new(:module_path => module_path) + end + + it 'should use :module_path for module_path' do + subject.module_path.should == module_path + end + + it 'should include module_path in error' do + subject.to_s.should == "Failed to load module (from #{module_path})" + end + end + + context 'with :module_path and :module_reference_name' do + subject do + described_class.new( + :module_path => module_path, + :module_reference_name => module_reference_name + ) + end + + it 'should include module_path and module_reference_name in error' do + subject.to_s.should == "Failed to load module (#{module_reference_name} from #{module_path})" + end + end + + context 'with :module_reference_name' do + subject do + described_class.new(:module_reference_name => module_reference_name) + end + + it 'should use :module_reference_name for module_reference_name' do + subject.module_reference_name.should == module_reference_name + end + + it 'should include module_reference_name in error' do + subject.to_s.should == "Failed to load module (#{module_reference_name})" + end + end + + end + end +end \ No newline at end of file diff --git a/spec/lib/msf/core/modules/loader/base_spec.rb b/spec/lib/msf/core/modules/loader/base_spec.rb index df0207d4c4..efce31a1a0 100644 --- a/spec/lib/msf/core/modules/loader/base_spec.rb +++ b/spec/lib/msf/core/modules/loader/base_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' require 'msf/core' describe Msf::Modules::Loader::Base do + include_context 'Msf::Modules::Loader::Base' + let(:described_class_pathname) do root_pathname.join('lib', 'msf', 'core', 'modules', 'loader', 'base.rb') end @@ -37,18 +39,6 @@ describe Msf::Modules::Loader::Base do 'rspec/mock' end - let(:parent_path) do - parent_pathname.to_s - end - - let(:parent_pathname) do - root_pathname.join('modules') - end - - let(:root_pathname) do - Pathname.new(Msf::Config.install_root) - end - let(:type) do Msf::MODULE_AUX end @@ -230,7 +220,7 @@ describe Msf::Modules::Loader::Base do context 'instance methods' do let(:module_manager) do - mock('Module Manager') + mock('Module Manager', :module_load_error_by_path => {}) end subject do @@ -323,13 +313,14 @@ describe Msf::Modules::Loader::Base do end it 'should call #namespace_module_transaction with the module full name and :reload => true' do + subject.stub(:read_module_content => module_content) + subject.should_receive(:namespace_module_transaction).with(module_full_name, hash_including(:reload => true)) subject.load_module(parent_path, type, module_reference_name) end it 'should set the parent_path on the namespace_module to match the parent_path passed to #load_module' do - module_manager.stub(:module_load_error_by_path => {}) module_manager.stub(:on_module_load) subject.stub(:read_module_content => module_content) @@ -339,7 +330,6 @@ describe Msf::Modules::Loader::Base do end it 'should call #read_module_content to get the module content so that #read_module_content can be overridden to change loading behavior' do - module_manager.stub(:module_load_error_by_path => {}) module_manager.stub(:on_module_load) subject.should_receive(:read_module_content).with(parent_path, type, module_reference_name).and_return(module_content) @@ -348,7 +338,6 @@ describe Msf::Modules::Loader::Base do it 'should call namespace_module.module_eval_with_lexical_scope with the module_path' do subject.stub(:read_module_content => malformed_module_content) - module_manager.stub(:module_load_error_by_path => {}) module_manager.stub(:on_module_load) # if the module eval error includes the module_path then the module_path was passed along correctly @@ -356,13 +345,29 @@ describe Msf::Modules::Loader::Base do subject.load_module(parent_path, type, module_reference_name, :reload => true).should be_false end + context 'with empty module content' do + before(:each) do + subject.stub(:read_module_content).with(parent_path, type, module_reference_name).and_return('') + end + + it 'should return false' do + subject.load_module(parent_path, type, module_reference_name).should be_false + end + + it 'should not attempt to make a new namespace_module' do + subject.should_not_receive(:namespace_module_transaction) + subject.load_module(parent_path, type, module_reference_name).should be_false + end + end + context 'with errors from namespace_module_eval_with_lexical_scope' do before(:each) do @namespace_module = mock('Namespace Module') @namespace_module.stub(:parent_path=) subject.stub(:namespace_module_transaction).and_yield(@namespace_module) - subject.stub(:read_module_content) + module_content = mock('Module Content', :empty? => false) + subject.stub(:read_module_content).and_return(module_content) end context 'with Interrupt' do @@ -409,16 +414,8 @@ describe Msf::Modules::Loader::Base do @namespace_module.stub(:version_compatible!).with(module_path, module_reference_name) end - it 'should report error class and string in module_manager.module_load_error_by_path' do - subject.load_module(parent_path, type, module_reference_name).should be_false - @module_load_error_by_path[module_path].should == "#{error_class} #{error}" - end - - it 'should report error class, string, and backtrace in the log' do - subject.should_receive(:elog).with( - # don't use join on backtrace as that will match implementation too closely - "#{error_class} #{error}:\n#{backtrace[0]}\n#{backtrace[1]}" - ) + it 'should record the load error using the original error' do + subject.should_receive(:load_error).with(module_path, error) subject.load_module(parent_path, type, module_reference_name).should be_false end end @@ -448,18 +445,8 @@ describe Msf::Modules::Loader::Base do ) end - it 'should report module_path and version compatibility error string in module_manager.module_load_error_by_path' do - subject.load_module(parent_path, type, module_reference_name).should be_false - - @module_load_error_by_path[module_path].should include(module_path) - @module_load_error_by_path[module_path].should include(version_compatibility_error.to_s) - end - - it 'should report backtrace of original error in the log' do - formatted_backtrace = "\n#{backtrace[0]}\n#{backtrace[1]}" - escaped_backtrace = Regexp.escape(formatted_backtrace) - - subject.should_receive(:elog).with(/#{escaped_backtrace}/) + it 'should record the load error using the Msf::Modules::VersionCompatibilityError' do + subject.should_receive(:load_error).with(module_path, version_compatibility_error) subject.load_module(parent_path, type, module_reference_name).should be_false end end @@ -479,7 +466,7 @@ describe Msf::Modules::Loader::Base do @namespace_module.stub(:module_eval_with_lexical_scope).with(module_content, module_path) metasploit_class = mock('Metasploit Class', :parent => @namespace_module) - @namespace_module.stub(:metasploit_class => metasploit_class) + @namespace_module.stub(:metasploit_class! => metasploit_class) subject.stub(:namespace_module_transaction).and_yield(@namespace_module) @@ -521,13 +508,8 @@ describe Msf::Modules::Loader::Base do ) end - it 'should report error in module_manage.module_load_error_by_path' do - subject.load_module(parent_path, type, module_reference_name).should be_false - @module_load_error_by_path[module_path].should == version_compatibility_error.to_s - end - - it 'should log error' do - subject.should_receive(:elog).with(version_compatibility_error.to_s) + it 'should record the load error' do + subject.should_receive(:load_error).with(module_path, version_compatibility_error) subject.load_module(parent_path, type, module_reference_name).should be_false end @@ -548,24 +530,27 @@ describe Msf::Modules::Loader::Base do end context 'without metasploit_class' do + let(:error) do + Msf::Modules::MetasploitClassCompatibilityError.new( + :module_path => module_path, + :module_reference_name => module_reference_name + ) + end + before(:each) do - @namespace_module.stub(:metasploit_class).and_return(nil) + @namespace_module.stub(:metasploit_class!).with(module_path, module_reference_name).and_raise(error) end - let(:error_message) do - 'Missing Metasploit class constant' - end - - it 'should log missing Metasploit class' do - subject.should_receive(:elog).with(error_message) + it 'should record load error' do + subject.should_receive( + :load_error + ).with( + module_path, + kind_of(Msf::Modules::MetasploitClassCompatibilityError) + ) subject.load_module(parent_path, type, module_reference_name).should be_false end - it 'should record error in module_manager.module_load_error_by_path' do - subject.load_module(parent_path, type, module_reference_name).should be_false - @module_load_error_by_path[module_path].should == error_message - end - it 'should return false' do subject.load_module(parent_path, type, module_reference_name).should be_false end @@ -583,7 +568,7 @@ describe Msf::Modules::Loader::Base do end before(:each) do - @namespace_module.stub(:metasploit_class => metasploit_class) + @namespace_module.stub(:metasploit_class! => metasploit_class) end it 'should check if it is usable' do diff --git a/spec/lib/msf/core/modules/loader/directory_spec.rb b/spec/lib/msf/core/modules/loader/directory_spec.rb index b371883f49..1a1728b21e 100644 --- a/spec/lib/msf/core/modules/loader/directory_spec.rb +++ b/spec/lib/msf/core/modules/loader/directory_spec.rb @@ -2,6 +2,126 @@ require 'spec_helper' require 'msf/core' require 'msf/core/modules/loader/directory' -describe Msf::Modules::Loader::Directory do +require 'msf/core' +describe Msf::Modules::Loader::Directory do + context 'instance methods' do + include_context 'Msf::Modules::Loader::Base' + + let(:module_manager) do + mock('Module Manager') + end + + let(:module_path) do + "#{parent_path}/exploits/#{module_reference_name}.rb" + end + + let(:type) do + 'exploit' + end + + subject do + described_class.new(module_manager) + end + + context '#load_module' do + context 'with existent module_path' do + let(:framework) do + framework = mock('Msf::Framework', :datastore => {}) + + events = mock('Events') + events.stub(:on_module_load) + events.stub(:on_module_created) + framework.stub(:events => events) + + framework + end + + let(:module_full_name) do + "#{type}/#{module_reference_name}" + end + + let(:module_manager) do + Msf::ModuleManager.new(framework) + end + + let(:module_reference_name) do + 'windows/smb/ms08_067_netapi' + end + + it 'should load a module that can be created' do + subject.load_module(parent_path, type, module_reference_name).should be_true + + created_module = module_manager.create(module_full_name) + + created_module.name.should == 'Microsoft Server Service Relative Path Stack Corruption' + end + end + + context 'without existent module_path' do + let(:module_reference_name) do + 'osx/armle/safari_libtiff' + end + + let(:error) do + Errno::ENOENT.new(module_path) + end + + before(:each) do + module_manager.stub(:file_changed? => true) + module_manager.stub(:module_load_error_by_path => {}) + end + + it 'should not raise an error' do + File.exist?(module_path).should be_false + + expect { + subject.load_module(parent_path, type, module_reference_name) + }.to_not raise_error + end + + it 'should return false' do + File.exist?(module_path).should be_false + + subject.load_module(parent_path, type, module_reference_name).should be_false + end + end + end + + context '#read_module_content' do + context 'with non-existent module_path' do + let(:module_reference_name) do + 'osx/armle/safari_libtiff' + end + + before(:each) do + subject.stub(:load_error).with(module_path, kind_of(Errno::ENOENT)) + end + + # this ensures that the File.exist?(module_path) checks are checking the same path as the code under test + it 'should attempt to open the expected module_path' do + File.should_receive(:open).with(module_path, 'rb') + File.exist?(module_path).should be_false + + subject.send(:read_module_content, parent_path, type, module_reference_name) + end + + it 'should not raise an error' do + expect { + subject.send(:read_module_content, parent_path, type, module_reference_name) + }.to_not raise_error + end + + it 'should return an empty string' do + subject.send(:read_module_content, parent_path, type, module_reference_name).should == '' + end + + it 'should record the load error' do + subject.should_receive(:load_error).with(module_path, kind_of(Errno::ENOENT)) + + subject.send(:read_module_content, parent_path, type, module_reference_name).should == '' + end + end + end + end end diff --git a/spec/lib/msf/core/modules/metasploit_class_compatibility_error_spec.rb b/spec/lib/msf/core/modules/metasploit_class_compatibility_error_spec.rb new file mode 100644 index 0000000000..b3a411a7d6 --- /dev/null +++ b/spec/lib/msf/core/modules/metasploit_class_compatibility_error_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +require 'msf/core/modules/metasploit_class_compatibility_error' + +describe Msf::Modules::MetasploitClassCompatibilityError do + it_should_behave_like 'Msf::Modules::Error subclass #initialize' +end \ No newline at end of file diff --git a/spec/lib/msf/core/modules/namespace_spec.rb b/spec/lib/msf/core/modules/namespace_spec.rb new file mode 100644 index 0000000000..10f10822f6 --- /dev/null +++ b/spec/lib/msf/core/modules/namespace_spec.rb @@ -0,0 +1,267 @@ +require 'spec_helper' + +require 'msf/core' +require 'msf/core/modules/namespace' + +describe Msf::Modules::Namespace do + let(:module_path) do + "parent/path/type_directory/#{module_reference_name}.rb" + end + + let(:module_reference_name) do + 'module/reference/name' + end + + subject do + mod = Module.new + mod.extend described_class + + mod + end + + context 'metasploit_class' do + before(:each) do + if major + subject.const_set("Metasploit#{major}", Class.new) + end + end + + context 'without Metasploit constant defined' do + let(:major) do + nil + end + + it 'should not be defined' do + metasploit_constants = subject.constants.select { |constant| + constant.to_s =~ /Metasploit/ + } + + metasploit_constants.should be_empty + end + end + + context 'with Metasploit1 constant defined' do + let(:major) do + 1 + end + + it 'should be defined' do + subject.const_defined?('Metasploit1').should be_true + end + + it 'should return the class' do + subject.metasploit_class.should be_a Class + end + end + + context 'with Metasploit2 constant defined' do + let(:major) do + 2 + end + + it 'should be defined' do + subject.const_defined?('Metasploit2').should be_true + end + + it 'should return the class' do + subject.metasploit_class.should be_a Class + end + end + + context 'with Metasploit3 constant defined' do + let(:major) do + 3 + end + + it 'should be defined' do + subject.const_defined?('Metasploit3').should be_true + end + + it 'should return the class' do + subject.metasploit_class.should be_a Class + end + end + + context 'with Metasploit4 constant defined' do + let(:major) do + 4 + end + + it 'should be defined' do + subject.const_defined?('Metasploit4').should be_true + end + + it 'should return the class' do + subject.metasploit_class.should be_a Class + end + end + + context 'with Metasploit5 constant defined' do + let(:major) do + 5 + end + + it 'should be defined' do + subject.const_defined?('Metasploit5').should be_true + end + + it 'should be newer than Msf::Framework::Major' do + major.should > Msf::Framework::Major + end + + it 'should return nil' do + subject.metasploit_class.should be_nil + end + end + end + + context 'metasploit_class!' do + it 'should call metasploit_class' do + subject.should_receive(:metasploit_class).and_return(Class.new) + + subject.metasploit_class!(module_path, module_reference_name) + end + + context 'with metasploit_class' do + let(:metasploit_class) do + Class.new + end + + before(:each) do + subject.stub(:metasploit_class => metasploit_class) + end + + it 'should return the metasploit_class' do + subject.metasploit_class!(module_path, module_reference_name).should == metasploit_class + end + end + + context 'without metasploit_class' do + before(:each) do + subject.stub(:metasploit_class => nil) + end + + it 'should raise a Msf::Modules::MetasploitClassCompatibilityError' do + expect { + subject.metasploit_class!(module_path, module_reference_name) + }.to raise_error(Msf::Modules::MetasploitClassCompatibilityError) + end + + context 'the Msf::Modules::MetasploitClassCompatibilityError' do + it 'should include the module path' do + error = nil + + begin + subject.metasploit_class!(module_path, module_reference_name) + rescue Msf::Modules::MetasploitClassCompatibilityError => error + end + + error.should_not be_nil + error.to_s.should include(module_path) + end + + it 'should include the module reference name' do + error = nil + + begin + subject.metasploit_class!(module_path, module_reference_name) + rescue Msf::Modules::MetasploitClassCompatibilityError => error + end + + error.should_not be_nil + error.to_s.should include(module_reference_name) + end + end + end + end + + context 'version_compatible!' do + context 'without RequiredVersions' do + it 'should not be defined' do + subject.const_defined?('RequiredVersions').should be_false + end + + it 'should not raise an error' do + expect { + subject.version_compatible!(module_path, module_reference_name) + }.to_not raise_error + end + end + + context 'with RequiredVersions defined' do + let(:minimum_api_version) do + 1 + end + + let(:minimum_core_version) do + 1 + end + + before(:each) do + subject.const_set( + :RequiredVersions, + [ + minimum_core_version, + minimum_api_version + ] + ) + end + + context 'with minimum Core version' do + it 'should be <= Msf::Framework::VersionCore' do + minimum_core_version.should <= Msf::Framework::VersionCore + end + + context 'without minimum API version' do + let(:minimum_api_version) do + 2 + end + + it 'should be > Msf::Framework::VersionAPI' do + minimum_api_version.should > Msf::Framework::VersionAPI + end + + it_should_behave_like 'Msf::Modules::VersionCompatibilityError' + end + + context 'with minimum API version' do + it 'should not raise an error' do + expect { + subject.version_compatible!(module_path, module_reference_name) + }.to_not raise_error(Msf::Modules::VersionCompatibilityError) + end + end + end + + context 'without minimum Core version' do + let(:minimum_core_version) do + 5 + end + + it 'should be > Msf::Framework::VersionCore' do + minimum_core_version.should > Msf::Framework::VersionCore + end + + context 'without minimum API version' do + let(:minimum_api_version) do + 2 + end + + it 'should be > Msf::Framework::VersionAPI' do + minimum_api_version.should > Msf::Framework::VersionAPI + end + + it_should_behave_like 'Msf::Modules::VersionCompatibilityError' + end + + context 'with minimum API version' do + it 'should be <= Msf::Framework::VersionAPI' do + minimum_api_version <= Msf::Framework::VersionAPI + end + + it_should_behave_like 'Msf::Modules::VersionCompatibilityError' + end + end + end + end +end \ No newline at end of file diff --git a/spec/lib/msf/core/modules/version_compatibility_error_spec.rb b/spec/lib/msf/core/modules/version_compatibility_error_spec.rb new file mode 100644 index 0000000000..9522f8bbf7 --- /dev/null +++ b/spec/lib/msf/core/modules/version_compatibility_error_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Msf::Modules::VersionCompatibilityError do + it_should_behave_like 'Msf::Modules::Error subclass #initialize' do + let(:minimum_api_version) do + 1 + end + + let(:minimum_core_version) do + 2 + end + + it 'should say cause was version check' do + subject.to_s.should match(/due to version check/) + end + + context 'with :minimum_api_version' do + subject do + described_class.new( + :minimum_api_version => minimum_api_version + ) + end + + it 'should set minimum_api_version' do + subject.minimum_api_version.should == minimum_api_version + end + + it 'should include minimum_api_version in error' do + subject.to_s.should match(/due to version check \(requires API >= #{minimum_api_version}\)/) + end + end + + context 'with :minimum_api_version and :minimum_core_version' do + subject do + described_class.new( + :minimum_api_version => minimum_api_version, + :minimum_core_version => minimum_core_version + ) + end + + it 'should include minimum_api_version and minimum_core_version in error' do + subject.to_s.should match(/due to version check \(requires API >= #{minimum_api_version} and Core >= #{minimum_core_version}\)/) + end + end + + context 'with :minimum_core_version' do + subject do + described_class.new( + :minimum_core_version => minimum_core_version + ) + end + + it 'should set minimum_core_version' do + subject.minimum_core_version.should == minimum_core_version + end + + it 'should include minimum_core_version in error' do + subject.to_s.should match(/due to version check \(requires Core >= #{minimum_core_version}\)/) + end + end + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f62338294f..6f8932bc27 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,6 +8,11 @@ root_pathname = spec_pathname.join('..').expand_path lib_pathname = root_pathname.join('lib') $LOAD_PATH.unshift(lib_pathname.to_s) +# must be first require and started before any other requires so that it can measure coverage of all following required +# code. It is after the rubygems and bundler only because Bundler.setup supplies the LOAD_PATH to simplecov. +require 'simplecov' +SimpleCov.start + require 'rspec/core' # Requires supporting ruby files with custom matchers and macros, etc, diff --git a/spec/support/shared/contexts/msf_modules_error_attributes.rb b/spec/support/shared/contexts/msf_modules_error_attributes.rb new file mode 100644 index 0000000000..74d6366f58 --- /dev/null +++ b/spec/support/shared/contexts/msf_modules_error_attributes.rb @@ -0,0 +1,13 @@ +shared_context 'Msf::Modules::Error attributes' do + let(:causal_message) do + 'rspec' + end + + let(:module_path) do + "parent/path/type/#{module_reference_name}.rb" + end + + let(:module_reference_name) do + 'module/reference/name' + end +end \ No newline at end of file diff --git a/spec/support/shared/contexts/msf_modules_loader_base.rb b/spec/support/shared/contexts/msf_modules_loader_base.rb new file mode 100644 index 0000000000..27d4a4e56a --- /dev/null +++ b/spec/support/shared/contexts/msf_modules_loader_base.rb @@ -0,0 +1,13 @@ +shared_context "Msf::Modules::Loader::Base" do + let(:parent_path) do + parent_pathname.to_s + end + + let(:parent_pathname) do + root_pathname.join('modules') + end + + let(:root_pathname) do + Pathname.new(Msf::Config.install_root) + end +end \ No newline at end of file diff --git a/spec/support/shared/examples/msf_modules_error_subclass_initialize.rb b/spec/support/shared/examples/msf_modules_error_subclass_initialize.rb new file mode 100644 index 0000000000..43a753a044 --- /dev/null +++ b/spec/support/shared/examples/msf_modules_error_subclass_initialize.rb @@ -0,0 +1,26 @@ +shared_examples_for 'Msf::Modules::Error subclass #initialize' do + context 'instance methods' do + context '#initialize' do + include_context 'Msf::Modules::Error attributes' + + subject do + described_class.new( + :module_path => module_path, + :module_reference_name => module_reference_name + ) + end + + it 'should include causal message in error' do + subject.to_s.should match(/due to .*/) + end + + it 'should set module_path' do + subject.module_path.should == module_path + end + + it 'should set module_reference_name' do + subject.module_reference_name.should == module_reference_name + end + end + end +end \ No newline at end of file diff --git a/spec/support/shared/examples/msf_modules_version_compatibility_error.rb b/spec/support/shared/examples/msf_modules_version_compatibility_error.rb new file mode 100644 index 0000000000..a2f11c7924 --- /dev/null +++ b/spec/support/shared/examples/msf_modules_version_compatibility_error.rb @@ -0,0 +1,32 @@ +shared_examples_for 'Msf::Modules::VersionCompatibilityError' do + let(:error) do + begin + subject.version_compatible!(module_path, module_reference_name) + rescue Msf::Modules::VersionCompatibilityError => error + end + + error + end + + it 'should be raised' do + expect { + subject.version_compatible!(module_path, module_reference_name) + }.to raise_error(Msf::Modules::VersionCompatibilityError) + end + + it 'should include minimum API version' do + error.to_s.should include(minimum_api_version.to_s) + end + + it 'should include minimum Core version' do + error.to_s.should include(minimum_core_version.to_s) + end + + it 'should include module path' do + error.to_s.should include(module_path) + end + + it 'should include module reference name' do + error.to_s.should include(module_reference_name) + end +end \ No newline at end of file