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:
Senior Engineer
2026-04-29 09:47:45 -04:00
committed by Michael Freno
parent f8f90502fa
commit 218de3b03b
40 changed files with 5225 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
import { DataSource, Severity } from "@shieldai/types";
export interface HIBPBreach {
name: string;
title: string;
domain: string;
loginCount: number;
passwordCount: number;
date: Date;
breachDate: Date;
addedDate: Date;
pwnCount: number;
dataClasses: string[];
logo: string;
}
export class HIBPService {
private baseUrl = "https://haveibeenpwned.com/api/v3";
private apiKey?: string;
constructor(apiKey?: string) {
this.apiKey = apiKey;
}
async checkEmail(email: string): Promise<HIBPBreach[]> {
const url = `${this.baseUrl}/breached-account/${encodeURIComponent(email)}`;
const headers: Record<string, string> = {
"User-Agent": "ShieldAI-DarkWatch/1.0",
"HIBP-API-Version": "3",
};
if (this.apiKey) {
headers["hibp-api-key"] = this.apiKey;
}
const response = await fetch(url, { headers });
if (response.status === 404) {
return [];
}
if (response.status === 410) {
throw new Error(`Email not found in HIBP: ${email}`);
}
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get("Retry-After") || "60", 10);
throw new Error(`HIBP rate limited. Retry after ${retryAfter}s`);
}
if (!response.ok) {
throw new Error(`HIBP API error: ${response.status} ${response.statusText}`);
}
return response.json() as Promise<HIBPBreach[]>;
}
async rangeQuery(hashPrefix: string): Promise<string[]> {
const url = `${this.baseUrl}/range/${hashPrefix}`;
const response = await fetch(url, {
headers: { "User-Agent": "ShieldAI-DarkWatch/1.0" },
});
if (!response.ok) {
throw new Error(`HIBP range API error: ${response.status}`);
}
const text = await response.text();
return text.split("\n").map((line) => line.split(":")[0].toUpperCase());
}
getSeverity(breach: HIBPBreach): Severity {
const criticalClasses = ["Password", "Email Address", "Bank Account", "Credit Card", "Social Security Number"];
const warningClasses = ["Phone Number", "IP Address", "Geolocation", "IP & User agent"];
const hasCritical = breach.dataClasses.some((c) => criticalClasses.includes(c));
const hasWarning = breach.dataClasses.some((c) => warningClasses.includes(c));
const breachAge = (Date.now() - breach.breachDate.getTime()) / (1000 * 60 * 60 * 24);
if (hasCritical) return Severity.CRITICAL;
if (hasWarning) return Severity.WARNING;
if (breachAge > 365) return Severity.INFO;
return Severity.WARNING;
}
mapToDataSource(): DataSource {
return DataSource.HIBP;
}
}