- 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
204 lines
5.8 KiB
TypeScript
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;
|
|
}
|
|
}
|