65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
|
|
import pino from 'pino';
|
||
|
|
import Redis from 'ioredis';
|
||
|
|
import { config } from '../config/index.js';
|
||
|
|
|
||
|
|
const logger = pino({ name: 'scheduled-discovery' });
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Scheduled discovery job — runs AWS + GitHub scans on a cron schedule.
|
||
|
|
* Uses Redis-based distributed lock to prevent concurrent scans.
|
||
|
|
*/
|
||
|
|
export class ScheduledDiscovery {
|
||
|
|
private redis: Redis;
|
||
|
|
private lockTtlMs: number;
|
||
|
|
|
||
|
|
constructor(redis: Redis, lockTtlMs = 10 * 60 * 1000) {
|
||
|
|
this.redis = redis;
|
||
|
|
this.lockTtlMs = lockTtlMs;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Attempt to acquire a distributed lock for a tenant scan.
|
||
|
|
* Returns true if lock acquired, false if another scan is running.
|
||
|
|
*/
|
||
|
|
async acquireLock(tenantId: string, scanner: string): Promise<boolean> {
|
||
|
|
const key = `scan_lock:${tenantId}:${scanner}`;
|
||
|
|
const result = await this.redis.set(key, Date.now().toString(), 'PX', this.lockTtlMs, 'NX');
|
||
|
|
return result === 'OK';
|
||
|
|
}
|
||
|
|
|
||
|
|
async releaseLock(tenantId: string, scanner: string): Promise<void> {
|
||
|
|
const key = `scan_lock:${tenantId}:${scanner}`;
|
||
|
|
await this.redis.del(key);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Run a scheduled scan for a tenant.
|
||
|
|
* Called by cron job or manual trigger.
|
||
|
|
*/
|
||
|
|
async runScan(tenantId: string, scanner: 'aws' | 'github'): Promise<{ status: string; discovered: number }> {
|
||
|
|
const locked = await this.acquireLock(tenantId, scanner);
|
||
|
|
if (!locked) {
|
||
|
|
logger.info({ tenantId, scanner }, 'Scan already in progress — skipping');
|
||
|
|
return { status: 'skipped', discovered: 0 };
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
logger.info({ tenantId, scanner }, 'Starting scheduled scan');
|
||
|
|
|
||
|
|
// TODO: Instantiate appropriate scanner and run
|
||
|
|
// const result = scanner === 'aws'
|
||
|
|
// ? await awsScanner.scan(region, account)
|
||
|
|
// : await githubScanner.scan(org);
|
||
|
|
|
||
|
|
// TODO: Merge results into catalog via CatalogService
|
||
|
|
|
||
|
|
return { status: 'completed', discovered: 0 };
|
||
|
|
} catch (err) {
|
||
|
|
logger.error({ tenantId, scanner, error: (err as Error).message }, 'Scheduled scan failed');
|
||
|
|
return { status: 'failed', discovered: 0 };
|
||
|
|
} finally {
|
||
|
|
await this.releaseLock(tenantId, scanner);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|