Add P2 SaaS CI, P4 scheduled discovery, P6 agent bridge (Redis pub/sub), Caddyfile

- P2: Gitea Actions CI for SaaS backend (separate from Go agent CI)
- P4: ScheduledDiscovery with Redis distributed lock to prevent concurrent scans
- P6: AgentBridge — Redis pub/sub for SaaS↔agent communication (approvals + step results)
- Caddyfile: self-hosted reverse proxy with auto-TLS for all 6 products
This commit is contained in:
2026-03-01 03:16:33 +00:00
parent bbbea3519e
commit 2ceeac1a11
4 changed files with 242 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
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);
}
}
}