Files
ShieldAI/services/darkwatch/src/scanner/ScanService.ts
Senior Engineer 218de3b03b 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
2026-04-29 09:47:45 -04:00

89 lines
2.5 KiB
TypeScript

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,
});
}
}