Skip to main content
Glama
security-extractor.ts8.52 kB
/** * Security extractor for OpenAPI specifications */ import type { OpenAPISpec, SecuritySchemeObject, SecurityRequirementObject, ReferenceObject } from '../types/index'; export interface ExtractedSecurityScheme { name: string; scheme: SecuritySchemeObject; usageCount: number; usedInOperations: string[]; } export interface SecurityAnalysis { schemes: ExtractedSecurityScheme[]; globalSecurity: SecurityRequirementObject[]; operationSecurity: Record<string, SecurityRequirementObject[]>; unsecuredOperations: string[]; } export class SecurityExtractor { /** * Extract all security information from OpenAPI specification */ static extractSecurity(spec: OpenAPISpec): SecurityAnalysis { const schemes = this.extractSecuritySchemes(spec); const globalSecurity = spec.security || []; const operationSecurity = this.extractOperationSecurity(spec); const unsecuredOperations = this.findUnsecuredOperations(spec); return { schemes, globalSecurity, operationSecurity, unsecuredOperations }; } /** * Extract security schemes from components */ private static extractSecuritySchemes(spec: OpenAPISpec): ExtractedSecurityScheme[] { const schemes: ExtractedSecurityScheme[] = []; if (!spec.components?.securitySchemes) { return schemes; } const usageCounts = this.analyzeSecurityUsage(spec); for (const [name, scheme] of Object.entries(spec.components.securitySchemes)) { if (this.isReferenceObject(scheme)) { continue; // Skip reference objects } const extractedScheme: ExtractedSecurityScheme = { name, scheme, usageCount: usageCounts[name]?.count || 0, usedInOperations: usageCounts[name]?.operations || [] }; schemes.push(extractedScheme); } return schemes; } /** * Analyze security scheme usage throughout the specification */ private static analyzeSecurityUsage(spec: OpenAPISpec): Record<string, { count: number; operations: string[] }> { const usage: Record<string, { count: number; operations: string[] }> = {}; // Check global security if (spec.security) { for (const requirement of spec.security) { for (const schemeName of Object.keys(requirement)) { if (!usage[schemeName]) { usage[schemeName] = { count: 0, operations: [] }; } usage[schemeName].count++; } } } // Check operation-level security if (spec.paths) { for (const [path, pathItem] of Object.entries(spec.paths)) { const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'] as const; for (const method of methods) { const operation = pathItem[method]; if (!operation) continue; const operationId = `${method.toUpperCase()} ${path}`; if (operation.security) { for (const requirement of operation.security) { for (const schemeName of Object.keys(requirement)) { if (!usage[schemeName]) { usage[schemeName] = { count: 0, operations: [] }; } usage[schemeName].count++; usage[schemeName].operations.push(operationId); } } } } } } return usage; } /** * Extract operation-level security requirements */ private static extractOperationSecurity(spec: OpenAPISpec): Record<string, SecurityRequirementObject[]> { const operationSecurity: Record<string, SecurityRequirementObject[]> = {}; if (!spec.paths) { return operationSecurity; } for (const [path, pathItem] of Object.entries(spec.paths)) { const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'] as const; for (const method of methods) { const operation = pathItem[method]; if (!operation) continue; const operationId = operation.operationId || `${method.toUpperCase()} ${path}`; if (operation.security) { operationSecurity[operationId] = operation.security; } else if (spec.security) { // Use global security if no operation-specific security operationSecurity[operationId] = spec.security; } } } return operationSecurity; } /** * Find operations without security requirements */ private static findUnsecuredOperations(spec: OpenAPISpec): string[] { const unsecured: string[] = []; if (!spec.paths) { return unsecured; } const hasGlobalSecurity = spec.security && spec.security.length > 0; for (const [path, pathItem] of Object.entries(spec.paths)) { const methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'] as const; for (const method of methods) { const operation = pathItem[method]; if (!operation) continue; const operationId = operation.operationId || `${method.toUpperCase()} ${path}`; // Check if operation has security const hasOperationSecurity = operation.security && operation.security.length > 0; const hasEmptyOperationSecurity = operation.security && operation.security.length === 0; if (hasEmptyOperationSecurity || (!hasOperationSecurity && !hasGlobalSecurity)) { unsecured.push(operationId); } } } return unsecured; } /** * Check if object is a reference */ private static isReferenceObject(obj: any): obj is ReferenceObject { return obj && typeof obj === 'object' && '$ref' in obj; } /** * Get security scheme statistics */ static getSecurityStats(analysis: SecurityAnalysis) { const stats = { totalSchemes: analysis.schemes.length, schemeTypes: {} as Record<string, number>, unusedSchemes: 0, globalSecurityCount: analysis.globalSecurity.length, unsecuredOperationsCount: analysis.unsecuredOperations.length, mostUsedScheme: null as string | null, leastUsedScheme: null as string | null }; // Count scheme types for (const scheme of analysis.schemes) { const type = scheme.scheme.type; stats.schemeTypes[type] = (stats.schemeTypes[type] || 0) + 1; if (scheme.usageCount === 0) { stats.unusedSchemes++; } } // Find most and least used schemes if (analysis.schemes.length > 0) { const sortedByUsage = [...analysis.schemes].sort((a, b) => b.usageCount - a.usageCount); stats.mostUsedScheme = sortedByUsage[0].name; stats.leastUsedScheme = sortedByUsage[sortedByUsage.length - 1].name; } return stats; } /** * Validate security configuration */ static validateSecurityConfig(analysis: SecurityAnalysis): { warnings: string[]; recommendations: string[]; } { const warnings: string[] = []; const recommendations: string[] = []; // Check for unused security schemes const unusedSchemes = analysis.schemes.filter(scheme => scheme.usageCount === 0); if (unusedSchemes.length > 0) { warnings.push(`Found ${unusedSchemes.length} unused security scheme(s): ${unusedSchemes.map(s => s.name).join(', ')}`); } // Check for unsecured operations if (analysis.unsecuredOperations.length > 0) { warnings.push(`Found ${analysis.unsecuredOperations.length} unsecured operation(s)`); recommendations.push('Consider adding security requirements to all operations'); } // Check for weak authentication methods const weakSchemes = analysis.schemes.filter(scheme => scheme.scheme.type === 'apiKey' && scheme.scheme.in === 'query' ); if (weakSchemes.length > 0) { recommendations.push('Avoid using API keys in query parameters; use headers or cookies instead'); } // Check for missing OAuth scopes documentation const oauthSchemes = analysis.schemes.filter(scheme => scheme.scheme.type === 'oauth2'); for (const scheme of oauthSchemes) { if (scheme.scheme.flows) { const hasEmptyScopes = Object.values(scheme.scheme.flows).some(flow => flow && Object.keys(flow.scopes || {}).length === 0 ); if (hasEmptyScopes) { recommendations.push(`OAuth2 scheme '${scheme.name}' should define scopes for better access control`); } } } return { warnings, recommendations }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zaizaizhao/mcp-swagger-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server