Path: blob/master/lib/msf/core/mcp/application.rb
70339 views
# frozen_string_literal: true12require 'msf/core/mcp'3require 'optparse'45module Msf::MCP6# Main application class that orchestrates the MCP server startup and lifecycle7class Application8VERSION = '0.1.0'9BANNER = <<~BANNER10MSF MCP Server v#{VERSION}11Model Context Protocol server for Metasploit Framework12BANNER1314# For testing purposes:15attr_reader :config, :msf_client, :mcp_server, :rate_limiter, :options, :rpc_manager1617# Initialize the application with command-line arguments18#19# @param argv [Array<String>] Command-line arguments20# @param output [IO] Output stream for messages (default: $stderr)21def initialize(argv = ARGV, output: $stderr)22@argv = argv.dup23@output = output24@options = {}25@config = nil26@msf_client = nil27@mcp_server = nil28@rate_limiter = nil29@rpc_manager = nil30end3132# Run the application33#34# @return [void]35def run36parse_arguments37install_signal_handlers38load_configuration39validate_configuration40initialize_logger41initialize_rate_limiter42ensure_rpc_server43initialize_metasploit_client44authenticate_metasploit45initialize_mcp_server46start_mcp_server47rescue Msf::MCP::Config::ValidationError, Msf::MCP::Config::ConfigurationError => e48handle_configuration_error(e)49rescue Msf::MCP::Metasploit::ConnectionError => e50handle_connection_error(e)51rescue Msf::MCP::Metasploit::APIError => e52handle_api_error(e)53rescue Msf::MCP::Metasploit::AuthenticationError => e54handle_authentication_error(e)55rescue Msf::MCP::Metasploit::RpcStartupError => e56handle_rpc_startup_error(e)57rescue StandardError => e58handle_fatal_error(e)59end6061# Shutdown the application gracefully62#63# Performs cleanup operations before process termination:64# - Logs shutdown event via Rex65# - Closes MCP server and Metasploit client connections66# - Cleans up resources67#68# @param signal [String] Signal name (e.g., 'INT', 'TERM')69# @return [void]70def shutdown(signal = 'INT')71ilog({72message: 'Shutting down',73context: { signal: "SIG#{signal}" }74}, LOG_SOURCE, LOG_INFO)75@mcp_server&.shutdown76@rpc_manager&.stop_rpc_server77@output.puts "\nShutdown complete"78end7980private8182# Parse command-line arguments83#84# @return [void]85def parse_arguments86parser = OptionParser.new do |opts|87opts.banner = BANNER + "\nUsage: msfmcp [options]"8889opts.on('--config PATH', 'Path to configuration file') do |path|90@options[:config_path] = File.expand_path(path)91end9293opts.on('--enable-logging', 'Enable file logging') do94@options[:enable_logging_cli] = true95end9697opts.on('--log-file PATH', 'Log file path (overrides config file)') do |path|98@options[:log_file_cli] = path99end100101opts.on('--user USER', 'MSF API username (for MessagePack auth)') do |user|102@options[:msf_user_cli] = user103end104105opts.on('--password PASS', 'MSF API password (for MessagePack auth)') do |password|106@options[:msf_password_cli] = password107end108109opts.on('--no-auto-start-rpc', 'Disable automatic RPC server startup') do110@options[:no_auto_start_rpc] = true111end112113opts.on('--mcp-transport TRANSPORT', 'MCP server transport type (\'stdio\' or \'http\')') do |transport|114@options[:mcp_transport] = transport115end116117opts.on('-h', '--help', 'Show this help message') do118@output.puts opts119exit 0120end121122opts.on('-v', '--version', 'Show version information') do123@output.puts "msfmcp version #{VERSION}"124exit 0125end126end127128parser.parse!(@argv)129end130131# Register a Rex log source when logging is enabled.132#133# Selects a JsonFlatfile sink pointed at the configured log path and wraps it134# with the sanitizing middleware unless sanitization has been explicitly135# disabled in the config.136#137# Priority: CLI flags > config file > defaults138#139# @return [void]140def initialize_logger141return unless @options[:enable_logging_cli] || @config.dig(:logging, :enabled)142143log_file = @options[:log_file_cli] || @config.dig(:logging, :log_file)144level = @config.dig(:logging, :level)145threshold = case @config.dig(:logging, :level).upcase146when 'DEBUG'147Rex::Logging::LEV_3148when 'INFO'149Rex::Logging::LEV_2150when 'WARN'151Rex::Logging::LEV_1152when 'ERROR'153Rex::Logging::LEV_0154end155inner = Msf::MCP::Logging::Sinks::JsonFlatfile.new(log_file)156sink = @config.dig(:logging, :sanitize) ? Msf::MCP::Logging::Sinks::Sanitizing.new(inner) : inner157158deregister_log_source(LOG_SOURCE) if log_source_registered?(LOG_SOURCE)159register_log_source(LOG_SOURCE, sink, threshold)160end161162# Install signal handlers for graceful shutdown163#164# @return [void]165def install_signal_handlers166Signal.trap('INT') { shutdown('INT'); exit 0 }167Signal.trap('TERM') { shutdown('TERM'); exit 0 }168end169170# Load configuration from file or use defaults171#172# @return [void]173def load_configuration174if @options[:config_path]175@output.puts "Loading configuration from #{@options[:config_path]}"176@config = Msf::MCP::Config::Loader.load(@options[:config_path])177else178@output.puts "No configuration file specified, using defaults"179@config = Msf::MCP::Config::Loader.load_from_hash({})180end181182# Apply CLI authentication overrides (highest priority)183if @options[:msf_user_cli]184@config[:msf_api][:user] = @options[:msf_user_cli]185end186if @options[:msf_password_cli]187@config[:msf_api][:password] = @options[:msf_password_cli]188end189if @options[:no_auto_start_rpc]190@config[:msf_api][:auto_start_rpc] = false191end192if @options[:mcp_transport]193@config[:mcp][:transport] = @options[:mcp_transport]194end195end196197# Validate the loaded configuration198#199# @return [void]200def validate_configuration201@output.puts "Validating configuration..."202Msf::MCP::Config::Validator.validate!(@config)203@output.puts "Configuration valid"204end205206# Initialize the rate limiter207#208# @return [void]209def initialize_rate_limiter210@rate_limiter = Msf::MCP::Security::RateLimiter.new(211requests_per_minute: @config.dig(:rate_limit, :requests_per_minute) || 60,212burst_size: @config.dig(:rate_limit, :burst_size)213)214end215216# Ensure the Metasploit RPC server is available, auto-starting if needed217#218# @return [void]219def ensure_rpc_server220@rpc_manager = Msf::MCP::RpcManager.new(221config: @config,222output: @output223)224@rpc_manager.ensure_rpc_available225end226227# Initialize the Metasploit client228#229# @return [void]230def initialize_metasploit_client231@output.puts "Connecting to Metasploit RPC at #{@config[:msf_api][:host]}:#{@config[:msf_api][:port]}"232@msf_client = Msf::MCP::Metasploit::Client.new(233api_type: @config[:msf_api][:type],234host: @config[:msf_api][:host],235port: @config[:msf_api][:port],236endpoint: @config[:msf_api][:endpoint],237token: @config[:msf_api][:token],238ssl: @config[:msf_api][:ssl]239)240end241242# Authenticate with Metasploit if using MessagePack243#244# @return [void]245def authenticate_metasploit246if @config[:msf_api][:type] == 'messagepack'247@output.puts "Authenticating with Metasploit..."248@msf_client.authenticate(@config[:msf_api][:user].to_s, @config[:msf_api][:password].to_s)249@output.puts "Authentication successful"250else251@output.puts "Using JSON-RPC with token authentication"252end253end254255# Initialize the MCP server256#257# @return [void]258def initialize_mcp_server259@output.puts "Initializing MCP server..."260@mcp_server = Msf::MCP::Server.new(261msf_client: @msf_client,262rate_limiter: @rate_limiter263)264end265266# Start the MCP server with configured transport267#268# @return [void]269def start_mcp_server270transport = (@config.dig(:mcp, :transport) || 'stdio').to_sym271host = @config.dig(:mcp, :host) || 'localhost'272port = @config.dig(:mcp, :port) || 3000273274if transport == :http275@output.puts "Starting MCP server on HTTP transport..."276@output.puts "Server listening on http://#{host}:#{port}"277@output.puts "Press Ctrl+C to shutdown"278@mcp_server.start(transport: :http, host: host, port: port)279else280@output.puts "Starting MCP server on stdio transport..."281@output.puts "Server ready - waiting for MCP requests"282@output.puts "Press Ctrl+C to shutdown"283@mcp_server.start(transport: :stdio)284end285end286287# Error handlers288289def handle_configuration_error(error)290@output.puts "Configuration validation failed: #{error.message}"291exit 1292end293294def handle_connection_error(error)295elog({296message: 'Connection error',297context: { host: @config[:msf_api][:host], port: @config[:msf_api][:port] },298exception: error299}, LOG_SOURCE, LOG_ERROR)300@output.puts "Connection error to Metasploit RPC at #{@config[:msf_api][:host]}:#{@config[:msf_api][:port]} - #{error.message}"301exit 1302end303304def handle_api_error(error)305elog({ message: 'Metasploit API error', exception: error }, LOG_SOURCE, LOG_ERROR)306@output.puts "Metasploit API error: #{error.message}"307exit 1308end309310def handle_authentication_error(error)311elog({312message: 'Authentication error',313context: { username: @config[:msf_api][:user].to_s },314exception: error315}, LOG_SOURCE, LOG_ERROR)316@output.puts "Authentication error (username: #{@config[:msf_api][:user]}): #{error.message}"317exit 1318end319320def handle_rpc_startup_error(error)321elog({ message: 'RPC startup error', exception: error }, LOG_SOURCE, LOG_ERROR)322@output.puts "RPC startup error: #{error.message}"323exit 1324end325326def handle_fatal_error(error)327elog({ message: 'Fatal error during startup', exception: error }, LOG_SOURCE, LOG_ERROR)328@output.puts "Fatal error: #{error.message}"329@output.puts error.backtrace.first(5).join("\n") if error.backtrace330exit 1331end332end333end334335336