Path: blob/master/spec/integration/msfmcpd/logging_pipeline_spec.rb
70330 views
# frozen_string_literal: true12require 'msf/core/mcp'3require 'stringio'4require 'tempfile'5require 'json'67RSpec.describe 'Logging Pipeline Integration' do8let(:output) { StringIO.new }9let(:log_file) { Tempfile.new(['logging_integration', '.log']).tap(&:close).path }10let(:log_src) { Msf::MCP::LOG_SOURCE }11let(:lvl_info) { Msf::MCP::LOG_INFO }12let(:lvl_warn) { Msf::MCP::LOG_WARN }13let(:lvl_error) { Msf::MCP::LOG_ERROR }1415after do16deregister_log_source(log_src) if log_source_registered?(log_src)17File.delete(log_file) if File.exist?(log_file)18end1920describe 'initialize_logger with sanitize enabled' do21it 'produces JSON log entries with sensitive data redacted' do22app = Msf::MCP::Application.new([], output: output)23app.send(:parse_arguments)24app.instance_variable_set(:@config, {25logging: {26enabled: true,27level: 'INFO',28log_file: log_file,29sanitize: true30}31})32app.send(:initialize_logger)3334ilog({ message: 'Connection established', context: { password: 's3cret', host: 'localhost' } }, log_src, lvl_info)3536content = File.read(log_file)37expect(content).not_to be_empty3839entry = JSON.parse(content.strip.split("\n").last)4041expect(entry['timestamp']).not_to be_nil42expect(entry['severity']).to eq('INFO')43expect(entry['message']).to include('Connection established')4445expect(entry['context']['password']).to eq('[REDACTED]')46expect(entry['context']['host']).to eq('localhost')4748expect(content).not_to include('s3cret')49end50end5152describe 'initialize_logger with sanitize disabled' do53it 'produces JSON log entries without redaction' do54app = Msf::MCP::Application.new([], output: output)55app.send(:parse_arguments)56app.instance_variable_set(:@config, {57logging: {58enabled: true,59level: 'INFO',60log_file: log_file,61sanitize: false62}63})64app.send(:initialize_logger)6566ilog({ message: 'Connection established', context: { password: 's3cret', host: 'localhost' } }, log_src, lvl_info)6768content = File.read(log_file)69entry = JSON.parse(content.strip.split("\n").last)7071expect(entry['severity']).to eq('INFO')72expect(entry['message']).to include('Connection established')7374expect(entry['context']['password']).to eq('s3cret')75expect(entry['context']['host']).to eq('localhost')76end77end7879describe 'log level filtering' do80it 'filters messages below the configured threshold' do81app = Msf::MCP::Application.new([], output: output)82app.send(:parse_arguments)83app.instance_variable_set(:@config, {84logging: {85enabled: true,86level: 'WARN',87log_file: log_file,88sanitize: false89}90})91app.send(:initialize_logger)9293# INFO (LEV_2) should be filtered at WARN (LEV_1) threshold94ilog({ message: 'This should be filtered' }, log_src, lvl_info)95# WARN (LEV_1) should pass96wlog({ message: 'This should appear' }, log_src, lvl_warn)97# ERROR (LEV_0) should pass98elog({ message: 'This error should appear' }, log_src, lvl_error)99100content = File.read(log_file)101lines = content.strip.split("\n").reject(&:empty?)102103expect(lines.length).to eq(2)104105entries = lines.map { |l| JSON.parse(l) }106expect(entries.map { |e| e['severity'] }).to contain_exactly('WARN', 'ERROR')107expect(content).not_to include('This should be filtered')108end109end110111describe 'exception logging through the pipeline' do112it 'formats exceptions as structured JSON with sanitized messages' do113app = Msf::MCP::Application.new([], output: output)114app.send(:parse_arguments)115app.instance_variable_set(:@config, {116logging: {117enabled: true,118level: 'ERROR',119log_file: log_file,120sanitize: true121}122})123app.send(:initialize_logger)124125error = StandardError.new('Failed with password=hunter2')126error.set_backtrace(['lib/msf/core/mcp/server.rb:42:in `start`'])127128elog({ message: 'Startup failed', exception: error }, log_src, lvl_error)129130content = File.read(log_file)131entry = JSON.parse(content.strip.split("\n").last)132133expect(entry['severity']).to eq('ERROR')134expect(entry['message']).to include('Startup failed')135expect(entry['exception']).to be_a(Hash)136expect(entry['exception']['class']).to eq('StandardError')137138expect(entry['exception']['message']).to include('[REDACTED]')139expect(entry['exception']['message']).not_to include('hunter2')140end141end142end143144145