- Merged singleton pattern + type exports from shared-db - Kept FieldEncryptionService from original db package - Upgraded to Prisma v6.2.0 (newer version) - Adopted shared-db's complete schema for multi-service platform - Updated 17 consumer imports across darkwatch, voiceprint, jobs, api - Standardized on @shieldai/db namespace Files changed: - packages/db/package.json (v0.1.0 → v0.2.0) - packages/db/src/index.ts (consolidated exports) - packages/db/prisma/schema.prisma (merged schema) - packages/db/prisma/seed.ts (updated for new schema) - 17 consumer files updated Co-Authored-By: Paperclip <noreply@paperclip.ing>
221 lines
7.0 KiB
TypeScript
221 lines
7.0 KiB
TypeScript
import { prisma, ExposureSource, ExposureSeverity, WatchlistType } from '@shieldai/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<ReturnType<ScanService['getWatchlistItems']>>
|
|
): 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();
|