308 lines
14 KiB
TypeScript
308 lines
14 KiB
TypeScript
/**
|
|
* ABE — composition root.
|
|
* Wires all modules together and starts the HTTP + WebSocket server.
|
|
*/
|
|
import http from 'http';
|
|
import { Server as SocketIOServer } from 'socket.io';
|
|
|
|
import { loadConfig } from './shared/infrastructure/Config';
|
|
import { createLogger } from './shared/infrastructure/Logger';
|
|
import { createDatabase } from './shared/infrastructure/DatabaseConnection';
|
|
import { InProcessEventBus } from './shared/infrastructure/InProcessEventBus';
|
|
|
|
import { runMigrations } from './db/migrator';
|
|
|
|
// Crawling module
|
|
import { KyselyCrawlSessionRepository } from './modules/crawling/infrastructure/repositories/KyselyCrawlSessionRepository';
|
|
import { KyselyStateRepository } from './modules/crawling/infrastructure/repositories/KyselyStateRepository';
|
|
import { StartCrawlCommand } from './modules/crawling/application/commands/StartCrawlCommand';
|
|
import { StopCrawlCommand } from './modules/crawling/application/commands/StopCrawlCommand';
|
|
import { GetSessionQuery } from './modules/crawling/application/queries/GetSessionQuery';
|
|
import { ListSessionsQuery } from './modules/crawling/application/queries/ListSessionsQuery';
|
|
|
|
// Findings module
|
|
import { KyselyFindingRepository } from './modules/findings/infrastructure/repositories/KyselyFindingRepository';
|
|
import { CreateFindingCommand } from './modules/findings/application/commands/CreateFindingCommand';
|
|
import { EnrichFindingCommand } from './modules/findings/application/commands/EnrichFindingCommand';
|
|
import { ResolveFindingCommand } from './modules/findings/application/commands/ResolveFindingCommand';
|
|
import { GetFindingQuery } from './modules/findings/application/queries/GetFindingQuery';
|
|
import { ListFindingsQuery } from './modules/findings/application/queries/ListFindingsQuery';
|
|
import { FindingStatsQuery } from './modules/findings/application/queries/FindingStatsQuery';
|
|
import { OnAnomalyDetected } from './modules/findings/application/event-handlers/OnAnomalyDetected';
|
|
import { NullAIEnricher } from './modules/findings/infrastructure/NullAIEnricher';
|
|
|
|
// Fuzzing module
|
|
import { FuzzingEngineAdapter } from './modules/fuzzing/infrastructure/adapters/FuzzingEngineAdapter';
|
|
import { RunFuzzCommand } from './modules/fuzzing/application/commands/RunFuzzCommand';
|
|
import { OnActionExecuted } from './modules/fuzzing/application/event-handlers/OnActionExecuted';
|
|
import { InMemoryFuzzSessionRepository } from './modules/fuzzing/infrastructure/repositories/InMemoryFuzzSessionRepository';
|
|
|
|
// Auth module
|
|
import { KyselyUserRepository } from './modules/auth/infrastructure/repositories/KyselyUserRepository';
|
|
import { KyselyOrganizationRepository } from './modules/auth/infrastructure/repositories/KyselyOrganizationRepository';
|
|
import { KyselyApiKeyRepository } from './modules/auth/infrastructure/repositories/KyselyApiKeyRepository';
|
|
import { KyselySessionRepository } from './modules/auth/infrastructure/repositories/KyselySessionRepository';
|
|
import { RegisterCommand } from './modules/auth/application/commands/RegisterCommand';
|
|
import { LoginCommand } from './modules/auth/application/commands/LoginCommand';
|
|
import { CreateOrganizationCommand } from './modules/auth/application/commands/CreateOrganizationCommand';
|
|
import { InviteMemberCommand } from './modules/auth/application/commands/InviteMemberCommand';
|
|
import { CreateApiKeyCommand } from './modules/auth/application/commands/CreateApiKeyCommand';
|
|
import { GetUserQuery } from './modules/auth/application/queries/GetUserQuery';
|
|
import { ListOrgMembersQuery } from './modules/auth/application/queries/ListOrgMembersQuery';
|
|
import { hashPassword, verifyPassword } from './modules/auth/infrastructure/auth/PasswordService';
|
|
|
|
// Reporting module
|
|
import { KyselyReportRepository } from './modules/reporting/infrastructure/repositories/KyselyReportRepository';
|
|
import { GenerateReportCommand } from './modules/reporting/application/commands/GenerateReportCommand';
|
|
|
|
// Integrations module
|
|
import { KyselyIntegrationRepository } from './modules/integrations/infrastructure/repositories/KyselyIntegrationRepository';
|
|
import { KyselyWebhookEndpointRepository } from './modules/integrations/infrastructure/repositories/KyselyWebhookEndpointRepository';
|
|
import { WebhookDispatcher } from './modules/integrations/infrastructure/webhooks/WebhookDispatcher';
|
|
import { OnFindingCreated } from './modules/integrations/application/event-handlers/OnFindingCreated';
|
|
|
|
// Licensing module
|
|
import { RSALicenseValidator } from './modules/licensing/infrastructure/validators/RSALicenseValidator';
|
|
import { LicenseService } from './modules/licensing/application/LicenseService';
|
|
|
|
// Visual regression module
|
|
import { KyselyVisualBaselineRepository, KyselyVisualComparisonRepository } from './modules/visual-regression/infrastructure/repositories/KyselyVisualRepository';
|
|
import { VisualRegressionAdapter } from './modules/visual-regression/infrastructure/adapters/VisualRegressionAdapter';
|
|
import { ApproveBaselineCommand } from './modules/visual-regression/application/commands/ApproveBaselineCommand';
|
|
import { RejectComparisonCommand } from './modules/visual-regression/application/commands/RejectComparisonCommand';
|
|
import { ApproveAllNewStatesCommand } from './modules/visual-regression/application/commands/ApproveAllNewStatesCommand';
|
|
import { ListComparisonsQuery } from './modules/visual-regression/application/queries/ListComparisonsQuery';
|
|
import { LocalStorageProvider } from './shared/infrastructure/StorageProvider';
|
|
import path from 'path';
|
|
|
|
// Scheduling module
|
|
import { KyselyScheduleRepository } from './modules/scheduling/infrastructure/repositories/KyselyScheduleRepository';
|
|
import { CreateScheduleCommand } from './modules/scheduling/application/commands/CreateScheduleCommand';
|
|
import { ToggleScheduleCommand } from './modules/scheduling/application/commands/ToggleScheduleCommand';
|
|
import { DeleteScheduleCommand } from './modules/scheduling/application/commands/DeleteScheduleCommand';
|
|
import { ListSchedulesQuery } from './modules/scheduling/application/queries/ListSchedulesQuery';
|
|
import { SchedulingService } from './modules/scheduling/application/SchedulingService';
|
|
|
|
// Job queue
|
|
import { SQLiteJobQueue } from './jobs/SQLiteJobQueue';
|
|
import { createExplorationJobHandler, EXPLORATION_JOB_TYPE } from './jobs/workers/ExplorationWorker';
|
|
import { createReportJobHandler, REPORT_JOB_TYPE } from './jobs/workers/ReportWorker';
|
|
|
|
// API + Realtime
|
|
import { createServer } from './api/server';
|
|
import { SocketGateway } from './realtime/SocketGateway';
|
|
|
|
async function bootstrap(): Promise<void> {
|
|
// Startup probe — measure total boot time
|
|
const startupAt = Date.now();
|
|
|
|
// 1. Config
|
|
const config = loadConfig();
|
|
|
|
// 2. Logger
|
|
const logger = createLogger({ level: config.log.level, nodeEnv: config.nodeEnv });
|
|
logger.info({ port: config.port, env: config.nodeEnv }, 'Starting ABE...');
|
|
|
|
// 3. Database + migrations
|
|
const db = createDatabase(config.db);
|
|
await runMigrations(db);
|
|
logger.info('Database migrations applied');
|
|
|
|
// 4. Event bus
|
|
const eventBus = new InProcessEventBus(logger);
|
|
|
|
// 5. Repositories
|
|
const sessionRepo = new KyselyCrawlSessionRepository(db);
|
|
const stateRepo = new KyselyStateRepository(db);
|
|
const findingRepo = new KyselyFindingRepository(db);
|
|
const reportRepo = new KyselyReportRepository(db);
|
|
const fuzzRepo = new InMemoryFuzzSessionRepository();
|
|
|
|
// Suppress unused warning for stateRepo — used by crawling infrastructure
|
|
void stateRepo;
|
|
|
|
// 6. Crawling use cases
|
|
const startCrawl = new StartCrawlCommand(sessionRepo, eventBus);
|
|
const stopCrawl = new StopCrawlCommand(sessionRepo, eventBus);
|
|
const getSession = new GetSessionQuery(sessionRepo);
|
|
const listSessions = new ListSessionsQuery(sessionRepo);
|
|
|
|
// 7. Findings use cases
|
|
const createFinding = new CreateFindingCommand(findingRepo, eventBus);
|
|
const enricher = new NullAIEnricher();
|
|
const enrichFinding = new EnrichFindingCommand(findingRepo, enricher, eventBus);
|
|
const resolveFinding = new ResolveFindingCommand(findingRepo, eventBus);
|
|
const getFinding = new GetFindingQuery(findingRepo);
|
|
const listFindings = new ListFindingsQuery(findingRepo);
|
|
const findingStats = new FindingStatsQuery(findingRepo);
|
|
|
|
// 8. Fuzzing use cases
|
|
const fuzzerEngine = new FuzzingEngineAdapter({ intensity: 'low', seed: 42 });
|
|
const runFuzz = new RunFuzzCommand(fuzzerEngine, fuzzRepo, eventBus);
|
|
|
|
// 9. Event handlers — subscribe to EventBus
|
|
const onAnomalyDetected = new OnAnomalyDetected(createFinding);
|
|
eventBus.subscribe('crawling.anomaly_detected', onAnomalyDetected);
|
|
|
|
const onActionExecuted = new OnActionExecuted(runFuzz);
|
|
eventBus.subscribe('crawling.action_executed', onActionExecuted);
|
|
|
|
// 10. Auth module
|
|
const userRepo = new KyselyUserRepository(db);
|
|
const orgRepo = new KyselyOrganizationRepository(db);
|
|
const apiKeyRepo = new KyselyApiKeyRepository(db);
|
|
const authSessionRepo = new KyselySessionRepository(db);
|
|
|
|
const registerCommand = new RegisterCommand(userRepo, eventBus, hashPassword);
|
|
const loginCommand = new LoginCommand(userRepo, authSessionRepo, eventBus, verifyPassword);
|
|
const createOrgCommand = new CreateOrganizationCommand(orgRepo, userRepo, eventBus);
|
|
const inviteMemberCommand = new InviteMemberCommand(orgRepo, userRepo, eventBus);
|
|
const createApiKeyCommand = new CreateApiKeyCommand(apiKeyRepo, userRepo);
|
|
const getUserQuery = new GetUserQuery(userRepo);
|
|
const listOrgMembersQuery = new ListOrgMembersQuery(orgRepo, userRepo);
|
|
|
|
// 11. Reporting use cases
|
|
const generateReport = new GenerateReportCommand(reportRepo, eventBus);
|
|
|
|
// 11b. Licensing
|
|
const licenseValidator = new RSALicenseValidator();
|
|
const licenseService = new LicenseService(licenseValidator);
|
|
|
|
// 11c. Integrations (moved from 11d)
|
|
const integrationRepo = new KyselyIntegrationRepository(db);
|
|
const webhookRepo = new KyselyWebhookEndpointRepository(db);
|
|
const webhookDispatcher = new WebhookDispatcher(webhookRepo, logger);
|
|
const onFindingCreated = new OnFindingCreated(integrationRepo, webhookRepo, webhookDispatcher, logger);
|
|
eventBus.subscribe('findings.finding_created', onFindingCreated);
|
|
|
|
// 12. Job queue (created before HTTP server so it can be injected)
|
|
const jobQueue = new SQLiteJobQueue(db, logger, config.jobs.pollIntervalMs);
|
|
jobQueue.registerHandler(
|
|
EXPLORATION_JOB_TYPE,
|
|
createExplorationJobHandler({ sessionRepo, eventBus, logger }),
|
|
);
|
|
jobQueue.registerHandler(REPORT_JOB_TYPE, createReportJobHandler({ logger, reportRepository: reportRepo, findingRepository: findingRepo }));
|
|
jobQueue.start();
|
|
|
|
// 11d. Visual regression module
|
|
const storageBasePath = path.join(process.cwd(), 'data');
|
|
const storageProvider = new LocalStorageProvider(storageBasePath);
|
|
const visualBaselineRepo = new KyselyVisualBaselineRepository(db);
|
|
const visualComparisonRepo = new KyselyVisualComparisonRepository(db);
|
|
const visualRegressionAdapter = new VisualRegressionAdapter(
|
|
storageProvider,
|
|
visualBaselineRepo,
|
|
visualComparisonRepo,
|
|
eventBus
|
|
);
|
|
void visualRegressionAdapter; // used by ExplorationOrchestrator in crawling infra
|
|
const listComparisons = new ListComparisonsQuery(visualComparisonRepo);
|
|
const approveBaseline = new ApproveBaselineCommand(visualComparisonRepo, visualBaselineRepo, eventBus);
|
|
const rejectComparison = new RejectComparisonCommand(visualComparisonRepo);
|
|
const approveAllNewStates = new ApproveAllNewStatesCommand(visualComparisonRepo, visualBaselineRepo, eventBus);
|
|
|
|
// 12b. Scheduling module (after job queue, since it enqueues jobs)
|
|
const scheduleRepo = new KyselyScheduleRepository(db);
|
|
const createSchedule = new CreateScheduleCommand(scheduleRepo, eventBus);
|
|
const toggleSchedule = new ToggleScheduleCommand(scheduleRepo, eventBus);
|
|
const deleteSchedule = new DeleteScheduleCommand(scheduleRepo, eventBus);
|
|
const listSchedules = new ListSchedulesQuery(scheduleRepo);
|
|
const schedulingService = new SchedulingService(scheduleRepo, jobQueue, eventBus, logger);
|
|
await schedulingService.start();
|
|
|
|
// 13. HTTP server
|
|
const app = createServer({
|
|
config,
|
|
logger,
|
|
db,
|
|
crawlingDeps: { startCrawl, stopCrawl, getSession, listSessions },
|
|
findingsDeps: { getFinding, listFindings, findingStats, resolveFinding, enrichFinding },
|
|
fuzzingDeps: { runFuzz, repository: fuzzRepo },
|
|
reportingDeps: { generateReport, reportRepository: reportRepo, jobQueue },
|
|
integrationsDeps: { integrationRepo, webhookRepo },
|
|
schedulingDeps: { createSchedule, toggleSchedule, deleteSchedule, listSchedules, schedulingService, scheduleRepo },
|
|
visualRegressionDeps: { listComparisons, approveBaseline, rejectComparison, approveAllNewStates },
|
|
licenseService,
|
|
authDeps: {
|
|
registerCommand,
|
|
loginCommand,
|
|
createOrgCommand,
|
|
inviteMemberCommand,
|
|
createApiKeyCommand,
|
|
getUserQuery,
|
|
listOrgMembersQuery,
|
|
sessionRepository: authSessionRepo,
|
|
apiKeyRepository: apiKeyRepo,
|
|
userRepository: userRepo,
|
|
},
|
|
});
|
|
|
|
const httpServer = http.createServer(app);
|
|
|
|
// 12. Socket.io + gateway
|
|
const io = new SocketIOServer(httpServer, {
|
|
cors: { origin: config.cors.origin, credentials: true },
|
|
});
|
|
const gateway = new SocketGateway(io, eventBus, logger);
|
|
gateway.start();
|
|
|
|
// 13. Start listening
|
|
await new Promise<void>((resolve) => {
|
|
httpServer.listen(config.port, config.host, resolve);
|
|
});
|
|
const startupMs = Date.now() - startupAt;
|
|
logger.info({ port: config.port, host: config.host, startupMs }, 'ABE server ready');
|
|
|
|
// 14. Graceful shutdown
|
|
let shuttingDown = false;
|
|
|
|
async function shutdown(signal: string): Promise<void> {
|
|
if (shuttingDown) return;
|
|
shuttingDown = true;
|
|
|
|
logger.info({ signal }, 'Shutting down...');
|
|
|
|
// Stop accepting new connections
|
|
httpServer.close();
|
|
|
|
// Close socket.io
|
|
io.close();
|
|
|
|
// Stop scheduling service
|
|
schedulingService.stop();
|
|
|
|
// Stop job queue and wait for active jobs
|
|
jobQueue.pause();
|
|
await jobQueue.waitForActive(30_000);
|
|
|
|
// Close database
|
|
try {
|
|
await db.destroy();
|
|
} catch (err) {
|
|
logger.warn({ err }, 'Error closing database');
|
|
}
|
|
|
|
logger.info('Shutdown complete');
|
|
process.exit(0);
|
|
}
|
|
|
|
// Force-exit if graceful shutdown takes too long
|
|
function forceExit(signal: string): void {
|
|
void shutdown(signal).catch(() => {
|
|
process.exit(1);
|
|
});
|
|
setTimeout(() => {
|
|
logger.error('Forced shutdown after 30s');
|
|
process.exit(1);
|
|
}, 30_000).unref();
|
|
}
|
|
|
|
process.on('SIGTERM', () => forceExit('SIGTERM'));
|
|
process.on('SIGINT', () => forceExit('SIGINT'));
|
|
}
|
|
|
|
bootstrap().catch((err: unknown) => {
|
|
console.error('Fatal: failed to start ABE', err);
|
|
process.exit(1);
|
|
});
|