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:
90
services/darkwatch/src/hibp/HIBPService.ts
Normal file
90
services/darkwatch/src/hibp/HIBPService.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user