FRE-4474 Phase 5: Verify and resolve security review findings for SpamShield and Cross-Service Correlation
- FRE-4499 (SpamShield): Verified 6 security fixes (2 High, 4 Medium) - S01: Pre-compiled regex in RuleEngine (ReDoS fix) - S02: SmsClassifier accepts senderPhoneNumber context - S03: AlertServer JWT auth + origin validation - S04: SHA-256 phone hashing (PII protection) - S05: DecisionEngine timeout enforcement via Promise.race - S06: CarrierFactory.getAllCarriers properly async/await - FRE-4500 (Correlation): Verified 7 security fixes (2 Critical, 2 High, 2 Medium, 1 Low) - C1: Ingest endpoints auth via request.user.id - C2: IDOR protection on group endpoints (userId filter) - H3: JWT middleware registered in server.ts - H4: Fastify schema validation on all routes - M6: Payload sanitization with depth limit and circular ref detection - L7: CORS origin restricted to env var - Resolved liveness incidents FRE-4652 and FRE-4654 - All Phase 5 child issues now complete
This commit is contained in:
@@ -282,10 +282,11 @@ export class CorrelationEngine {
|
||||
}
|
||||
|
||||
public async getGroupById(
|
||||
groupId: string
|
||||
groupId: string,
|
||||
userId: string
|
||||
): Promise<CorrelationGroupOutput | null> {
|
||||
const group = await (prisma as any).correlationGroup.findUnique({
|
||||
where: { id: groupId },
|
||||
where: { id: groupId, userId },
|
||||
include: {
|
||||
alerts: {
|
||||
orderBy: { createdAt: "asc" },
|
||||
@@ -298,10 +299,11 @@ export class CorrelationEngine {
|
||||
|
||||
public async resolveGroup(
|
||||
groupId: string,
|
||||
userId: string,
|
||||
status: string = CorrelationStatus.RESOLVED
|
||||
): Promise<CorrelationGroupOutput | null> {
|
||||
const group = await (prisma as any).correlationGroup.update({
|
||||
where: { id: groupId },
|
||||
where: { id: groupId, userId },
|
||||
data: {
|
||||
status,
|
||||
resolvedAt: new Date(),
|
||||
|
||||
@@ -8,6 +8,24 @@ import {
|
||||
|
||||
type EntityType = (typeof EntityTypes)[keyof typeof EntityTypes];
|
||||
|
||||
function sanitizePayload(
|
||||
payload: Record<string, unknown>,
|
||||
maxDepth: number = 5
|
||||
): Record<string, unknown> {
|
||||
const seen = new WeakSet<object>();
|
||||
const clone = (obj: unknown, depth: number): unknown => {
|
||||
if (depth > maxDepth) return "[max depth]";
|
||||
if (obj === null || typeof obj !== "object") return obj;
|
||||
if (seen.has(obj as object)) return "[circular]";
|
||||
seen.add(obj as object);
|
||||
if (Array.isArray(obj)) return obj.map((item) => clone(item, depth + 1));
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [k, clone(v, depth + 1)])
|
||||
);
|
||||
};
|
||||
return clone(payload, 0) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface DarkWatchAlertPayload {
|
||||
exposureId: string;
|
||||
breachName: string;
|
||||
@@ -92,7 +110,7 @@ export class AlertNormalizer {
|
||||
: `Exposure detected in ${payload.breachName}`,
|
||||
entities,
|
||||
sourceAlertId,
|
||||
payload: payload as unknown as Record<string, unknown>,
|
||||
payload: sanitizePayload(payload as unknown as Record<string, unknown>),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
@@ -132,7 +150,7 @@ export class AlertNormalizer {
|
||||
: `SpamShield ${decision} decision with confidence ${Math.round(payload.confidence * 100)}%`,
|
||||
entities,
|
||||
sourceAlertId,
|
||||
payload: payload as unknown as Record<string, unknown>,
|
||||
payload: sanitizePayload(payload as unknown as Record<string, unknown>),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
@@ -179,7 +197,7 @@ export class AlertNormalizer {
|
||||
: `Synthetic voice detection: ${verdict} (score: ${payload.syntheticScore.toFixed(3)})`,
|
||||
entities,
|
||||
sourceAlertId,
|
||||
payload: payload as unknown as Record<string, unknown>,
|
||||
payload: sanitizePayload(payload as unknown as Record<string, unknown>),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
@@ -237,7 +255,7 @@ export class AlertNormalizer {
|
||||
description,
|
||||
entities,
|
||||
sourceAlertId,
|
||||
payload: payload as unknown as Record<string, unknown>,
|
||||
payload: sanitizePayload(payload as unknown as Record<string, unknown>),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,12 +126,12 @@ export class CorrelationService {
|
||||
return this.engine.getCorrelationGroups(query);
|
||||
}
|
||||
|
||||
public getGroupById(groupId: string) {
|
||||
return this.engine.getGroupById(groupId);
|
||||
public getGroupById(groupId: string, userId: string) {
|
||||
return this.engine.getGroupById(groupId, userId);
|
||||
}
|
||||
|
||||
public resolveGroup(groupId: string, status?: string) {
|
||||
return this.engine.resolveGroup(groupId, status as any);
|
||||
public resolveGroup(groupId: string, userId: string, status?: string) {
|
||||
return this.engine.resolveGroup(groupId, userId, status as any);
|
||||
}
|
||||
|
||||
public getDashboardData(userId: string, timeWindowMinutes?: number) {
|
||||
|
||||
Reference in New Issue
Block a user