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:
104
services/darkwatch/src/matching/MatchingEngine.ts
Normal file
104
services/darkwatch/src/matching/MatchingEngine.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import prisma from "@shieldai/db";
|
||||
import { ExposureResult, DataSource, Severity } from "@shieldai/types";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
export class MatchingEngine {
|
||||
async matchExposure(
|
||||
watchListItemId: string,
|
||||
dataSource: DataSource,
|
||||
breachName: string,
|
||||
exposedAt: Date,
|
||||
dataType: string[],
|
||||
severity: Severity,
|
||||
details?: string
|
||||
): Promise<ExposureResult | null> {
|
||||
const contentHash = this.computeContentHash(dataSource, breachName, watchListItemId);
|
||||
|
||||
const existing = await prisma.exposure.findUnique({
|
||||
where: { contentHash },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const exposure = await prisma.exposure.create({
|
||||
data: {
|
||||
watchListItemId,
|
||||
dataSource,
|
||||
breachName,
|
||||
exposedAt,
|
||||
dataType,
|
||||
severity,
|
||||
details,
|
||||
contentHash,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
dataSource: exposure.dataSource,
|
||||
breachName: exposure.breachName,
|
||||
exposedAt: exposure.exposedAt,
|
||||
dataType: exposure.dataType,
|
||||
severity: exposure.severity,
|
||||
details: exposure.details || "",
|
||||
};
|
||||
}
|
||||
|
||||
async getExposuresForUser(userId: string): Promise<ExposureResult[]> {
|
||||
const items = await prisma.watchListItem.findMany({
|
||||
where: { userId },
|
||||
include: { exposures: true },
|
||||
});
|
||||
|
||||
const results: ExposureResult[] = [];
|
||||
for (const item of items) {
|
||||
for (const exp of item.exposures) {
|
||||
results.push({
|
||||
dataSource: exp.dataSource,
|
||||
breachName: exp.breachName,
|
||||
exposedAt: exp.exposedAt,
|
||||
dataType: exp.dataType,
|
||||
severity: exp.severity,
|
||||
details: exp.details || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results.sort((a, b) => {
|
||||
const severityOrder = { CRITICAL: 0, WARNING: 1, INFO: 2 };
|
||||
return severityOrder[a.severity] - severityOrder[b.severity];
|
||||
});
|
||||
}
|
||||
|
||||
async getExposureById(exposureId: string): Promise<ExposureResult | null> {
|
||||
const exposure = await prisma.exposure.findUnique({
|
||||
where: { id: exposureId },
|
||||
include: { watchListItem: true },
|
||||
});
|
||||
|
||||
if (!exposure) return null;
|
||||
|
||||
return {
|
||||
dataSource: exposure.dataSource,
|
||||
breachName: exposure.breachName,
|
||||
exposedAt: exposure.exposedAt,
|
||||
dataType: exposure.dataType,
|
||||
severity: exposure.severity,
|
||||
details: exposure.details || "",
|
||||
};
|
||||
}
|
||||
|
||||
countExposures(userId: string): Promise<number> {
|
||||
return prisma.exposure.count({
|
||||
where: {
|
||||
watchListItem: { userId },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
computeContentHash(dataSource: DataSource, breachName: string, watchListItemId: string): string {
|
||||
const raw = `${dataSource}:${breachName}:${watchListItemId}`;
|
||||
return createHash("sha256").update(raw).digest("hex");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user