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
This commit is contained in:
@@ -3,11 +3,15 @@ import { Redis } from 'ioredis';
|
||||
import { Pool } from 'pg';
|
||||
import { AwsDiscoveryScanner } from './aws-scanner.js';
|
||||
import { GitHubDiscoveryScanner } from './github-scanner.js';
|
||||
import { CloudFormationDiscoveryScanner } from './cloudformation-scanner.js';
|
||||
import { ApiGatewayDiscoveryScanner } from './apigateway-scanner.js';
|
||||
import { CatalogService } from '../catalog/service.js';
|
||||
import { withTenant } from '../data/db.js';
|
||||
|
||||
const logger = pino({ name: 'scheduled-discovery' });
|
||||
|
||||
export type ScannerType = 'aws' | 'github' | 'cloudformation' | 'apigateway';
|
||||
|
||||
export class ScheduledDiscovery {
|
||||
private redis: Redis;
|
||||
private pool: Pool;
|
||||
@@ -30,7 +34,7 @@ export class ScheduledDiscovery {
|
||||
await this.redis.del(key);
|
||||
}
|
||||
|
||||
async runScan(tenantId: string, scanner: 'aws' | 'github'): Promise<{ status: string; discovered: number }> {
|
||||
async runScan(tenantId: string, scanner: ScannerType): Promise<{ status: string; discovered: number }> {
|
||||
const locked = await this.acquireLock(tenantId, scanner);
|
||||
if (!locked) {
|
||||
logger.info({ tenantId, scanner }, 'Scan already in progress — skipping');
|
||||
@@ -50,28 +54,38 @@ export class ScheduledDiscovery {
|
||||
const catalog = new CatalogService(this.pool);
|
||||
|
||||
if (scanner === 'aws') {
|
||||
// Get tenant's AWS config (region, credentials would come from tenant settings)
|
||||
const awsScanner = new AwsDiscoveryScanner('us-east-1');
|
||||
const result = await awsScanner.scan(tenantId);
|
||||
const isPartial = result.status === 'partial_failure';
|
||||
|
||||
const merged = await catalog.mergeAwsDiscovery(tenantId, result.services, isPartial);
|
||||
|
||||
await this.recordScanResult(tenantId, scanner, result.status, result.discovered, result.errors);
|
||||
return { status: result.status, discovered: merged };
|
||||
} else {
|
||||
// GitHub scan — would need org name + token from tenant settings
|
||||
} else if (scanner === 'github') {
|
||||
const { Octokit } = await import('@octokit/rest');
|
||||
const octokit = new Octokit(); // Would use tenant's GitHub token
|
||||
const octokit = new Octokit();
|
||||
const ghScanner = new GitHubDiscoveryScanner(octokit);
|
||||
const result = await ghScanner.scan(tenantId); // Would use tenant's org name
|
||||
const result = await ghScanner.scan(tenantId);
|
||||
const isPartial = result.status === 'partial_failure';
|
||||
|
||||
const merged = await catalog.mergeGitHubDiscovery(tenantId, result.repos, isPartial);
|
||||
|
||||
await this.recordScanResult(tenantId, scanner, result.status, result.discovered, result.errors);
|
||||
return { status: result.status, discovered: merged };
|
||||
} else if (scanner === 'cloudformation') {
|
||||
const cfnScanner = new CloudFormationDiscoveryScanner('us-east-1');
|
||||
const result = await cfnScanner.scan(tenantId);
|
||||
const isPartial = result.status === 'partial_failure';
|
||||
const merged = await catalog.mergeAwsDiscovery(tenantId, result.services, isPartial);
|
||||
await this.recordScanResult(tenantId, scanner, result.status, result.discovered, result.errors);
|
||||
return { status: result.status, discovered: merged };
|
||||
} else if (scanner === 'apigateway') {
|
||||
const apigwScanner = new ApiGatewayDiscoveryScanner('us-east-1');
|
||||
const result = await apigwScanner.scan(tenantId);
|
||||
const isPartial = result.status === 'partial_failure';
|
||||
const merged = await catalog.mergeAwsDiscovery(tenantId, result.services, isPartial);
|
||||
await this.recordScanResult(tenantId, scanner, result.status, result.discovered, result.errors);
|
||||
return { status: result.status, discovered: merged };
|
||||
}
|
||||
|
||||
return { status: 'failed', discovered: 0 };
|
||||
} catch (err) {
|
||||
logger.error({ tenantId, scanner, error: (err as Error).message }, 'Scheduled scan failed');
|
||||
await this.recordScanResult(tenantId, scanner, 'failed', 0, [(err as Error).message]);
|
||||
|
||||
Reference in New Issue
Block a user