diff --git a/web/src/App.vue b/web/src/App.vue index 914984d..f6a1b48 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -76,6 +76,7 @@ const navLinks = [ { to: '/search', label: 'Search' }, { to: '/diagnose', label: 'Diagnose' }, { to: '/incidents', label: 'Incidents' }, + { to: '/alerts', label: 'Alerts' }, { to: '/bundles', label: 'Bundles' }, { to: '/sources', label: 'Sources' }, { to: '/context', label: 'Context' }, diff --git a/web/src/router/index.ts b/web/src/router/index.ts index b2c7f97..e5bba57 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -8,6 +8,7 @@ import BundlesView from '@/views/BundlesView.vue' import SettingsView from '@/views/SettingsView.vue' import ContextView from '@/views/ContextView.vue' import BlocklistView from '@/views/BlocklistView.vue' +import SecurityAlertsView from '@/views/SecurityAlertsView.vue' export default createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -17,6 +18,7 @@ export default createRouter({ { path: '/search', component: LogSearchView }, { path: '/diagnose', component: DiagnoseView }, { path: '/incidents', component: IncidentsView }, + { path: '/alerts', component: SecurityAlertsView }, { path: '/bundles', component: BundlesView }, { path: '/sources', component: SourcesView }, { path: '/context', component: ContextView }, diff --git a/web/src/views/DashboardView.vue b/web/src/views/DashboardView.vue index 98a9c4f..3d6a73a 100644 --- a/web/src/views/DashboardView.vue +++ b/web/src/views/DashboardView.vue @@ -52,6 +52,16 @@ {{ incidentsLoading ? '…' : activeIncidents }}

+ +

Unreviewed Alerts

+

+ {{ alertsLoading ? '…' : unackedAlerts }} +

+
@@ -201,6 +211,8 @@ const loading = ref(true) const incidents = ref([]) const incidentsLoading = ref(true) const watchSources = ref([]) +const unackedAlerts = ref(0) +const alertsLoading = ref(true) const activeIncidents = computed(() => incidents.value.filter(i => !i.ended_at).length @@ -217,7 +229,7 @@ const isStale = computed(() => { }) onMounted(async () => { - await Promise.all([loadStats(), loadIncidents(), loadWatchStatus()]) + await Promise.all([loadStats(), loadIncidents(), loadWatchStatus(), loadAlertCount()]) }) async function loadStats() { @@ -245,6 +257,14 @@ async function loadWatchStatus() { } catch { /* non-critical */ } } +async function loadAlertCount() { + try { + const res = await fetch(`${BASE}/turnstone/api/anomaly/detections?unacked_only=true&limit=1000`) + if (res.ok) unackedAlerts.value = (await res.json()).total ?? 0 + } catch { /* non-critical — scorer may be disabled */ } + finally { alertsLoading.value = false } +} + function healthDot(errors: number, total: number): string { if (errors === 0) return 'bg-green-500' const ratio = errors / Math.max(total, 1) diff --git a/web/src/views/SecurityAlertsView.vue b/web/src/views/SecurityAlertsView.vue new file mode 100644 index 0000000..7ac5361 --- /dev/null +++ b/web/src/views/SecurityAlertsView.vue @@ -0,0 +1,458 @@ + + + + +