import { prisma, ExposureSource, ExposureSeverity, WatchlistType } from '@shieldsai/shared-db'; import { createHash } from 'crypto'; function hashIdentifier(identifier: string): string { return createHash('sha256').update(identifier.toLowerCase().trim()).digest('hex'); } function determineSeverity( source: ExposureSource, dataType: WatchlistType ): ExposureSeverity { const criticalSources = [ExposureSource.darkWebForum, ExposureSource.honeypot]; const warningSources = [ExposureSource.hibp, ExposureSource.shodan]; const criticalTypes = [WatchlistType.ssn]; if (criticalTypes.includes(dataType)) return ExposureSeverity.critical; if (criticalSources.includes(source)) return ExposureSeverity.critical; if (warningSources.includes(source)) return ExposureSeverity.warning; return ExposureSeverity.info; } export class ScanService { async checkHIBP(email: string): Promise<{ exposed: boolean; sources: string[] }> { try { const response = await fetch( `https://hibp.com/api/v2/${encodeURIComponent(email)}`, { headers: { 'hibp-api-key': process.env.HIBP_API_KEY || '', Accept: 'application/json', }, signal: AbortSignal.timeout(15000), } ); if (response.status === 404) { return { exposed: false, sources: [] }; } if (!response.ok) { console.error(`[ScanService:HIBP] Status ${response.status} for ${email}`); return { exposed: false, sources: [] }; } const data = await response.json(); const sources = Array.isArray(data) ? data.map((p: { Name: string }) => p.Name) : []; return { exposed: sources.length > 0, sources }; } catch (error) { console.error('[ScanService:HIBP] Error:', error); return { exposed: false, sources: [] }; } } async checkShodan(domain: string): Promise<{ exposed: boolean; ports: string[]; ips: string[] }> { try { const response = await fetch( `https://api.shodan.io/shodan/host/${encodeURIComponent(domain)}`, { headers: { Authorization: `Bearer ${process.env.SHODAN_API_KEY || ''}`, }, signal: AbortSignal.timeout(15000), } ); if (response.status === 404) { return { exposed: false, ports: [], ips: [] }; } if (!response.ok) { console.error(`[ScanService:Shodan] Status ${response.status} for ${domain}`); return { exposed: false, ports: [], ips: [] }; } const data = await response.json(); return { exposed: !!data.ip_str, ports: data.ports?.map(String) || [], ips: [data.ip_str || ''], }; } catch (error) { console.error('[ScanService:Shodan] Error:', error); return { exposed: false, ports: [], ips: [] }; } } async processSubscriptionScan( subscriptionId: string, watchlistItems: Awaited> ): Promise<{ exposuresCreated: number; exposuresUpdated: number }> { let exposuresCreated = 0; let exposuresUpdated = 0; for (const item of watchlistItems) { const identifier = item.value; const identifierHash = hashIdentifier(identifier); switch (item.type) { case WatchlistType.email: { const hibpResult = await this.checkHIBP(identifier); if (hibpResult.exposed) { for (const source of hibpResult.sources) { const existing = await prisma.exposure.findFirst({ where: { subscriptionId, source: ExposureSource.hibp, identifierHash, metadata: { path: ['dbName'], equals: source }, }, }); if (existing) { await prisma.exposure.update({ where: { id: existing.id }, data: { detectedAt: new Date() }, }); exposuresUpdated++; } else { await prisma.exposure.create({ data: { subscriptionId, watchlistItemId: item.id, source: ExposureSource.hibp, dataType: item.type, identifier, identifierHash, severity: determineSeverity(ExposureSource.hibp, item.type), isFirstTime: true, metadata: { dbName: source }, detectedAt: new Date(), }, }); exposuresCreated++; } } } break; } case WatchlistType.domain: { const shodanResult = await this.checkShodan(identifier); if (shodanResult.exposed) { const existing = await prisma.exposure.findFirst({ where: { subscriptionId, source: ExposureSource.shodan, identifierHash, }, }); if (existing) { await prisma.exposure.update({ where: { id: existing.id }, data: { detectedAt: new Date(), metadata: { ports: shodanResult.ports, ips: shodanResult.ips }, }, }); exposuresUpdated++; } else { await prisma.exposure.create({ data: { subscriptionId, watchlistItemId: item.id, source: ExposureSource.shodan, dataType: item.type, identifier, identifierHash, severity: determineSeverity(ExposureSource.shodan, item.type), isFirstTime: true, metadata: { ports: shodanResult.ports, ips: shodanResult.ips }, detectedAt: new Date(), }, }); exposuresCreated++; } } break; } default: { const existing = await prisma.exposure.findFirst({ where: { subscriptionId, watchlistItemId: item.id, identifierHash }, }); if (!existing) { await prisma.exposure.create({ data: { subscriptionId, watchlistItemId: item.id, source: ExposureSource.darkWebForum, dataType: item.type, identifier, identifierHash, severity: determineSeverity(ExposureSource.darkWebForum, item.type), isFirstTime: true, detectedAt: new Date(), }, }); exposuresCreated++; } break; } } } return { exposuresCreated, exposuresUpdated }; } async getWatchlistItems(subscriptionId: string) { return prisma.watchlistItem.findMany({ where: { subscriptionId, isActive: true }, }); } } export const scanService = new ScanService();