@burtthecoder/mcp-shodan 1.0.16 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/build/helpers.js +69 -0
- package/build/index.js +19 -640
- package/build/tools/cpe-lookup.js +49 -0
- package/build/tools/cve-lookup.js +63 -0
- package/build/tools/cves-by-product.js +141 -0
- package/build/tools/dns-lookup.js +34 -0
- package/build/tools/ip-lookup.js +60 -0
- package/build/tools/reverse-dns-lookup.js +35 -0
- package/build/tools/shodan-search.js +71 -0
- package/build/types.js +1 -0
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -181,7 +181,7 @@ npm run build
|
|
|
181
181
|
|
|
182
182
|
## Requirements
|
|
183
183
|
|
|
184
|
-
- Node.js (
|
|
184
|
+
- Node.js (v20 or later)
|
|
185
185
|
- A valid [Shodan API Key](https://account.shodan.io/)
|
|
186
186
|
|
|
187
187
|
## Troubleshooting
|
|
@@ -235,9 +235,15 @@ If you see module loading errors:
|
|
|
235
235
|
|
|
236
236
|
## Development
|
|
237
237
|
|
|
238
|
-
|
|
238
|
+
Build the project:
|
|
239
239
|
```bash
|
|
240
|
-
npm
|
|
240
|
+
npm install
|
|
241
|
+
npm run build
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Test interactively with FastMCP's built-in dev tool:
|
|
245
|
+
```bash
|
|
246
|
+
npx fastmcp dev build/index.js
|
|
241
247
|
```
|
|
242
248
|
|
|
243
249
|
## Error Handling
|
|
@@ -254,6 +260,7 @@ The server includes comprehensive error handling for:
|
|
|
254
260
|
|
|
255
261
|
## Version History
|
|
256
262
|
|
|
263
|
+
- v1.1.0: Migrated from raw `@modelcontextprotocol/sdk` to [FastMCP](https://github.com/punkpeye/fastmcp) — modular tool files, automatic schema validation, simplified error handling
|
|
257
264
|
- v1.0.12: Added reverse DNS lookup and improved output formatting
|
|
258
265
|
- v1.0.7: Added CVEs by Product search functionality and renamed vulnerabilities tool to cve_lookup
|
|
259
266
|
- v1.0.6: Added CVEDB integration for enhanced CVE lookups and CPE search functionality
|
package/build/helpers.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { UserError } from "fastmcp";
|
|
3
|
+
export const API_BASE_URL = "https://api.shodan.io";
|
|
4
|
+
export const CVEDB_API_URL = "https://cvedb.shodan.io";
|
|
5
|
+
export const SHODAN_API_KEY = process.env.SHODAN_API_KEY;
|
|
6
|
+
export async function queryShodan(endpoint, params) {
|
|
7
|
+
try {
|
|
8
|
+
const response = await axios.get(`${API_BASE_URL}${endpoint}`, {
|
|
9
|
+
params: { ...params, key: SHODAN_API_KEY },
|
|
10
|
+
timeout: 10000,
|
|
11
|
+
});
|
|
12
|
+
return response.data;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
const errorMessage = error.response?.data?.error || error.message;
|
|
16
|
+
console.error(`Shodan API error: ${errorMessage}`);
|
|
17
|
+
throw new UserError(`Shodan API error: ${errorMessage}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function queryCVEDB(cveId) {
|
|
21
|
+
try {
|
|
22
|
+
const response = await axios.get(`${CVEDB_API_URL}/cve/${cveId}`);
|
|
23
|
+
return response.data;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.response?.status === 422) {
|
|
27
|
+
throw new UserError(`Invalid CVE ID format: ${cveId}`);
|
|
28
|
+
}
|
|
29
|
+
if (error.response?.status === 404) {
|
|
30
|
+
throw new UserError(`CVE not found: ${cveId}`);
|
|
31
|
+
}
|
|
32
|
+
throw new UserError(`CVEDB API error: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function queryCPEDB(params) {
|
|
36
|
+
try {
|
|
37
|
+
const response = await axios.get(`${CVEDB_API_URL}/cpes`, { params });
|
|
38
|
+
return response.data;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.response?.status === 422) {
|
|
42
|
+
throw new UserError(`Invalid parameters: ${error.response.data?.detail || error.message}`);
|
|
43
|
+
}
|
|
44
|
+
throw new UserError(`CVEDB API error: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function queryCVEsByProduct(params) {
|
|
48
|
+
try {
|
|
49
|
+
const response = await axios.get(`${CVEDB_API_URL}/cves`, { params });
|
|
50
|
+
return response.data;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.response?.status === 422) {
|
|
54
|
+
throw new UserError(`Invalid parameters: ${error.response.data?.detail || error.message}`);
|
|
55
|
+
}
|
|
56
|
+
throw new UserError(`CVEDB API error: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function getCvssSeverity(score) {
|
|
60
|
+
if (score >= 9.0)
|
|
61
|
+
return "Critical";
|
|
62
|
+
if (score >= 7.0)
|
|
63
|
+
return "High";
|
|
64
|
+
if (score >= 4.0)
|
|
65
|
+
return "Medium";
|
|
66
|
+
if (score >= 0.1)
|
|
67
|
+
return "Low";
|
|
68
|
+
return "None";
|
|
69
|
+
}
|
package/build/index.js
CHANGED
|
@@ -1,160 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, InitializeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import axios from "axios";
|
|
2
|
+
import { FastMCP } from "fastmcp";
|
|
6
3
|
import dotenv from "dotenv";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
4
|
+
import { registerIpLookup } from "./tools/ip-lookup.js";
|
|
5
|
+
import { registerShodanSearch } from "./tools/shodan-search.js";
|
|
6
|
+
import { registerCveLookup } from "./tools/cve-lookup.js";
|
|
7
|
+
import { registerDnsLookup } from "./tools/dns-lookup.js";
|
|
8
|
+
import { registerReverseDnsLookup } from "./tools/reverse-dns-lookup.js";
|
|
9
|
+
import { registerCpeLookup } from "./tools/cpe-lookup.js";
|
|
10
|
+
import { registerCvesByProduct } from "./tools/cves-by-product.js";
|
|
12
11
|
dotenv.config();
|
|
13
|
-
|
|
14
|
-
const SHODAN_API_KEY = process.env.SHODAN_API_KEY;
|
|
15
|
-
if (!SHODAN_API_KEY) {
|
|
12
|
+
if (!process.env.SHODAN_API_KEY) {
|
|
16
13
|
throw new Error("SHODAN_API_KEY environment variable is required.");
|
|
17
14
|
}
|
|
18
|
-
const
|
|
19
|
-
const CVEDB_API_URL = "https://cvedb.shodan.io";
|
|
20
|
-
// Logging Helper Function
|
|
21
|
-
function logToFile(message) {
|
|
22
|
-
try {
|
|
23
|
-
const timestamp = new Date().toISOString();
|
|
24
|
-
const formattedMessage = `[${timestamp}] ${message}\n`;
|
|
25
|
-
fs.appendFileSync(logFilePath, formattedMessage, "utf8");
|
|
26
|
-
console.error(formattedMessage.trim()); // Use stderr for logging to avoid interfering with stdout
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
console.error(`Failed to write to log file: ${error}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
// Tool Schemas
|
|
33
|
-
const IpLookupArgsSchema = z.object({
|
|
34
|
-
ip: z.string().describe("The IP address to query."),
|
|
35
|
-
});
|
|
36
|
-
const ShodanSearchArgsSchema = z.object({
|
|
37
|
-
query: z.string().describe("Search query for Shodan."),
|
|
38
|
-
max_results: z
|
|
39
|
-
.number()
|
|
40
|
-
.optional()
|
|
41
|
-
.default(10)
|
|
42
|
-
.describe("Maximum results to return."),
|
|
43
|
-
});
|
|
44
|
-
const CVELookupArgsSchema = z.object({
|
|
45
|
-
cve: z.string()
|
|
46
|
-
.regex(/^CVE-\d{4}-\d{4,}$/i, "Must be a valid CVE ID format (e.g., CVE-2021-44228)")
|
|
47
|
-
.describe("The CVE identifier to query (format: CVE-YYYY-NNNNN)."),
|
|
48
|
-
});
|
|
49
|
-
const DnsLookupArgsSchema = z.object({
|
|
50
|
-
hostnames: z.array(z.string()).describe("List of hostnames to resolve."),
|
|
51
|
-
});
|
|
52
|
-
const ReverseDnsLookupArgsSchema = z.object({
|
|
53
|
-
ips: z.array(z.string()).describe("List of IP addresses to perform reverse DNS lookup on."),
|
|
54
|
-
});
|
|
55
|
-
const CpeLookupArgsSchema = z.object({
|
|
56
|
-
product: z.string().describe("The name of the product to search for CPEs."),
|
|
57
|
-
count: z.boolean().optional().default(false).describe("If true, returns only the count of matching CPEs."),
|
|
58
|
-
skip: z.number().optional().default(0).describe("Number of CPEs to skip (for pagination)."),
|
|
59
|
-
limit: z.number().optional().default(1000).describe("Maximum number of CPEs to return (max 1000)."),
|
|
60
|
-
});
|
|
61
|
-
const CVEsByProductArgsSchema = z.object({
|
|
62
|
-
cpe23: z.string().optional().describe("The CPE version 2.3 identifier (format: cpe:2.3:part:vendor:product:version)."),
|
|
63
|
-
product: z.string().optional().describe("The name of the product to search for CVEs."),
|
|
64
|
-
count: z.boolean().optional().default(false).describe("If true, returns only the count of matching CVEs."),
|
|
65
|
-
is_kev: z.boolean().optional().default(false).describe("If true, returns only CVEs with the KEV flag set."),
|
|
66
|
-
sort_by_epss: z.boolean().optional().default(false).describe("If true, sorts CVEs by EPSS score in descending order."),
|
|
67
|
-
skip: z.number().optional().default(0).describe("Number of CVEs to skip (for pagination)."),
|
|
68
|
-
limit: z.number().optional().default(1000).describe("Maximum number of CVEs to return (max 1000)."),
|
|
69
|
-
start_date: z.string().optional().describe("Start date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS)."),
|
|
70
|
-
end_date: z.string().optional().describe("End date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS).")
|
|
71
|
-
}).refine(data => !(data.cpe23 && data.product), { message: "Cannot specify both cpe23 and product. Use only one." }).refine(data => data.cpe23 || data.product, { message: "Must specify either cpe23 or product." });
|
|
72
|
-
// Helper Function to Query Shodan API
|
|
73
|
-
async function queryShodan(endpoint, params) {
|
|
74
|
-
try {
|
|
75
|
-
const response = await axios.get(`${API_BASE_URL}${endpoint}`, {
|
|
76
|
-
params: { ...params, key: SHODAN_API_KEY },
|
|
77
|
-
timeout: 10000,
|
|
78
|
-
});
|
|
79
|
-
return response.data;
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
const errorMessage = error.response?.data?.error || error.message;
|
|
83
|
-
logToFile(`Shodan API error: ${errorMessage}`);
|
|
84
|
-
throw new Error(`Shodan API error: ${errorMessage}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// Helper Function for CVE lookups using CVEDB
|
|
88
|
-
async function queryCVEDB(cveId) {
|
|
89
|
-
try {
|
|
90
|
-
logToFile(`Querying CVEDB for: ${cveId}`);
|
|
91
|
-
const response = await axios.get(`${CVEDB_API_URL}/cve/${cveId}`);
|
|
92
|
-
return response.data;
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
if (error.response?.status === 422) {
|
|
96
|
-
throw new Error(`Invalid CVE ID format: ${cveId}`);
|
|
97
|
-
}
|
|
98
|
-
if (error.response?.status === 404) {
|
|
99
|
-
throw new Error(`CVE not found: ${cveId}`);
|
|
100
|
-
}
|
|
101
|
-
throw new Error(`CVEDB API error: ${error.message}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
// Helper Function for CPE lookups using CVEDB
|
|
105
|
-
async function queryCPEDB(params) {
|
|
106
|
-
try {
|
|
107
|
-
logToFile(`Querying CVEDB for CPEs with params: ${JSON.stringify(params)}`);
|
|
108
|
-
const response = await axios.get(`${CVEDB_API_URL}/cpes`, { params });
|
|
109
|
-
return response.data;
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
if (error.response?.status === 422) {
|
|
113
|
-
throw new Error(`Invalid parameters: ${error.response.data?.detail || error.message}`);
|
|
114
|
-
}
|
|
115
|
-
throw new Error(`CVEDB API error: ${error.message}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
// Helper Function for CVEs by product/CPE lookups using CVEDB
|
|
119
|
-
async function queryCVEsByProduct(params) {
|
|
120
|
-
try {
|
|
121
|
-
logToFile(`Querying CVEDB for CVEs with params: ${JSON.stringify(params)}`);
|
|
122
|
-
const response = await axios.get(`${CVEDB_API_URL}/cves`, { params });
|
|
123
|
-
return response.data;
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
if (error.response?.status === 422) {
|
|
127
|
-
throw new Error(`Invalid parameters: ${error.response.data?.detail || error.message}`);
|
|
128
|
-
}
|
|
129
|
-
throw new Error(`CVEDB API error: ${error.message}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
// Server Setup
|
|
133
|
-
const server = new Server({
|
|
15
|
+
const server = new FastMCP({
|
|
134
16
|
name: "shodan-mcp",
|
|
135
17
|
version: "1.0.0",
|
|
136
|
-
|
|
137
|
-
capabilities: {
|
|
138
|
-
tools: {
|
|
139
|
-
listChanged: true,
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
// Handle Initialization
|
|
144
|
-
server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
|
145
|
-
logToFile("Received initialize request.");
|
|
146
|
-
return {
|
|
147
|
-
protocolVersion: "2024-11-05",
|
|
148
|
-
capabilities: {
|
|
149
|
-
tools: {
|
|
150
|
-
listChanged: true,
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
serverInfo: {
|
|
154
|
-
name: "shodan-mcp",
|
|
155
|
-
version: "1.0.0",
|
|
156
|
-
},
|
|
157
|
-
instructions: `This MCP server provides comprehensive access to Shodan's network intelligence and security services:
|
|
18
|
+
instructions: `This MCP server provides comprehensive access to Shodan's network intelligence and security services:
|
|
158
19
|
|
|
159
20
|
- Network Reconnaissance: Query detailed information about IP addresses, including open ports, services, and vulnerabilities
|
|
160
21
|
- DNS Operations: Forward and reverse DNS lookups for domains and IP addresses
|
|
@@ -162,494 +23,12 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
|
|
162
23
|
- Device Discovery: Search Shodan's database of internet-connected devices with advanced filtering
|
|
163
24
|
|
|
164
25
|
Each tool provides structured, formatted output for easy analysis and integration.`,
|
|
165
|
-
};
|
|
166
|
-
});
|
|
167
|
-
// Register Tools
|
|
168
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
169
|
-
const tools = [
|
|
170
|
-
{
|
|
171
|
-
name: "ip_lookup",
|
|
172
|
-
description: "Retrieve comprehensive information about an IP address, including geolocation, open ports, running services, SSL certificates, hostnames, and cloud provider details if available. Returns service banners and HTTP server information when present.",
|
|
173
|
-
inputSchema: zodToJsonSchema(IpLookupArgsSchema),
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
name: "shodan_search",
|
|
177
|
-
description: "Search Shodan's database of internet-connected devices. Returns detailed information about matching devices including services, vulnerabilities, and geographic distribution. Supports advanced search filters and returns country-based statistics.",
|
|
178
|
-
inputSchema: zodToJsonSchema(ShodanSearchArgsSchema),
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
name: "cve_lookup",
|
|
182
|
-
description: "Query detailed vulnerability information from Shodan's CVEDB. Returns comprehensive CVE details including CVSS scores (v2/v3), EPSS probability and ranking, KEV status, proposed mitigations, ransomware associations, and affected products (CPEs).",
|
|
183
|
-
inputSchema: zodToJsonSchema(CVELookupArgsSchema),
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
name: "dns_lookup",
|
|
187
|
-
description: "Resolve domain names to IP addresses using Shodan's DNS service. Supports batch resolution of multiple hostnames in a single query. Returns IP addresses mapped to their corresponding hostnames.",
|
|
188
|
-
inputSchema: zodToJsonSchema(DnsLookupArgsSchema),
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
name: "cpe_lookup",
|
|
192
|
-
description: "Search for Common Platform Enumeration (CPE) entries by product name in Shodan's CVEDB. Supports pagination and can return either full CPE details or just the total count. Useful for identifying specific versions and configurations of software and hardware.",
|
|
193
|
-
inputSchema: zodToJsonSchema(CpeLookupArgsSchema),
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
name: "cves_by_product",
|
|
197
|
-
description: "Search for vulnerabilities affecting specific products or CPEs. Supports filtering by KEV status, sorting by EPSS score, date ranges, and pagination. Can search by product name or CPE 2.3 identifier. Returns detailed vulnerability information including severity scores and impact assessments.",
|
|
198
|
-
inputSchema: zodToJsonSchema(CVEsByProductArgsSchema),
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
name: "reverse_dns_lookup",
|
|
202
|
-
description: "Perform reverse DNS lookups to find hostnames associated with IP addresses. Supports batch lookups of multiple IP addresses in a single query. Returns all known hostnames for each IP address, with clear indication when no hostnames are found.",
|
|
203
|
-
inputSchema: zodToJsonSchema(ReverseDnsLookupArgsSchema),
|
|
204
|
-
},
|
|
205
|
-
];
|
|
206
|
-
logToFile("Registered tools.");
|
|
207
|
-
return { tools };
|
|
208
|
-
});
|
|
209
|
-
// Handle Tool Calls
|
|
210
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
211
|
-
logToFile(`Tool called: ${request.params.name}`);
|
|
212
|
-
try {
|
|
213
|
-
const { name, arguments: args } = request.params;
|
|
214
|
-
switch (name) {
|
|
215
|
-
case "ip_lookup": {
|
|
216
|
-
const parsedIpArgs = IpLookupArgsSchema.safeParse(args);
|
|
217
|
-
if (!parsedIpArgs.success) {
|
|
218
|
-
throw new Error("Invalid ip_lookup arguments");
|
|
219
|
-
}
|
|
220
|
-
const result = await queryShodan(`/shodan/host/${parsedIpArgs.data.ip}`, {});
|
|
221
|
-
// Format the response in a user-friendly way
|
|
222
|
-
const formattedResult = {
|
|
223
|
-
"IP Information": {
|
|
224
|
-
"IP Address": result.ip_str,
|
|
225
|
-
"Organization": result.org,
|
|
226
|
-
"ISP": result.isp,
|
|
227
|
-
"ASN": result.asn,
|
|
228
|
-
"Last Update": result.last_update
|
|
229
|
-
},
|
|
230
|
-
"Location": {
|
|
231
|
-
"Country": result.country_name,
|
|
232
|
-
"City": result.city,
|
|
233
|
-
"Coordinates": `${result.latitude}, ${result.longitude}`,
|
|
234
|
-
"Region": result.region_code
|
|
235
|
-
},
|
|
236
|
-
"Services": result.ports.map((port) => {
|
|
237
|
-
const service = result.data.find((d) => d.port === port);
|
|
238
|
-
return {
|
|
239
|
-
"Port": port,
|
|
240
|
-
"Protocol": service?.transport || "unknown",
|
|
241
|
-
"Service": service?.data?.trim() || "No banner",
|
|
242
|
-
...(service?.http ? {
|
|
243
|
-
"HTTP": {
|
|
244
|
-
"Server": service.http.server,
|
|
245
|
-
"Title": service.http.title,
|
|
246
|
-
}
|
|
247
|
-
} : {})
|
|
248
|
-
};
|
|
249
|
-
}),
|
|
250
|
-
"Cloud Provider": result.data[0]?.cloud ? {
|
|
251
|
-
"Provider": result.data[0].cloud.provider,
|
|
252
|
-
"Service": result.data[0].cloud.service,
|
|
253
|
-
"Region": result.data[0].cloud.region
|
|
254
|
-
} : "Not detected",
|
|
255
|
-
"Hostnames": result.hostnames || [],
|
|
256
|
-
"Domains": result.domains || [],
|
|
257
|
-
"Tags": result.tags || []
|
|
258
|
-
};
|
|
259
|
-
return {
|
|
260
|
-
content: [
|
|
261
|
-
{
|
|
262
|
-
type: "text",
|
|
263
|
-
text: JSON.stringify(formattedResult, null, 2),
|
|
264
|
-
},
|
|
265
|
-
],
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
case "shodan_search": {
|
|
269
|
-
const parsedSearchArgs = ShodanSearchArgsSchema.safeParse(args);
|
|
270
|
-
if (!parsedSearchArgs.success) {
|
|
271
|
-
throw new Error("Invalid search arguments");
|
|
272
|
-
}
|
|
273
|
-
const result = await queryShodan("/shodan/host/search", {
|
|
274
|
-
query: parsedSearchArgs.data.query,
|
|
275
|
-
limit: parsedSearchArgs.data.max_results,
|
|
276
|
-
});
|
|
277
|
-
// Format the response in a user-friendly way
|
|
278
|
-
const formattedResult = {
|
|
279
|
-
"Search Summary": {
|
|
280
|
-
"Query": parsedSearchArgs.data.query,
|
|
281
|
-
"Total Results": result.total,
|
|
282
|
-
"Results Returned": result.matches.length
|
|
283
|
-
},
|
|
284
|
-
"Country Distribution": result.facets?.country?.map(country => ({
|
|
285
|
-
"Country": country.value,
|
|
286
|
-
"Count": country.count,
|
|
287
|
-
"Percentage": `${((country.count / result.total) * 100).toFixed(2)}%`
|
|
288
|
-
})) || [],
|
|
289
|
-
"Matches": result.matches.map(match => ({
|
|
290
|
-
"Basic Information": {
|
|
291
|
-
"IP Address": match.ip_str,
|
|
292
|
-
"Organization": match.org,
|
|
293
|
-
"ISP": match.isp,
|
|
294
|
-
"ASN": match.asn,
|
|
295
|
-
"Last Update": match.timestamp
|
|
296
|
-
},
|
|
297
|
-
"Location": {
|
|
298
|
-
"Country": match.location.country_name,
|
|
299
|
-
"City": match.location.city || "Unknown",
|
|
300
|
-
"Region": match.location.region_code || "Unknown",
|
|
301
|
-
"Coordinates": `${match.location.latitude}, ${match.location.longitude}`
|
|
302
|
-
},
|
|
303
|
-
"Service Details": {
|
|
304
|
-
"Port": match.port,
|
|
305
|
-
"Transport": match.transport,
|
|
306
|
-
"Product": match.product || "Unknown",
|
|
307
|
-
"Version": match.version || "Unknown",
|
|
308
|
-
"CPE": match.cpe || []
|
|
309
|
-
},
|
|
310
|
-
"Web Information": match.http ? {
|
|
311
|
-
"Server": match.http.server,
|
|
312
|
-
"Title": match.http.title,
|
|
313
|
-
"Robots.txt": match.http.robots ? "Present" : "Not found",
|
|
314
|
-
"Sitemap": match.http.sitemap ? "Present" : "Not found"
|
|
315
|
-
} : "No HTTP information",
|
|
316
|
-
"Hostnames": match.hostnames,
|
|
317
|
-
"Domains": match.domains
|
|
318
|
-
}))
|
|
319
|
-
};
|
|
320
|
-
return {
|
|
321
|
-
content: [
|
|
322
|
-
{
|
|
323
|
-
type: "text",
|
|
324
|
-
text: JSON.stringify(formattedResult, null, 2),
|
|
325
|
-
},
|
|
326
|
-
],
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
case "cve_lookup": {
|
|
330
|
-
const parsedCveArgs = CVELookupArgsSchema.safeParse(args);
|
|
331
|
-
if (!parsedCveArgs.success) {
|
|
332
|
-
throw new Error("Invalid CVE format. Please use format: CVE-YYYY-NNNNN (e.g., CVE-2021-44228)");
|
|
333
|
-
}
|
|
334
|
-
const cveId = parsedCveArgs.data.cve.toUpperCase();
|
|
335
|
-
logToFile(`Looking up CVE: ${cveId}`);
|
|
336
|
-
try {
|
|
337
|
-
const result = await queryCVEDB(cveId);
|
|
338
|
-
// Helper function to format CVSS score severity
|
|
339
|
-
const getCvssSeverity = (score) => {
|
|
340
|
-
if (score >= 9.0)
|
|
341
|
-
return "Critical";
|
|
342
|
-
if (score >= 7.0)
|
|
343
|
-
return "High";
|
|
344
|
-
if (score >= 4.0)
|
|
345
|
-
return "Medium";
|
|
346
|
-
if (score >= 0.1)
|
|
347
|
-
return "Low";
|
|
348
|
-
return "None";
|
|
349
|
-
};
|
|
350
|
-
// Format the response in a user-friendly way
|
|
351
|
-
const formattedResult = {
|
|
352
|
-
"Basic Information": {
|
|
353
|
-
"CVE ID": result.cve_id,
|
|
354
|
-
"Published": new Date(result.published_time).toLocaleString(),
|
|
355
|
-
"Summary": result.summary
|
|
356
|
-
},
|
|
357
|
-
"Severity Scores": {
|
|
358
|
-
"CVSS v3": result.cvss_v3 ? {
|
|
359
|
-
"Score": result.cvss_v3,
|
|
360
|
-
"Severity": getCvssSeverity(result.cvss_v3)
|
|
361
|
-
} : "Not available",
|
|
362
|
-
"CVSS v2": result.cvss_v2 ? {
|
|
363
|
-
"Score": result.cvss_v2,
|
|
364
|
-
"Severity": getCvssSeverity(result.cvss_v2)
|
|
365
|
-
} : "Not available",
|
|
366
|
-
"EPSS": result.epss ? {
|
|
367
|
-
"Score": `${(result.epss * 100).toFixed(2)}%`,
|
|
368
|
-
"Ranking": `Top ${(result.ranking_epss * 100).toFixed(2)}%`
|
|
369
|
-
} : "Not available"
|
|
370
|
-
},
|
|
371
|
-
"Impact Assessment": {
|
|
372
|
-
"Known Exploited Vulnerability": result.kev ? "Yes" : "No",
|
|
373
|
-
"Proposed Action": result.propose_action || "No specific action proposed",
|
|
374
|
-
"Ransomware Campaign": result.ransomware_campaign || "No known ransomware campaigns"
|
|
375
|
-
},
|
|
376
|
-
"Affected Products": result.cpes?.length > 0 ? result.cpes : ["No specific products listed"],
|
|
377
|
-
"Additional Information": {
|
|
378
|
-
"References": result.references?.length > 0 ? result.references : ["No references provided"]
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
|
-
return {
|
|
382
|
-
content: [
|
|
383
|
-
{
|
|
384
|
-
type: "text",
|
|
385
|
-
text: JSON.stringify(formattedResult, null, 2),
|
|
386
|
-
},
|
|
387
|
-
],
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
catch (error) {
|
|
391
|
-
return {
|
|
392
|
-
content: [
|
|
393
|
-
{
|
|
394
|
-
type: "text",
|
|
395
|
-
text: error.message,
|
|
396
|
-
},
|
|
397
|
-
],
|
|
398
|
-
isError: true,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
case "dns_lookup": {
|
|
403
|
-
const parsedDnsArgs = DnsLookupArgsSchema.safeParse(args);
|
|
404
|
-
if (!parsedDnsArgs.success) {
|
|
405
|
-
throw new Error("Invalid dns_lookup arguments");
|
|
406
|
-
}
|
|
407
|
-
// Join hostnames with commas for the API request
|
|
408
|
-
const hostnamesString = parsedDnsArgs.data.hostnames.join(",");
|
|
409
|
-
const result = await queryShodan("/dns/resolve", {
|
|
410
|
-
hostnames: hostnamesString
|
|
411
|
-
});
|
|
412
|
-
// Format the response in a user-friendly way
|
|
413
|
-
const formattedResult = {
|
|
414
|
-
"DNS Resolutions": Object.entries(result).map(([hostname, ip]) => ({
|
|
415
|
-
"Hostname": hostname,
|
|
416
|
-
"IP Address": ip
|
|
417
|
-
})),
|
|
418
|
-
"Summary": {
|
|
419
|
-
"Total Lookups": Object.keys(result).length,
|
|
420
|
-
"Queried Hostnames": parsedDnsArgs.data.hostnames
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
return {
|
|
424
|
-
content: [
|
|
425
|
-
{
|
|
426
|
-
type: "text",
|
|
427
|
-
text: JSON.stringify(formattedResult, null, 2)
|
|
428
|
-
},
|
|
429
|
-
],
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
case "cpe_lookup": {
|
|
433
|
-
const parsedCpeArgs = CpeLookupArgsSchema.safeParse(args);
|
|
434
|
-
if (!parsedCpeArgs.success) {
|
|
435
|
-
throw new Error("Invalid cpe_lookup arguments");
|
|
436
|
-
}
|
|
437
|
-
try {
|
|
438
|
-
const result = await queryCPEDB({
|
|
439
|
-
product: parsedCpeArgs.data.product,
|
|
440
|
-
count: parsedCpeArgs.data.count,
|
|
441
|
-
skip: parsedCpeArgs.data.skip,
|
|
442
|
-
limit: parsedCpeArgs.data.limit
|
|
443
|
-
});
|
|
444
|
-
// Format the response based on whether it's a count request or full CPE list
|
|
445
|
-
const formattedResult = parsedCpeArgs.data.count
|
|
446
|
-
? { total_cpes: result.total }
|
|
447
|
-
: {
|
|
448
|
-
cpes: result.cpes,
|
|
449
|
-
skip: parsedCpeArgs.data.skip,
|
|
450
|
-
limit: parsedCpeArgs.data.limit,
|
|
451
|
-
total_returned: result.cpes.length
|
|
452
|
-
};
|
|
453
|
-
return {
|
|
454
|
-
content: [
|
|
455
|
-
{
|
|
456
|
-
type: "text",
|
|
457
|
-
text: JSON.stringify(formattedResult, null, 2),
|
|
458
|
-
},
|
|
459
|
-
],
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
catch (error) {
|
|
463
|
-
return {
|
|
464
|
-
content: [
|
|
465
|
-
{
|
|
466
|
-
type: "text",
|
|
467
|
-
text: error.message,
|
|
468
|
-
},
|
|
469
|
-
],
|
|
470
|
-
isError: true,
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
case "cves_by_product": {
|
|
475
|
-
const parsedArgs = CVEsByProductArgsSchema.safeParse(args);
|
|
476
|
-
if (!parsedArgs.success) {
|
|
477
|
-
throw new Error("Invalid arguments. Must provide either cpe23 or product name, but not both.");
|
|
478
|
-
}
|
|
479
|
-
try {
|
|
480
|
-
const result = await queryCVEsByProduct({
|
|
481
|
-
cpe23: parsedArgs.data.cpe23,
|
|
482
|
-
product: parsedArgs.data.product,
|
|
483
|
-
count: parsedArgs.data.count,
|
|
484
|
-
is_kev: parsedArgs.data.is_kev,
|
|
485
|
-
sort_by_epss: parsedArgs.data.sort_by_epss,
|
|
486
|
-
skip: parsedArgs.data.skip,
|
|
487
|
-
limit: parsedArgs.data.limit,
|
|
488
|
-
start_date: parsedArgs.data.start_date,
|
|
489
|
-
end_date: parsedArgs.data.end_date
|
|
490
|
-
});
|
|
491
|
-
// Helper function to format CVSS score severity
|
|
492
|
-
const getCvssSeverity = (score) => {
|
|
493
|
-
if (score >= 9.0)
|
|
494
|
-
return "Critical";
|
|
495
|
-
if (score >= 7.0)
|
|
496
|
-
return "High";
|
|
497
|
-
if (score >= 4.0)
|
|
498
|
-
return "Medium";
|
|
499
|
-
if (score >= 0.1)
|
|
500
|
-
return "Low";
|
|
501
|
-
return "None";
|
|
502
|
-
};
|
|
503
|
-
// Format the response based on whether it's a count request or full CVE list
|
|
504
|
-
const formattedResult = parsedArgs.data.count
|
|
505
|
-
? {
|
|
506
|
-
"Query Information": {
|
|
507
|
-
"Product": parsedArgs.data.product || "N/A",
|
|
508
|
-
"CPE 2.3": parsedArgs.data.cpe23 || "N/A",
|
|
509
|
-
"KEV Only": parsedArgs.data.is_kev ? "Yes" : "No",
|
|
510
|
-
"Sort by EPSS": parsedArgs.data.sort_by_epss ? "Yes" : "No"
|
|
511
|
-
},
|
|
512
|
-
"Results": {
|
|
513
|
-
"Total CVEs Found": result.total
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
: {
|
|
517
|
-
"Query Information": {
|
|
518
|
-
"Product": parsedArgs.data.product || "N/A",
|
|
519
|
-
"CPE 2.3": parsedArgs.data.cpe23 || "N/A",
|
|
520
|
-
"KEV Only": parsedArgs.data.is_kev ? "Yes" : "No",
|
|
521
|
-
"Sort by EPSS": parsedArgs.data.sort_by_epss ? "Yes" : "No",
|
|
522
|
-
"Date Range": parsedArgs.data.start_date ?
|
|
523
|
-
`${parsedArgs.data.start_date} to ${parsedArgs.data.end_date || 'now'}` :
|
|
524
|
-
"All dates"
|
|
525
|
-
},
|
|
526
|
-
"Results Summary": {
|
|
527
|
-
"Total CVEs Found": result.total,
|
|
528
|
-
"CVEs Returned": result.cves.length,
|
|
529
|
-
"Page": `${Math.floor(parsedArgs.data.skip / parsedArgs.data.limit) + 1}`,
|
|
530
|
-
"CVEs per Page": parsedArgs.data.limit
|
|
531
|
-
},
|
|
532
|
-
"Vulnerabilities": result.cves.map((cve) => ({
|
|
533
|
-
"Basic Information": {
|
|
534
|
-
"CVE ID": cve.cve_id,
|
|
535
|
-
"Published": new Date(cve.published_time).toLocaleString(),
|
|
536
|
-
"Summary": cve.summary
|
|
537
|
-
},
|
|
538
|
-
"Severity Scores": {
|
|
539
|
-
"CVSS v3": cve.cvss_v3 ? {
|
|
540
|
-
"Score": cve.cvss_v3,
|
|
541
|
-
"Severity": getCvssSeverity(cve.cvss_v3)
|
|
542
|
-
} : "Not available",
|
|
543
|
-
"CVSS v2": cve.cvss_v2 ? {
|
|
544
|
-
"Score": cve.cvss_v2,
|
|
545
|
-
"Severity": getCvssSeverity(cve.cvss_v2)
|
|
546
|
-
} : "Not available",
|
|
547
|
-
"EPSS": cve.epss ? {
|
|
548
|
-
"Score": `${(cve.epss * 100).toFixed(2)}%`,
|
|
549
|
-
"Ranking": `Top ${(cve.ranking_epss * 100).toFixed(2)}%`
|
|
550
|
-
} : "Not available"
|
|
551
|
-
},
|
|
552
|
-
"Impact Assessment": {
|
|
553
|
-
"Known Exploited Vulnerability": cve.kev ? "Yes" : "No",
|
|
554
|
-
"Proposed Action": cve.propose_action || "No specific action proposed",
|
|
555
|
-
"Ransomware Campaign": cve.ransomware_campaign || "No known ransomware campaigns"
|
|
556
|
-
},
|
|
557
|
-
"References": cve.references?.length > 0 ? cve.references : ["No references provided"]
|
|
558
|
-
}))
|
|
559
|
-
};
|
|
560
|
-
return {
|
|
561
|
-
content: [
|
|
562
|
-
{
|
|
563
|
-
type: "text",
|
|
564
|
-
text: JSON.stringify(formattedResult, null, 2),
|
|
565
|
-
},
|
|
566
|
-
],
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
catch (error) {
|
|
570
|
-
return {
|
|
571
|
-
content: [
|
|
572
|
-
{
|
|
573
|
-
type: "text",
|
|
574
|
-
text: error.message,
|
|
575
|
-
},
|
|
576
|
-
],
|
|
577
|
-
isError: true,
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
case "reverse_dns_lookup": {
|
|
582
|
-
const parsedArgs = ReverseDnsLookupArgsSchema.safeParse(args);
|
|
583
|
-
if (!parsedArgs.success) {
|
|
584
|
-
throw new Error("Invalid reverse_dns_lookup arguments");
|
|
585
|
-
}
|
|
586
|
-
// Join IPs with commas for the API request
|
|
587
|
-
const ipsString = parsedArgs.data.ips.join(",");
|
|
588
|
-
const result = await queryShodan("/dns/reverse", {
|
|
589
|
-
ips: ipsString
|
|
590
|
-
});
|
|
591
|
-
// Format the response in a user-friendly way
|
|
592
|
-
const formattedResult = {
|
|
593
|
-
"Reverse DNS Resolutions": Object.entries(result).map(([ip, hostnames]) => ({
|
|
594
|
-
"IP Address": ip,
|
|
595
|
-
"Hostnames": hostnames.length > 0 ? hostnames : ["No hostnames found"]
|
|
596
|
-
})),
|
|
597
|
-
"Summary": {
|
|
598
|
-
"Total IPs Queried": parsedArgs.data.ips.length,
|
|
599
|
-
"IPs with Results": Object.keys(result).length,
|
|
600
|
-
"Queried IP Addresses": parsedArgs.data.ips
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
return {
|
|
604
|
-
content: [
|
|
605
|
-
{
|
|
606
|
-
type: "text",
|
|
607
|
-
text: JSON.stringify(formattedResult, null, 2)
|
|
608
|
-
},
|
|
609
|
-
],
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
default:
|
|
613
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
catch (error) {
|
|
617
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
618
|
-
logToFile(`Error handling tool call: ${errorMessage}`);
|
|
619
|
-
return {
|
|
620
|
-
content: [
|
|
621
|
-
{
|
|
622
|
-
type: "text",
|
|
623
|
-
text: `Error: ${errorMessage}`,
|
|
624
|
-
},
|
|
625
|
-
],
|
|
626
|
-
isError: true,
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
|
-
// Start the Server
|
|
631
|
-
async function runServer() {
|
|
632
|
-
logToFile("Starting Shodan MCP Server...");
|
|
633
|
-
try {
|
|
634
|
-
const transport = new StdioServerTransport();
|
|
635
|
-
await server.connect(transport);
|
|
636
|
-
logToFile("Shodan MCP Server is running.");
|
|
637
|
-
}
|
|
638
|
-
catch (error) {
|
|
639
|
-
logToFile(`Error connecting server: ${error.message}`);
|
|
640
|
-
process.exit(1);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
// Handle process events
|
|
644
|
-
process.on('uncaughtException', (error) => {
|
|
645
|
-
logToFile(`Uncaught exception: ${error.message}`);
|
|
646
|
-
process.exit(1);
|
|
647
|
-
});
|
|
648
|
-
process.on('unhandledRejection', (reason) => {
|
|
649
|
-
logToFile(`Unhandled rejection: ${reason}`);
|
|
650
|
-
process.exit(1);
|
|
651
|
-
});
|
|
652
|
-
runServer().catch((error) => {
|
|
653
|
-
logToFile(`Fatal error: ${error.message}`);
|
|
654
|
-
process.exit(1);
|
|
655
26
|
});
|
|
27
|
+
registerIpLookup(server);
|
|
28
|
+
registerShodanSearch(server);
|
|
29
|
+
registerCveLookup(server);
|
|
30
|
+
registerDnsLookup(server);
|
|
31
|
+
registerReverseDnsLookup(server);
|
|
32
|
+
registerCpeLookup(server);
|
|
33
|
+
registerCvesByProduct(server);
|
|
34
|
+
server.start({ transportType: "stdio" });
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { queryCPEDB } from "../helpers.js";
|
|
3
|
+
export function registerCpeLookup(server) {
|
|
4
|
+
server.addTool({
|
|
5
|
+
name: "cpe_lookup",
|
|
6
|
+
description: "Search for Common Platform Enumeration (CPE) entries by product name in Shodan's CVEDB. Supports pagination and can return either full CPE details or just the total count. Useful for identifying specific versions and configurations of software and hardware.",
|
|
7
|
+
parameters: z.object({
|
|
8
|
+
product: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe("The name of the product to search for CPEs."),
|
|
11
|
+
count: z
|
|
12
|
+
.boolean()
|
|
13
|
+
.optional()
|
|
14
|
+
.default(false)
|
|
15
|
+
.describe("If true, returns only the count of matching CPEs."),
|
|
16
|
+
skip: z
|
|
17
|
+
.number()
|
|
18
|
+
.optional()
|
|
19
|
+
.default(0)
|
|
20
|
+
.describe("Number of CPEs to skip (for pagination)."),
|
|
21
|
+
limit: z
|
|
22
|
+
.number()
|
|
23
|
+
.optional()
|
|
24
|
+
.default(1000)
|
|
25
|
+
.describe("Maximum number of CPEs to return (max 1000)."),
|
|
26
|
+
}),
|
|
27
|
+
annotations: {
|
|
28
|
+
readOnlyHint: true,
|
|
29
|
+
openWorldHint: true,
|
|
30
|
+
},
|
|
31
|
+
execute: async (args) => {
|
|
32
|
+
const result = await queryCPEDB({
|
|
33
|
+
product: args.product,
|
|
34
|
+
count: args.count,
|
|
35
|
+
skip: args.skip,
|
|
36
|
+
limit: args.limit,
|
|
37
|
+
});
|
|
38
|
+
const formattedResult = args.count
|
|
39
|
+
? { total_cpes: result.total }
|
|
40
|
+
: {
|
|
41
|
+
cpes: result.cpes,
|
|
42
|
+
skip: args.skip,
|
|
43
|
+
limit: args.limit,
|
|
44
|
+
total_returned: result.cpes.length,
|
|
45
|
+
};
|
|
46
|
+
return JSON.stringify(formattedResult, null, 2);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { queryCVEDB, getCvssSeverity } from "../helpers.js";
|
|
3
|
+
export function registerCveLookup(server) {
|
|
4
|
+
server.addTool({
|
|
5
|
+
name: "cve_lookup",
|
|
6
|
+
description: "Query detailed vulnerability information from Shodan's CVEDB. Returns comprehensive CVE details including CVSS scores (v2/v3), EPSS probability and ranking, KEV status, proposed mitigations, ransomware associations, and affected products (CPEs).",
|
|
7
|
+
parameters: z.object({
|
|
8
|
+
cve: z
|
|
9
|
+
.string()
|
|
10
|
+
.regex(/^CVE-\d{4}-\d{4,}$/i, "Must be a valid CVE ID format (e.g., CVE-2021-44228)")
|
|
11
|
+
.describe("The CVE identifier to query (format: CVE-YYYY-NNNNN)."),
|
|
12
|
+
}),
|
|
13
|
+
annotations: {
|
|
14
|
+
readOnlyHint: true,
|
|
15
|
+
openWorldHint: true,
|
|
16
|
+
},
|
|
17
|
+
execute: async (args) => {
|
|
18
|
+
const cveId = args.cve.toUpperCase();
|
|
19
|
+
const result = await queryCVEDB(cveId);
|
|
20
|
+
const formattedResult = {
|
|
21
|
+
"Basic Information": {
|
|
22
|
+
"CVE ID": result.cve_id,
|
|
23
|
+
Published: new Date(result.published_time).toLocaleString(),
|
|
24
|
+
Summary: result.summary,
|
|
25
|
+
},
|
|
26
|
+
"Severity Scores": {
|
|
27
|
+
"CVSS v3": result.cvss_v3
|
|
28
|
+
? {
|
|
29
|
+
Score: result.cvss_v3,
|
|
30
|
+
Severity: getCvssSeverity(result.cvss_v3),
|
|
31
|
+
}
|
|
32
|
+
: "Not available",
|
|
33
|
+
"CVSS v2": result.cvss_v2
|
|
34
|
+
? {
|
|
35
|
+
Score: result.cvss_v2,
|
|
36
|
+
Severity: getCvssSeverity(result.cvss_v2),
|
|
37
|
+
}
|
|
38
|
+
: "Not available",
|
|
39
|
+
EPSS: result.epss
|
|
40
|
+
? {
|
|
41
|
+
Score: `${(result.epss * 100).toFixed(2)}%`,
|
|
42
|
+
Ranking: `Top ${(result.ranking_epss * 100).toFixed(2)}%`,
|
|
43
|
+
}
|
|
44
|
+
: "Not available",
|
|
45
|
+
},
|
|
46
|
+
"Impact Assessment": {
|
|
47
|
+
"Known Exploited Vulnerability": result.kev ? "Yes" : "No",
|
|
48
|
+
"Proposed Action": result.propose_action || "No specific action proposed",
|
|
49
|
+
"Ransomware Campaign": result.ransomware_campaign || "No known ransomware campaigns",
|
|
50
|
+
},
|
|
51
|
+
"Affected Products": result.cpes?.length > 0
|
|
52
|
+
? result.cpes
|
|
53
|
+
: ["No specific products listed"],
|
|
54
|
+
"Additional Information": {
|
|
55
|
+
References: result.references?.length > 0
|
|
56
|
+
? result.references
|
|
57
|
+
: ["No references provided"],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
return JSON.stringify(formattedResult, null, 2);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { UserError } from "fastmcp";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { queryCVEsByProduct, getCvssSeverity } from "../helpers.js";
|
|
4
|
+
export function registerCvesByProduct(server) {
|
|
5
|
+
server.addTool({
|
|
6
|
+
name: "cves_by_product",
|
|
7
|
+
description: "Search for vulnerabilities affecting specific products or CPEs. Supports filtering by KEV status, sorting by EPSS score, date ranges, and pagination. Can search by product name or CPE 2.3 identifier. Returns detailed vulnerability information including severity scores and impact assessments.",
|
|
8
|
+
parameters: z.object({
|
|
9
|
+
cpe23: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("The CPE version 2.3 identifier (format: cpe:2.3:part:vendor:product:version)."),
|
|
13
|
+
product: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("The name of the product to search for CVEs."),
|
|
17
|
+
count: z
|
|
18
|
+
.boolean()
|
|
19
|
+
.optional()
|
|
20
|
+
.default(false)
|
|
21
|
+
.describe("If true, returns only the count of matching CVEs."),
|
|
22
|
+
is_kev: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.default(false)
|
|
26
|
+
.describe("If true, returns only CVEs with the KEV flag set."),
|
|
27
|
+
sort_by_epss: z
|
|
28
|
+
.boolean()
|
|
29
|
+
.optional()
|
|
30
|
+
.default(false)
|
|
31
|
+
.describe("If true, sorts CVEs by EPSS score in descending order."),
|
|
32
|
+
skip: z
|
|
33
|
+
.number()
|
|
34
|
+
.optional()
|
|
35
|
+
.default(0)
|
|
36
|
+
.describe("Number of CVEs to skip (for pagination)."),
|
|
37
|
+
limit: z
|
|
38
|
+
.number()
|
|
39
|
+
.optional()
|
|
40
|
+
.default(1000)
|
|
41
|
+
.describe("Maximum number of CVEs to return (max 1000)."),
|
|
42
|
+
start_date: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Start date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS)."),
|
|
46
|
+
end_date: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("End date for filtering CVEs (format: YYYY-MM-DDTHH:MM:SS)."),
|
|
50
|
+
}),
|
|
51
|
+
annotations: {
|
|
52
|
+
readOnlyHint: true,
|
|
53
|
+
openWorldHint: true,
|
|
54
|
+
},
|
|
55
|
+
execute: async (args) => {
|
|
56
|
+
if (args.cpe23 && args.product) {
|
|
57
|
+
throw new UserError("Cannot specify both cpe23 and product. Use only one.");
|
|
58
|
+
}
|
|
59
|
+
if (!args.cpe23 && !args.product) {
|
|
60
|
+
throw new UserError("Must specify either cpe23 or product.");
|
|
61
|
+
}
|
|
62
|
+
const result = await queryCVEsByProduct({
|
|
63
|
+
cpe23: args.cpe23,
|
|
64
|
+
product: args.product,
|
|
65
|
+
count: args.count,
|
|
66
|
+
is_kev: args.is_kev,
|
|
67
|
+
sort_by_epss: args.sort_by_epss,
|
|
68
|
+
skip: args.skip,
|
|
69
|
+
limit: args.limit,
|
|
70
|
+
start_date: args.start_date,
|
|
71
|
+
end_date: args.end_date,
|
|
72
|
+
});
|
|
73
|
+
const formattedResult = args.count
|
|
74
|
+
? {
|
|
75
|
+
"Query Information": {
|
|
76
|
+
Product: args.product || "N/A",
|
|
77
|
+
"CPE 2.3": args.cpe23 || "N/A",
|
|
78
|
+
"KEV Only": args.is_kev ? "Yes" : "No",
|
|
79
|
+
"Sort by EPSS": args.sort_by_epss ? "Yes" : "No",
|
|
80
|
+
},
|
|
81
|
+
Results: {
|
|
82
|
+
"Total CVEs Found": result.total,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
: {
|
|
86
|
+
"Query Information": {
|
|
87
|
+
Product: args.product || "N/A",
|
|
88
|
+
"CPE 2.3": args.cpe23 || "N/A",
|
|
89
|
+
"KEV Only": args.is_kev ? "Yes" : "No",
|
|
90
|
+
"Sort by EPSS": args.sort_by_epss ? "Yes" : "No",
|
|
91
|
+
"Date Range": args.start_date
|
|
92
|
+
? `${args.start_date} to ${args.end_date || "now"}`
|
|
93
|
+
: "All dates",
|
|
94
|
+
},
|
|
95
|
+
"Results Summary": {
|
|
96
|
+
"Total CVEs Found": result.total,
|
|
97
|
+
"CVEs Returned": result.cves.length,
|
|
98
|
+
Page: `${Math.floor(args.skip / args.limit) + 1}`,
|
|
99
|
+
"CVEs per Page": args.limit,
|
|
100
|
+
},
|
|
101
|
+
Vulnerabilities: result.cves.map((cve) => ({
|
|
102
|
+
"Basic Information": {
|
|
103
|
+
"CVE ID": cve.cve_id,
|
|
104
|
+
Published: new Date(cve.published_time).toLocaleString(),
|
|
105
|
+
Summary: cve.summary,
|
|
106
|
+
},
|
|
107
|
+
"Severity Scores": {
|
|
108
|
+
"CVSS v3": cve.cvss_v3
|
|
109
|
+
? {
|
|
110
|
+
Score: cve.cvss_v3,
|
|
111
|
+
Severity: getCvssSeverity(cve.cvss_v3),
|
|
112
|
+
}
|
|
113
|
+
: "Not available",
|
|
114
|
+
"CVSS v2": cve.cvss_v2
|
|
115
|
+
? {
|
|
116
|
+
Score: cve.cvss_v2,
|
|
117
|
+
Severity: getCvssSeverity(cve.cvss_v2),
|
|
118
|
+
}
|
|
119
|
+
: "Not available",
|
|
120
|
+
EPSS: cve.epss
|
|
121
|
+
? {
|
|
122
|
+
Score: `${(cve.epss * 100).toFixed(2)}%`,
|
|
123
|
+
Ranking: `Top ${(cve.ranking_epss * 100).toFixed(2)}%`,
|
|
124
|
+
}
|
|
125
|
+
: "Not available",
|
|
126
|
+
},
|
|
127
|
+
"Impact Assessment": {
|
|
128
|
+
"Known Exploited Vulnerability": cve.kev ? "Yes" : "No",
|
|
129
|
+
"Proposed Action": cve.propose_action || "No specific action proposed",
|
|
130
|
+
"Ransomware Campaign": cve.ransomware_campaign ||
|
|
131
|
+
"No known ransomware campaigns",
|
|
132
|
+
},
|
|
133
|
+
References: cve.references?.length > 0
|
|
134
|
+
? cve.references
|
|
135
|
+
: ["No references provided"],
|
|
136
|
+
})),
|
|
137
|
+
};
|
|
138
|
+
return JSON.stringify(formattedResult, null, 2);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { queryShodan } from "../helpers.js";
|
|
3
|
+
export function registerDnsLookup(server) {
|
|
4
|
+
server.addTool({
|
|
5
|
+
name: "dns_lookup",
|
|
6
|
+
description: "Resolve domain names to IP addresses using Shodan's DNS service. Supports batch resolution of multiple hostnames in a single query. Returns IP addresses mapped to their corresponding hostnames.",
|
|
7
|
+
parameters: z.object({
|
|
8
|
+
hostnames: z
|
|
9
|
+
.array(z.string())
|
|
10
|
+
.describe("List of hostnames to resolve."),
|
|
11
|
+
}),
|
|
12
|
+
annotations: {
|
|
13
|
+
readOnlyHint: true,
|
|
14
|
+
openWorldHint: true,
|
|
15
|
+
},
|
|
16
|
+
execute: async (args) => {
|
|
17
|
+
const hostnamesString = args.hostnames.join(",");
|
|
18
|
+
const result = await queryShodan("/dns/resolve", {
|
|
19
|
+
hostnames: hostnamesString,
|
|
20
|
+
});
|
|
21
|
+
const formattedResult = {
|
|
22
|
+
"DNS Resolutions": Object.entries(result).map(([hostname, ip]) => ({
|
|
23
|
+
Hostname: hostname,
|
|
24
|
+
"IP Address": ip,
|
|
25
|
+
})),
|
|
26
|
+
Summary: {
|
|
27
|
+
"Total Lookups": Object.keys(result).length,
|
|
28
|
+
"Queried Hostnames": args.hostnames,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
return JSON.stringify(formattedResult, null, 2);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { queryShodan } from "../helpers.js";
|
|
3
|
+
export function registerIpLookup(server) {
|
|
4
|
+
server.addTool({
|
|
5
|
+
name: "ip_lookup",
|
|
6
|
+
description: "Retrieve comprehensive information about an IP address, including geolocation, open ports, running services, SSL certificates, hostnames, and cloud provider details if available. Returns service banners and HTTP server information when present.",
|
|
7
|
+
parameters: z.object({
|
|
8
|
+
ip: z.string().describe("The IP address to query."),
|
|
9
|
+
}),
|
|
10
|
+
annotations: {
|
|
11
|
+
readOnlyHint: true,
|
|
12
|
+
openWorldHint: true,
|
|
13
|
+
},
|
|
14
|
+
execute: async (args) => {
|
|
15
|
+
const result = await queryShodan(`/shodan/host/${args.ip}`, {});
|
|
16
|
+
const formattedResult = {
|
|
17
|
+
"IP Information": {
|
|
18
|
+
"IP Address": result.ip_str,
|
|
19
|
+
Organization: result.org,
|
|
20
|
+
ISP: result.isp,
|
|
21
|
+
ASN: result.asn,
|
|
22
|
+
"Last Update": result.last_update,
|
|
23
|
+
},
|
|
24
|
+
Location: {
|
|
25
|
+
Country: result.country_name,
|
|
26
|
+
City: result.city,
|
|
27
|
+
Coordinates: `${result.latitude}, ${result.longitude}`,
|
|
28
|
+
Region: result.region_code,
|
|
29
|
+
},
|
|
30
|
+
Services: result.ports.map((port) => {
|
|
31
|
+
const service = result.data.find((d) => d.port === port);
|
|
32
|
+
return {
|
|
33
|
+
Port: port,
|
|
34
|
+
Protocol: service?.transport || "unknown",
|
|
35
|
+
Service: service?.data?.trim() || "No banner",
|
|
36
|
+
...(service?.http
|
|
37
|
+
? {
|
|
38
|
+
HTTP: {
|
|
39
|
+
Server: service.http.server,
|
|
40
|
+
Title: service.http.title,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
: {}),
|
|
44
|
+
};
|
|
45
|
+
}),
|
|
46
|
+
"Cloud Provider": result.data[0]?.cloud
|
|
47
|
+
? {
|
|
48
|
+
Provider: result.data[0].cloud.provider,
|
|
49
|
+
Service: result.data[0].cloud.service,
|
|
50
|
+
Region: result.data[0].cloud.region,
|
|
51
|
+
}
|
|
52
|
+
: "Not detected",
|
|
53
|
+
Hostnames: result.hostnames || [],
|
|
54
|
+
Domains: result.domains || [],
|
|
55
|
+
Tags: result.tags || [],
|
|
56
|
+
};
|
|
57
|
+
return JSON.stringify(formattedResult, null, 2);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { queryShodan } from "../helpers.js";
|
|
3
|
+
export function registerReverseDnsLookup(server) {
|
|
4
|
+
server.addTool({
|
|
5
|
+
name: "reverse_dns_lookup",
|
|
6
|
+
description: "Perform reverse DNS lookups to find hostnames associated with IP addresses. Supports batch lookups of multiple IP addresses in a single query. Returns all known hostnames for each IP address, with clear indication when no hostnames are found.",
|
|
7
|
+
parameters: z.object({
|
|
8
|
+
ips: z
|
|
9
|
+
.array(z.string())
|
|
10
|
+
.describe("List of IP addresses to perform reverse DNS lookup on."),
|
|
11
|
+
}),
|
|
12
|
+
annotations: {
|
|
13
|
+
readOnlyHint: true,
|
|
14
|
+
openWorldHint: true,
|
|
15
|
+
},
|
|
16
|
+
execute: async (args) => {
|
|
17
|
+
const ipsString = args.ips.join(",");
|
|
18
|
+
const result = await queryShodan("/dns/reverse", {
|
|
19
|
+
ips: ipsString,
|
|
20
|
+
});
|
|
21
|
+
const formattedResult = {
|
|
22
|
+
"Reverse DNS Resolutions": Object.entries(result).map(([ip, hostnames]) => ({
|
|
23
|
+
"IP Address": ip,
|
|
24
|
+
Hostnames: hostnames.length > 0 ? hostnames : ["No hostnames found"],
|
|
25
|
+
})),
|
|
26
|
+
Summary: {
|
|
27
|
+
"Total IPs Queried": args.ips.length,
|
|
28
|
+
"IPs with Results": Object.keys(result).length,
|
|
29
|
+
"Queried IP Addresses": args.ips,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
return JSON.stringify(formattedResult, null, 2);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { queryShodan } from "../helpers.js";
|
|
3
|
+
export function registerShodanSearch(server) {
|
|
4
|
+
server.addTool({
|
|
5
|
+
name: "shodan_search",
|
|
6
|
+
description: "Search Shodan's database of internet-connected devices. Returns detailed information about matching devices including services, vulnerabilities, and geographic distribution. Supports advanced search filters and returns country-based statistics.",
|
|
7
|
+
parameters: z.object({
|
|
8
|
+
query: z.string().describe("Search query for Shodan."),
|
|
9
|
+
max_results: z
|
|
10
|
+
.number()
|
|
11
|
+
.optional()
|
|
12
|
+
.default(10)
|
|
13
|
+
.describe("Maximum results to return."),
|
|
14
|
+
}),
|
|
15
|
+
annotations: {
|
|
16
|
+
readOnlyHint: true,
|
|
17
|
+
openWorldHint: true,
|
|
18
|
+
},
|
|
19
|
+
execute: async (args) => {
|
|
20
|
+
const result = await queryShodan("/shodan/host/search", {
|
|
21
|
+
query: args.query,
|
|
22
|
+
limit: args.max_results,
|
|
23
|
+
});
|
|
24
|
+
const formattedResult = {
|
|
25
|
+
"Search Summary": {
|
|
26
|
+
Query: args.query,
|
|
27
|
+
"Total Results": result.total,
|
|
28
|
+
"Results Returned": result.matches.length,
|
|
29
|
+
},
|
|
30
|
+
"Country Distribution": result.facets?.country?.map((country) => ({
|
|
31
|
+
Country: country.value,
|
|
32
|
+
Count: country.count,
|
|
33
|
+
Percentage: `${((country.count / result.total) * 100).toFixed(2)}%`,
|
|
34
|
+
})) || [],
|
|
35
|
+
Matches: result.matches.map((match) => ({
|
|
36
|
+
"Basic Information": {
|
|
37
|
+
"IP Address": match.ip_str,
|
|
38
|
+
Organization: match.org,
|
|
39
|
+
ISP: match.isp,
|
|
40
|
+
ASN: match.asn,
|
|
41
|
+
"Last Update": match.timestamp,
|
|
42
|
+
},
|
|
43
|
+
Location: {
|
|
44
|
+
Country: match.location.country_name,
|
|
45
|
+
City: match.location.city || "Unknown",
|
|
46
|
+
Region: match.location.region_code || "Unknown",
|
|
47
|
+
Coordinates: `${match.location.latitude}, ${match.location.longitude}`,
|
|
48
|
+
},
|
|
49
|
+
"Service Details": {
|
|
50
|
+
Port: match.port,
|
|
51
|
+
Transport: match.transport,
|
|
52
|
+
Product: match.product || "Unknown",
|
|
53
|
+
Version: match.version || "Unknown",
|
|
54
|
+
CPE: match.cpe || [],
|
|
55
|
+
},
|
|
56
|
+
"Web Information": match.http
|
|
57
|
+
? {
|
|
58
|
+
Server: match.http.server,
|
|
59
|
+
Title: match.http.title,
|
|
60
|
+
"Robots.txt": match.http.robots ? "Present" : "Not found",
|
|
61
|
+
Sitemap: match.http.sitemap ? "Present" : "Not found",
|
|
62
|
+
}
|
|
63
|
+
: "No HTTP information",
|
|
64
|
+
Hostnames: match.hostnames,
|
|
65
|
+
Domains: match.domains,
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
return JSON.stringify(formattedResult, null, 2);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@burtthecoder/mcp-shodan",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.20",
|
|
5
5
|
"description": "A Model Context Protocol server for Shodan API queries.",
|
|
6
6
|
"main": "build/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
"prepublishOnly": "npm run build"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@modelcontextprotocol/sdk": "^0.6.0",
|
|
16
15
|
"axios": "^1.7.8",
|
|
17
16
|
"dotenv": "^16.4.5",
|
|
18
|
-
"
|
|
19
|
-
"zod
|
|
17
|
+
"fastmcp": "^3.33.0",
|
|
18
|
+
"zod": "^3.23.0"
|
|
20
19
|
},
|
|
21
20
|
"devDependencies": {
|
|
22
21
|
"typescript": "^5.3.3",
|