Files
dd0c/products/04-lightweight-idp/src/discovery/apigateway-scanner.ts
Max cfe269a031
Some checks failed
CI — P4 Portal / test (push) Failing after 32s
CI — P4 Portal / build-push (push) Has been skipped
feat(portal): add CloudFormation/APIGateway scanners, analytics endpoints, search caching
- CloudFormation scanner: discovers stacks and maps resources to services
- API Gateway scanner: discovers REST/HTTP APIs and routes
- Analytics API: ownership coverage, health scorecards, tech debt indicators
- Redis prefix cache for Cmd+K search (60s TTL)
- 005_analytics.sql migration for aggregation helpers
2026-03-03 06:36:24 +00:00

204 lines
5.8 KiB
TypeScript

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<ScanResult> {
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<DiscoveredService[]> {
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<string, string>;
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<string[]> {
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<DiscoveredService[]> {
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<string, string>;
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<string[]> {
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;
}
}