Path: blob/master/spec/plugins/mcp/option_validator_spec.rb
74512 views
# frozen_string_literal: true12require 'spec_helper'3require 'rex/text'4require Metasploit::Framework.root.join('plugins/mcp.rb').to_path56RSpec.describe Msf::Plugin::MCP do7include_context 'Msf::UIDriver'89let(:framework) { instance_double(Msf::Framework) }10let(:output) { driver_output }11let(:base_opts) { { 'LocalOutput' => output } }1213let(:plugins_collection) do14instance_double('Msf::PluginManager').tap do |pm|15allow(pm).to receive(:find).and_return(nil)16allow(pm).to receive(:load).and_return(true)17end18end1920let(:threads_manager) do21instance_double('Msf::Framework::ThreadManager').tap do |tm|22allow(tm).to receive(:spawn).and_return(Thread.new {})23end24end2526let(:mock_client) do27instance_double('Msf::MCP::Metasploit::Client').tap do |c|28allow(c).to receive(:authenticate).and_return('token')29allow(c).to receive(:shutdown)30end31end3233before do34allow(framework).to receive(:plugins).and_return(plugins_collection)35allow(framework).to receive(:threads).and_return(threads_manager)36stub_const('Msf::MCP::Metasploit::Client', Class.new do37def initialize(**_args); end38def authenticate(*_args); 'token'; end39def shutdown; end40end)41stub_const('Msf::MCP::Security::RateLimiter', Class.new do42def initialize(**_args); end43end)44stub_const('Msf::MCP::Server', Class.new do45def initialize(**_args); end46def start(**_args); end47def shutdown; end48end)49allow(Rex::Text).to receive(:rand_text_alphanumeric).with(12).and_return('abcdefghijkl')5051mock_dispatcher = instance_double(described_class::McpCommandDispatcher)52allow(mock_dispatcher).to receive(:plugin=)53allow_any_instance_of(described_class).to receive(:add_console_dispatcher).and_return(mock_dispatcher)54allow_any_instance_of(described_class).to receive(:remove_console_dispatcher)55end5657subject(:plugin) { described_class.new(framework, base_opts) }5859describe '#validate_options!' do60describe 'ServerPort' do61it 'accepts port 1' do62expect { plugin.send(:validate_options!, { 'ServerPort' => '1' }) }.not_to raise_error63end6465it 'accepts port 3000' do66expect { plugin.send(:validate_options!, { 'ServerPort' => '3000' }) }.not_to raise_error67end6869it 'accepts port 65535' do70expect { plugin.send(:validate_options!, { 'ServerPort' => '65535' }) }.not_to raise_error71end7273it 'rejects port 0' do74expect { plugin.send(:validate_options!, { 'ServerPort' => '0' }) }.to raise_error(Msf::MCP::Config::ValidationError, /ServerPort/)75end7677it 'rejects port 65536' do78expect { plugin.send(:validate_options!, { 'ServerPort' => '65536' }) }.to raise_error(Msf::MCP::Config::ValidationError, /ServerPort/)79end8081it 'rejects non-numeric value "abc"' do82expect { plugin.send(:validate_options!, { 'ServerPort' => 'abc' }) }.to raise_error(Msf::MCP::Config::ValidationError, /ServerPort/)83end8485it 'is optional (nil is accepted)' do86expect { plugin.send(:validate_options!, {}) }.not_to raise_error87end88end8990describe 'RpcPort' do91it 'accepts port 1' do92expect { plugin.send(:validate_options!, { 'RpcPort' => '1' }) }.not_to raise_error93end9495it 'accepts port 55552' do96expect { plugin.send(:validate_options!, { 'RpcPort' => '55552' }) }.not_to raise_error97end9899it 'accepts port 65535' do100expect { plugin.send(:validate_options!, { 'RpcPort' => '65535' }) }.not_to raise_error101end102103it 'rejects port 0' do104expect { plugin.send(:validate_options!, { 'RpcPort' => '0' }) }.to raise_error(Msf::MCP::Config::ValidationError, /RpcPort/)105end106107it 'rejects port 65536' do108expect { plugin.send(:validate_options!, { 'RpcPort' => '65536' }) }.to raise_error(Msf::MCP::Config::ValidationError, /RpcPort/)109end110111it 'rejects non-numeric value "abc"' do112expect { plugin.send(:validate_options!, { 'RpcPort' => 'abc' }) }.to raise_error(Msf::MCP::Config::ValidationError, /RpcPort/)113end114end115116describe 'RpcSSL' do117it 'accepts "true"' do118expect { plugin.send(:validate_options!, { 'RpcSSL' => 'true' }) }.not_to raise_error119end120121it 'accepts "false"' do122expect { plugin.send(:validate_options!, { 'RpcSSL' => 'false' }) }.not_to raise_error123end124125it 'rejects "yes"' do126expect { plugin.send(:validate_options!, { 'RpcSSL' => 'yes' }) }.to raise_error(Msf::MCP::Config::ValidationError, /RpcSSL/)127end128129it 'rejects "1"' do130expect { plugin.send(:validate_options!, { 'RpcSSL' => '1' }) }.to raise_error(Msf::MCP::Config::ValidationError, /RpcSSL/)131end132133it 'rejects empty string' do134expect { plugin.send(:validate_options!, { 'RpcSSL' => '' }) }.to raise_error(Msf::MCP::Config::ValidationError, /RpcSSL/)135end136end137138describe 'RateLimit' do139it 'accepts value 1' do140expect { plugin.send(:validate_options!, { 'RateLimit' => '1' }) }.not_to raise_error141end142143it 'accepts value 60' do144expect { plugin.send(:validate_options!, { 'RateLimit' => '60' }) }.not_to raise_error145end146147it 'accepts value 10000' do148expect { plugin.send(:validate_options!, { 'RateLimit' => '10000' }) }.not_to raise_error149end150151it 'rejects value 0' do152expect { plugin.send(:validate_options!, { 'RateLimit' => '0' }) }.to raise_error(Msf::MCP::Config::ValidationError, /RateLimit/)153end154155it 'rejects value 10001' do156expect { plugin.send(:validate_options!, { 'RateLimit' => '10001' }) }.to raise_error(Msf::MCP::Config::ValidationError, /RateLimit/)157end158159it 'rejects non-numeric value "fast"' do160expect { plugin.send(:validate_options!, { 'RateLimit' => 'fast' }) }.to raise_error(Msf::MCP::Config::ValidationError, /RateLimit/)161end162end163164describe 'RPC credential pairing' do165it 'raises an error when RpcUser is provided without RpcPass' do166expect {167plugin.send(:validate_options!, { 'RpcUser' => 'msf', 'RpcPass' => '' })168}.to raise_error(Msf::MCP::Config::ValidationError, /RpcPass/)169end170171it 'raises an error when RpcUser is provided and RpcPass is nil' do172expect {173plugin.send(:validate_options!, { 'RpcUser' => 'msf' })174}.to raise_error(Msf::MCP::Config::ValidationError, /RpcPass/)175end176177it 'raises an error when RpcPass is provided without RpcUser' do178expect {179plugin.send(:validate_options!, { 'RpcPass' => 'secret', 'RpcUser' => '' })180}.to raise_error(Msf::MCP::Config::ValidationError, /RpcUser/)181end182183it 'raises an error when RpcPass is provided and RpcUser is nil' do184expect {185plugin.send(:validate_options!, { 'RpcPass' => 'secret' })186}.to raise_error(Msf::MCP::Config::ValidationError, /RpcUser/)187end188189it 'accepts when both RpcUser and RpcPass are provided' do190expect {191plugin.send(:validate_options!, { 'RpcUser' => 'msf', 'RpcPass' => 'secret' })192}.not_to raise_error193end194195it 'accepts when neither RpcUser nor RpcPass is provided' do196expect { plugin.send(:validate_options!, {}) }.not_to raise_error197end198end199200describe 'error messages' do201it 'names the offending option in the error for ServerPort' do202expect {203plugin.send(:validate_options!, { 'ServerPort' => 'bad' })204}.to raise_error(Msf::MCP::Config::ValidationError, /Invalid value for ServerPort/)205end206207it 'includes the expected format for ServerPort' do208expect {209plugin.send(:validate_options!, { 'ServerPort' => 'bad' })210}.to raise_error(Msf::MCP::Config::ValidationError, /integer between 1 and 65535/)211end212213it 'names the offending option in the error for RpcSSL' do214expect {215plugin.send(:validate_options!, { 'RpcSSL' => 'maybe' })216}.to raise_error(Msf::MCP::Config::ValidationError, /Invalid value for RpcSSL/)217end218219it 'includes the expected format for RpcSSL' do220expect {221plugin.send(:validate_options!, { 'RpcSSL' => 'maybe' })222}.to raise_error(Msf::MCP::Config::ValidationError, /\"true\" or \"false\"/)223end224225it 'names the offending option in the error for RateLimit' do226expect {227plugin.send(:validate_options!, { 'RateLimit' => '-1' })228}.to raise_error(Msf::MCP::Config::ValidationError, /Invalid value for RateLimit/)229end230231it 'includes the expected format for RateLimit' do232expect {233plugin.send(:validate_options!, { 'RateLimit' => '-1' })234}.to raise_error(Msf::MCP::Config::ValidationError, /integer between 1 and 10000/)235end236end237end238239describe '#resolve_config' do240describe 'provided options appear in the resolved config' do241it 'maps ServerHost to mcp[:host]' do242config = plugin.send(:resolve_config, { 'ServerHost' => '0.0.0.0' })243expect(config[:mcp][:host]).to eq('0.0.0.0')244end245246it 'maps ServerPort to mcp[:port]' do247config = plugin.send(:resolve_config, { 'ServerPort' => '8080' })248expect(config[:mcp][:port]).to eq(8080)249end250251it 'maps RateLimit to rate_limit[:requests_per_minute]' do252config = plugin.send(:resolve_config, { 'RateLimit' => '120' })253expect(config[:rate_limit][:requests_per_minute]).to eq(120)254end255256it 'sets rate_limit[:burst_size] equal to requests_per_minute' do257config = plugin.send(:resolve_config, { 'RateLimit' => '120' })258expect(config[:rate_limit][:burst_size]).to eq(120)259end260end261262describe 'default values when options are omitted' do263let(:config) { plugin.send(:resolve_config, {}) }264265it 'defaults mcp[:transport] to "http"' do266expect(config[:mcp][:transport]).to eq('http')267end268269it 'defaults mcp[:host] to "localhost"' do270expect(config[:mcp][:host]).to eq('localhost')271end272273it 'defaults mcp[:port] to 3000' do274expect(config[:mcp][:port]).to eq(3000)275end276277it 'defaults rate_limit[:requests_per_minute] to 60' do278expect(config[:rate_limit][:requests_per_minute]).to eq(60)279end280281it 'defaults rate_limit[:burst_size] to 60' do282expect(config[:rate_limit][:burst_size]).to eq(60)283end284end285286end287end288289290