Add cross-service alert correlation system FRE-4500
- Unified alert types (AlertSource, AlertCategory, CorrelationStatus, EntityType) - NormalizedAlert and CorrelationGroup Prisma models - AlertNormalizer for all 4 services (DarkWatch, SpamShield, VoicePrint, CallAnalysis) - CorrelationEngine with temporal + entity-based correlation detection - CorrelationService orchestrator with dashboard API - Correlation API routes (/api/v1/correlation/*) - Service emitters wired to DarkWatch, SpamShield, VoicePrint - pnpm workspace config for monorepo
This commit is contained in:
@@ -9,8 +9,9 @@
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@shieldai/db": "0.1.0",
|
||||
"@shieldai/types": "0.1.0",
|
||||
"@shieldai/db": "workspace:*",
|
||||
"@shieldai/types": "workspace:*",
|
||||
"@shieldai/correlation": "workspace:*",
|
||||
"node-cache": "^5.1.2"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import prisma from "@shieldai/db";
|
||||
import { AlertChannel, AlertStatus, Severity } from "@shieldai/types";
|
||||
import { emitDarkWatchAlert } from "@shieldai/correlation";
|
||||
import { createHash } from "crypto";
|
||||
import NodeCache from "node-cache";
|
||||
|
||||
@@ -28,7 +29,7 @@ export class AlertPipeline {
|
||||
|
||||
if (existing) return false;
|
||||
|
||||
await prisma.alert.create({
|
||||
const alert = await prisma.alert.create({
|
||||
data: {
|
||||
userId,
|
||||
exposureId,
|
||||
@@ -39,6 +40,24 @@ export class AlertPipeline {
|
||||
},
|
||||
});
|
||||
|
||||
const exposure = await prisma.exposure.findUnique({
|
||||
where: { id: exposureId },
|
||||
include: { watchListItem: true },
|
||||
});
|
||||
|
||||
if (exposure) {
|
||||
emitDarkWatchAlert(
|
||||
userId,
|
||||
exposureId,
|
||||
alert.id,
|
||||
exposure.breachName,
|
||||
severity,
|
||||
channel,
|
||||
exposure.dataType,
|
||||
exposure.dataSource
|
||||
).catch((err) => console.error(`[Correlation] DarkWatch emit failed:`, err));
|
||||
}
|
||||
|
||||
this.cache.set(dedupKey, true, this.dedupWindowMs / 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@shieldai/db": "0.1.0",
|
||||
"@shieldai/types": "0.1.0",
|
||||
"@shieldai/db": "workspace:*",
|
||||
"@shieldai/types": "workspace:*",
|
||||
"@shieldai/correlation": "workspace:*",
|
||||
"@prisma/client": "^6.2.0",
|
||||
"libphonenumber-js": "^1.10.50",
|
||||
"ws": "^8.16.0"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PrismaClient, SpamFeedback, SpamRule, SpamAuditLog } from '@prisma/client';
|
||||
import { FieldEncryptionService } from '@shieldai/db';
|
||||
import { generateRequestId } from '@shieldai/types';
|
||||
import { emitSpamShieldAlert } from '@shieldai/correlation';
|
||||
import { spamConfig, spamFeatureFlags } from '../config/spamshield.config';
|
||||
import { CircuitBreaker, CircuitBreakerError, CircuitState, CircuitBreakerMetrics } from '../circuit-breaker';
|
||||
import { validatePhoneNumber as validateE164 } from '../utils/phone-validation';
|
||||
@@ -213,7 +214,7 @@ export class SpamShieldService {
|
||||
confidence = Math.min(confidence, 1.0);
|
||||
const decision = confidence > 0.8 ? 'BLOCK' : confidence > 0.5 ? 'FLAG' : 'ALLOW';
|
||||
|
||||
await prisma.spamAuditLog.create({
|
||||
const auditLog = await prisma.spamAuditLog.create({
|
||||
data: {
|
||||
userId: 'system',
|
||||
phoneNumber: validated,
|
||||
@@ -223,6 +224,17 @@ export class SpamShieldService {
|
||||
},
|
||||
});
|
||||
|
||||
if (decision === 'BLOCK' || decision === 'FLAG') {
|
||||
emitSpamShieldAlert(
|
||||
'system',
|
||||
auditLog.id,
|
||||
validated,
|
||||
decision,
|
||||
confidence,
|
||||
ruleMatches
|
||||
).catch((err) => console.error(`[Correlation] SpamShield emit failed:`, err));
|
||||
}
|
||||
|
||||
return { decision, confidence, ruleMatches };
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@shieldai/db": "0.1.0",
|
||||
"@shieldai/types": "0.1.0",
|
||||
"@shieldai/db": "workspace:*",
|
||||
"@shieldai/types": "workspace:*",
|
||||
"@shieldai/correlation": "workspace:*",
|
||||
"node-cache": "^5.1.2"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -2,6 +2,7 @@ import prisma from "@shieldai/db";
|
||||
import { AudioPreprocessor, AudioFeatures } from "../preprocessor/AudioPreprocessor";
|
||||
import { EmbeddingService, EmbeddingOutput } from "../embedding/EmbeddingService";
|
||||
import { VoiceEnrollmentService } from "../enrollment/VoiceEnrollmentService";
|
||||
import { emitVoicePrintAlert } from "@shieldai/correlation";
|
||||
import {
|
||||
AnalyzeAudioInput,
|
||||
AnalysisJobStatus,
|
||||
@@ -78,6 +79,19 @@ export class AnalysisService {
|
||||
},
|
||||
});
|
||||
|
||||
if (result.verdict === DetectionVerdict.SYNTHETIC || result.verdict === DetectionVerdict.UNCERTAIN) {
|
||||
emitVoicePrintAlert(
|
||||
userId,
|
||||
job.id,
|
||||
result.verdict,
|
||||
result.syntheticScore,
|
||||
result.confidence,
|
||||
result.matchedEnrollmentId || undefined,
|
||||
result.matchedSimilarity || undefined,
|
||||
input.analysisType || undefined
|
||||
).catch((err) => console.error(`[Correlation] VoicePrint emit failed:`, err));
|
||||
}
|
||||
|
||||
return {
|
||||
jobId: job.id,
|
||||
syntheticScore: result.syntheticScore,
|
||||
|
||||
Reference in New Issue
Block a user