import pino from 'pino'; import { APIGatewayClient, GetRestApisCommand, GetResourcesCommand, } from '@aws-sdk/client-api-gateway'; import { ApiGatewayV2Client, GetApisCommand, GetRoutesCommand, } from '@aws-sdk/client-apigatewayv2'; import type { DiscoveredService, ScanResult } from './aws-scanner.js'; const logger = pino({ name: 'discovery-apigateway' }); export class ApiGatewayDiscoveryScanner { private apigwClient: APIGatewayClient; private apigwV2Client: ApiGatewayV2Client; private region: string; constructor(region: string, credentials?: any) { const config = { region, ...(credentials ? { credentials } : {}) }; this.apigwClient = new APIGatewayClient(config); this.apigwV2Client = new ApiGatewayV2Client(config); this.region = region; } async scan(account: string): Promise { const services: DiscoveredService[] = []; const errors: string[] = []; try { const restApis = await this.scanRestApis(account); services.push(...restApis); } catch (err) { errors.push(`REST API scan failed: ${(err as Error).message}`); logger.warn({ region: this.region, error: (err as Error).message }, 'REST API scan failed'); } try { const httpApis = await this.scanHttpApis(account); services.push(...httpApis); } catch (err) { errors.push(`HTTP API scan failed: ${(err as Error).message}`); logger.warn({ region: this.region, error: (err as Error).message }, 'HTTP API scan failed'); } const status = errors.length === 0 ? 'success' : services.length > 0 ? 'partial_failure' : 'failed'; return { status, discovered: services.length, errors, services }; } private async scanRestApis(account: string): Promise { const results: DiscoveredService[] = []; let position: string | undefined; do { const response = await this.apigwClient.send(new GetRestApisCommand({ position, limit: 500, })); for (const api of response.items ?? []) { const tags = (api.tags ?? {}) as Record; const owner = tags['owner'] ?? tags['team'] ?? tags['Owner'] ?? tags['Team']; let routes: string[] = []; try { routes = await this.getRestApiRoutes(api.id!); } catch (err) { logger.warn({ apiId: api.id, error: (err as Error).message }, 'Failed to list REST API resources'); } const arn = `arn:aws:apigateway:${this.region}::/restapis/${api.id}`; results.push({ name: api.name!, type: 'apigateway-rest', arn, region: this.region, account, tags, owner, ownerSource: owner ? 'aws-tag' : undefined, metadata: { apiId: api.id, description: api.description, createdDate: api.createdDate, endpointConfiguration: api.endpointConfiguration?.types, routeCount: routes.length, routes: routes.slice(0, 100), }, discoveredAt: new Date(), }); } position = response.position; } while (position); return results; } private async getRestApiRoutes(restApiId: string): Promise { const routes: string[] = []; let position: string | undefined; do { const response = await this.apigwClient.send(new GetResourcesCommand({ restApiId, position, limit: 500, })); for (const resource of response.items ?? []) { if (resource.resourceMethods) { for (const method of Object.keys(resource.resourceMethods)) { routes.push(`${method} ${resource.path}`); } } } position = response.position; } while (position); return routes; } private async scanHttpApis(account: string): Promise { const results: DiscoveredService[] = []; let nextToken: string | undefined; do { const response = await this.apigwV2Client.send(new GetApisCommand({ NextToken: nextToken, MaxResults: '100', })); for (const api of response.Items ?? []) { const tags = (api.Tags ?? {}) as Record; const owner = tags['owner'] ?? tags['team'] ?? tags['Owner'] ?? tags['Team']; let routes: string[] = []; try { routes = await this.getHttpApiRoutes(api.ApiId!); } catch (err) { logger.warn({ apiId: api.ApiId, error: (err as Error).message }, 'Failed to list HTTP API routes'); } const arn = `arn:aws:apigateway:${this.region}::/apis/${api.ApiId}`; results.push({ name: api.Name!, type: `apigateway-${(api.ProtocolType ?? 'HTTP').toLowerCase()}`, arn, region: this.region, account, tags, owner, ownerSource: owner ? 'aws-tag' : undefined, metadata: { apiId: api.ApiId, description: api.Description, protocolType: api.ProtocolType, apiEndpoint: api.ApiEndpoint, createdDate: api.CreatedDate, routeCount: routes.length, routes: routes.slice(0, 100), }, discoveredAt: new Date(), }); } nextToken = response.NextToken; } while (nextToken); return results; } private async getHttpApiRoutes(apiId: string): Promise { const routes: string[] = []; let nextToken: string | undefined; do { const response = await this.apigwV2Client.send(new GetRoutesCommand({ ApiId: apiId, NextToken: nextToken, MaxResults: '100', })); for (const route of response.Items ?? []) { routes.push(route.RouteKey ?? 'unknown'); } nextToken = response.NextToken; } while (nextToken); return routes; } }