Files
ShieldAI/packages/db/prisma/schema.prisma
Michael Freno b01b79d02a Add ReDoS validation for SpamRule.pattern field (FRE-4512)
- Create regex-validation utility with ReDoS detection (nested quantifiers,
  overlapping alternations, complexity limits)
- Add @db.VarChar(500) constraint on pattern field in Prisma schema
- Integrate validation in rule-engine at load time and evaluation time
- Add 46 unit tests covering syntax, ReDoS patterns, complexity, edge cases

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-02 07:23:39 -04:00

413 lines
10 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum SubscriptionTier {
BASIC
PLUS
PREMIUM
}
enum IdentifierType {
EMAIL
PHONE
SSN
}
enum WatchListStatus {
ACTIVE
PAUSED
}
enum Severity {
LOW
INFO
MEDIUM
WARNING
HIGH
CRITICAL
}
enum AlertChannel {
EMAIL
PUSH
SMS
}
enum AlertStatus {
PENDING
SENT
READ
}
enum ScanJobStatus {
PENDING
RUNNING
COMPLETED
FAILED
}
enum DataSource {
HIBP
SECURITY_TRAILS
CENSYS
SHODAN
HONEYPOT
}
enum AnalysisJobStatus {
PENDING
RUNNING
COMPLETED
FAILED
}
enum AnalysisType {
SYNTHETIC_DETECTION
VOICE_MATCH
BATCH
}
enum DetectionVerdict {
NATURAL
SYNTHETIC
UNCERTAIN
}
model User {
id String @id @default(uuid())
email String @unique
name String?
subscriptionTier SubscriptionTier @default(BASIC)
familyGroupId String?
watchListItems WatchListItem[]
alerts Alert[]
scanJobs ScanJob[]
scanSchedules ScanSchedule[]
voiceEnrollments VoiceEnrollment[]
analysisJobs AnalysisJob[]
spamFeedback SpamFeedback[]
spamCallAnalyses SpamCallAnalysis[]
spamAuditLogs SpamAuditLog[]
normalizedAlerts NormalizedAlert[]
correlationGroups CorrelationGroup[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
}
model WatchListItem {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
identifierType IdentifierType
identifierValue String
identifierHash String @unique
status WatchListStatus @default(ACTIVE)
exposures Exposure[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([identifierHash])
}
model Exposure {
id String @id @default(uuid())
watchListItemId String
watchListItem WatchListItem @relation(fields: [watchListItemId], references: [id], onDelete: Cascade)
dataSource DataSource
breachName String
exposedAt DateTime
dataType String[]
severity Severity
details String?
contentHash String @unique
alert Alert?
createdAt DateTime @default(now())
@@index([watchListItemId])
@@index([contentHash])
@@index([dataSource])
}
model Alert {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
exposureId String @unique
exposure Exposure @relation(fields: [exposureId], references: [id], onDelete: Cascade)
severity Severity
channel AlertChannel
status AlertStatus @default(PENDING)
dedupKey String
sentAt DateTime?
createdAt DateTime @default(now())
@@index([userId, status])
@@index([dedupKey])
}
model ScanJob {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
status ScanJobStatus @default(PENDING)
source DataSource?
resultCount Int @default(0)
errorMessage String?
scheduledBy String?
webhookEvents WebhookEvent[]
completedAt DateTime?
createdAt DateTime @default(now())
@@index([userId, status])
@@index([createdAt])
}
enum ScheduleStatus {
ACTIVE
PAUSED
}
model ScanSchedule {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
intervalMinutes Int // minutes between scans
cronExpression String // cron expression for scheduling
status ScheduleStatus @default(ACTIVE)
lastScanAt DateTime?
nextScanAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId])
@@index([status])
}
enum WebhookEventType {
SCAN_TRIGGER
BREACH_DETECTED
SUBSCRIPTION_CHANGE
}
model WebhookEvent {
id String @id @default(uuid())
eventType WebhookEventType
payload String
source String?
signature String?
processed Boolean @default(false)
processedAt DateTime?
scanJobId String?
scanJob ScanJob? @relation(fields: [scanJobId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
@@index([eventType, processed])
@@index([createdAt])
}
model VoiceEnrollment {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
label String
embeddingVector Float[]
embeddingDim Int @default(192)
audioFilePath String?
sampleRate Int @default(16000)
durationSec Float?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([embeddingDim])
}
model AnalysisJob {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
analysisType AnalysisType
audioFilePath String
status AnalysisJobStatus @default(PENDING)
result AnalysisResult?
errorMessage String?
completedAt DateTime?
createdAt DateTime @default(now())
@@index([userId, status])
@@index([createdAt])
}
model AnalysisResult {
id String @id @default(uuid())
analysisJobId String @unique
analysisJob AnalysisJob @relation(fields: [analysisJobId], references: [id], onDelete: Cascade)
syntheticScore Float
verdict DetectionVerdict
matchedEnrollmentId String?
matchedSimilarity Float?
confidence Float
processingTimeMs Int
modelVersion String?
metadata String?
createdAt DateTime @default(now())
@@index([analysisJobId])
@@index([verdict])
}
enum SpamDecision {
BLOCK
FLAG
ALLOW
}
model SpamFeedback {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
phoneNumber String // AES-256 encrypted PII
phoneNumberHash String // SHA-256 hash for anonymized lookup
isSpam Boolean
label String?
metadata String? // Unbounded JSON
createdAt DateTime @default(now())
@@index([userId])
@@index([phoneNumberHash])
@@index([createdAt])
}
model SpamCallAnalysis {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
phoneNumber String
callTimestamp DateTime
hiyaReputationScore Float?
truecallerSpamScore Float?
decision SpamDecision
confidence Float
ruleMatches String[] // IDs of matched rules
auditLogs SpamAuditLog[]
createdAt DateTime @default(now())
@@index([userId])
@@index([phoneNumber])
@@index([callTimestamp])
}
model SpamRule {
id String @id @default(uuid())
name String @unique
pattern String @db.VarChar(500) // Regex pattern - validated for ReDoS at application layer
decision SpamDecision
description String?
isActive Boolean @default(true)
priority Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive])
@@index([priority])
}
model SpamAuditLog {
id String @id @default(uuid())
analysisId String?
analysis SpamCallAnalysis? @relation(fields: [analysisId], references: [id], onDelete: SetNull)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
phoneNumber String
decision SpamDecision
reason String
ruleId String?
createdAt DateTime @default(now())
@@index([userId])
@@index([createdAt])
@@index([decision])
}
enum AlertSource {
DARKWATCH
SPAMSHIELD
VOICEPRINT
CALL_ANALYSIS
}
enum AlertCategory {
BREACH_EXPOSURE
SPAM_CALL
SPAM_SMS
SYNTHETIC_VOICE
VOICE_MISMATCH
CALL_QUALITY
CALL_ANOMALY
CALL_EVENT
}
enum CorrelationStatus {
ACTIVE
RESOLVED
FALSE_POSITIVE
}
enum EntityType {
PHONE_NUMBER
EMAIL
USER_ID
CALL_ID
IP_ADDRESS
}
model NormalizedAlert {
id String @id @default(uuid())
source AlertSource
category AlertCategory
severity Severity
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
title String
description String
entities Json // [{ type: EntityType, value: string }]
sourceAlertId String
groupId String?
correlationGroup CorrelationGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)
payload Json
createdAt DateTime @default(now())
@@index([userId, createdAt])
@@index([groupId])
@@index([sourceAlertId])
@@index([source])
@@index([severity])
}
model CorrelationGroup {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade, map: "corr_user_idx")
entities Json // [{ type: EntityType, value: string }]
highestSeverity Severity
status CorrelationStatus @default(ACTIVE)
alertCount Int @default(0)
alerts NormalizedAlert[]
summary String?
resolvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId, status])
@@index([createdAt])
}