#
# Specs
#

require 'spec_helper'

#
# Project
#

require 'metasploit/framework/database'
require 'msf/core'

describe Msf::DBManager do
	include_context 'Msf::DBManager'

	subject do
		db_manager
	end

	it_should_behave_like 'Msf::DBManager::Migration'
	it_should_behave_like 'Msf::DBManager::ImportMsfXml'

	context '#initialize_metasploit_data_models' do
		def initialize_metasploit_data_models
			db_manager.initialize_metasploit_data_models
		end

		it 'should not add duplicate paths to ActiveRecord::Migrator.migrations_paths' do
			initialize_metasploit_data_models

			expect {
				initialize_metasploit_data_models
			}.to_not change {
				ActiveRecord::Migrator.migrations_paths.length
			}

			ActiveRecord::Migrator.migrations_paths.uniq.should == ActiveRecord::Migrator.migrations_paths
		end
	end

	context '#purge_all_module_details' do
		def purge_all_module_details
			db_manager.purge_all_module_details
		end

		let(:migrated) do
			false
		end

		let(:module_detail_count) do
			2
		end

		let!(:module_details) do
			FactoryGirl.create_list(
					:mdm_module_detail,
			    module_detail_count
			)
		end

		before(:each) do
			db_manager.stub(:migrated => migrated)
		end

		context 'with migrated' do
			let(:migrated) do
				true
			end

			let(:modules_caching) do
				false
			end

			before(:each) do
				db_manager.stub(:modules_caching => modules_caching)
			end

			context 'with modules_caching' do
				let(:modules_caching) do
					true
				end

				it 'should not destroy Mdm::Module::Details' do
					expect {
						purge_all_module_details
					}.to_not change(Mdm::Module::Detail, :count)
				end
			end

			context 'without modules_caching' do
				it 'should create a connection' do
					# in purge_all_module_details
					# in after(:each)
					ActiveRecord::Base.connection_pool.should_receive(:with_connection).twice.and_call_original

					purge_all_module_details
				end

				it 'should destroy all Mdm::Module::Details' do
					expect {
						purge_all_module_details
					}.to change(Mdm::Module::Detail, :count).by(-module_detail_count)
				end
			end
		end

		context 'without migrated' do
			it 'should not destroy Mdm::Module::Details' do
				expect {
					purge_all_module_details
				}.to_not change(Mdm::Module::Detail, :count)
			end
		end
	end

	context '#report_session' do
		let(:options) do
			{}
		end

		subject(:report_session) do
			db_manager.report_session(options)
		end

		context 'with active' do
			let(:active) do
				true
			end

      it 'should create connection' do
	      # 1st time from with_established_connection
	      # 2nd time from report_session
	      ActiveRecord::Base.connection_pool.should_receive(:with_connection).exactly(2).times

	      report_session
      end

			context 'with :session' do
				before(:each) do
					options[:session] = session
				end

				context 'with Msf::Session' do
					let(:exploit_datastore) do
						Msf::ModuleDataStore.new(module_instance).tap do |datastore|
							datastore['ParentModule'] = parent_module_fullname

							remote_port = rand(2 ** 16 - 1)
							datastore['RPORT'] = remote_port
						end
					end

					let(:host) do
						FactoryGirl.create(:mdm_host, :workspace => session_workspace)
					end

					let(:module_instance) do
						name = 'multi/handler'

						mock(
								'Msf::Module',
								:fullname => "exploit/#{name}",
								:framework => framework,
								:name => name
						)
					end

					let(:options_workspace) do
						FactoryGirl.create(:mdm_workspace)
					end

					let(:parent_module_fullname) do
						"exploit/#{parent_module_name}"
					end

					let(:parent_module_name) do
						'windows/smb/ms08_067_netapi'
					end

					let(:parent_path) do
						Metasploit::Framework.root.join('modules').to_path
					end

					let(:session) do
						session_class.new.tap do |session|
							session.exploit_datastore = exploit_datastore
							session.info = 'Info'
							session.platform = 'Platform'
							session.session_host = host.address
							session.sid = rand(100)
							session.type = 'Session Type'
							session.via_exploit = 'exploit/multi/handler'
							session.via_payload = 'payload/single/windows/metsvc_bind_tcp'
							session.workspace = session_workspace.name
						end
					end

					let(:session_class) do
						Class.new do
							include Msf::Session

							attr_accessor :datastore
							attr_accessor :platform
							attr_accessor :type
							attr_accessor :via_exploit
							attr_accessor :via_payload
						end
					end

					let(:session_workspace) do
						FactoryGirl.create(:mdm_workspace)
					end

					before(:each) do
						reference_name = 'multi/handler'
						path = File.join(parent_path, 'exploits', reference_name)

						# fake cache data for exploit/multi/handler so it can be loaded
						framework.modules.send(
								:module_info_by_path=,
								{
										path =>
												{
														:parent_path => parent_path,
														:reference_name => reference_name,
														:type => 'exploit',
												}
								}
						)

						FactoryGirl.create(
								:mdm_module_detail,
								:fullname => parent_module_fullname,
								:name => parent_module_name
						)
					end

					context 'with :workspace' do
						before(:each) do
							options[:workspace] = options_workspace
						end

						it 'should not find workspace from session' do
							db_manager.should_not_receive(:find_workspace)

							report_session
						end
					end

					context 'without :workspace' do
						it 'should find workspace from session' do
							db_manager.should_receive(:find_workspace).with(session.workspace).and_call_original

							report_session
						end

            it 'should pass session.workspace to #find_or_create_host' do
	            db_manager.should_receive(:find_or_create_host).with(
			            hash_including(
					            :workspace => session_workspace
			            )
	            ).and_return(host)

	            report_session
            end
					end

					context 'with workspace from either :workspace or session' do
						it 'should pass normalized host from session as :host to #find_or_create_host' do
							normalized_host = mock('Normalized Host')
							db_manager.stub(:normalize_host).with(session).and_return(normalized_host)
							# stub report_vuln so its use of find_or_create_host and normalize_host doesn't interfere.
							db_manager.stub(:report_vuln)

							db_manager.should_receive(:find_or_create_host).with(
									hash_including(
											:host => normalized_host
									)
							).and_return(host)

							report_session
						end

						context 'with session responds to arch' do
							let(:arch) do
								FactoryGirl.generate :mdm_host_arch
							end

							before(:each) do
								session.stub(:arch => arch)
							end

							it 'should pass :arch to #find_or_create_host' do
								db_manager.should_receive(:find_or_create_host).with(
										hash_including(
												:arch => arch
										)
								).and_call_original

								report_session
							end
						end

						context 'without session responds to arch' do
							it 'should not pass :arch to #find_or_create_host' do
								db_manager.should_receive(:find_or_create_host).with(
										hash_excluding(
												:arch
										)
								).and_call_original

								report_session
							end
						end

						it 'should create an Mdm::Session' do
							expect {
								report_session
							}.to change(Mdm::Session, :count).by(1)
						end

						it { should be_an Mdm::Session }

						it 'should set session.db_record to created Mdm::Session' do
							mdm_session = report_session

							session.db_record.should == mdm_session
						end

						context 'with session.via_exploit' do
							it 'should create session.via_exploit module' do
								framework.modules.should_receive(:create).with(session.via_exploit).and_call_original

								report_session
							end

							it 'should create Mdm::Vuln' do
								expect {
									report_session
								}.to change(Mdm::Vuln, :count).by(1)
							end

							context 'created Mdm::Vuln' do
								let(:mdm_session) do
									Mdm::Session.last
								end

								let(:rport) do
									nil
								end

								before(:each) do
									Timecop.freeze

									session.exploit_datastore['RPORT'] = rport

									report_session
								end

								after(:each) do
									Timecop.return
								end

								subject(:vuln) do
									Mdm::Vuln.last
								end

								its(:host) { should == Mdm::Host.last }
								its(:refs) { should == [] }
								its(:exploited_at) { should be_within(1.second).of(Time.now.utc) }

								context "with session.via_exploit 'exploit/multi/handler'" do
									context "with session.exploit_datastore['ParentModule']" do
										its(:info) { should == "Exploited by #{parent_module_fullname} to create Session #{mdm_session.id}" }
										its(:name) { should == parent_module_name }
									end
								end

								context "without session.via_exploit 'exploit/multi/handler'" do
									let(:reference_name) do
										'windows/smb/ms08_067_netapi'
									end

									before(:each) do
										path = File.join(
												parent_path,
												'exploits',
												"#{reference_name}.rb"
										)
										type = 'exploit'

										# fake cache data for ParentModule so it can be loaded
										framework.modules.send(
												:module_info_by_path=,
												{
														path =>
																{
																		:parent_path => parent_path,
																		:reference_name => reference_name,
																		:type => type,
																}
												}
										)

										session.via_exploit = "#{type}/#{reference_name}"
									end

									its(:info) { should == "Exploited by #{session.via_exploit} to create Session #{mdm_session.id}"}
									its(:name) { should == reference_name }
								end

								context 'with RPORT' do
									let(:rport) do
										# use service.port instead of having service use rport so
										# that service is forced to exist before call to
										# report_service, which happens right after using rport in
										# outer context's before(:each)
										service.port
									end

									let(:service) do
										FactoryGirl.create(
												:mdm_service,
												:host => host
										)
									end

									its(:service) { should == service }
								end

								context 'without RPORT' do
									its(:service) { should be_nil }
								end
							end

							context 'created Mdm::ExploitAttempt' do
								let(:rport) do
									nil
								end

								before(:each) do
									Timecop.freeze

									session.exploit_datastore['RPORT'] = rport

									report_session
								end

								after(:each) do
									Timecop.return
								end

								subject(:exploit_attempt) do
									Mdm::ExploitAttempt.last
								end

								its(:attempted_at) { should be_within(1.second).of(Time.now.utc) }
								# @todo https://www.pivotaltracker.com/story/show/48362615
								its(:session_id) { should == Mdm::Session.last.id }
								its(:exploited) { should == true }
								# @todo https://www.pivotaltracker.com/story/show/48362615
								its(:vuln_id) { should == Mdm::Vuln.last.id }

								context "with session.via_exploit 'exploit/multi/handler'" do
									context "with session.datastore['ParentModule']" do
										its(:module) { should == parent_module_fullname }
									end
								end

								context "without session.via_exploit 'exploit/multi/handler'" do
									before(:each) do
										session.via_exploit = parent_module_fullname
									end

									its(:module) { should == session.via_exploit }
								end
							end
						end

						context 'returned Mdm::Session' do
							before(:each) do
								Timecop.freeze
							end

							after(:each) do
								Timecop.return
							end

							subject(:mdm_session) do
								report_session
							end

							#
							# Ensure session has attributes present so its on mdm_session are
							# not just comparing nils.
							#

							it 'should have session.info present' do
								session.info.should be_present
							end

							it 'should have session.sid present' do
								session.sid.should be_present
							end

							it 'should have session.platform present' do
								session.platform.should be_present
							end

							it 'should have session.type present' do
								session.type.should be_present
							end

							it 'should have session.via_exploit present' do
								session.via_exploit.should be_present
							end

							it 'should have session.via_payload present' do
								session.via_exploit.should be_present
							end

							its(:datastore) { should == session.exploit_datastore.to_h }
							its(:desc) { should == session.info }
							its(:host_id) { should == Mdm::Host.last.id }
							its(:last_seen) { should be_within(1.second).of(Time.now.utc) }
							its(:local_id) { should == session.sid }
							its(:opened_at) { should be_within(1.second).of(Time.now.utc) }
							its(:platform) { should == session.platform }
							its(:routes) { should == [] }
							its(:stype) { should == session.type }
							its(:via_payload) { should == session.via_payload }

							context "with session.via_exploit 'exploit/multi/handler'" do
								it "should have session.via_exploit of 'exploit/multi/handler'" do
									session.via_exploit.should == 'exploit/multi/handler'
								end

								context "with session.exploit_datastore['ParentModule']" do
									it "should have session.exploit_datastore['ParentModule']" do
										session.exploit_datastore['ParentModule'].should_not be_nil
									end

									its(:via_exploit) { should == parent_module_fullname }
								end
							end

							context "without session.via_exploit 'exploit/multi/handler'" do
								before(:each) do
									reference_name = 'windows/smb/ms08_067_netapi'
									path = File.join(
											parent_path,
											'exploits',
											"#{reference_name}.rb"
									)
									type = 'exploit'

									# fake cache data for ParentModule so it can be loaded
									framework.modules.send(
											:module_info_by_path=,
											{
													path =>
															{
																	:parent_path => parent_path,
																	:reference_name => reference_name,
																	:type => type,
															}
											}
									)

									session.via_exploit = "#{type}/#{reference_name}"
								end

								it "should not have session.via_exploit of 'exploit/multi/handler'" do
									session.via_exploit.should_not == 'exploit/multi/handler'
								end

								its(:via_exploit) { should == session.via_exploit }
							end
						end
					end
				end

				context 'without Msf::Session' do
					let(:session) do
						mock('Not a Msf::Session')
					end

					it 'should raise ArgumentError' do
						expect {
							report_session
						}.to raise_error(ArgumentError, "Invalid :session, expected Msf::Session")
					end
				end
			end

			context 'without :session' do
				context 'with :host' do
					before(:each) do
						options[:host] = host
					end

					context 'with Mdm::Host' do
						let(:host) do
							FactoryGirl.create(:mdm_host)
						end

						context 'created Mdm::Session' do
							let(:closed_at) do
								nil
							end

							let(:close_reason) do
								'Closed because...'
							end

							let(:description) do
								'Session Description'
							end

							let(:exploit_full_name) do
								'exploit/windows/smb/ms08_067_netapi'
							end

							let(:last_seen) do
								nil
							end

							let(:opened_at) do
								Time.now.utc - 5.minutes
							end

							let(:payload_full_name) do
								'payload/singles/windows/metsvc_reverse_tcp'
							end

							let(:platform) do
								'Host Platform'
							end

							let(:routes) do
								nil
							end

							let(:session_type) do
								'Session Type'
							end

							before(:each) do
								options[:closed_at] = closed_at
								options[:close_reason] = close_reason
								options[:desc] = description
								options[:last_seen] = last_seen
								options[:opened_at] = opened_at
								options[:platform] = platform
								options[:routes] = routes
								options[:stype] = session_type
								options[:via_payload] = payload_full_name
								options[:via_exploit] = exploit_full_name
							end

							subject(:mdm_session) do
								report_session
							end

							its(:close_reason) { should == close_reason }
							its(:desc) { should == description }
							its(:host) { should == host }
							its(:platform) { should == platform }
							its(:stype) { should == session_type }
							its(:via_exploit) { should == exploit_full_name }
							its(:via_payload) { should == payload_full_name }

							context 'with :last_seen' do
								let(:last_seen) do
									opened_at
								end

								its(:last_seen) { should == last_seen }
							end

							context 'with :closed_at' do
								let(:closed_at) do
									opened_at + 1.minute
								end

								its(:closed_at) { should == closed_at }
							end

							context 'without :closed_at' do
								its(:closed_at) { should == nil }
							end

							context 'without :last_seen' do
								context 'with :closed_at' do
									let(:closed_at) do
										opened_at + 1.minute
									end

									its(:last_seen) { should == closed_at }
								end

								context 'without :closed_at' do
									its(:last_seen) { should be_nil }
								end
							end

							context 'with :routes' do
								let(:routes) do
									FactoryGirl.build_list(
											:mdm_route,
											1,
											:session => nil
									)
								end

								its(:routes) { should == routes }
							end

							context 'without :routes' do
								its(:routes) { should == [] }
							end
						end
					end

					context 'without Mdm::Host' do
						let(:host) do
							'192.168.0.1'
						end

						it 'should raise ArgumentError' do
							expect {
								report_session
							}.to raise_error(ArgumentError, "Invalid :host, expected Host object")
						end
					end
				end

				context 'without :host' do
					it 'should raise ArgumentError' do
						expect {
							report_session
						}.to raise_error(ArgumentError)
					end
				end
			end
		end

		context 'without active' do
			let(:active) do
				false
			end

			it { should be_nil }

			it 'should not create a connection' do
				# 1st time for with_established_connection
				ActiveRecord::Base.connection_pool.should_receive(:with_connection).once

				report_session
			end
		end
	end

	context '#remove_module_details' do
		def remove_module_details
			db_manager.remove_module_details(mtype, refname)
		end

		let(:migrated) do
			false
		end

		let(:mtype) do
			FactoryGirl.generate :mdm_module_detail_mtype
		end

		let(:refname) do
			FactoryGirl.generate :mdm_module_detail_refname
		end

		let!(:module_detail) do
			FactoryGirl.create(
					:mdm_module_detail
			)
		end

		before(:each) do
			db_manager.stub(:migrated => migrated)
		end

		context 'with migrated' do
			let(:migrated) do
				true
			end

			let!(:module_detail) do
				FactoryGirl.create(:mdm_module_detail)
			end

			context 'with matching Mdm::Module::Detail' do
				let(:mtype) do
					module_detail.mtype
				end

				let(:refname) do
					module_detail.refname
				end

				it 'should destroy Mdm::Module::Detail' do
					expect {
						remove_module_details
					}.to change(Mdm::Module::Detail, :count).by(-1)
				end
			end

			context 'without matching Mdm::Module::Detail' do
				it 'should not destroy Mdm::Module::Detail' do
					expect {
						remove_module_details
					}.to_not change(Mdm::Module::Detail, :count)
				end
			end
		end

		context 'without migrated' do
			it 'should not destroy Mdm::Module::Detail' do
				expect {
					remove_module_details
				}.to_not change(Mdm::Module::Detail, :count)
			end
		end
	end

	context '#search_modules' do
    subject(:search_modules) do
      db_manager.search_modules(search_string)
    end

		let(:module_details) do
			search_modules.to_a
		end

		context 'with app keyword' do
			let(:search_string) do
				"app:#{app}"
			end

			before(:each) do
				Mdm::Module::Detail::STANCES.each do |stance|
					FactoryGirl.create(:mdm_module_detail, :stance => stance)
				end
			end

			context 'with client' do
				let(:app) do
					'client'
				end

				it "should match Mdm::Module::Detail#stance 'passive'" do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.stance == 'passive'
					}.should be_true
				end
			end

			context 'with server' do
				let(:app) do
					'server'
				end

				it "should match Mdm::Module::Detail#stance 'aggressive'" do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.stance == 'aggressive'
					}.should be_true
				end
			end
		end

		context 'with author keyword' do
			let(:search_string) do
        # us inspect so strings with spaces are quoted correctly
				"author:#{author}"
			end

			let!(:module_authors) do
				FactoryGirl.create_list(:mdm_module_author, 2)
			end

			let(:target_module_author) do
				module_authors.first
			end

			context 'with Mdm::Module::Author#email' do
				let(:author) do
					target_module_author.email
				end

				it 'should match Mdm::Module::Author#email' do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.authors.any? { |module_author|
							module_author.email == target_module_author.email
						}
					}.should be_true
				end
			end

			context 'with Mdm::Module::Author#name' do
        let(:author) do
          # use inspect to quote space in name
          target_module_author.name.inspect
        end

				it 'should match Mdm::Module::Author#name' do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.authors.any? { |module_author|
							module_author.name == target_module_author.name
						}
					}.should be_true
				end
			end
		end

		it_should_behave_like 'Msf::DBManager#search_modules Mdm::Module::Ref#name keyword', :bid
		it_should_behave_like 'Msf::DBManager#search_modules Mdm::Module::Ref#name keyword', :cve
		it_should_behave_like 'Msf::DBManager#search_modules Mdm::Module::Ref#name keyword', :edb

		context 'with name keyword' do
			let(:search_string) do
				"name:#{name}"
			end

			let!(:existing_module_details) do
				FactoryGirl.create_list(:mdm_module_detail, 2)
			end

			let(:target_module_detail) do
				existing_module_details.first
			end

			context 'with Mdm::Module::Detail#fullname' do
				let(:name) do
					target_module_detail.fullname
				end

				it 'should match Mdm::Module::Detail#fullname' do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.fullname == target_module_detail.fullname
					}.should be_true
				end
			end

			context 'with Mdm::Module::Detail#name' do
				let(:name) do
          # use inspect so spaces are inside quotes
					target_module_detail.name.inspect
				end

				it 'should match Mdm::Module::Detail#name' do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.name == target_module_detail.name
					}.should be_true
				end
			end
		end

		it_should_behave_like 'Msf::DBManager#search_modules Mdm::Module::Platform#name or Mdm::Module::Target#name keyword', :os

		it_should_behave_like 'Msf::DBManager#search_modules Mdm::Module::Ref#name keyword', :osvdb

		it_should_behave_like 'Msf::DBManager#search_modules Mdm::Module::Platform#name or Mdm::Module::Target#name keyword', :platform

		context 'with ref keyword' do
			let(:ref) do
				FactoryGirl.generate :mdm_module_ref_name
			end

			let(:search_string) do
        # use inspect to quote spaces in string
				"ref:#{ref.inspect}"
			end

			let!(:module_ref) do
				FactoryGirl.create(:mdm_module_ref)
			end

			context 'with Mdm::Module::Ref#name' do
				let(:ref) do
					module_ref.name
				end

				it 'should match Mdm::Module::Ref#name' do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.refs.any? { |module_ref|
							module_ref.name == ref
						}
					}.should be_true
				end
			end

			context 'without Mdm::Module::Ref#name' do
				it 'should not match Mdm::Module::Ref#name' do
					module_details.count.should == 0
				end
			end
		end

		context 'with type keyword' do
			let(:type) do
				FactoryGirl.generate :mdm_module_detail_mtype
			end

			let(:search_string) do
				"type:#{type}"
			end

			let(:target_module_detail) do
				all_module_details.first
			end

			let!(:all_module_details) do
				FactoryGirl.create_list(:mdm_module_detail, 2)
			end

			context 'with Mdm::Module::Ref#name' do
				let(:type) do
					target_module_detail.mtype
				end

				it 'should match Mdm::Module::Detail#mtype' do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.mtype == type
					}.should be_true
				end
			end

			context 'without Mdm::Module::Detail#mtype' do
				it 'should not match Mdm::Module::Detail#mtype' do
					module_details.count.should == 0
				end
			end
		end

		context 'without keyword' do
			context 'with Mdm::Module::Action#name' do
				let(:search_string) do
					module_action.name
				end

				let!(:module_action) do
					FactoryGirl.create(:mdm_module_action)
				end

				it 'should match Mdm::Module::Action#name' do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.actions.any? { |module_action|
							module_action.name == search_string
						}
					}.should be_true
				end
			end

			context 'with Mdm::Module::Arch#name' do
				let(:search_string) do
					module_arch.name
				end

				let!(:module_arch) do
					FactoryGirl.create(:mdm_module_arch)
				end

				it 'should match Mdm::Module::Arch#name' do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.archs.any? { |module_arch|
							module_arch.name == search_string
						}
					}.should be_true
				end
			end

			context 'with Mdm::Module::Author#name' do
				let(:search_string) do
					module_author.name
				end

				let!(:module_author) do
					FactoryGirl.create(:mdm_module_author)
				end

				it 'should match Mdm::Module::Author#name' do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.authors.any? { |module_author|
							module_author.name == search_string
						}
					}.should be_true
				end
			end

			context 'with Mdm::Module::Detail' do
				let(:target_module_detail) do
					all_module_details.first
				end

				let!(:all_module_details) do
					FactoryGirl.create_list(:mdm_module_detail, 3)
				end

				context 'with #description' do
					let(:search_string) do
            # use inspect to quote spaces in string
						target_module_detail.description.inspect
					end

					it 'should match Mdm::Module::Detail#description' do
						module_details.count.should == 1

						module_details.all? { |module_detail|
							module_detail.description == target_module_detail.description
						}.should be_true
					end
				end

				context 'with #fullname' do
					let(:search_string) do
						target_module_detail.fullname
					end

					it 'should match Mdm::Module::Detail#fullname' do
						module_details.count.should == 1

						module_details.all? { |module_detail|
							module_detail.fullname == search_string
						}.should be_true
					end
				end

				context 'with #name' do
					let(:search_string) do
            # use inspect to quote spaces in string
						target_module_detail.name.inspect
					end

					it 'should match Mdm::Module::Detail#name' do
						module_details.count.should == 1

						module_details.all? { |module_detail|
							module_detail.name == target_module_detail.name
						}.should be_true
					end
				end
			end

			context 'with Mdm::Module::Platform#name' do
				let(:search_string) do
					module_platform.name
				end

				let!(:module_platform) do
					FactoryGirl.create(:mdm_module_platform)
				end

				it 'should match Mdm::Module::Platform#name' do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.platforms.any? { |module_platform|
							module_platform.name == search_string
						}
					}.should be_true
				end
			end

			context 'with Mdm::Module::Ref#name' do
				let(:search_string) do
					module_ref.name
				end

				let!(:module_ref) do
					FactoryGirl.create(:mdm_module_ref)
				end

				it 'should match Mdm::Module::Ref#name' do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.refs.any? { |module_ref|
							module_ref.name == search_string
						}
					}.should be_true
				end
			end

			context 'with Mdm::Module::Target#name' do
				let(:search_string) do
					module_target.name
				end

				let!(:module_target) do
					FactoryGirl.create(:mdm_module_target)
				end

				it 'should match Mdm::Module::Target#name' do
					module_details.count.should > 0

					module_details.all? { |module_detail|
						module_detail.targets.any? { |module_target|
							module_target.name == search_string
						}
					}.should be_true
				end
			end
		end
	end

	context '#update_all_module_details' do
		def update_all_module_details
			db_manager.update_all_module_details
		end

		let(:migrated) do
			false
		end

		before(:each) do
			db_manager.stub(:migrated => migrated)
		end

		context 'with migrated' do
			let(:migrated) do
				true
			end

			let(:modules_caching) do
				true
			end

			before(:each) do
				db_manager.stub(:modules_caching => modules_caching)
			end

			context 'with modules_caching' do
				it 'should not update module details' do
					db_manager.should_not_receive(:update_module_details)

					update_all_module_details
				end
			end

			context 'without modules_caching' do
				let(:modules_caching) do
					false
				end

				it 'should create a connection' do
					ActiveRecord::Base.connection_pool.should_receive(:with_connection).twice.and_call_original

					update_all_module_details
				end

				it 'should set framework.cache_thread to current thread and then nil around connection' do
					framework.should_receive(:cache_thread=).with(Thread.current).ordered
					ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered
					framework.should_receive(:cache_thread=).with(nil).ordered

					update_all_module_details

					ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered.and_call_original
				end

				it 'should set modules_cached to false and then true around connection' do
					db_manager.should_receive(:modules_cached=).with(false).ordered
					ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered
					db_manager.should_receive(:modules_cached=).with(true).ordered

					update_all_module_details

					ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered.and_call_original
				end

				it 'should set modules_caching to true and then false around connection' do
					db_manager.should_receive(:modules_caching=).with(true).ordered
					ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered
					db_manager.should_receive(:modules_caching=).with(false).ordered

					update_all_module_details

					ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered.and_call_original
				end

				context 'with Mdm::Module::Details' do
					let(:module_pathname) do
						parent_pathname.join(
								'exploits',
						    "#{reference_name}.rb"
						)
					end

					let(:modification_time) do
						module_pathname.mtime
					end

					let(:parent_pathname) do
						Metasploit::Framework.root.join('modules')
					end

					let(:reference_name) do
						'windows/smb/ms08_067_netapi'
					end

					let(:type) do
						'exploit'
					end

					let!(:module_detail) do
						# needs to reference a real module so that it can be loaded
						FactoryGirl.create(
								:mdm_module_detail,
								:file => module_pathname.to_path,
								:mtime => modification_time,
								:mtype => type,
						    :ready => ready,
						    :refname => reference_name
						)
					end

					context '#ready' do
						context 'false' do
							let(:ready) do
								false
							end

							it_should_behave_like 'Msf::DBManager#update_all_module_details refresh'
						end

						context 'true' do
							let(:ready) do
								true
							end

							context 'with existing Mdm::Module::Detail#file' do
								context 'with same Mdm::Module::Detail#mtime and File.mtime' do
									it 'should not update module details' do
										db_manager.should_not_receive(:update_module_details)

										update_all_module_details
									end
								end

								context 'without same Mdm::Module::Detail#mtime and File.mtime' do
									let(:modification_time) do
										# +1 as rand can return 0 and the time must be different for
										# this context.
										super() - (rand(1.day) + 1)
									end

									it_should_behave_like 'Msf::DBManager#update_all_module_details refresh'
								end
							end

							# Emulates a module being removed or renamed
							context 'without existing Mdm::Module::Detail#file' do
								# have to compute modification manually since the
								# `module_pathname` refers to a non-existent file and
								# `module_pathname.mtime` would error.
								let(:modification_time) do
									Time.now.utc - 1.day
								end

								let(:module_pathname) do
									parent_pathname.join('exploits', 'deleted.rb')
								end

								it 'should not update module details' do
									db_manager.should_not_receive(:update_module_details)

									update_all_module_details
								end
							end
						end
					end
				end
			end
		end

		context 'without migrated' do
			it 'should not update module details' do
				db_manager.should_not_receive(:update_module_details)

				update_all_module_details
			end
		end
	end

	context '#update_module_details' do
		def update_module_details
			db_manager.update_module_details(module_instance)
		end

		let(:loader) do
			loader = framework.modules.send(:loaders).find { |loader|
				loader.loadable?(parent_path)
			}

			# Override load_error so that rspec will print it instead of going to framework log
			def loader.load_error(module_path, error)
				raise error
			end

			loader
		end

		let(:migrated) do
			false
		end

		let(:module_instance) do
			# make sure the module is loaded into the module_set
			loaded = loader.load_module(parent_path, module_type, module_reference_name)

			unless loaded
				module_path = loader.module_path(parent_path, type, module_reference_name)

				fail "#{description} failed to load: #{module_path}"
			end

			module_set.create(module_reference_name)
		end

		let(:module_set) do
			framework.modules.module_set(module_type)
		end

		let(:module_type) do
			'exploit'
		end

		let(:module_reference_name) do
			'windows/smb/ms08_067_netapi'
		end

		let(:parent_path) do
			parent_pathname.to_path
		end

		let(:parent_pathname) do
			Metasploit::Framework.root.join('modules')
		end

		let(:type_directory) do
			'exploits'
		end

		before(:each) do
			db_manager.stub(:migrated => migrated)
		end

		context 'with migrated' do
			let(:migrated) do
				true
			end

			it 'should create connection' do
				ActiveRecord::Base.connection_pool.should_receive(:with_connection)
				ActiveRecord::Base.connection_pool.should_receive(:with_connection).and_call_original

				update_module_details
			end

			it 'should call module_to_details_hash to get Mdm::Module::Detail attributes and association attributes' do
				db_manager.should_receive(:module_to_details_hash).and_call_original

				update_module_details
			end

			it 'should create an Mdm::Module::Detail' do
				expect {
					update_module_details
				}.to change(Mdm::Module::Detail, :count).by(1)
			end


			context 'module_to_details_hash' do
				let(:module_to_details_hash) do
					{
							:mtype => module_type,
							:privileged => privileged,
							:rank => rank,
							:refname => module_reference_name,
							:stance => stance
					}
				end

				let(:privileged) do
					FactoryGirl.generate :mdm_module_detail_privileged
				end

				let(:rank) do
					FactoryGirl.generate :mdm_module_detail_rank
				end

				let(:stance) do
					FactoryGirl.generate :mdm_module_detail_stance
				end

				before(:each) do
					db_manager.stub(
							:module_to_details_hash
					).with(
							module_instance
					).and_return(
							module_to_details_hash
					)
				end

				context 'Mdm::Module::Detail' do
					subject(:module_detail) do
						Mdm::Module::Detail.last
					end

					before(:each) do
						update_module_details
					end

					its(:mtype) { should == module_type }
					its(:privileged) { should == privileged }
					its(:rank) { should == rank }
					its(:ready) { should == true }
					its(:refname) { should == module_reference_name }
					its(:stance) { should == stance }
				end

				context 'with :bits' do
					let(:bits) do
						[]
					end

					before(:each) do
						module_to_details_hash[:bits] = bits
					end

					context 'with :action' do
						let(:name) do
							FactoryGirl.generate :mdm_module_action_name
						end

						let(:bits) do
							super() << [
									:action,
							    {
									    :name => name
							    }
							]
						end

						it 'should create an Mdm::Module::Action' do
							expect {
								update_module_details
							}.to change(Mdm::Module::Action, :count).by(1)
						end

						context 'Mdm::Module::Action' do
							subject(:module_action) do
								module_detail.actions.last
							end

							let(:module_detail) do
								Mdm::Module::Detail.last
							end

							before(:each) do
								update_module_details
							end

							its(:name) { should == name }
						end
					end

					context 'with :arch' do
						let(:name) do
							FactoryGirl.generate :mdm_module_arch_name
						end

						let(:bits) do
							super() << [
									:arch,
							    {
									    :name => name
							    }
							]
						end

						it 'should create an Mdm::Module::Arch' do
							expect {
								update_module_details
							}.to change(Mdm::Module::Arch, :count).by(1)
						end

						context 'Mdm::Module::Arch' do
							subject(:module_arch) do
								module_detail.archs.last
							end

							let(:module_detail) do
								Mdm::Module::Detail.last
							end

							before(:each) do
								update_module_details
							end

							its(:name) { should == name }
						end
					end

					context 'with :author' do
						let(:email) do
							FactoryGirl.generate :mdm_module_author_email
						end

						let(:name) do
							FactoryGirl.generate :mdm_module_author_name
						end

						let(:bits) do
							super() << [
									:author,
							    {
									    :email => email,
							        :name => name
							    }
							]
						end

						it 'should create an Mdm::Module::Author' do
							expect {
								update_module_details
							}.to change(Mdm::Module::Author, :count).by(1)
						end

						context 'Mdm::Module::Author' do
							subject(:module_author) do
								module_detail.authors.last
							end

							let(:module_detail) do
								Mdm::Module::Detail.last
							end

							before(:each) do
								update_module_details
							end

							its(:name) { should == name }
							its(:email) { should == email }
						end
					end

					context 'with :platform' do
						let(:bits) do
							super() << [
									:platform,
							    {
									    :name => name
							    }
							]
						end

						let(:name) do
							FactoryGirl.generate :mdm_module_platform_name
						end

						it 'should create an Mdm::Module::Platform' do
							expect {
								update_module_details
							}.to change(Mdm::Module::Platform, :count).by(1)
						end

						context 'Mdm::Module::Platform' do
							subject(:module_platform) do
								module_detail.platforms.last
							end

							let(:module_detail) do
								Mdm::Module::Detail.last
							end

							before(:each) do
								update_module_details
							end

							its(:name) { should == name }
						end
					end

					context 'with :ref' do
						let(:bits) do
							super() << [
									:ref,
							    {
									    :name => name
							    }
							]
						end

						let(:name) do
							FactoryGirl.generate :mdm_module_ref_name
						end

						it 'should create an Mdm::Module::Ref' do
							expect {
								update_module_details
							}.to change(Mdm::Module::Ref, :count).by(1)
						end

						context 'Mdm::Module::Ref' do
							subject(:module_ref) do
								module_detail.refs.last
							end

							let(:module_detail) do
								Mdm::Module::Detail.last
							end

							before(:each) do
								update_module_details
							end

							its(:name) { should == name }
						end
					end

					context 'with :target' do
						let(:bits) do
							super() << [
									:target,
									{
											:index => index,
											:name => name
									}
							]
						end

						let(:index) do
							FactoryGirl.generate :mdm_module_target_index
						end

						let(:name) do
							FactoryGirl.generate :mdm_module_target_name
						end

						it 'should create an Mdm::Module::Target' do
							expect {
								update_module_details
							}.to change(Mdm::Module::Target, :count).by(1)
						end

						context 'Mdm::Module::Target' do
							subject(:module_target) do
								module_detail.targets.last
							end

							let(:module_detail) do
								Mdm::Module::Detail.last
							end

							before(:each) do
								update_module_details
							end

							its(:index) { should == index }
							its(:name) { should == name }
						end
					end
				end
			end

			it_should_behave_like 'Msf::DBManager#update_module_details with module',
														:reference_name => 'admin/2wire/xslt_password_reset',
														:type => 'auxiliary'

			it_should_behave_like 'Msf::DBManager#update_module_details with module',
														:reference_name => 'generic/none',
														:type => 'encoder'

			it_should_behave_like 'Msf::DBManager#update_module_details with module',
														:reference_name => 'windows/smb/ms08_067_netapi',
														:type => 'exploit'

			it_should_behave_like 'Msf::DBManager#update_module_details with module',
														:reference_name => 'x64/simple',
														:type => 'nop'

			# @todo determine how to load a single payload to test payload type outside of msfconsole

 			it_should_behave_like 'Msf::DBManager#update_module_details with module',
														:reference_name => 'windows/escalate/screen_unlock',
														:type => 'post'
		end

		context 'without migrated' do
			it 'should not create an Mdm::Module::Detail' do
				expect {
					update_module_details
				}.to_not change(Mdm::Module::Detail, :count)
			end
		end
	end
end
