Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/spec/integration/msfmcpd/tool_execution_db_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 - Database Queries' 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 'Database Query Integration with HTTP' do
24
it 'executes host query 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 db.hosts endpoint with realistic response
35
hosts_stub = stub_request(:post, api_url)
36
.with(body: ['db.hosts', 'test_token', { workspace: 'default' }].to_msgpack)
37
.to_return(
38
status: 200,
39
body: {
40
'hosts' => [
41
{
42
'address' => '192.168.1.100',
43
'mac' => '00:11:22:33:44:55',
44
'name' => 'server01',
45
'os_name' => 'Linux',
46
'os_flavor' => 'Ubuntu',
47
'state' => 'alive',
48
'created_at' => 1609459200,
49
'updated_at' => 1640995200
50
},
51
{
52
'address' => '192.168.1.101',
53
'mac' => '00:11:22:33:44:56',
54
'name' => 'server02',
55
'os_name' => 'Windows',
56
'os_flavor' => 'Server 2019',
57
'state' => 'alive',
58
'created_at' => 1609459300,
59
'updated_at' => 1640995300
60
}
61
]
62
}.to_msgpack,
63
headers: { 'Content-Type' => 'binary/message-pack' }
64
)
65
66
# Create rate limiter
67
limiter = Msf::MCP::Security::RateLimiter.new(requests_per_minute: 60, burst_size: 10)
68
69
# Create authenticated client
70
client = Msf::MCP::Metasploit::MessagePackClient.new(
71
host: host,
72
port: port,
73
endpoint: endpoint
74
)
75
client.authenticate(user, password)
76
77
# Create server context
78
server_context = {
79
msf_client: client,
80
rate_limiter: limiter
81
}
82
83
# Execute host query through complete stack
84
result = Msf::MCP::Tools::HostInfo.call(
85
workspace: 'default',
86
server_context: server_context
87
)
88
89
# Verify HTTP request was made
90
expect(hosts_stub).to have_been_requested.once
91
92
# Verify MCP response structure
93
expect(result).to be_a(MCP::Tool::Response)
94
expect(result.content).to be_an(Array)
95
expect(result.content.first[:type]).to eq('text')
96
97
# Verify data transformation occurred correctly
98
data = result.structured_content[:data]
99
expect(data).to be_an(Array)
100
expect(data.length).to eq(2)
101
expect(data.first[:address]).to eq('192.168.1.100')
102
expect(data.first[:hostname]).to eq('server01')
103
expect(data.first[:os_name]).to eq('Linux')
104
105
# Verify timestamps transformed to ISO 8601
106
expect(data.first[:created_at]).to eq('2021-01-01T00:00:00Z')
107
expect(data.first[:updated_at]).to eq('2022-01-01T00:00:00Z')
108
109
# Verify metadata
110
metadata = result.structured_content[:metadata]
111
expect(metadata[:workspace]).to eq('default')
112
expect(metadata[:total_items]).to eq(2)
113
expect(metadata[:returned_items]).to eq(2)
114
end
115
116
it 'applies filters correctly through HTTP request' do
117
# Stub authentication
118
stub_request(:post, api_url)
119
.with(body: ['auth.login', user, password].to_msgpack)
120
.to_return(
121
status: 200,
122
body: { 'result' => 'success', 'token' => 'test_token' }.to_msgpack,
123
headers: { 'Content-Type' => 'binary/message-pack' }
124
)
125
126
# Stub db.hosts with filters
127
hosts_stub = stub_request(:post, api_url)
128
.with(body: ['db.hosts', 'test_token', { workspace: 'default', addresses: '192.168.1.0/24', only_up: true }].to_msgpack)
129
.to_return(
130
status: 200,
131
body: {
132
'hosts' => [
133
{
134
'address' => '192.168.1.100',
135
'mac' => '00:11:22:33:44:55',
136
'name' => 'filtered_host',
137
'state' => 'alive',
138
'created_at' => 1609459200
139
}
140
]
141
}.to_msgpack,
142
headers: { 'Content-Type' => 'binary/message-pack' }
143
)
144
145
limiter = Msf::MCP::Security::RateLimiter.new(requests_per_minute: 60, burst_size: 10)
146
client = Msf::MCP::Metasploit::MessagePackClient.new(
147
host: host,
148
port: port,
149
endpoint: endpoint
150
)
151
client.authenticate(user, password)
152
153
server_context = {
154
msf_client: client,
155
rate_limiter: limiter
156
}
157
158
# Execute query with filters
159
result = Msf::MCP::Tools::HostInfo.call(
160
workspace: 'default',
161
addresses: '192.168.1.0/24',
162
only_up: true,
163
server_context: server_context
164
)
165
166
# Verify HTTP request with filters was made
167
expect(hosts_stub).to have_been_requested.once
168
169
# Verify filtered results
170
expect(result).to be_a(MCP::Tool::Response)
171
data = result.structured_content[:data]
172
expect(data.length).to eq(1)
173
expect(data.first[:address]).to eq('192.168.1.100')
174
end
175
176
it 'executes service query with multiple filters through HTTP' do
177
# Stub authentication
178
stub_request(:post, api_url)
179
.with(body: ['auth.login', user, password].to_msgpack)
180
.to_return(
181
status: 200,
182
body: { 'result' => 'success', 'token' => 'test_token' }.to_msgpack,
183
headers: { 'Content-Type' => 'binary/message-pack' }
184
)
185
186
# Stub db.services with filters
187
# Note: MessagePack hash key order may vary, so we match any request to db.services
188
services_stub = stub_request(:post, api_url)
189
.with { |request|
190
body = MessagePack.unpack(request.body)
191
body[0] == 'db.services' && body[1] == 'test_token' &&
192
body[2].is_a?(Hash) && body[2]['workspace'] == 'default'
193
}
194
.to_return(
195
status: 200,
196
body: {
197
'services' => [
198
{
199
'host' => '192.168.1.100',
200
'port' => 445,
201
'proto' => 'tcp',
202
'name' => 'microsoft-ds',
203
'state' => 'open',
204
'created_at' => 1609459200,
205
'updated_at' => 1640995200
206
}
207
]
208
}.to_msgpack,
209
headers: { 'Content-Type' => 'binary/message-pack' }
210
)
211
212
limiter = Msf::MCP::Security::RateLimiter.new(requests_per_minute: 60, burst_size: 10)
213
client = Msf::MCP::Metasploit::MessagePackClient.new(
214
host: host,
215
port: port,
216
endpoint: endpoint
217
)
218
client.authenticate(user, password)
219
220
server_context = {
221
msf_client: client,
222
rate_limiter: limiter
223
}
224
225
# Execute service query with multiple filters
226
result = Msf::MCP::Tools::ServiceInfo.call(
227
workspace: 'default',
228
host: '192.168.1.100',
229
ports: '445',
230
protocol: 'tcp',
231
server_context: server_context
232
)
233
234
# Verify HTTP request with all filters was made
235
expect(services_stub).to have_been_requested.once
236
237
# Verify results
238
expect(result).to be_a(MCP::Tool::Response)
239
data = result.structured_content[:data]
240
expect(data.length).to eq(1)
241
expect(data.first[:host_address]).to eq('192.168.1.100')
242
expect(data.first[:port]).to eq(445)
243
expect(data.first[:protocol]).to eq('tcp')
244
expect(data.first[:name]).to eq('microsoft-ds')
245
end
246
247
it 'handles pagination correctly across HTTP boundary' do
248
# Stub authentication
249
stub_request(:post, api_url)
250
.with(body: ['auth.login', user, password].to_msgpack)
251
.to_return(
252
status: 200,
253
body: { 'result' => 'success', 'token' => 'test_token' }.to_msgpack,
254
headers: { 'Content-Type' => 'binary/message-pack' }
255
)
256
257
# Stub db.hosts with multiple results
258
hosts_stub = stub_request(:post, api_url)
259
.with(body: ['db.hosts', 'test_token', { workspace: 'default' }].to_msgpack)
260
.to_return(
261
status: 200,
262
body: {
263
'hosts' => [
264
{ 'address' => '192.168.1.1', 'name' => 'host1', 'state' => 'alive', 'created_at' => 1609459200 },
265
{ 'address' => '192.168.1.2', 'name' => 'host2', 'state' => 'alive', 'created_at' => 1609459300 },
266
{ 'address' => '192.168.1.3', 'name' => 'host3', 'state' => 'alive', 'created_at' => 1609459400 },
267
{ 'address' => '192.168.1.4', 'name' => 'host4', 'state' => 'alive', 'created_at' => 1609459500 },
268
{ 'address' => '192.168.1.5', 'name' => 'host5', 'state' => 'alive', 'created_at' => 1609459600 }
269
]
270
}.to_msgpack,
271
headers: { 'Content-Type' => 'binary/message-pack' }
272
)
273
274
limiter = Msf::MCP::Security::RateLimiter.new(requests_per_minute: 60, burst_size: 10)
275
client = Msf::MCP::Metasploit::MessagePackClient.new(
276
host: host,
277
port: port,
278
endpoint: endpoint
279
)
280
client.authenticate(user, password)
281
282
server_context = {
283
msf_client: client,
284
rate_limiter: limiter
285
}
286
287
# Execute query with pagination (offset=1, limit=2 means items 2 and 3)
288
result = Msf::MCP::Tools::HostInfo.call(
289
workspace: 'default',
290
limit: 2,
291
offset: 1,
292
server_context: server_context
293
)
294
295
# Verify HTTP request was made
296
expect(hosts_stub).to have_been_requested.once
297
298
# Verify pagination applied correctly
299
expect(result).to be_a(MCP::Tool::Response)
300
data = result.structured_content[:data]
301
expect(data.length).to eq(2)
302
expect(data.first[:address]).to eq('192.168.1.2')
303
expect(data.last[:address]).to eq('192.168.1.3')
304
305
# Verify pagination metadata
306
metadata = result.structured_content[:metadata]
307
expect(metadata[:limit]).to eq(2)
308
expect(metadata[:offset]).to eq(1)
309
expect(metadata[:total_items]).to eq(5)
310
expect(metadata[:returned_items]).to eq(2)
311
end
312
end
313
end
314
315