- 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
105 lines
2.7 KiB
TypeScript
105 lines
2.7 KiB
TypeScript
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");
|
|
}
|
|
}
|