UNPKG

okta-mcp-server

Version:

Model Context Protocol (MCP) server for Okta API operations with support for bulk operations and caching

347 lines 12.7 kB
/** * Audit middleware for automatic capture of MCP operations */ import { randomUUID } from 'crypto'; import { logger } from '../../utils/logger.js'; export class AuditMiddleware { auditLogger; extractActor; extractContext; skipPaths; performanceThreshold; activeRequests = new Map(); constructor(options) { this.auditLogger = options.auditLogger; this.extractActor = options.extractActor || this.defaultExtractActor; this.extractContext = options.extractContext || this.defaultExtractContext; this.skipPaths = new Set(options.skipPaths || []); this.performanceThreshold = options.performanceThreshold || 5000; // 5 seconds } /** * Wrap a tool handler with audit logging */ wrapToolHandler(toolName, handler) { return (async (...args) => { const request = args[0]; const requestId = randomUUID(); const startTime = Date.now(); const memoryStart = process.memoryUsage().heapUsed; // Store request info for tracking this.activeRequests.set(requestId, { startTime, memoryStart }); // Extract actor and context const actor = this.extractActor(request); const context = { requestId, ...this.extractContext(request), }; let result; let error; try { // Call the actual handler result = await handler(...args); // Log successful tool call await this.logToolCall({ timestamp: new Date(startTime), actor, action: { type: 'tool.call', method: toolName, result: result.isError ? 'failure' : 'success', }, request: { method: toolName, parameters: request.params.arguments, }, response: { status: result.isError ? 400 : 200, }, performance: this.calculatePerformance(requestId), context, }); return result; } catch (err) { error = err; // Log error await this.logToolCall({ timestamp: new Date(startTime), actor, action: { type: 'tool.call', method: toolName, result: 'error', errorCode: err.code || 'UNKNOWN', errorMessage: err instanceof Error ? err.message : String(err), }, request: { method: toolName, parameters: request.params.arguments, }, response: { status: 500, error: String(err), }, performance: this.calculatePerformance(requestId), context, }); throw err; } finally { // Clean up request tracking this.activeRequests.delete(requestId); } }); } /** * Wrap a resource handler with audit logging */ wrapResourceHandler(resourceType, handler) { return (async (...args) => { const uri = args[0]; const requestId = randomUUID(); const startTime = Date.now(); const memoryStart = process.memoryUsage().heapUsed; // Store request info for tracking this.activeRequests.set(requestId, { startTime, memoryStart }); // Extract resource info from URI const resource = this.extractResourceFromUri(uri); // Extract actor and context (from second arg if available) const request = args[1]; const actor = this.extractActor(request); const context = { requestId, ...this.extractContext(request), }; let result; let error; try { // Call the actual handler result = await handler(...args); // Log successful resource read await this.logResourceRead({ timestamp: new Date(startTime), actor, action: { type: 'resource.read', method: resourceType, result: 'success', }, resource, request: { method: 'read', path: uri, }, response: { status: 200, }, performance: this.calculatePerformance(requestId), context, }); return result; } catch (err) { error = err; // Log error await this.logResourceRead({ timestamp: new Date(startTime), actor, action: { type: 'resource.read', method: resourceType, result: 'error', errorCode: err.code || 'UNKNOWN', errorMessage: err instanceof Error ? err.message : String(err), }, resource, request: { method: 'read', path: uri, }, response: { status: err.status || 500, error: String(err), }, performance: this.calculatePerformance(requestId), context, }); throw err; } finally { // Clean up request tracking this.activeRequests.delete(requestId); } }); } /** * Create an Express-style middleware for HTTP transports */ createHttpMiddleware() { return async (req, res, next) => { const requestId = req.headers['x-request-id'] || randomUUID(); const startTime = Date.now(); const memoryStart = process.memoryUsage().heapUsed; // Store request info this.activeRequests.set(requestId, { startTime, memoryStart }); // Extract actor const actor = { id: req.user?.id || req.headers['x-client-id'] || 'anonymous', type: req.user ? 'user' : 'service', ipAddress: req.ip || req.connection?.remoteAddress, userAgent: req.headers['user-agent'], sessionId: req.session?.id, }; // Extract context const context = { requestId, correlationId: req.headers['x-correlation-id'], clientApplication: req.headers['x-client-application'], clientVersion: req.headers['x-client-version'], }; // Capture response const originalSend = res.send; const middleware = this; res.send = function (data) { res.send = originalSend; // Log the request const performance = middleware.calculatePerformance(requestId); middleware.auditLogger .log({ timestamp: new Date(startTime), actor, action: { type: 'http.request', method: req.method, result: res.statusCode < 400 ? 'success' : 'failure', }, request: { method: req.method, path: req.path, parameters: { ...req.query, ...req.body }, size: req.headers['content-length'] ? parseInt(req.headers['content-length']) : undefined, }, response: { status: res.statusCode, size: Buffer.byteLength(data), }, performance, context, }) .catch((err) => logger.error('Failed to log audit entry:', err)); // Clean up middleware.activeRequests.delete(requestId); return originalSend.call(this, data); }; next(); }; } /** * Log a tool call */ async logToolCall(entry) { // Check performance threshold if (entry.performance && entry.performance.duration > this.performanceThreshold) { logger.warn(`Tool call ${entry.action.method} exceeded performance threshold: ${entry.performance.duration}ms`); } await this.auditLogger.log(entry); } /** * Log a resource read */ async logResourceRead(entry) { // Check performance threshold if (entry.performance && entry.performance.duration > this.performanceThreshold) { logger.warn(`Resource read ${entry.action.method} exceeded performance threshold: ${entry.performance.duration}ms`); } await this.auditLogger.log(entry); } /** * Default actor extraction */ defaultExtractActor(request) { return { id: request?.user?.id || request?.clientId || 'system', type: request?.user ? 'user' : 'service', ipAddress: request?.ip, userAgent: request?.userAgent, sessionId: request?.sessionId, }; } /** * Default context extraction */ defaultExtractContext(request) { return { correlationId: request?.correlationId, environment: process.env['NODE_ENV'], clientApplication: request?.clientApplication, clientVersion: request?.clientVersion, }; } /** * Extract resource info from URI */ extractResourceFromUri(uri) { const match = uri.match(/^okta:\/\/([^/]+)(?:\/(.+))?/); if (!match) return undefined; return { type: match[1], id: match[2] || 'all', }; } /** * Calculate performance metrics */ calculatePerformance(requestId) { const requestInfo = this.activeRequests.get(requestId); if (!requestInfo) return undefined; const duration = Date.now() - requestInfo.startTime; const memoryUsed = process.memoryUsage().heapUsed - requestInfo.memoryStart; return { duration, memoryUsed: memoryUsed > 0 ? memoryUsed : undefined, }; } /** * Create audit context for a request */ static createContext(request) { return { requestId: request?.id || randomUUID(), correlationId: request?.correlationId, environment: process.env['NODE_ENV'], region: process.env['AWS_REGION'] || process.env['REGION'], clientApplication: request?.clientApplication, clientVersion: request?.clientVersion, }; } /** * Create audit actor from various sources */ static createActor(source) { if (source?.user) { return { id: source.user.id || source.user.email || 'unknown', type: 'user', ipAddress: source.ip || source.ipAddress, userAgent: source.userAgent || source.headers?.['user-agent'], sessionId: source.sessionId || source.session?.id, }; } if (source?.service) { return { id: source.service.id || source.service.name || 'unknown-service', type: 'service', ipAddress: source.ip || source.ipAddress, }; } return { id: 'system', type: 'system', }; } } //# sourceMappingURL=audit-middleware.js.map