Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/spec/integration/msfmcpd/tool_execution_search_spec.rb
70330 views
1
# frozen_string_literal: true
2
3
require 'msf/core/mcp'
4
require 'webmock/rspec'
5
6
RSpec.describe 'Tool Execution End-to-End - Search Modules' do
7
# Disable real HTTP connections for integration tests
8
before(:all) do
9
WebMock.disable_net_connect!(allow_localhost: false)
10
end
11
12
after(:all) do
13
WebMock.allow_net_connect!
14
end
15
16
let(:host) { 'localhost' }
17
let(:port) { 55553 }
18
let(:endpoint) { '/api/' }
19
let(:api_url) { "https://#{host}:#{port}#{endpoint}" }
20
let(:user) { 'test_user' }
21
let(:password) { 'test_password' }
22
23
describe 'Module Search Integration with HTTP' do
24
it 'executes module search through complete HTTP request flow' do
25
# Stub authentication endpoint
26
stub_request(:post, api_url)
27
.with(body: ['auth.login', user, password].to_msgpack)
28
.to_return(
29
status: 200,
30
body: { 'result' => 'success', 'token' => 'test_token' }.to_msgpack,
31
headers: { 'Content-Type' => 'binary/message-pack' }
32
)
33
34
# Stub module search endpoint with realistic response
35
# Note: The MessagePackClient returns the unpacked response directly,
36
# so we need to return an array, not a hash with 'modules' key
37
search_stub = stub_request(:post, api_url)
38
.with(body: ['module.search', 'test_token', 'smb'].to_msgpack)
39
.to_return(
40
status: 200,
41
body: [
42
{
43
'name' => 'ms17_010_eternalblue',
44
'fullname' => 'exploit/windows/smb/ms17_010_eternalblue',
45
'type' => 'exploit',
46
'rank' => 'excellent',
47
'disclosuredate' => '2017-03-14',
48
'description' => 'MS17-010 EternalBlue SMB Remote Windows Kernel Pool Corruption'
49
},
50
{
51
'name' => 'smb_version',
52
'fullname' => 'auxiliary/scanner/smb/smb_version',
53
'type' => 'auxiliary',
54
'rank' => 'normal',
55
'description' => 'SMB Version Detection'
56
}
57
].to_msgpack,
58
headers: { 'Content-Type' => 'binary/message-pack' }
59
)
60
61
# Create rate limiter
62
limiter = Msf::MCP::Security::RateLimiter.new(requests_per_minute: 60, burst_size: 10)
63
64
# Create authenticated client
65
client = Msf::MCP::Metasploit::MessagePackClient.new(
66
host: host,
67
port: port,
68
endpoint: endpoint
69
)
70
client.authenticate(user, password)
71
72
# Create server context
73
server_context = {
74
msf_client: client,
75
rate_limiter: limiter
76
}
77
78
# Execute search through complete stack
79
result = Msf::MCP::Tools::SearchModules.call(
80
query: 'smb',
81
server_context: server_context
82
)
83
84
# Verify HTTP request was made
85
expect(search_stub).to have_been_requested.once
86
87
# Verify MCP response structure
88
expect(result).to be_a(MCP::Tool::Response)
89
expect(result.content).to be_an(Array)
90
expect(result.content.first[:type]).to eq('text')
91
92
# Verify data transformation occurred correctly
93
data = result.structured_content[:data]
94
expect(data).to be_an(Array)
95
expect(data.length).to eq(2)
96
expect(data.first[:fullname]).to eq('exploit/windows/smb/ms17_010_eternalblue')
97
expect(data.first[:type]).to eq('exploit')
98
99
# Verify metadata
100
metadata = result.structured_content[:metadata]
101
expect(metadata[:query]).to eq('smb')
102
expect(metadata[:total_items]).to eq(2)
103
expect(metadata[:returned_items]).to eq(2)
104
end
105
106
it 'handles empty search results through complete HTTP flow' do
107
# Stub authentication
108
stub_request(:post, api_url)
109
.with(body: ['auth.login', user, password].to_msgpack)
110
.to_return(
111
status: 200,
112
body: { 'result' => 'success', 'token' => 'test_token' }.to_msgpack,
113
headers: { 'Content-Type' => 'binary/message-pack' }
114
)
115
116
# Stub search with empty results
117
search_stub = stub_request(:post, api_url)
118
.with(body: ['module.search', 'test_token', 'nonexistent_module_xyz'].to_msgpack)
119
.to_return(
120
status: 200,
121
body: [].to_msgpack,
122
headers: { 'Content-Type' => 'binary/message-pack' }
123
)
124
125
limiter = Msf::MCP::Security::RateLimiter.new(requests_per_minute: 60, burst_size: 10)
126
client = Msf::MCP::Metasploit::MessagePackClient.new(
127
host: host,
128
port: port,
129
endpoint: endpoint
130
)
131
client.authenticate(user, password)
132
133
server_context = {
134
msf_client: client,
135
rate_limiter: limiter
136
}
137
138
# Execute search
139
result = Msf::MCP::Tools::SearchModules.call(
140
query: 'nonexistent_module_xyz',
141
server_context: server_context
142
)
143
144
# Verify HTTP request was made
145
expect(search_stub).to have_been_requested.once
146
147
# Verify empty results handled correctly
148
expect(result).to be_a(MCP::Tool::Response)
149
expect(result.structured_content[:data]).to eq([])
150
expect(result.structured_content[:metadata][:total_items]).to eq(0)
151
expect(result.structured_content[:metadata][:returned_items]).to eq(0)
152
end
153
154
it 'applies pagination correctly through HTTP request' do
155
# Stub authentication
156
stub_request(:post, api_url)
157
.with(body: ['auth.login', user, password].to_msgpack)
158
.to_return(
159
status: 200,
160
body: { 'result' => 'success', 'token' => 'test_token' }.to_msgpack,
161
headers: { 'Content-Type' => 'binary/message-pack' }
162
)
163
164
# Stub search with multiple results
165
search_stub = stub_request(:post, api_url)
166
.with(body: ['module.search', 'test_token', 'scanner'].to_msgpack)
167
.to_return(
168
status: 200,
169
body: [
170
{ 'fullname' => 'auxiliary/scanner/http/http_version', 'type' => 'auxiliary', 'name' => 'http_version' },
171
{ 'fullname' => 'auxiliary/scanner/smb/smb_version', 'type' => 'auxiliary', 'name' => 'smb_version' },
172
{ 'fullname' => 'auxiliary/scanner/ssh/ssh_version', 'type' => 'auxiliary', 'name' => 'ssh_version' },
173
{ 'fullname' => 'auxiliary/scanner/ftp/ftp_version', 'type' => 'auxiliary', 'name' => 'ftp_version' }
174
].to_msgpack,
175
headers: { 'Content-Type' => 'binary/message-pack' }
176
)
177
178
limiter = Msf::MCP::Security::RateLimiter.new(requests_per_minute: 60, burst_size: 10)
179
client = Msf::MCP::Metasploit::MessagePackClient.new(
180
host: host,
181
port: port,
182
endpoint: endpoint
183
)
184
client.authenticate(user, password)
185
186
server_context = {
187
msf_client: client,
188
rate_limiter: limiter
189
}
190
191
# Execute search with pagination
192
result = Msf::MCP::Tools::SearchModules.call(
193
query: 'scanner',
194
limit: 2,
195
offset: 1,
196
server_context: server_context
197
)
198
199
# Verify HTTP request was made
200
expect(search_stub).to have_been_requested.once
201
202
# Verify pagination applied correctly (offset=1, limit=2 means items 2 and 3)
203
expect(result).to be_a(MCP::Tool::Response)
204
data = result.structured_content[:data]
205
expect(data.length).to eq(2)
206
expect(data.first[:fullname]).to eq('auxiliary/scanner/smb/smb_version')
207
expect(data.last[:fullname]).to eq('auxiliary/scanner/ssh/ssh_version')
208
209
# Verify pagination metadata
210
metadata = result.structured_content[:metadata]
211
expect(metadata[:limit]).to eq(2)
212
expect(metadata[:offset]).to eq(1)
213
expect(metadata[:total_items]).to eq(4)
214
expect(metadata[:returned_items]).to eq(2)
215
end
216
end
217
end
218
219