- Add input-validation.ts with regex, path, and URL validation utilities - Validate regex patterns before RegExp creation to prevent ReDoS - Block dangerous nested quantifiers (a+)+, (a*)+, etc. - Prevent path traversal with directory escape detection - Block localhost, private IPs, and non-http/https protocols for SSRF - Add SecurityOptions for configurable validation (allowPrivateIPs, etc.) - Include 33 security tests (unit + integration) Fixes #362 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
163 lines
5.3 KiB
TypeScript
163 lines
5.3 KiB
TypeScript
import { z } from 'zod';
|
|
import { parseSpec } from '../lib/parser.js';
|
|
import { formatEndpoints } from '../utils/format.js';
|
|
import { HTTP_METHODS } from '../lib/types.js';
|
|
import { successResponse, errorResponse } from '../lib/tool-response.js';
|
|
import { createSafeRegex, validateRegexPattern } from '../lib/input-validation.js';
|
|
import type { ToolResponse } from '../lib/tool-response.js';
|
|
import type { EndpointInfo, EndpointFilter, ParameterInfo, ResponseInfo } from '../lib/types.js';
|
|
import type { OpenAPIV3 } from 'openapi-types';
|
|
|
|
export const queryToolName = 'query-endpoints';
|
|
|
|
export const queryToolDescription = 'Search and filter API endpoints in an OpenAPI spec. Filter by method, path pattern, tag, or operation ID.';
|
|
|
|
export const queryToolSchema = {
|
|
path: z.string().describe('Path to the OpenAPI/Swagger spec file'),
|
|
method: z.string().optional().describe('Filter by HTTP method (GET, POST, etc.)'),
|
|
pathPattern: z.string().optional().describe('Regex pattern to match path'),
|
|
tag: z.string().optional().describe('Filter by tag name'),
|
|
operationId: z.string().optional().describe('Filter by exact operation ID'),
|
|
noCache: z.boolean().optional().describe('Bypass cache and parse fresh'),
|
|
};
|
|
|
|
export async function queryToolHandler(args: {
|
|
path: string;
|
|
method?: string;
|
|
pathPattern?: string;
|
|
tag?: string;
|
|
operationId?: string;
|
|
noCache?: boolean;
|
|
}): Promise<ToolResponse> {
|
|
try {
|
|
// Validate regex pattern before use (ReDoS protection)
|
|
if (args.pathPattern) {
|
|
const regexValidation = validateRegexPattern(args.pathPattern);
|
|
if (!regexValidation.valid) {
|
|
return errorResponse(
|
|
regexValidation.error ?? 'Invalid path pattern',
|
|
'validating path pattern'
|
|
);
|
|
}
|
|
}
|
|
|
|
const { spec } = await parseSpec(args.path, { noCache: args.noCache });
|
|
|
|
const filter: EndpointFilter = {
|
|
method: args.method?.toLowerCase(),
|
|
pathPattern: args.pathPattern,
|
|
tag: args.tag,
|
|
operationId: args.operationId,
|
|
};
|
|
|
|
const endpoints = extractEndpoints(spec, filter);
|
|
const text = formatEndpoints(endpoints);
|
|
|
|
return successResponse(text, { count: endpoints.length, endpoints });
|
|
} catch (err) {
|
|
return errorResponse((err as Error).message, 'querying endpoints');
|
|
}
|
|
}
|
|
|
|
function extractEndpoints(spec: object, filter: EndpointFilter): EndpointInfo[] {
|
|
const endpoints: EndpointInfo[] = [];
|
|
const paths = (spec as { paths?: Record<string, object> }).paths || {};
|
|
|
|
for (const [pathName, pathItem] of Object.entries(paths)) {
|
|
if (!pathItem) continue;
|
|
|
|
for (const method of HTTP_METHODS) {
|
|
const operation = (pathItem as Record<string, unknown>)[method] as OpenAPIV3.OperationObject | undefined;
|
|
if (!operation) continue;
|
|
|
|
// Apply filters
|
|
if (filter.method && method !== filter.method) continue;
|
|
|
|
if (filter.pathPattern) {
|
|
// Use safe regex creation (already validated in handler)
|
|
const regex = createSafeRegex(filter.pathPattern);
|
|
if (regex && !regex.test(pathName)) continue;
|
|
}
|
|
|
|
if (filter.tag && (!operation.tags || !operation.tags.includes(filter.tag))) continue;
|
|
|
|
if (filter.operationId && operation.operationId !== filter.operationId) continue;
|
|
|
|
// Extract endpoint info
|
|
const endpoint: EndpointInfo = {
|
|
method,
|
|
path: pathName,
|
|
operationId: operation.operationId,
|
|
summary: operation.summary,
|
|
description: operation.description,
|
|
tags: operation.tags || [],
|
|
parameters: extractParameters(operation, pathItem as OpenAPIV3.PathItemObject),
|
|
requestBody: extractRequestBody(operation),
|
|
responses: extractResponses(operation),
|
|
};
|
|
|
|
endpoints.push(endpoint);
|
|
}
|
|
}
|
|
|
|
return endpoints;
|
|
}
|
|
|
|
function extractParameters(
|
|
operation: OpenAPIV3.OperationObject,
|
|
pathItem: OpenAPIV3.PathItemObject
|
|
): ParameterInfo[] {
|
|
const params: ParameterInfo[] = [];
|
|
|
|
// Combine path-level and operation-level parameters
|
|
const allParams = [
|
|
...(pathItem.parameters || []),
|
|
...(operation.parameters || []),
|
|
] as OpenAPIV3.ParameterObject[];
|
|
|
|
for (const param of allParams) {
|
|
if ('$ref' in param) continue; // Skip refs (should be dereferenced)
|
|
|
|
params.push({
|
|
name: param.name,
|
|
in: param.in as 'query' | 'header' | 'path' | 'cookie',
|
|
required: param.required || false,
|
|
description: param.description,
|
|
schema: param.schema as object | undefined,
|
|
});
|
|
}
|
|
|
|
return params;
|
|
}
|
|
|
|
function extractRequestBody(operation: OpenAPIV3.OperationObject): EndpointInfo['requestBody'] {
|
|
if (!operation.requestBody) return undefined;
|
|
|
|
const body = operation.requestBody as OpenAPIV3.RequestBodyObject;
|
|
if ('$ref' in body) return undefined; // Skip refs
|
|
|
|
return {
|
|
required: body.required || false,
|
|
description: body.description,
|
|
contentTypes: Object.keys(body.content || {}),
|
|
};
|
|
}
|
|
|
|
function extractResponses(operation: OpenAPIV3.OperationObject): ResponseInfo[] {
|
|
const responses: ResponseInfo[] = [];
|
|
|
|
if (!operation.responses) return responses;
|
|
|
|
for (const [statusCode, response] of Object.entries(operation.responses)) {
|
|
const resp = response as OpenAPIV3.ResponseObject;
|
|
if ('$ref' in resp) continue; // Skip refs
|
|
|
|
responses.push({
|
|
statusCode,
|
|
description: resp.description,
|
|
contentTypes: Object.keys(resp.content || {}),
|
|
});
|
|
}
|
|
|
|
return responses;
|
|
}
|