diff --git a/lib/fastlib.rb b/lib/fastlib.rb index c709928683..4af25ae111 100755 --- a/lib/fastlib.rb +++ b/lib/fastlib.rb @@ -182,12 +182,14 @@ class FastLib end + # This method provides a way to create a FASTLIB archive programatically. # - # This method provides a way to create a FASTLIB archive programatically, - # the key arguments are the name of the destination archive, the base - # directory that should be excluded from the archived path, and finally - # the list of specific files and directories to include in the archive. - # + # @param [String] lib the output path for the archive + # @param [String] flag a string containing the hex values for the flags ({FLAG_COMPRESS} and {FLAG_ENCRYPT}). + # @param [String] bdir the path to the base directory which will be stripped from all paths included in the archive + # @param [Array] dirs list of directories/files to pack into the archive. All dirs should be under bdir so + # that the paths are stripped correctly. + # @return [void] def self.dump(lib, flag, bdir, *dirs) head = "" data = "" diff --git a/lib/msf/core/module_manager/loading.rb b/lib/msf/core/module_manager/loading.rb index 39a53fcb51..6b344537f9 100644 --- a/lib/msf/core/module_manager/loading.rb +++ b/lib/msf/core/module_manager/loading.rb @@ -26,11 +26,11 @@ module Msf::ModuleManager::Loading def file_changed?(path) changed = false - module_details = self.module_info_by_path[path] + module_info = self.module_info_by_path[path] # if uncached then it counts as changed # Payloads can't be cached due to stage/stager matching - if module_details.nil? or module_details[:mtype] == Msf::MODULE_PAYLOAD + if module_info.nil? or module_info[:type] == Msf::MODULE_PAYLOAD changed = true else begin @@ -39,7 +39,7 @@ module Msf::ModuleManager::Loading # if the file does not exist now, that's a change changed = true else - cached_modification_time = module_details[:mtime].to_i + cached_modification_time = module_info[:modification_time].to_i # if the file's modification time's different from the cache, then it's changed if current_modification_time != cached_modification_time diff --git a/lib/msf/core/modules/loader/archive.rb b/lib/msf/core/modules/loader/archive.rb index dfd42edcaf..1a562b5a35 100644 --- a/lib/msf/core/modules/loader/archive.rb +++ b/lib/msf/core/modules/loader/archive.rb @@ -41,7 +41,7 @@ class Msf::Modules::Loader::Archive < Msf::Modules::Loader::Base type = entry.split('/', 2)[0] type = type.singularize - unless module_manager.enablement_by_type[type] + unless module_manager.type_enabled?(type) next end diff --git a/lib/msf/core/modules/loader/base.rb b/lib/msf/core/modules/loader/base.rb index 4cf57c3c38..a7c304b6b0 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/version_compatibility_error' # Responsible for loading modules for {Msf::ModuleManager}. # @@ -77,7 +78,6 @@ class Msf::Modules::Loader::Base raise ::NotImplementedError end - # Loads a module from the supplied path and module_reference_name. # # @param [String] parent_path The path under which the module exists. This is not necessarily the same path as passed @@ -104,14 +104,18 @@ class Msf::Modules::Loader::Base force = options[:force] || false reload = options[:reload] || false - unless force or module_manager.file_changed?(parent_path) - dlog("Cached module from #{parent_path} has not changed.", 'core', LEV_2) + module_path = self.module_path(parent_path, type, module_reference_name) + file_changed = module_manager.file_changed?(module_path) + + unless force or file_changed + dlog("Cached module from #{module_path} has not changed.", 'core', LEV_2) return false end + reload ||= force || file_changed + metasploit_class = nil - module_path = self.module_path(parent_path, type, module_reference_name) 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 @@ -121,7 +125,7 @@ class Msf::Modules::Loader::Base begin namespace_module.module_eval_with_lexical_scope(module_content, module_path) - # handle interrupts as pass-throughs unlike other Exceptions + # handle interrupts as pass-throughs unlike other Exceptions so users can bail with Ctrl+C rescue ::Interrupt raise rescue ::Exception => error @@ -191,6 +195,11 @@ class Msf::Modules::Loader::Base # not trigger an ambiguous name warning, which would cause the reloaded module to not be stored in the # ModuleManager. module_manager.delete(module_reference_name) + + # Delete the original copy of the module in the type-specific module set stores the reloaded module and doesn't + # trigger an ambiguous name warning + module_set = module_manager.module_set(type) + module_set.delete(module_reference_name) end # Do some processing on the loaded module to get it into the right associations @@ -290,7 +299,6 @@ class Msf::Modules::Loader::Base dlog("Reloading module #{module_reference_name}...", 'core') - if load_module(parent_path, type, module_reference_name, :force => true, :reload => true) # Create a new instance of the module reloaded_module_instance = module_manager.create(module_reference_name) @@ -486,20 +494,18 @@ class Msf::Modules::Loader::Base elog("Reloading namespace_module #{previous_namespace_module} when :reload => false") end + relative_name = namespace_module_names.last + if previous_namespace_module parent_module = previous_namespace_module.parent - relative_name = namespace_module_names.last - - # remove_const is private, so invoke in instance context - parent_module.instance_eval do - remove_const relative_name - end - else - parent_module = nil - relative_name = namespace_module_names.last + # remove_const is private, so use send to bypass + parent_module.send(:remove_const, relative_name) end namespace_module = create_namespace_module(namespace_module_names) + # Get the parent module from the created module so that restore_namespace_module can remove namespace_module's + # constant if needed. + parent_module = namespace_module.parent begin loaded = block.call(namespace_module) @@ -534,19 +540,29 @@ class Msf::Modules::Loader::Base # # @param [Module] parent_module The .parent of namespace_module before it was removed from the constant tree. # @param [String] relative_name The name of the constant under parent_module where namespace_module was attached. - # @param [Module] namespace_module The previous namespace module containing the old module content. + # @param [Module, nil] namespace_module The previous namespace module containing the old module content. If `nil`, + # then the relative_name constant is removed from parent_module, but nothing is set as the new constant. # @return [void] def restore_namespace_module(parent_module, relative_name, namespace_module) - if parent_module and namespace_module - # the const may have been redefined by {#create_namespace_module}, in which case that new namespace_module needs - # to be removed so the original can replace it. - if parent_module.const_defined? relative_name - parent_module.instance_eval do - remove_const relative_name - end - end + if parent_module + # If there is a current module with relative_name + if parent_module.const_defined?(relative_name) + # if the current value isn't the value to be restored. + if parent_module.const_get(relative_name) != namespace_module + # remove_const is private, so use send to bypass + parent_module.send(:remove_const, relative_name) - parent_module.const_set(relative_name, namespace_module) + # if there was a previous module, not set it to the name + if namespace_module + parent_module.const_set(relative_name, namespace_module) + end + end + else + # if there was a previous module, but there isn't a current module, then restore the previous module + if namespace_module + parent_module.const_set(relative_name, namespace_module) + end + end end end diff --git a/spec/lib/fastlib_spec.rb b/spec/lib/fastlib_spec.rb new file mode 100644 index 0000000000..90f8d0ceac --- /dev/null +++ b/spec/lib/fastlib_spec.rb @@ -0,0 +1,231 @@ +require 'spec_helper' + +require 'msf/core' + +describe FastLib do + let(:archived_paths) do + [ + File.join('auxiliary', 'scanner', 'portscan', 'xmas.rb'), + File.join('exploits', 'windows', 'smb', 'ms08_067_netapi.rb') + ] + end + + let(:base_path) do + File.join(Msf::Config.install_root, 'modules') + end + + let(:extension) do + '.fastlib' + end + + let(:flag_compress) do + 0x01 + end + + let(:flag_encrypt) do + 0x02 + end + + let(:unarchived_paths) do + archived_paths.collect { |archived_path| + File.join(base_path, archived_path) + } + end + + context 'CONSTANTS' do + context 'flags' do + it 'should have compression' do + described_class::FLAG_COMPRESS.should == flag_compress + end + + it 'should have encryption' do + described_class::FLAG_ENCRYPT.should == flag_encrypt + end + end + end + + context 'class methods' do + context 'dump' do + let(:flag_string) do + flags.to_s(16) + end + + before(:each) do + FastLib.cache.clear + end + + around(:each) do |example| + Dir.mktmpdir do |directory| + @destination_path = File.join(directory, "rspec#{extension}") + + example.run + end + end + + context 'without compression and without encryption' do + let(:flags) do + 0x0 + end + + it 'should create an archive' do + File.exist?(@destination_path).should be_false + + described_class.dump(@destination_path, flag_string, base_path, *unarchived_paths) + + File.exist?(@destination_path).should be_true + end + + context 'cache' do + it 'should populate' do + FastLib.cache[@destination_path].should be_nil + + described_class.dump(@destination_path, flag_string, base_path, *unarchived_paths) + + FastLib.cache[@destination_path].should be_a Hash + end + + it 'should include flags' do + described_class.dump(@destination_path, flag_string, base_path, *unarchived_paths) + + FastLib.cache[@destination_path][:fastlib_flags].should == flags + end + + pending "Fix https://www.pivotaltracker.com/story/show/38730815" do + it 'should include header' do + described_class.dump(@destination_path, flag_string, base_path, *unarchived_paths) + header = FastLib.cache[@destination_path][:fastlib_header] + modification_time = File.mtime(@destination_path).utc.to_i + + header.should be_a Array + # @todo figure out why 12 before header length + header[0].should == 12 + # @todo figure out why header length is 0 + header[1].should == 0 + header[2].should == modification_time + end + + it 'should include archived paths' do + described_class.dump(@destination_path, flag_string, base_path, *unarchived_paths) + cache = FastLib.cache[@destination_path] + + archived_path = File.join('exploits', 'windows', 'smb', 'ms08_067_netapi.rb') + unarchived_path = File.join(base_path, archived_path) + + # make sure that the unarchived module exists and hasn't be deleted or renamed before expecting it to be + # in the archive. + File.exist?(unarchived_path).should be_true + cache[archived_path].should_not be_nil + end + end + end + end + + context 'with compression and without encryption' do + let(:flags) do + flag_compress + end + + it 'should create an archive' do + File.exist?(@destination_path).should be_false + + described_class.dump(@destination_path, flag_string, base_path, *unarchived_paths) + + File.exist?(@destination_path).should be_true + end + + it 'should be smaller than the uncompressed archive' do + uncompressed_path = "#{@destination_path}.uncompressed" + compressed_path = "#{@destination_path}.compressed" + + File.exist?(uncompressed_path).should be_false + File.exist?(compressed_path).should be_false + + described_class.dump(uncompressed_path, '', base_path, *unarchived_paths) + described_class.dump(compressed_path, flag_string, base_path, *unarchived_paths) + + File.exist?(uncompressed_path).should be_true + File.exist?(compressed_path).should be_true + + File.size(compressed_path).should < File.size(uncompressed_path) + end + end + + context 'without compression and with encryption' do + let(:flags) do + flag_encrypt + end + + it 'should create an archive' do + File.exist?(@destination_path).should be_false + + described_class.dump(@destination_path, flag_string, base_path, *unarchived_paths) + + File.exist?(@destination_path).should be_true + end + end + + context 'with compression and with encryption' do + let(:flags) do + flag_compress | flag_encrypt + end + + it 'should create an archive' do + File.exist?(@destination_path).should be_false + + described_class.dump(@destination_path, flag_string, base_path, *unarchived_paths) + + File.exist?(@destination_path).should be_true + end + end + end + + context 'list' do + around(:each) do |example| + Dir.mktmpdir do |directory| + @destination_path = File.join(directory, "rspec#{extension}") + + FastLib.dump(@destination_path, FastLib::FLAG_COMPRESS.to_s, base_path, *unarchived_paths) + + example.run + end + end + + # ensure modules expected to be listed actually exist + it 'should use existent unarchived modules' do + unarchived_paths.each do |unarchived_path| + File.exist?(unarchived_path).should be_true + end + end + + context 'with cached dump', :pending => "Fix https://www.pivotaltracker.com/story/show/38730815" do + it 'should have dump cached' do + FastLib.cache[@destination_path].should_not be_nil + end + + it 'should list archived paths' do + paths = FastLib.list(@destination_path) + + paths.length.should == archived_paths.length + paths.should == archived_paths + end + end + + context 'without cached dump' do + before(:each) do + FastLib.cache.clear + end + + it 'should not have dump cache' do + FastLib.cache[@destination_path].should be_nil + end + + it 'should list archived paths' do + paths = FastLib.list(@destination_path) + + paths.length.should == archived_paths.length + paths.should == archived_paths + end + end + end + end +end \ No newline at end of file diff --git a/spec/msf/core/module_manager_spec.rb b/spec/lib/msf/core/module_manager_spec.rb similarity index 51% rename from spec/msf/core/module_manager_spec.rb rename to spec/lib/msf/core/module_manager_spec.rb index 78730a8c66..714d545f64 100644 --- a/spec/msf/core/module_manager_spec.rb +++ b/spec/lib/msf/core/module_manager_spec.rb @@ -107,4 +107,82 @@ describe Msf::ModuleManager do end end end + + context '#file_changed?' do + let(:module_basename) do + [basename_prefix, '.rb'] + end + + it 'should return true if module info is not cached' do + Tempfile.open(module_basename) do |tempfile| + module_path = tempfile.path + + subject.send(:module_info_by_path)[module_path].should be_nil + subject.file_changed?(module_path).should be_true + end + end + + it 'should return true if the cached type is Msf::MODULE_PAYLOAD' do + Tempfile.open(module_basename) do |tempfile| + module_path = tempfile.path + modification_time = File.mtime(module_path) + + subject.send(:module_info_by_path)[module_path] = { + # :modification_time must match so that it is the :type that is causing the `true` and not the + # :modification_time causing the `true`. + :modification_time => modification_time, + :type => Msf::MODULE_PAYLOAD + } + + subject.file_changed?(module_path).should be_true + end + end + + context 'with cache module info and not a payload module' do + it 'should return true if the file does not exist on the file system' do + tempfile = Tempfile.new(module_basename) + module_path = tempfile.path + modification_time = File.mtime(module_path).to_i + + subject.send(:module_info_by_path)[module_path] = { + :modification_time => modification_time + } + + tempfile.unlink + + File.exist?(module_path).should be_false + subject.file_changed?(module_path).should be_true + end + + it 'should return true if modification time does not match the cached modification time' do + Tempfile.open(module_basename) do |tempfile| + module_path = tempfile.path + modification_time = File.mtime(module_path).to_i + cached_modification_time = (modification_time * rand).to_i + + subject.send(:module_info_by_path)[module_path] = { + :modification_time => cached_modification_time + } + + cached_modification_time.should_not == modification_time + subject.file_changed?(module_path).should be_true + end + end + + it 'should return false if modification time does match the cached modification time' do + Tempfile.open(module_basename) do |tempfile| + module_path = tempfile.path + modification_time = File.mtime(module_path).to_i + cached_modification_time = modification_time + + subject.send(:module_info_by_path)[module_path] = { + :modification_time => cached_modification_time + } + + cached_modification_time.should == modification_time + subject.file_changed?(module_path).should be_false + end + end + end + end end \ No newline at end of file diff --git a/spec/lib/msf/core/modules/loader/archive_spec.rb b/spec/lib/msf/core/modules/loader/archive_spec.rb new file mode 100644 index 0000000000..b0edf9850c --- /dev/null +++ b/spec/lib/msf/core/modules/loader/archive_spec.rb @@ -0,0 +1,275 @@ +require 'spec_helper' + +require 'msf/core' + +describe Msf::Modules::Loader::Archive do + let(:archive_extension) do + '.fastlib' + end + + context 'CONSTANTS' do + it 'should have extension' do + described_class::ARCHIVE_EXTENSION.should == archive_extension + end + end + + context 'instance methods' do + let(:enabled_type) do + 'exploit' + end + + let(:enabled_type_directory) do + 'exploits' + end + + let(:framework) do + mock('Framework') + end + + let(:module_extension) do + '.rb' + end + + let(:module_manager) do + # DO NOT mock module_manager to ensure that no protected methods are being called. + Msf::ModuleManager.new(framework, [enabled_type]) + end + + let(:module_reference_name) do + 'module/reference/name' + end + + subject do + described_class.new(module_manager) + end + + context '#each_module_reference_name' do + let(:disabled_module_content) do + <<-EOS + class Metasploit3 < Msf::Auxiliary + end + EOS + end + + let(:disabled_type) do + 'auxiliary' + end + + let(:disabled_type_directory) do + 'auxiliary' + end + + let(:enabled_module_content) do + <<-EOS + class Metasploit3 < Msf::Exploit::Remote + end + EOS + end + + around(:each) do |example| + Dir.mktmpdir do |directory| + @base_path = directory + + # make a .svn directory to be ignored + subversion_path = File.join(@base_path, '.svn') + FileUtils.mkdir_p subversion_path + + # make a type directory that should be ignored because it's not enabled + disabled_type_path = File.join(@base_path, disabled_type_directory) + FileUtils.mkdir_p disabled_type_path + + # + # create a valid module in the disabled type directory to make sure it's the enablement that's preventing the + # yield + # + + disabled_module_path = File.join(disabled_type_path, "#{disabled_type}#{module_extension}") + + File.open(disabled_module_path, 'wb') do |f| + f.write(disabled_module_content) + end + + # make a type directory that should not be ignored because it is enabled + enabled_module_path = File.join( + @base_path, + enabled_type_directory, + "#{module_reference_name}#{module_extension}" + ) + enabled_module_directory = File.dirname(enabled_module_path) + FileUtils.mkdir_p enabled_module_directory + + File.open(enabled_module_path, 'wb') do |f| + f.write(enabled_module_content) + end + + Dir.mktmpdir do |archive_directory| + @archive_path = File.join(archive_directory, "rspec#{archive_extension}") + FastLib.dump(@archive_path, FastLib::FLAG_COMPRESS.to_s(16), @base_path, @base_path) + + # @todo Fix https://www.pivotaltracker.com/story/show/38730815 and the cache won't need to be cleared as a work-around + FastLib.cache.clear + + example.run + end + end + end + + # this checks that the around(:each) is working + it 'should have an existent FastLib' do + File.exist?(@archive_path).should be_true + end + + it 'should ignore .svn directories' do + subject.send(:each_module_reference_name, @archive_path) do |parent_path, type, module_reference_name| + parent_path.should_not include('.svn') + end + end + + it 'should ignore types that are not enabled' do + module_manager.type_enabled?(disabled_type).should be_false + + subject.send(:each_module_reference_name, @archive_path) do |parent_path, type, module_reference_name| + type.should_not == disabled_type + end + end + + it 'should yield (parent_path, type, module_reference_name) with parent_path equal to the archive path' do + subject.send(:each_module_reference_name, @archive_path) do |parent_path, type, module_reference_name| + parent_path.should == @archive_path + end + end + + it 'should yield (parent_path, type, module_reference_name) with type equal to enabled type' do + module_manager.type_enabled?(enabled_type).should be_true + + subject.send(:each_module_reference_name, @archive_path) do |parent_path, type, module_reference_name| + type.should == enabled_type + end + end + + it 'should yield (path, type, module_reference_name) with module_reference_name without extension' do + subject.send(:each_module_reference_name, @archive_path) do |parent_path, type, module_reference_name| + module_reference_name.should_not match(/#{Regexp.escape(module_extension)}$/) + module_reference_name.should == module_reference_name + end + end + + # ensure that the block is actually being run so that shoulds in the block aren't just being skipped + it 'should yield the correct number of tuples' do + actual_count = 0 + + subject.send(:each_module_reference_name, @archive_path) do |parent_path, type, module_reference_name| + actual_count += 1 + end + + actual_count.should == 1 + end + end + + context '#loadable?' do + it 'should return true if the path has ARCHIVE_EXTENSION as file extension' do + path = "path/to/archive#{archive_extension}" + + File.extname(path).should == described_class::ARCHIVE_EXTENSION + subject.loadable?(path).should be_true + end + + it 'should return false if the path contains ARCHIVE_EXTENSION, but it is not the file extension' do + path = "path/to/archive#{archive_extension}.bak" + + path.should include(described_class::ARCHIVE_EXTENSION) + File.extname(path).should_not == described_class::ARCHIVE_EXTENSION + subject.loadable?(path).should be_false + end + end + + context '#module_path' do + let(:parent_path) do + "path/to/archive#{archive_extension}" + end + + let(:type) do + 'exploit' + end + + let(:type_directory) do + 'exploits' + end + + it 'should use typed_path to convert the type name to a type directory' do + subject.should_receive(:typed_path).with(type, module_reference_name) + + subject.send(:module_path, parent_path, type, module_reference_name) + end + + it "should separate the archive path from the entry path with '::'" do + module_path = subject.send(:module_path, parent_path, type, module_reference_name) + + module_path.should == "#{parent_path}::#{type_directory}/#{module_reference_name}.rb" + end + end + + context '#read_module_path' do + let(:module_reference_name) do + 'windows/smb/ms08_067_netapi' + end + + let(:type) do + enabled_type + end + + let(:type_directory) do + enabled_type_directory + end + + let(:archived_path) do + File.join(type_directory, "#{module_reference_name}#{module_extension}") + end + + let(:base_path) do + File.join(Msf::Config.install_root, 'modules') + end + + let(:flag_string) do + flags.to_s(16) + end + + let(:flags) do + 0x0 + end + + let(:unarchived_path) do + File.join(base_path, archived_path) + end + + it 'should read modules that exist' do + File.exist?(unarchived_path).should be_true + end + + around(:each) do |example| + Dir.mktmpdir do |directory| + @parent_path = File.join(directory, 'rspec.fastlib') + + FastLib.dump(@parent_path, flag_string, base_path, unarchived_path) + + # @todo Fix https://www.pivotaltracker.com/story/show/38730815 so cache from dump is correct + FastLib.cache.clear + + example.run + end + end + + context 'with uncompressed archive' do + it_should_behave_like 'Msf::Modules::Loader::Archive#read_module_content' + end + + context 'with compressed archive' do + let(:flags) do + FastLib::FLAG_COMPRESS + end + + it_should_behave_like 'Msf::Modules::Loader::Archive#read_module_content' + 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 new file mode 100644 index 0000000000..df0207d4c4 --- /dev/null +++ b/spec/lib/msf/core/modules/loader/base_spec.rb @@ -0,0 +1,1423 @@ +require 'spec_helper' + +require 'msf/core' + +describe Msf::Modules::Loader::Base do + let(:described_class_pathname) do + root_pathname.join('lib', 'msf', 'core', 'modules', 'loader', 'base.rb') + end + + let(:malformed_module_content) do + <<-EOS + class Metasploit3 + # purposeful typo to check that module path is used in backtrace + inclde Exploit::Remote::Tcp + end + EOS + end + + let(:module_content) do + <<-EOS + class Metasploit3 < Msf::Auxiliary + # fully-qualified name is Msf::GoodRanking, so this will failing if lexical scope is not captured + Rank = GoodRanking + end + EOS + end + + let(:module_full_name) do + "#{type}/#{module_reference_name}" + end + + let(:module_path) do + parent_pathname.join('auxiliary', 'rspec', 'mock.rb').to_s + end + + let(:module_reference_name) 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 + + context 'CONSTANTS' do + + context 'DIRECTORY_BY_TYPE' do + let(:directory_by_type) do + described_class::DIRECTORY_BY_TYPE + end + + it 'should be defined' do + described_class.const_defined?(:DIRECTORY_BY_TYPE).should be_true + end + + it 'should map Msf::MODULE_AUX to auxiliary' do + directory_by_type[Msf::MODULE_AUX].should == 'auxiliary' + end + + it 'should map Msf::MODULE_ENCODER to encoders' do + directory_by_type[Msf::MODULE_ENCODER].should == 'encoders' + end + + it 'should map Msf::MODULE_EXPLOIT to exploits' do + directory_by_type[Msf::MODULE_EXPLOIT].should == 'exploits' + end + + it 'should map Msf::MODULE_NOP to nops' do + directory_by_type[Msf::MODULE_NOP].should == 'nops' + end + + it 'should map Msf::MODULE_PAYLOAD to payloads' do + directory_by_type[Msf::MODULE_PAYLOAD].should == 'payloads' + end + + it 'should map Msf::MODULE_POST to post' do + directory_by_type[Msf::MODULE_POST].should == 'post' + end + end + + context 'NAMESPACE_MODULE_LINE' do + it 'should be line number for first line of NAMESPACE_MODULE_CONTENT' do + file_lines = [] + + described_class_pathname.open do |f| + file_lines = f.to_a + end + + # -1 because file lines are 1-based, but array is 0-based + file_line = file_lines[described_class::NAMESPACE_MODULE_LINE - 1] + + constant_lines = described_class::NAMESPACE_MODULE_CONTENT.lines.to_a + constant_line = constant_lines.first + + file_line.should == constant_line + end + end + + context 'NAMESPACE_MODULE_CONTENT' do + context 'derived module' do + let(:namespace_module_names) do + ['Msf', 'Modules', 'Mod617578696c696172792f72737065632f6d6f636b'] + end + + let(:namespace_module) do + Object.module_eval( + <<-EOS + module #{namespace_module_names[0]} + module #{namespace_module_names[1]} + module #{namespace_module_names[2]} + #{described_class::NAMESPACE_MODULE_CONTENT} + end + end + end + EOS + ) + + namespace_module_names.join('::').constantize + end + + context 'loader' do + it 'should be a read/write attribute' do + loader = mock('Loader') + namespace_module.loader = loader + + namespace_module.loader.should == loader + end + end + + context 'module_eval_with_lexical_scope' do + it 'should capture the lexical scope' do + expect { + namespace_module.module_eval_with_lexical_scope(module_content, module_path) + }.to_not raise_error(NameError) + end + + context 'with malformed module content' do + it 'should use module path in module_eval' do + error = nil + + begin + namespace_module.module_eval_with_lexical_scope(malformed_module_content, module_path) + rescue NoMethodError => error + # don't put the should in the rescue because if there is no error, then the example will still be + # successful. + end + + error.should_not be_nil + error.backtrace[0].should include(module_path) + end + end + end + + context 'parent_path' do + it 'should be a read/write attribute' do + parent_path = mock('Parent Path') + namespace_module.parent_path = parent_path + + namespace_module.parent_path.should == parent_path + end + end + end + end + + context 'MODULE_EXTENSION' do + it 'should only support ruby source modules' do + described_class::MODULE_EXTENSION.should == '.rb' + end + end + + context 'MODULE_SEPARATOR' do + it 'should make valid module names' do + name = ['Msf', 'Modules'].join(described_class::MODULE_SEPARATOR) + name.constantize.should == Msf::Modules + end + end + + context 'NAMESPACE_MODULE_NAMES' do + it 'should be under Msf so that Msf constants resolve from lexical scope' do + described_class::NAMESPACE_MODULE_NAMES.should include('Msf') + end + + it "should not be directly under Msf so that modules don't collide with core namespaces" do + direct_index = described_class::NAMESPACE_MODULE_NAMES.index('Msf') + last_index = described_class::NAMESPACE_MODULE_NAMES.length - 1 + + last_index.should > direct_index + end + end + + context 'UNIT_TEST_REGEX' do + it 'should match test suite files' do + described_class::UNIT_TEST_REGEX.should match('rb.ts.rb') + end + + it 'should match unit test files' do + described_class::UNIT_TEST_REGEX.should match('rb.ut.rb') + end + end + end + + context 'class methods' do + context 'typed_path' do + it 'should have MODULE_EXTENSION for the extension name' do + typed_path = described_class.typed_path(Msf::MODULE_AUX, module_reference_name) + + File.extname(typed_path).should == described_class::MODULE_EXTENSION + end + + # Don't iterate over a Hash here as that would too closely mirror the actual implementation and not test anything + it_should_behave_like 'typed_path', 'Msf::MODULE_AUX' => 'auxiliary' + it_should_behave_like 'typed_path', 'Msf::MODULE_ENCODER' => 'encoders' + it_should_behave_like 'typed_path', 'Msf::MODULE_EXPLOIT' => 'exploits' + it_should_behave_like 'typed_path', 'Msf::MODULE_NOP' => 'nops' + it_should_behave_like 'typed_path', 'Msf::MODULE_PAYLOAD' => 'payloads' + it_should_behave_like 'typed_path', 'Msf::MODULE_POST' => 'post' + end + end + + context 'instance methods' do + let(:module_manager) do + mock('Module Manager') + end + + subject do + described_class.new(module_manager) + end + + context '#initialize' do + it 'should set @module_manager' do + loader = described_class.new(module_manager) + loader.instance_variable_get(:@module_manager).should == module_manager + end + end + + context '#loadable?' do + it 'should be abstract' do + expect { + subject.loadable?(parent_pathname.to_s) + }.to raise_error(NotImplementedError) + end + end + + context '#load_module' do + let(:parent_path) do + parent_pathname.to_s + end + + let(:type) do + Msf::MODULE_AUX + end + + before(:each) do + subject.stub(:module_path => module_path) + end + + it 'should return false if :force is false and the file has not been changed' do + module_manager.stub(:file_changed? => false) + + subject.load_module(parent_path, type, module_reference_name, :force => false).should be_false + end + + it 'should call file_changed? with the module_path' do + module_manager.should_receive(:file_changed?).with(module_path).and_return(false) + + subject.load_module(parent_path, type, module_reference_name, :force => false) + end + + context 'with file changed' do + let(:module_full_name) do + File.join('auxiliary', module_reference_name) + end + + let(:namespace_module) do + Msf::Modules.const_get(relative_name) + end + + let(:relative_name) do + 'Mod617578696c696172792f72737065632f6d6f636b' + end + + before(:each) do + # capture in a local so that instance_eval can access it + relative_name = self.relative_name + + # remove module from previous examples so reload error aren't logged + if Msf::Modules.const_defined? relative_name + Msf::Modules.instance_eval do + remove_const relative_name + end + end + + # create an namespace module that can be restored + module Msf + module Modules + module Mod617578696c696172792f72737065632f6d6f636b + class Metasploit3 < Msf::Auxiliary + + end + end + end + end + + @original_namespace_module = Msf::Modules::Mod617578696c696172792f72737065632f6d6f636b + + module_manager.stub(:delete).with(module_reference_name) + module_manager.stub(:file_changed?).with(module_path).and_return(true) + + module_set = mock('Module Set') + module_set.stub(:delete).with(module_reference_name) + module_manager.stub(:module_set).with(type).and_return(module_set) + end + + it 'should call #namespace_module_transaction with the module full name and :reload => true' do + 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) + + subject.load_module(parent_path, type, module_reference_name).should be_true + namespace_module.parent_path.should == parent_path + 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) + subject.load_module(parent_path, type, module_reference_name).should be_true + end + + 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 + subject.should_receive(:elog).with(/#{Regexp.escape(module_path)}/) + subject.load_module(parent_path, type, module_reference_name, :reload => true).should be_false + 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) + end + + context 'with Interrupt' do + it 'should re-raise' do + @namespace_module.stub(:module_eval_with_lexical_scope).and_raise(Interrupt) + + expect { + subject.load_module(parent_path, type, module_reference_name) + }.to raise_error(Interrupt) + end + end + + context 'with other Exception' do + let(:backtrace) do + [ + 'Backtrace Line 1', + 'Backtrace Line 2' + ] + end + + let(:error) do + error_class.new(error_message) + end + + let(:error_class) do + ArgumentError + end + + let(:error_message) do + 'This is rspec. Your argument is invalid.' + end + + before(:each) do + @namespace_module.stub(:module_eval_with_lexical_scope).and_raise(error) + + @module_load_error_by_path = {} + module_manager.stub(:module_load_error_by_path => @module_load_error_by_path) + + error.stub(:backtrace => backtrace) + end + + context 'with version compatibility' do + before(:each) 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]}" + ) + subject.load_module(parent_path, type, module_reference_name).should be_false + end + end + + context 'without version compatibility' do + let(:version_compatibility_error) do + Msf::Modules::VersionCompatibilityError.new( + :module_path => module_path, + :module_reference_name => module_reference_name, + :minimum_api_version => infinity, + :minimum_core_version => infinity + ) + end + + let(:infinity) do + 0.0 / 0.0 + end + + before(:each) do + @namespace_module.stub( + :version_compatible! + ).with( + module_path, + module_reference_name + ).and_raise( + version_compatibility_error + ) + 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}/) + subject.load_module(parent_path, type, module_reference_name).should be_false + end + end + + it 'should return false' do + @namespace_module.stub(:version_compatible!).with(module_path, module_reference_name) + + subject.load_module(parent_path, type, module_reference_name).should be_false + end + end + end + + context 'without module_eval errors' do + before(:each) do + @namespace_module = mock('Namespace Module') + @namespace_module.stub(:parent_path=) + @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) + + subject.stub(:namespace_module_transaction).and_yield(@namespace_module) + + subject.stub(:read_module_content).with(parent_path, type, module_reference_name).and_return(module_content) + + @module_load_error_by_path = {} + module_manager.stub(:module_load_error_by_path => @module_load_error_by_path) + end + + it 'should check for version compatibility' do + module_manager.stub(:on_module_load) + + @namespace_module.should_receive(:version_compatible!).with(module_path, module_reference_name) + subject.load_module(parent_path, type, module_reference_name) + end + + context 'without version compatibility' do + let(:version_compatibility_error) do + Msf::Modules::VersionCompatibilityError.new( + :module_path => module_path, + :module_reference_name => module_reference_name, + :minimum_api_version => infinity, + :minimum_core_version => infinity + ) + end + + let(:infinity) do + 0.0 / 0.0 + end + + before(:each) do + @namespace_module.stub( + :version_compatible! + ).with( + module_path, + module_reference_name + ).and_raise( + version_compatibility_error + ) + 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) + subject.load_module(parent_path, type, module_reference_name).should be_false + end + + it 'should return false' do + subject.load_module(parent_path, type, module_reference_name).should be_false + end + + it 'should restore the old namespace module' do + + end + end + + context 'with version compatibility' do + before(:each) do + @namespace_module.stub(:version_compatible!).with(module_path, module_reference_name) + + module_manager.stub(:on_module_load) + end + + context 'without metasploit_class' do + before(:each) do + @namespace_module.stub(:metasploit_class).and_return(nil) + end + + let(:error_message) do + 'Missing Metasploit class constant' + end + + it 'should log missing Metasploit class' do + subject.should_receive(:elog).with(error_message) + 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 + + it 'should restore the old namespace module' do + subject.load_module(parent_path, type, module_reference_name).should be_false + Msf::Modules.const_defined?(relative_name).should be_true + Msf::Modules.const_get(relative_name).should == @original_namespace_module + end + end + + context 'with metasploit_class' do + let(:metasploit_class) do + mock('Metasploit Class') + end + + before(:each) do + @namespace_module.stub(:metasploit_class => metasploit_class) + end + + it 'should check if it is usable' do + subject.should_receive(:usable?).with(metasploit_class).and_return(true) + subject.load_module(parent_path, type, module_reference_name).should be_true + end + + context 'without usable metasploit_class' do + before(:each) do + subject.stub(:usable? => false) + end + + it 'should log information' do + subject.should_receive(:ilog).with(/#{module_reference_name}/, 'core', LEV_1) + subject.load_module(parent_path, type, module_reference_name).should be_false + end + + it 'should return false' do + subject.load_module(parent_path, type, module_reference_name).should be_false + end + + it 'should restore the old namespace module' do + subject.load_module(parent_path, type, module_reference_name).should be_false + Msf::Modules.const_defined?(relative_name).should be_true + Msf::Modules.const_get(relative_name).should == @original_namespace_module + end + end + + context 'with usable metasploit_class' do + before(:each) do + # remove the mocked namespace_module since happy-path/real loading is occurring in this context + subject.unstub(:namespace_module_transaction) + end + + it 'should log load information' do + subject.should_receive(:ilog).with(/#{module_reference_name}/, 'core', LEV_2) + subject.load_module(parent_path, type, module_reference_name).should be_true + end + + it 'should delete any pre-existing load errors from module_manager.module_load_error_by_path' do + original_load_error = "Back in my day this module didn't load" + module_manager.module_load_error_by_path[module_path] = original_load_error + + module_manager.module_load_error_by_path[module_path].should == original_load_error + subject.load_module(parent_path, type, module_reference_name).should be_true + module_manager.module_load_error_by_path[module_path].should be_nil + end + + it 'should return true' do + subject.load_module(parent_path, type, module_reference_name).should be_true + end + + context 'with module_reference_name already in module_manager' do + let(:framework) do + framework = mock('Framework', :datastore => {}) + framework.stub_chain(:events, :on_module_load) + + framework + end + + let(:metasploit_class) do + @original_namespace_module::Metasploit3 + end + + let(:module_manager) do + Msf::ModuleManager.new(framework) + end + + before(:each) do + # remove the stub from before(:each) in context 'with version compatibility' + module_manager.unstub(:on_module_load) + + # remove the stubs from before(:each) in context 'with file changed' + module_manager.unstub(:delete) + module_manager.unstub(:module_set) + end + + it 'should not cause an ambiguous module_reference_name in the module_manager' do + module_manager[module_reference_name] = metasploit_class + + subject.load_module(parent_path, type, module_reference_name).should be_true + module_manager.send(:ambiguous_module_reference_name_set).should be_empty + end + + it 'should not cause an ambiguous module_reference_name in the type module_set' do + module_set = module_manager.module_set(type) + module_set[module_reference_name] = metasploit_class + + subject.load_module(parent_path, type, module_reference_name).should be_true + module_set.send(:ambiguous_module_reference_name_set).should be_empty + end + + context 'without file changed' do + before(:each) do + module_manager.stub(:file_changed => false) + end + + context 'with :force => true' do + it 'should not cause an ambiguous module_reference_name in the module_manager' do + module_manager[module_reference_name] = metasploit_class + + subject.load_module(parent_path, type, module_reference_name, :force => true).should be_true + module_manager.send(:ambiguous_module_reference_name_set).should be_empty + end + + it 'should not cause an ambiguous module_reference_name in the type module_set' do + module_set = module_manager.module_set(type) + module_set[module_reference_name] = metasploit_class + + subject.load_module(parent_path, type, module_reference_name, :force => true).should be_true + module_set.send(:ambiguous_module_reference_name_set).should be_empty + end + end + + context 'with :reload => true' do + it 'should not cause an ambiguous module_reference_name in the module_manager' do + module_manager[module_reference_name] = metasploit_class + + subject.load_module(parent_path, type, module_reference_name, :reload => true).should be_true + module_manager.send(:ambiguous_module_reference_name_set).should be_empty + end + + it 'should not cause an ambiguous module_reference_name in the type module_set' do + module_set = module_manager.module_set(type) + module_set[module_reference_name] = metasploit_class + + subject.load_module(parent_path, type, module_reference_name, :reload => true).should be_true + module_set.send(:ambiguous_module_reference_name_set).should be_empty + end + end + end + end + + it 'should call module_manager.on_module_load' do + module_manager.should_receive(:on_module_load) + subject.load_module(parent_path, type, module_reference_name).should be_true + end + + context 'with :recalculate_by_type' do + it 'should set the type to be recalculated' do + recalculate_by_type = {} + + subject.load_module( + parent_path, + type, + module_reference_name, + :recalculate_by_type => recalculate_by_type + ).should be_true + recalculate_by_type[type].should be_true + end + end + + context 'with :count_by_type' do + it 'should set the count to 1 if it does not exist' do + count_by_type = {} + + count_by_type.has_key?(type).should be_false + subject.load_module( + parent_path, + type, + module_reference_name, + :count_by_type => count_by_type + ).should be_true + count_by_type[type].should == 1 + end + + it 'should increment the count if it does exist' do + original_count = 1 + count_by_type = { + type => original_count + } + + subject.load_module( + parent_path, + type, + module_reference_name, + :count_by_type => count_by_type + ).should be_true + + incremented_count = original_count + 1 + count_by_type[type].should == incremented_count + end + end + end + end + end + end + end + end + + context '#create_namespace_module' do + let(:namespace_module_names) do + [ + 'Msf', + 'Modules', + relative_name + ] + end + + let(:relative_name) do + 'Mod0' + end + + before(:each) do + # capture in local variable so it works in instance_eval + relative_name = self.relative_name + + if Msf::Modules.const_defined? relative_name + Msf::Modules.instance_eval do + remove_const relative_name + end + end + end + + it 'should wrap NAMESPACE_MODULE_CONTENT with module declarations matching namespace_module_names' do + Object.should_receive( + :module_eval + ).with( + "module #{namespace_module_names[0]}\n" \ + "module #{namespace_module_names[1]}\n" \ + "module #{namespace_module_names[2]}\n" \ + "#{described_class::NAMESPACE_MODULE_CONTENT}\n" \ + "end\n" \ + "end\n" \ + "end", + anything, + anything + ) + + namespace_module = mock('Namespace Module') + namespace_module.stub(:loader=) + subject.stub(:current_module => namespace_module) + + subject.send(:create_namespace_module, namespace_module_names) + end + + it "should set the module_eval path to the loader's __FILE__" do + Object.should_receive( + :module_eval + ).with( + anything, + described_class_pathname.to_s, + anything + ) + + namespace_module = mock('Namespace Module') + namespace_module.stub(:loader=) + subject.stub(:current_module => namespace_module) + + subject.send(:create_namespace_module, namespace_module_names) + end + + it 'should set the module_eval line to compensate for the wrapping module declarations' do + Object.should_receive( + :module_eval + ).with( + anything, + anything, + described_class::NAMESPACE_MODULE_LINE - namespace_module_names.length + ) + + namespace_module = mock('Namespace Module') + namespace_module.stub(:loader=) + subject.stub(:current_module => namespace_module) + + subject.send(:create_namespace_module, namespace_module_names) + end + + it "should set the namespace_module's module loader to itself" do + namespace_module = mock('Namespace Module') + + namespace_module.should_receive(:loader=).with(subject) + + subject.stub(:current_module => namespace_module) + + subject.send(:create_namespace_module, namespace_module_names) + end + end + + context '#current_module' do + let(:module_names) do + [ + 'Msf', + 'Modules', + relative_name + ] + end + + let(:relative_name) do + 'Mod0' + end + + before(:each) do + # copy to local variable so it is accessible in instance_eval + relative_name = self.relative_name + + if Msf::Modules.const_defined? relative_name + Msf::Modules.instance_eval do + remove_const relative_name + end + end + end + + it 'should return nil if the module is not defined' do + Msf::Modules.const_defined?(relative_name).should be_false + subject.send(:current_module, module_names).should be_nil + end + + it 'should return the module if it is defined' do + module Msf + module Modules + module Mod0 + end + end + end + + subject.send(:current_module, module_names).should == Msf::Modules::Mod0 + end + end + + context '#each_module_reference_name' do + it 'should be abstract' do + expect { + subject.send(:each_module_reference_name, parent_path) + }.to raise_error(NotImplementedError) + end + end + + context '#module_path' do + it 'should be abstract' do + expect { + subject.send(:module_path, parent_path, Msf::MODULE_AUX, module_reference_name) + }.to raise_error(NotImplementedError) + end + end + + context '#module_path?' do + it 'should return false if path is hidden' do + hidden_path = '.hidden/path/file.rb' + + subject.send(:module_path?, hidden_path).should be_false + end + + it 'should return false if the file extension is not MODULE_EXTENSION' do + non_module_extension = '.c' + path = "path/with/wrong/extension#{non_module_extension}" + + non_module_extension.should_not == described_class::MODULE_EXTENSION + subject.send(:module_path?, path).should be_false + end + + it 'should return false if the file is a unit test' do + unit_test_extension = '.rb.ut.rb' + path = "path/to/unit_test#{unit_test_extension}" + + subject.send(:module_path?, path).should be_false + end + + it 'should return false if the file is a test suite' do + test_suite_extension = '.rb.ts.rb' + path = "path/to/test_suite#{test_suite_extension}" + + subject.send(:module_path?, path).should be_false + end + + it 'should return true otherwise' do + subject.send(:module_path?, module_path).should be_true + end + end + + context '#module_reference_name_from_path' do + it 'should strip MODULE_EXTENSION from the end of the path' do + path_without_extension = "a#{described_class::MODULE_EXTENSION}.dir/a" + path = "#{path_without_extension}#{described_class::MODULE_EXTENSION}" + + subject.send(:module_reference_name_from_path, path).should == path_without_extension + end + end + + context '#namespace_module_name' do + it 'should prefix the name with Msf::Modules::' do + subject.send(:namespace_module_name, module_full_name).should start_with('Msf::Modules::') + end + + it 'should prefix the relative name with Mod' do + namespace_module_name = subject.send(:namespace_module_name, module_full_name) + relative_name = namespace_module_name.gsub(/^.*::/, '') + + relative_name.should start_with('Mod') + end + + it 'should be reversible' do + namespace_module_name = subject.send(:namespace_module_name, module_full_name) + unpacked_name = namespace_module_name.gsub(/^.*::Mod/, '') + + [unpacked_name].pack('H*').should == module_full_name + end + end + + context '#namespace_module_names' do + it "should prefix the array with ['Msf', 'Modules']" do + subject.send(:namespace_module_names, module_full_name).should start_with(['Msf', 'Modules']) + end + + it 'should prefix the relative name with Mod' do + namespace_module_names = subject.send(:namespace_module_names, module_full_name) + + namespace_module_names.last.should start_with('Mod') + end + + it 'should be reversible' do + namespace_module_names = subject.send(:namespace_module_names, module_full_name) + relative_name = namespace_module_names.last + unpacked_name = relative_name.gsub(/^Mod/, '') + + [unpacked_name].pack('H*').should == module_full_name + end + end + + context '#namespace_module_transaction' do + let(:relative_name) do + 'Mod617578696c696172792f72737065632f6d6f636b' + end + + context 'with pre-existing namespace module' do + before(:each) do + module Msf + module Modules + module Mod617578696c696172792f72737065632f6d6f636b + class Metasploit3 + + end + end + end + end + + @existent_namespace_module = Msf::Modules::Mod617578696c696172792f72737065632f6d6f636b + end + + context 'with :reload => false' do + it 'should log an error' do + subject.should_receive(:elog).with(/Reloading.*when :reload => false/) + + subject.send(:namespace_module_transaction, module_full_name, :reload => false) do |namespace_module| + true + end + end + end + + it 'should remove the pre-existing namespace module' do + Msf::Modules.should_receive(:remove_const).with(relative_name) + + subject.send(:namespace_module_transaction, module_full_name) do |namespace_module| + true + end + end + + it 'should create a new namespace module for the block' do + subject.send(:namespace_module_transaction, module_full_name) do |namespace_module| + namespace_module.should_not == @existent_namespace_module + + expect { + namespace_module::Metasploit3 + }.to raise_error(NameError) + + true + end + end + + context 'with an Exception from the block' do + let(:error_class) do + NameError + end + + let(:error_message) do + "SayMyName" + end + + it 'should restore the previous namespace module' do + Msf::Modules.const_get(relative_name).should == @existent_namespace_module + + begin + subject.send(:namespace_module_transaction, module_full_name) do |namespace_module| + current_constant = Msf::Modules.const_get(relative_name) + + current_constant.should == namespace_module + current_constant.should_not == @existent_namespace_module + + raise error_class, error_message + end + rescue error_class => error + end + + Msf::Modules.const_get(relative_name).should == @existent_namespace_module + end + + it 'should re-raise the error' do + expect { + subject.send(:namespace_module_transaction, module_full_name) do |namespace_module| + raise error_class, error_message + end + }.to raise_error(error_class, error_message) + end + end + + context 'with the block returning false' do + it 'should restore the previous namespace module' do + Msf::Modules.const_get(relative_name).should == @existent_namespace_module + + subject.send(:namespace_module_transaction, module_full_name) do |namespace_module| + current_constant = Msf::Modules.const_get(relative_name) + + current_constant.should == namespace_module + current_constant.should_not == @existent_namespace_module + + false + end + + Msf::Modules.const_get(relative_name).should == @existent_namespace_module + end + + it 'should return false' do + subject.send(:namespace_module_transaction, module_full_name) { |namespace_module| + false + }.should be_false + end + end + + context 'with the block returning true' do + it 'should not restore the previous namespace module' do + Msf::Modules.const_get(relative_name).should == @existent_namespace_module + + subject.send(:namespace_module_transaction, module_full_name) do |namespace_module| + true + end + + current_constant = Msf::Modules.const_get(relative_name) + + current_constant.should_not be_nil + current_constant.should_not == @existent_namespace_module + end + + it 'should return true' do + subject.send(:namespace_module_transaction, module_full_name) { |namespace_module| + true + }.should be_true + end + end + end + + context 'without pre-existing namespace module' do + before(:each) do + relative_name = self.relative_name + + if Msf::Modules.const_defined? relative_name + Msf::Modules.send(:remove_const, relative_name) + end + end + + it 'should create a new namespace module' do + expect { + Msf::Modules.const_get(relative_name) + }.to raise_error(NameError) + + subject.send(:namespace_module_transaction, module_full_name) do |namespace_module| + Msf::Modules.const_get(relative_name).should == namespace_module + end + end + + context 'with an Exception from the block' do + let(:error_class) do + Exception + end + + let(:error_message) do + 'Error Message' + end + + it 'should remove the created namespace module' do + Msf::Modules.const_defined?(relative_name).should be_false + + begin + subject.send(:namespace_module_transaction, module_full_name) do |namespace_module| + Msf::Module.const_defined?(relative_name).should be_true + + raise error_class, error_message + end + rescue error_class + end + + Msf::Modules.const_defined?(relative_name).should be_false + end + + it 'should re-raise the error' do + expect { + subject.send(:namespace_module_transaction, module_full_name) do |namespace_module| + raise error_class, error_message + end + }.to raise_error(error_class, error_message) + end + end + + context 'with the block returning false' do + it 'should remove the created namespace module' do + Msf::Modules.const_defined?(relative_name).should be_false + + subject.send(:namespace_module_transaction, module_full_name) do |namespace_module| + Msf::Modules.const_defined?(relative_name).should be_true + + false + end + + Msf::Modules.const_defined?(relative_name).should be_false + end + + it 'should return false' do + subject.send(:namespace_module_transaction, module_full_name) { |namespace_module| + false + }.should be_false + end + end + + context 'with the block returning true' do + it 'should not restore the non-existent previous namespace module' do + Msf::Modules.const_defined?(relative_name).should be_false + + created_namespace_module = nil + + subject.send(:namespace_module_transaction, module_full_name) do |namespace_module| + Msf::Modules.const_defined?(relative_name).should be_true + + created_namespace_module = namespace_module + + true + end + + Msf::Modules.const_defined?(relative_name).should be_true + Msf::Modules.const_get(relative_name).should == created_namespace_module + end + + it 'should return true' do + subject.send(:namespace_module_transaction, module_full_name) { |namespace_module| + true + }.should be_true + end + end + end + end + + context '#read_module_content' do + it 'should be abstract' do + type = Msf::MODULE_AUX + + expect { + subject.send(:read_module_content, parent_pathname.to_s, type, module_reference_name) + }.to raise_error(NotImplementedError) + end + end + + context '#restore_namespace_module' do + let(:parent_module) do + Msf::Modules + end + + let(:relative_name) do + 'Mod0' + end + + it 'should do nothing if parent_module is nil' do + parent_module = nil + + allow_message_expectations_on_nil + parent_module.should_not_receive(:remove_const) + parent_module.should_not_receive(:const_set) + + subject.send(:restore_namespace_module, parent_module, relative_name, @original_namespace_module) + end + + context 'with namespace_module nil' do + let(:namespace_module) do + nil + end + + it 'should remove relative_name' do + parent_module.should_receive(:remove_const).with(relative_name) + + subject.send(:restore_namespace_module, parent_module, relative_name, namespace_module) + end + + it 'should not set the relative_name constant to anything' do + parent_module.should_not_receive(:const_set) + + subject.send(:restore_namespace_module, parent_module, relative_name, namespace_module) + end + end + + context 'with parent_module and namespace_module' do + before(:each) do + module Msf + module Modules + module Mod0 + class Metasploit3 + + end + end + end + end + + @original_namespace_module = Msf::Modules::Mod0 + + Msf::Modules.send(:remove_const, relative_name) + end + + context 'with relative_name being a defined constant' do + before(:each) do + module Msf + module Modules + module Mod0 + class Metasploit2 + + end + end + end + end + + @current_namespace_module = Msf::Modules::Mod0 + end + + context 'with the current constant being the namespace_module' do + it 'should not change the constant' do + parent_module.const_defined?(relative_name).should be_true + + current_module = parent_module.const_get(relative_name) + current_module.should == @current_namespace_module + + subject.send(:restore_namespace_module, parent_module, relative_name, @current_namespace_module) + + parent_module.const_defined?(relative_name).should be_true + restored_module = parent_module.const_get(relative_name) + restored_module.should == current_module + restored_module.should == @current_namespace_module + end + + it 'should not remove the constant and then set it' do + parent_module.should_not_receive(:remove_const).with(relative_name) + parent_module.should_not_receive(:const_set).with(relative_name, @current_namespace_module) + + subject.send(:restore_namespace_module, parent_module, relative_name, @current_namespace_module) + end + end + + context 'without the current constant being the namespace_module' do + it 'should remove relative_name from parent_module' do + parent_module.const_defined?(relative_name).should be_true + parent_module.should_receive(:remove_const).with(relative_name) + + subject.send(:restore_namespace_module, parent_module, relative_name, @original_namespace_module) + end + + it 'should restore the module to the constant' do + parent_module.const_get(relative_name).should_not == @original_namespace_module + + subject.send(:restore_namespace_module, parent_module, relative_name, @original_namespace_module) + + parent_module.const_get(relative_name).should == @original_namespace_module + end + end + end + + context 'without relative_name being a defined constant' do + it 'should set relative_name on parent_module to namespace_module' do + parent_module.const_defined?(relative_name).should be_false + + subject.send(:restore_namespace_module, parent_module, relative_name, @original_namespace_module) + + parent_module.const_defined?(relative_name).should be_true + parent_module.const_get(relative_name).should == @original_namespace_module + end + end + end + end + + context '#typed_path' do + it 'should delegate to the class method' do + type = Msf::MODULE_EXPLOIT + + described_class.should_receive(:typed_path).with(type, module_reference_name) + subject.send(:typed_path, type, module_reference_name) + end + end + + context '#usable?' do + context 'without metasploit_class responding to is_usable' do + it 'should return true' do + metasploit_class = mock('Metasploit Class') + metasploit_class.should_not respond_to(:is_usable) + + subject.send(:usable?, metasploit_class).should be_true + end + end + + context 'with metasploit_class responding to is_usable' do + it 'should delegate to metasploit_class.is_usable' do + # not a proper return, but guarantees that delegation is actually happening + usability = 'maybe' + metasploit_class = mock('Metasploit Class', :is_usable => usability) + + subject.send(:usable?, metasploit_class).should == usability + end + + context 'with error from metasploit_class.is_usable' do + let(:error) do + 'Expected error' + end + + let(:metasploit_class) do + metasploit_class = mock('Metasploit Class') + + metasploit_class.stub(:is_usable).and_raise(error) + + metasploit_class + end + + it 'should log error' do + subject.should_receive(:elog).with(/#{error}/) + + subject.send(:usable?, metasploit_class) + end + + it 'should return false' do + subject.send(:usable?, metasploit_class).should be_false + end + end + end + end + end +end \ No newline at end of file diff --git a/spec/lib/msf/core/modules/loader/directory_spec.rb b/spec/lib/msf/core/modules/loader/directory_spec.rb new file mode 100644 index 0000000000..b371883f49 --- /dev/null +++ b/spec/lib/msf/core/modules/loader/directory_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' +require 'msf/core' +require 'msf/core/modules/loader/directory' + +describe Msf::Modules::Loader::Directory do + +end diff --git a/spec/msf/core/task_manager_spec.rb b/spec/lib/msf/core/task_manager_spec.rb similarity index 100% rename from spec/msf/core/task_manager_spec.rb rename to spec/lib/msf/core/task_manager_spec.rb diff --git a/spec/msf/util/exe_spec.rb b/spec/lib/msf/util/exe_spec.rb similarity index 100% rename from spec/msf/util/exe_spec.rb rename to spec/lib/msf/util/exe_spec.rb diff --git a/spec/rex/parser/nmap_xml_spec.rb b/spec/lib/rex/parser/nmap_xml_spec.rb similarity index 100% rename from spec/rex/parser/nmap_xml_spec.rb rename to spec/lib/rex/parser/nmap_xml_spec.rb diff --git a/spec/rex/socket/range_walker_spec.rb b/spec/lib/rex/socket/range_walker_spec.rb similarity index 100% rename from spec/rex/socket/range_walker_spec.rb rename to spec/lib/rex/socket/range_walker_spec.rb diff --git a/spec/support/shared/examples/msf_modules_loader_archive_read_module_content.rb b/spec/support/shared/examples/msf_modules_loader_archive_read_module_content.rb new file mode 100644 index 0000000000..005f3f4fda --- /dev/null +++ b/spec/support/shared/examples/msf_modules_loader_archive_read_module_content.rb @@ -0,0 +1,13 @@ +shared_examples_for 'Msf::Modules::Loader::Archive#read_module_content' do + it 'should be able to read the module content' do + archived_module_content = subject.send(:read_module_content, @parent_path, type, module_reference_name) + unarchived_module_content = '' + + File.open(unarchived_path) do |f| + unarchived_module_content = f.read + end + + unarchived_module_content.should_not be_empty + archived_module_content.should == unarchived_module_content + end +end \ No newline at end of file diff --git a/spec/support/shared/examples/typed_path.rb b/spec/support/shared/examples/typed_path.rb new file mode 100644 index 0000000000..d393fdb06b --- /dev/null +++ b/spec/support/shared/examples/typed_path.rb @@ -0,0 +1,27 @@ +shared_examples_for 'typed_path' do |map={}| + if map.length < 1 + raise ArgumentError, + "type_path shared example requires a hash mapping the type constant name to the directory name: " \ + "it_should_behave_like 'type_path', 'Msf::Auxiliary' => 'auxiliary'" + end + + if map.length > 1 + raise ArgumentError, + "only one constant to directory mapping should be passed to each shared example, not #{map.length}" + end + + type_constant_name, directory = map.shift + + context "with #{type_constant_name} type" do + let(:type_constant) do + type_constant_name.constantize + end + + it "should start with #{directory} directory" do + typed_path = described_class.typed_path(type_constant, module_reference_name) + first_directory = typed_path.split(File::SEPARATOR).first + + first_directory.should == directory + end + end +end \ No newline at end of file