474 lines
17 KiB
TypeScript
474 lines
17 KiB
TypeScript
import {
|
|
OperationsHealth,
|
|
SafeAuditEvent,
|
|
ComponentHealthStatus,
|
|
HealthComponentKey,
|
|
OperationsOverviewData,
|
|
OverviewSecurityErrorItem,
|
|
SafeIncidentSummary,
|
|
} from '../components/ops/types';
|
|
|
|
export const HEALTH_COMPONENT_METADATA: Record<
|
|
HealthComponentKey,
|
|
{ name: string; description: string }
|
|
> = {
|
|
operations_api: {
|
|
name: 'Operations API',
|
|
description: 'FastAPI core endpoint runtime & ingress',
|
|
},
|
|
operations_database: {
|
|
name: 'Operations Database',
|
|
description: 'PostgreSQL ops schema relational persistence',
|
|
},
|
|
session_validation: {
|
|
name: 'Session Validation',
|
|
description: '__Host-motorsignal_ops token verification & authority',
|
|
},
|
|
audit_persistence: {
|
|
name: 'Audit Persistence',
|
|
description: 'Immutable operational security activity persistence',
|
|
},
|
|
};
|
|
|
|
export const APPROVED_AUDIT_METADATA_KEYS = new Set([
|
|
'component',
|
|
'status_code',
|
|
'subsystem',
|
|
'duration_ms',
|
|
'policy',
|
|
'action_scope',
|
|
'session_valid',
|
|
'mfa_verified',
|
|
'migration_version',
|
|
'records_affected',
|
|
]);
|
|
|
|
export function isValidHealthStatus(status: unknown): status is ComponentHealthStatus {
|
|
return (
|
|
status === 'available' ||
|
|
status === 'degraded' ||
|
|
status === 'unavailable' ||
|
|
status === 'unknown'
|
|
);
|
|
}
|
|
|
|
export function normalizeHealthResponse(raw: unknown): OperationsHealth {
|
|
const fallbackCheckedAt = new Date().toISOString();
|
|
if (!raw || typeof raw !== 'object') {
|
|
return {
|
|
status: 'unavailable',
|
|
checked_at: fallbackCheckedAt,
|
|
components: {
|
|
operations_api: { status: 'unavailable', description: HEALTH_COMPONENT_METADATA.operations_api.description },
|
|
operations_database: { status: 'unavailable', description: HEALTH_COMPONENT_METADATA.operations_database.description },
|
|
session_validation: { status: 'unavailable', description: HEALTH_COMPONENT_METADATA.session_validation.description },
|
|
audit_persistence: { status: 'unavailable', description: HEALTH_COMPONENT_METADATA.audit_persistence.description },
|
|
},
|
|
};
|
|
}
|
|
|
|
const obj = raw as Record<string, unknown>;
|
|
const rawStatus = obj.status;
|
|
const overallStatus: ComponentHealthStatus = isValidHealthStatus(rawStatus)
|
|
? rawStatus
|
|
: 'unknown';
|
|
|
|
const checkedAt =
|
|
typeof obj.checked_at === 'string' && obj.checked_at.length > 0
|
|
? obj.checked_at
|
|
: fallbackCheckedAt;
|
|
|
|
const rawComponents =
|
|
obj.components && typeof obj.components === 'object'
|
|
? (obj.components as Record<string, unknown>)
|
|
: {};
|
|
|
|
const keys: HealthComponentKey[] = [
|
|
'operations_api',
|
|
'operations_database',
|
|
'session_validation',
|
|
'audit_persistence',
|
|
];
|
|
|
|
const components: Record<
|
|
HealthComponentKey,
|
|
{ status: ComponentHealthStatus; description: string }
|
|
> = {
|
|
operations_api: { status: 'unknown', description: HEALTH_COMPONENT_METADATA.operations_api.description },
|
|
operations_database: { status: 'unknown', description: HEALTH_COMPONENT_METADATA.operations_database.description },
|
|
session_validation: { status: 'unknown', description: HEALTH_COMPONENT_METADATA.session_validation.description },
|
|
audit_persistence: { status: 'unknown', description: HEALTH_COMPONENT_METADATA.audit_persistence.description },
|
|
};
|
|
|
|
for (const key of keys) {
|
|
const rawComp = rawComponents[key] as Record<string, unknown> | undefined;
|
|
const compStatus = rawComp && isValidHealthStatus(rawComp.status)
|
|
? rawComp.status
|
|
: isValidHealthStatus(obj[key])
|
|
? (obj[key] as ComponentHealthStatus)
|
|
: overallStatus;
|
|
|
|
components[key] = {
|
|
status: compStatus,
|
|
description: (rawComp?.description as string) || HEALTH_COMPONENT_METADATA[key].description,
|
|
};
|
|
}
|
|
|
|
return {
|
|
status: overallStatus,
|
|
checked_at: checkedAt,
|
|
components,
|
|
};
|
|
}
|
|
|
|
export function sanitizeAuditEvent(raw: unknown): SafeAuditEvent | null {
|
|
if (!raw || typeof raw !== 'object') {
|
|
return null; // Fail closed
|
|
}
|
|
|
|
const obj = raw as Record<string, unknown>;
|
|
|
|
// Required: timestamp (ISO string)
|
|
if (typeof obj.timestamp !== 'string' || !obj.timestamp.trim()) {
|
|
return null;
|
|
}
|
|
const timestamp = obj.timestamp.trim();
|
|
|
|
// Required: action
|
|
if (typeof obj.action !== 'string' || !obj.action.trim()) {
|
|
return null;
|
|
}
|
|
const action = obj.action.trim();
|
|
|
|
// Required: result
|
|
if (typeof obj.result !== 'string' || !obj.result.trim()) {
|
|
return null;
|
|
}
|
|
const result = obj.result.trim();
|
|
|
|
// Required: actor_identity (exact OPS-3B contract, no actor_email alias)
|
|
if (typeof obj.actor_identity !== 'string' || !obj.actor_identity.trim()) {
|
|
return null; // Fail closed: missing verified identity
|
|
}
|
|
const actor_identity = obj.actor_identity.trim();
|
|
|
|
// Optional transport ID (unrendered in UI)
|
|
const id =
|
|
typeof obj.id === 'string' && obj.id.trim()
|
|
? obj.id.trim()
|
|
: undefined;
|
|
|
|
// Optional: actor_role
|
|
const actor_role =
|
|
typeof obj.actor_role === 'string' && obj.actor_role.trim()
|
|
? obj.actor_role.trim()
|
|
: undefined;
|
|
|
|
// Optional: target_resource (exact OPS-3B contract, no target alias)
|
|
const target_resource =
|
|
typeof obj.target_resource === 'string' && obj.target_resource.trim()
|
|
? obj.target_resource.trim()
|
|
: undefined;
|
|
|
|
// Optional: reason_code
|
|
const reason_code =
|
|
typeof obj.reason_code === 'string' && obj.reason_code.trim()
|
|
? obj.reason_code.trim()
|
|
: undefined;
|
|
|
|
// Optional: case_reference
|
|
const case_reference =
|
|
typeof obj.case_reference === 'string' && obj.case_reference.trim()
|
|
? obj.case_reference.trim()
|
|
: undefined;
|
|
|
|
// Explicit Allowlist Metadata Projection only
|
|
let metadata: Record<string, string | number | boolean> | undefined = undefined;
|
|
if (obj.metadata && typeof obj.metadata === 'object' && !Array.isArray(obj.metadata)) {
|
|
const rawMeta = obj.metadata as Record<string, unknown>;
|
|
const safeMeta: Record<string, string | number | boolean> = {};
|
|
for (const [k, v] of Object.entries(rawMeta)) {
|
|
if (!APPROVED_AUDIT_METADATA_KEYS.has(k)) {
|
|
continue; // Drop unapproved metadata key
|
|
}
|
|
if (typeof v === 'string' && v.length <= 100) {
|
|
safeMeta[k] = v;
|
|
} else if (typeof v === 'number' || typeof v === 'boolean') {
|
|
safeMeta[k] = v;
|
|
}
|
|
}
|
|
if (Object.keys(safeMeta).length > 0) {
|
|
metadata = safeMeta;
|
|
}
|
|
}
|
|
|
|
return {
|
|
id,
|
|
timestamp,
|
|
action,
|
|
result,
|
|
actor_identity,
|
|
actor_role,
|
|
target_resource,
|
|
reason_code,
|
|
case_reference,
|
|
metadata,
|
|
};
|
|
}
|
|
|
|
// ============================================================
|
|
// Incident Sanitizers & Validators (OPS-4)
|
|
// ============================================================
|
|
|
|
export function isValidIncidentSeverity(severity: unknown): severity is import('../components/ops/types').IncidentSeverity {
|
|
return (
|
|
severity === 'low' ||
|
|
severity === 'medium' ||
|
|
severity === 'high' ||
|
|
severity === 'critical'
|
|
);
|
|
}
|
|
|
|
export function isValidIncidentStatus(status: unknown): status is import('../components/ops/types').IncidentStatus {
|
|
return (
|
|
status === 'open' ||
|
|
status === 'investigating' ||
|
|
status === 'monitoring' ||
|
|
status === 'resolved' ||
|
|
status === 'closed' ||
|
|
status === 'archived'
|
|
);
|
|
}
|
|
|
|
const ALLOWED_INCIDENT_TRANSITIONS: Record<string, Set<string>> = {
|
|
open: new Set(['investigating', 'resolved', 'closed']),
|
|
investigating: new Set(['monitoring', 'resolved', 'closed']),
|
|
monitoring: new Set(['investigating', 'resolved', 'closed']),
|
|
resolved: new Set(['closed', 'investigating']),
|
|
closed: new Set(['investigating']), // Reopen (Owner only)
|
|
archived: new Set(), // Terminal
|
|
};
|
|
|
|
export function isValidStatusTransition(
|
|
current: import('../components/ops/types').IncidentStatus,
|
|
target: import('../components/ops/types').IncidentStatus,
|
|
role: import('../components/ops/types').OperationsRole
|
|
): boolean {
|
|
if (current === target) return false;
|
|
if (current === 'archived') return false; // Terminal
|
|
if (target === 'archived') return role === 'owner'; // Owner only
|
|
if (current === 'closed' && target === 'investigating') return role === 'owner'; // Owner only
|
|
const allowed = ALLOWED_INCIDENT_TRANSITIONS[current];
|
|
return allowed ? allowed.has(target) : false;
|
|
}
|
|
|
|
export function sanitizeIncidentSummary(raw: unknown): import('../components/ops/types').SafeIncidentSummary | null {
|
|
if (!raw || typeof raw !== 'object') return null;
|
|
const obj = raw as Record<string, unknown>;
|
|
|
|
if (typeof obj.case_reference !== 'string' || !obj.case_reference.trim()) return null;
|
|
const case_reference = obj.case_reference.trim();
|
|
|
|
if (typeof obj.title !== 'string' || !obj.title.trim()) return null;
|
|
const title = obj.title.trim();
|
|
|
|
if (!isValidIncidentSeverity(obj.severity)) return null;
|
|
const severity = obj.severity;
|
|
|
|
if (!isValidIncidentStatus(obj.status)) return null;
|
|
const status = obj.status;
|
|
|
|
if (typeof obj.created_at !== 'string' || !obj.created_at.trim()) return null;
|
|
const created_at = obj.created_at.trim();
|
|
|
|
const updated_at = typeof obj.updated_at === 'string' && obj.updated_at.trim()
|
|
? obj.updated_at.trim()
|
|
: created_at;
|
|
|
|
return {
|
|
case_reference,
|
|
title,
|
|
severity,
|
|
status,
|
|
created_at,
|
|
updated_at,
|
|
};
|
|
}
|
|
|
|
export function sanitizeIncidentDetail(raw: unknown): import('../components/ops/types').SafeIncidentDetail | null {
|
|
if (!raw || typeof raw !== 'object') return null;
|
|
const obj = raw as Record<string, unknown>;
|
|
|
|
const summaryData = sanitizeIncidentSummary(raw);
|
|
if (!summaryData) return null;
|
|
|
|
if (typeof obj.summary !== 'string' || !obj.summary.trim()) return null;
|
|
const summary = obj.summary.trim();
|
|
|
|
if (typeof obj.created_by_identity !== 'string' || !obj.created_by_identity.trim()) return null;
|
|
const created_by_identity = obj.created_by_identity.trim();
|
|
|
|
const closed_at = typeof obj.closed_at === 'string' && obj.closed_at.trim()
|
|
? obj.closed_at.trim()
|
|
: null;
|
|
|
|
const resolution = typeof obj.resolution === 'string' && obj.resolution.trim()
|
|
? obj.resolution.trim()
|
|
: null;
|
|
|
|
const closed_by_identity = typeof obj.closed_by_identity === 'string' && obj.closed_by_identity.trim()
|
|
? obj.closed_by_identity.trim()
|
|
: null;
|
|
|
|
return {
|
|
...summaryData,
|
|
summary,
|
|
created_by_identity,
|
|
closed_at,
|
|
resolution,
|
|
closed_by_identity,
|
|
};
|
|
}
|
|
|
|
export function sanitizeIncidentNote(raw: unknown): import('../components/ops/types').SafeIncidentNote | null {
|
|
if (!raw || typeof raw !== 'object') return null;
|
|
const obj = raw as Record<string, unknown>;
|
|
|
|
if (typeof obj.created_at !== 'string' || !obj.created_at.trim()) return null;
|
|
const created_at = obj.created_at.trim();
|
|
|
|
if (typeof obj.created_by_identity !== 'string' || !obj.created_by_identity.trim()) return null;
|
|
const created_by_identity = obj.created_by_identity.trim();
|
|
|
|
if (typeof obj.body !== 'string' || !obj.body.trim()) return null;
|
|
const body = obj.body.trim();
|
|
|
|
return {
|
|
created_at,
|
|
created_by_identity,
|
|
body,
|
|
};
|
|
}
|
|
|
|
export function normalizeOverviewResponse(raw: unknown): OperationsOverviewData {
|
|
const fallbackGeneratedAt = new Date().toISOString();
|
|
const defaultHealth = normalizeHealthResponse(null);
|
|
|
|
if (!raw || typeof raw !== 'object') {
|
|
return {
|
|
generated_at: fallbackGeneratedAt,
|
|
users: {
|
|
total_registered: 0,
|
|
active_24h: 0,
|
|
active_7d: 0,
|
|
new_this_week: 0,
|
|
},
|
|
membership: {
|
|
total_subscribers: 0,
|
|
premium_members: 0,
|
|
free_members: 0,
|
|
conversion_rate_pct: 0,
|
|
},
|
|
activity: {
|
|
web_active_sessions_24h: 0,
|
|
web_logins_24h: 0,
|
|
web_signups_7d: 0,
|
|
mobile_active_sessions_24h: 0,
|
|
mobile_rides_today: 0,
|
|
mobile_rides_active: 0,
|
|
mobile_datapoints_24h: 0,
|
|
},
|
|
incidents: {
|
|
total_open: 0,
|
|
critical_count: 0,
|
|
high_count: 0,
|
|
medium_count: 0,
|
|
low_count: 0,
|
|
recent_open: [],
|
|
},
|
|
errors: {
|
|
errors_24h: 0,
|
|
denials_24h: 0,
|
|
recent: [],
|
|
},
|
|
health: defaultHealth,
|
|
};
|
|
}
|
|
|
|
const obj = raw as Record<string, unknown>;
|
|
const rawUsers = obj.users && typeof obj.users === 'object' ? (obj.users as Record<string, unknown>) : {};
|
|
const rawMembership = obj.membership && typeof obj.membership === 'object' ? (obj.membership as Record<string, unknown>) : {};
|
|
const rawActivity = obj.activity && typeof obj.activity === 'object' ? (obj.activity as Record<string, unknown>) : {};
|
|
const rawIncidents = obj.incidents && typeof obj.incidents === 'object' ? (obj.incidents as Record<string, unknown>) : {};
|
|
const rawErrors = obj.errors && typeof obj.errors === 'object' ? (obj.errors as Record<string, unknown>) : {};
|
|
|
|
const rawRecentOpen = Array.isArray(rawIncidents.recent_open) ? rawIncidents.recent_open : [];
|
|
const safeRecentOpen: SafeIncidentSummary[] = rawRecentOpen
|
|
.map(sanitizeIncidentSummary)
|
|
.filter((i): i is SafeIncidentSummary => i !== null);
|
|
|
|
const rawRecentErrors = Array.isArray(rawErrors.recent) ? rawErrors.recent : [];
|
|
const safeRecentErrors: OverviewSecurityErrorItem[] = rawRecentErrors
|
|
.map((e): OverviewSecurityErrorItem | null => {
|
|
if (!e || typeof e !== 'object') return null;
|
|
const errObj = e as Record<string, unknown>;
|
|
if (typeof errObj.timestamp !== 'string' || !errObj.timestamp.trim()) return null;
|
|
if (typeof errObj.action !== 'string' || !errObj.action.trim()) return null;
|
|
if (typeof errObj.actor_identity !== 'string' || !errObj.actor_identity.trim()) return null;
|
|
return {
|
|
id: typeof errObj.id === 'string' && errObj.id.trim() ? errObj.id.trim() : undefined,
|
|
timestamp: errObj.timestamp.trim(),
|
|
action: errObj.action.trim(),
|
|
actor_identity: errObj.actor_identity.trim(),
|
|
reason_code: typeof errObj.reason_code === 'string' && errObj.reason_code.trim() ? errObj.reason_code.trim() : undefined,
|
|
result: typeof errObj.result === 'string' && errObj.result.trim() ? errObj.result.trim() : 'failure',
|
|
target_resource: typeof errObj.target_resource === 'string' && errObj.target_resource.trim() ? errObj.target_resource.trim() : undefined,
|
|
};
|
|
})
|
|
.filter((e): e is OverviewSecurityErrorItem => e !== null);
|
|
|
|
return {
|
|
generated_at: typeof obj.generated_at === 'string' && obj.generated_at.trim() ? obj.generated_at.trim() : fallbackGeneratedAt,
|
|
users: {
|
|
total_registered: typeof rawUsers.total_registered === 'number' ? rawUsers.total_registered : 0,
|
|
active_24h: typeof rawUsers.active_24h === 'number' ? rawUsers.active_24h : 0,
|
|
active_7d: typeof rawUsers.active_7d === 'number' ? rawUsers.active_7d : 0,
|
|
new_this_week: typeof rawUsers.new_this_week === 'number' ? rawUsers.new_this_week : 0,
|
|
},
|
|
membership: {
|
|
total_subscribers: typeof rawMembership.total_subscribers === 'number' ? rawMembership.total_subscribers : 0,
|
|
premium_members: typeof rawMembership.premium_members === 'number'
|
|
? rawMembership.premium_members
|
|
: typeof rawMembership.pro_members === 'number'
|
|
? rawMembership.pro_members
|
|
: 0,
|
|
free_members: typeof rawMembership.free_members === 'number' ? rawMembership.free_members : 0,
|
|
conversion_rate_pct: typeof rawMembership.conversion_rate_pct === 'number' ? rawMembership.conversion_rate_pct : 0,
|
|
},
|
|
activity: {
|
|
web_active_sessions_24h: typeof rawActivity.web_active_sessions_24h === 'number' ? rawActivity.web_active_sessions_24h : 0,
|
|
web_logins_24h: typeof rawActivity.web_logins_24h === 'number' ? rawActivity.web_logins_24h : 0,
|
|
web_signups_7d: typeof rawActivity.web_signups_7d === 'number' ? rawActivity.web_signups_7d : 0,
|
|
mobile_active_sessions_24h: typeof rawActivity.mobile_active_sessions_24h === 'number' ? rawActivity.mobile_active_sessions_24h : 0,
|
|
mobile_rides_today: typeof rawActivity.mobile_rides_today === 'number' ? rawActivity.mobile_rides_today : 0,
|
|
mobile_rides_active: typeof rawActivity.mobile_rides_active === 'number' ? rawActivity.mobile_rides_active : 0,
|
|
mobile_datapoints_24h: typeof rawActivity.mobile_datapoints_24h === 'number' ? rawActivity.mobile_datapoints_24h : 0,
|
|
},
|
|
incidents: {
|
|
total_open: typeof rawIncidents.total_open === 'number' ? rawIncidents.total_open : safeRecentOpen.length,
|
|
critical_count: typeof rawIncidents.critical_count === 'number' ? rawIncidents.critical_count : 0,
|
|
high_count: typeof rawIncidents.high_count === 'number' ? rawIncidents.high_count : 0,
|
|
medium_count: typeof rawIncidents.medium_count === 'number' ? rawIncidents.medium_count : 0,
|
|
low_count: typeof rawIncidents.low_count === 'number' ? rawIncidents.low_count : 0,
|
|
recent_open: safeRecentOpen,
|
|
},
|
|
errors: {
|
|
errors_24h: typeof rawErrors.errors_24h === 'number' ? rawErrors.errors_24h : 0,
|
|
denials_24h: typeof rawErrors.denials_24h === 'number' ? rawErrors.denials_24h : 0,
|
|
recent: safeRecentErrors,
|
|
},
|
|
health: normalizeHealthResponse(obj.health),
|
|
};
|
|
}
|
|
|