mirror of
https://github.com/andreicscs/HoneyWire
synced 2026-06-26 12:39:53 +00:00
364 lines
13 KiB
TypeScript
364 lines
13 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import { api } from '../../api/client'
|
|
import { useFleetStore } from '../Fleet/fleet'
|
|
import { useAppStore } from '../System/app'
|
|
|
|
// --- TYPES & INTERFACES ---
|
|
|
|
export interface EventPayload {
|
|
id: string
|
|
timestamp: string
|
|
nodeId: string
|
|
sensorId: string
|
|
severity: 'info' | 'low' | 'medium' | 'high' | 'critical'
|
|
eventTrigger: string
|
|
source: string
|
|
target: string
|
|
isRead: boolean
|
|
isArchived: boolean
|
|
details?: Record<string, any>
|
|
}
|
|
|
|
export interface SeverityProjection {
|
|
timeframe: string
|
|
total: number
|
|
critical: number
|
|
high: number
|
|
medium: number
|
|
low: number
|
|
info: number
|
|
}
|
|
|
|
export interface ThreatVelocityProjection {
|
|
timeframe: string
|
|
bucketSizeMs: number
|
|
generatedAt: number
|
|
bucketTimestamps: number[]
|
|
labels: string[]
|
|
exactTimes: string[]
|
|
series: Record<string, number[]>
|
|
recentEventCount: number
|
|
}
|
|
|
|
export interface SummaryProjection {
|
|
timeframe: string
|
|
totalEvents: number
|
|
bySensor: Record<string, number>
|
|
}
|
|
|
|
export interface EventsState {
|
|
events: EventPayload[]
|
|
unreadCount: number
|
|
activeEvent: EventPayload | null
|
|
isFetching: boolean
|
|
severityProjection: SeverityProjection | null
|
|
threatVelocityProjection: ThreatVelocityProjection | null
|
|
summaryProjection: SummaryProjection | null
|
|
isFetchingThreatVelocityProjection: boolean
|
|
isFetchingSummaryProjection: boolean
|
|
lastVelocityInvalidation: number | null
|
|
}
|
|
|
|
let severityAbortController: AbortController | null = null
|
|
let velocityAbortController: AbortController | null = null
|
|
let summaryAbortController: AbortController | null = null
|
|
|
|
// --- NORMALIZATION ---
|
|
const normalizeEvent = (e: any): EventPayload => {
|
|
return {
|
|
...e,
|
|
isRead: Boolean(e.isRead),
|
|
isArchived: Boolean(e.isArchived)
|
|
}
|
|
}
|
|
|
|
export const useEventsStore = defineStore('events', () => {
|
|
// ==========================================
|
|
// SINGLE STATE TREE
|
|
// ==========================================
|
|
const state = ref<EventsState>({
|
|
events: [],
|
|
unreadCount: 0,
|
|
activeEvent: null,
|
|
isFetching: false,
|
|
severityProjection: null,
|
|
threatVelocityProjection: null,
|
|
summaryProjection: null,
|
|
isFetchingThreatVelocityProjection: false,
|
|
isFetchingSummaryProjection: false,
|
|
lastVelocityInvalidation: null
|
|
})
|
|
|
|
const fleetStore = useFleetStore()
|
|
const appStore = useAppStore()
|
|
|
|
// ==========================================
|
|
// ENCAPSULATED GETTERS (Public API)
|
|
// ==========================================
|
|
const events = computed<EventPayload[]>(() => state.value.events)
|
|
const unreadCount = computed<number>(() => state.value.unreadCount)
|
|
const activeEvent = computed<EventPayload | null>(() => state.value.activeEvent)
|
|
const isFetching = computed<boolean>(() => state.value.isFetching)
|
|
const severityProjection = computed<SeverityProjection | null>(() => state.value.severityProjection)
|
|
const threatVelocityProjection = computed<ThreatVelocityProjection | null>(() => state.value.threatVelocityProjection)
|
|
const summaryProjection = computed<SummaryProjection | null>(() => state.value.summaryProjection)
|
|
const isFetchingThreatVelocityProjection = computed<boolean>(() => state.value.isFetchingThreatVelocityProjection)
|
|
const isFetchingSummaryProjection = computed<boolean>(() => state.value.isFetchingSummaryProjection)
|
|
const lastVelocityInvalidation = computed<number | null>(() => state.value.lastVelocityInvalidation)
|
|
|
|
/**
|
|
* Return events filtered by the fleet store's active selection.
|
|
* Note: We enforce composite key validation for sensors!
|
|
*/
|
|
const filteredEvents = computed<EventPayload[]>(() => {
|
|
const selectedNode = fleetStore.selectedNode
|
|
const selectedSensor = fleetStore.selectedSensor
|
|
const isArchiveView = !!appStore.viewingArchive
|
|
|
|
let currentEvents = state.value.events.filter(e => {
|
|
const isArchived = e.isArchived === true
|
|
return isArchiveView ? isArchived : !isArchived
|
|
})
|
|
|
|
if (!selectedNode && !selectedSensor) {
|
|
return currentEvents
|
|
}
|
|
|
|
if (selectedNode && !selectedSensor) {
|
|
return currentEvents.filter(e => e.nodeId === selectedNode?.id)
|
|
}
|
|
|
|
if (selectedNode && selectedSensor) {
|
|
return currentEvents.filter(
|
|
e => e.nodeId === selectedNode?.id && e.sensorId === selectedSensor?.sensorId
|
|
)
|
|
}
|
|
|
|
return currentEvents
|
|
})
|
|
|
|
// ==========================================
|
|
// ACTIONS & GATEKEEPERS
|
|
// ==========================================
|
|
|
|
const refreshUnreadCount = async (): Promise<void> => {
|
|
try {
|
|
const res = await api.get('/api/v2/events/unread')
|
|
const data = await res.json()
|
|
state.value.unreadCount = data.count
|
|
} catch {
|
|
state.value.unreadCount = 0
|
|
}
|
|
}
|
|
|
|
const fetchEvents = async (isArchived: boolean = false, nodeId: string | null = null, sensorId: string | null = null): Promise<void> => {
|
|
try {
|
|
state.value.isFetching = true
|
|
const params = new URLSearchParams()
|
|
params.append('archived', isArchived ? 'true' : 'false')
|
|
if (nodeId) params.append('nodeId', nodeId)
|
|
if (sensorId) params.append('sensorId', sensorId)
|
|
|
|
const res = await api.get(`/api/v2/events?${params.toString()}`)
|
|
state.value.events = (await res.json() as any[]).map(normalizeEvent)
|
|
|
|
await refreshUnreadCount()
|
|
} catch (e) {
|
|
console.error('Failed to fetch events', e)
|
|
} finally {
|
|
state.value.isFetching = false
|
|
}
|
|
}
|
|
|
|
const fetchSeverityProjection = async (timeframe: string = 'alltime', nodeId: string | null = null, sensorId: string | null = null): Promise<void> => {
|
|
if (severityAbortController) severityAbortController.abort()
|
|
severityAbortController = new AbortController()
|
|
|
|
try {
|
|
const params = new URLSearchParams({ timeframe, viewingArchive: appStore.viewingArchive ? 'true' : 'false' })
|
|
if (nodeId) params.append('node', nodeId)
|
|
if (sensorId) params.append('sensor', sensorId)
|
|
|
|
const response = await api.get(`/api/v2/events/severity?${params.toString()}`, { signal: severityAbortController.signal })
|
|
state.value.severityProjection = (await response.json()) as SeverityProjection
|
|
} catch (e: any) {
|
|
if (e.name !== 'AbortError') console.error('Failed to fetch severity projection', e)
|
|
}
|
|
}
|
|
|
|
const fetchThreatVelocityProjection = async (timeframe: string = '24H', nodeId: string | null = null, sensorId: string | null = null, viewingArchive: boolean = false): Promise<void> => {
|
|
if (velocityAbortController) velocityAbortController.abort()
|
|
velocityAbortController = new AbortController()
|
|
|
|
try {
|
|
state.value.isFetchingThreatVelocityProjection = true
|
|
const params = new URLSearchParams({ timeframe, archived: viewingArchive ? 'true' : 'false' })
|
|
if (nodeId) params.append('nodeId', nodeId)
|
|
if (sensorId) params.append('sensorId', sensorId)
|
|
|
|
const response = await api.get(`/api/v2/events/velocity?${params.toString()}`, { signal: velocityAbortController.signal })
|
|
state.value.threatVelocityProjection = (await response.json()) as ThreatVelocityProjection
|
|
} catch (e: any) {
|
|
if (e.name !== 'AbortError') console.error('Velocity fetch failed:', e)
|
|
} finally {
|
|
state.value.isFetchingThreatVelocityProjection = false
|
|
}
|
|
}
|
|
|
|
const fetchSummaryProjection = async (timeframe: string = '24H', nodeId: string | null = null, sensorId: string | null = null): Promise<void> => {
|
|
if (summaryAbortController) summaryAbortController.abort()
|
|
summaryAbortController = new AbortController()
|
|
|
|
try {
|
|
state.value.isFetchingSummaryProjection = true
|
|
const params = new URLSearchParams({ timeframe, archived: appStore.viewingArchive ? 'true' : 'false' })
|
|
if (nodeId) params.append('nodeId', nodeId)
|
|
if (sensorId) params.append('sensorId', sensorId)
|
|
|
|
const response = await api.get(`/api/v2/events/summary?${params.toString()}`, { signal: summaryAbortController.signal })
|
|
state.value.summaryProjection = (await response.json()) as SummaryProjection
|
|
} catch (e: any) {
|
|
if (e.name !== 'AbortError') console.error('Summary fetch failed:', e)
|
|
} finally {
|
|
state.value.isFetchingSummaryProjection = false
|
|
}
|
|
}
|
|
|
|
const invalidateThreatVelocityProjection = (): void => {
|
|
state.value.lastVelocityInvalidation = Date.now()
|
|
}
|
|
|
|
const markAllRead = async (): Promise<{ success: boolean; error?: string }> => {
|
|
try {
|
|
await api.patch('/api/v2/events/read')
|
|
state.value.events.forEach(e => (e.isRead = true))
|
|
state.value.unreadCount = 0
|
|
return { success: true }
|
|
} catch (err) {
|
|
console.error('Failed to mark all events as read:', err)
|
|
return { success: false, error: 'Failed to mark events as read. Please try again.' }
|
|
}
|
|
}
|
|
|
|
const markEventRead = async (eventId: string): Promise<{ success: boolean; error?: string }> => {
|
|
const ev = state.value.events.find(e => e.id === eventId)
|
|
if (!ev || ev.isRead) return { success: true }
|
|
|
|
const wasRead = ev.isRead
|
|
ev.isRead = true
|
|
state.value.unreadCount = Math.max(0, state.value.unreadCount - 1)
|
|
|
|
try {
|
|
await api.patch(`/api/v2/events/${eventId}/read`)
|
|
return { success: true }
|
|
} catch (err) {
|
|
ev.isRead = wasRead
|
|
state.value.unreadCount = Math.max(0, state.value.unreadCount + 1)
|
|
console.error('Failed to mark event as read:', err)
|
|
return { success: false, error: 'Failed to mark event as read.' }
|
|
}
|
|
}
|
|
|
|
const archiveEvent = async (eventId: string): Promise<{ success: boolean; error?: string }> => {
|
|
const originalEvents = [...state.value.events]
|
|
try {
|
|
await api.patch(`/api/v2/events/${eventId}/archive`)
|
|
state.value.events = state.value.events.filter(e => e.id !== eventId)
|
|
state.value.activeEvent = null
|
|
await refreshUnreadCount()
|
|
// Refetch analytics projections so the charts zero out
|
|
fetchSeverityProjection('alltime', fleetStore.selectedNode?.id, fleetStore.selectedSensor?.sensorId)
|
|
fetchSummaryProjection('24H', fleetStore.selectedNode?.id, fleetStore.selectedSensor?.sensorId)
|
|
invalidateThreatVelocityProjection()
|
|
return { success: true }
|
|
} catch (err) {
|
|
console.error('Failed to archive event:', err)
|
|
state.value.events = originalEvents
|
|
return { success: false, error: 'Failed to archive event. Please try again.' }
|
|
}
|
|
}
|
|
|
|
const archiveAll = async (): Promise<{ success: boolean; error?: string }> => {
|
|
try {
|
|
await api.patch('/api/v2/events/archive-all')
|
|
await fetchEvents(false, fleetStore.selectedNode?.id, fleetStore.selectedSensor?.sensorId)
|
|
// Refetch analytics projections so the charts zero out
|
|
fetchSeverityProjection('alltime', fleetStore.selectedNode?.id, fleetStore.selectedSensor?.sensorId)
|
|
fetchSummaryProjection('24H', fleetStore.selectedNode?.id, fleetStore.selectedSensor?.sensorId)
|
|
invalidateThreatVelocityProjection()
|
|
return { success: true }
|
|
} catch (err) {
|
|
console.error('Failed to archive all events:', err)
|
|
return { success: false, error: 'Failed to archive events. Please try again.' }
|
|
}
|
|
}
|
|
|
|
const purgeEvents = (): void => {
|
|
state.value.events = []
|
|
state.value.unreadCount = 0
|
|
|
|
// Refetch analytics projections so the charts zero out
|
|
fetchSeverityProjection('alltime', fleetStore.selectedNode?.id, fleetStore.selectedSensor?.sensorId)
|
|
fetchSummaryProjection('24H', fleetStore.selectedNode?.id, fleetStore.selectedSensor?.sensorId)
|
|
invalidateThreatVelocityProjection()
|
|
}
|
|
|
|
const clearSummaryProjection = (): void => {
|
|
state.value.summaryProjection = null
|
|
}
|
|
|
|
const handleWsEvent = (rawPayload: any): void => {
|
|
const payload = normalizeEvent(rawPayload)
|
|
|
|
const selectedNode = fleetStore.selectedNode
|
|
const selectedSensor = fleetStore.selectedSensor
|
|
|
|
state.value.unreadCount++
|
|
|
|
const noFilter = !selectedNode && !selectedSensor
|
|
const nodeOnlyMatch = selectedNode && !selectedSensor && payload.nodeId === selectedNode?.id
|
|
const sensorMatch = selectedSensor && selectedNode && payload.nodeId === selectedNode?.id && payload.sensorId === selectedSensor?.sensorId
|
|
|
|
if (noFilter || nodeOnlyMatch || sensorMatch) {
|
|
state.value.events.unshift(payload)
|
|
}
|
|
|
|
const affectsCurrentView =
|
|
(!appStore.viewingArchive) &&
|
|
(!selectedNode || selectedNode?.id === payload.nodeId) &&
|
|
(!selectedSensor || selectedSensor?.sensorId === payload.sensorId)
|
|
|
|
if (affectsCurrentView) {
|
|
fetchSeverityProjection('alltime', selectedNode?.id, selectedSensor?.sensorId)
|
|
fetchSummaryProjection('24H', selectedNode?.id, selectedSensor?.sensorId)
|
|
invalidateThreatVelocityProjection()
|
|
}
|
|
}
|
|
|
|
return {
|
|
events,
|
|
unreadCount,
|
|
activeEvent,
|
|
isFetching,
|
|
severityProjection,
|
|
threatVelocityProjection,
|
|
summaryProjection,
|
|
isFetchingThreatVelocityProjection,
|
|
isFetchingSummaryProjection,
|
|
lastVelocityInvalidation,
|
|
filteredEvents,
|
|
fetchEvents,
|
|
fetchSeverityProjection,
|
|
fetchThreatVelocityProjection,
|
|
fetchSummaryProjection,
|
|
invalidateThreatVelocityProjection,
|
|
markAllRead,
|
|
markEventRead,
|
|
archiveEvent,
|
|
archiveAll,
|
|
purgeEvents,
|
|
clearSummaryProjection,
|
|
handleWsEvent,
|
|
}
|
|
}) |