FRE-4471: Scaffold DarkWatch MVP — monorepo, schema, services, API routes, tests
- Turborepo monorepo structure (packages: api, db, types, jobs; services: darkwatch) - Prisma schema: User, WatchListItem, Exposure, Alert, ScanJob models - WatchListService: CRUD with normalization, dedup, tier-based limits - HIBPService: API integration with severity scoring - MatchingEngine: exact-match with content hash dedup - AlertPipeline: dedup window, email notifications - ScanService: orchestrates watch list -> HIBP -> match -> alert flow - BullMQ job workers for scan and alert processing - Fastify API routes: watchlist, exposures, alerts, scan - Docker Compose: PostgreSQL 16 + Redis 7 - 15 unit tests passing - Implementation plan document uploaded
This commit is contained in:
88
services/darkwatch/src/scanner/ScanService.ts
Normal file
88
services/darkwatch/src/scanner/ScanService.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import prisma from "@shieldai/db";
|
||||
import { WatchListService } from "./watchlist/WatchListService";
|
||||
import { HIBPService } from "./hibp/HIBPService";
|
||||
import { MatchingEngine } from "./matching/MatchingEngine";
|
||||
import { AlertPipeline } from "./alerts/AlertPipeline";
|
||||
import { DataSource, ScanJobStatus } from "@shieldai/types";
|
||||
|
||||
export class ScanService {
|
||||
private watchList: WatchListService;
|
||||
private hibp: HIBPService;
|
||||
private matching: MatchingEngine;
|
||||
private alerts: AlertPipeline;
|
||||
|
||||
constructor() {
|
||||
this.watchList = new WatchListService();
|
||||
this.hibp = new HIBPService(process.env.HIBP_API_KEY);
|
||||
this.matching = new MatchingEngine();
|
||||
this.alerts = new AlertPipeline();
|
||||
}
|
||||
|
||||
async runScan(userId: string, source?: DataSource): Promise<number> {
|
||||
const job = await prisma.scanJob.create({
|
||||
data: {
|
||||
userId,
|
||||
status: ScanJobStatus.RUNNING,
|
||||
source: source || DataSource.HIBP,
|
||||
},
|
||||
});
|
||||
|
||||
let resultCount = 0;
|
||||
|
||||
try {
|
||||
const activeItems = await this.watchList.getActiveItems(userId);
|
||||
|
||||
for (const item of activeItems) {
|
||||
if (item.identifierType === "EMAIL") {
|
||||
const breaches = await this.hibp.checkEmail(item.identifierValue);
|
||||
for (const breach of breaches) {
|
||||
const severity = this.hibp.getSeverity(breach);
|
||||
const matched = await this.matching.matchExposure(
|
||||
item.id,
|
||||
DataSource.HIBP,
|
||||
breach.name,
|
||||
breach.breachDate,
|
||||
breach.dataClasses,
|
||||
severity,
|
||||
`Breach: ${breach.title}. Domain: ${breach.domain}. PwnCount: ${breach.pwnCount}`
|
||||
);
|
||||
|
||||
if (matched) {
|
||||
resultCount++;
|
||||
await this.alerts.createAlert(userId, matched.dataSource + ":" + breach.name, severity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.scanJob.update({
|
||||
where: { id: job.id },
|
||||
data: {
|
||||
status: ScanJobStatus.COMPLETED,
|
||||
resultCount,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await prisma.scanJob.update({
|
||||
where: { id: job.id },
|
||||
data: {
|
||||
status: ScanJobStatus.FAILED,
|
||||
errorMessage: message,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return resultCount;
|
||||
}
|
||||
|
||||
async getScanHistory(userId: string, limit = 20) {
|
||||
return prisma.scanJob.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user