Skip to content

Commit 72ebe3d

Browse files
Backlog/v12 threat intelligense (#2412)
* fix[backend](mcp): added jsonschemas to eventprocessing tools * fix[frontend](threat-intelligense): added generic data view on no env indicators detected * fix[frontend](threat-intelligense): added missing translations
1 parent 80c448b commit 72ebe3d

11 files changed

Lines changed: 76 additions & 26 deletions

File tree

frontend/src/features/threat-intel/components/MatchOverviewCard.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ const TYPE_FAMILIES: Record<'ip' | 'url' | 'domain' | 'signatures', string[]> =
1515
],
1616
}
1717

18+
const compact = (n: number) =>
19+
n.toLocaleString(undefined, { notation: 'compact', maximumFractionDigits: 1 })
20+
1821
// Show time-of-day for sub-day intervals, date for day+ intervals.
1922
function formatTs(ts: number, interval: string): string {
2023
const perDay = /min|hour|^[0-9]+[mh]$/i.test(interval)
@@ -100,8 +103,11 @@ export function MatchOverviewCard({ body, interval }: MatchOverviewCardProps) {
100103
{t('threatIntel.overview.title')}
101104
</div>
102105
<div className="mt-1 flex items-baseline gap-3">
103-
<span className="text-3xl font-semibold tabular-nums">
104-
{query.isPending ? '—' : total.toLocaleString()}
106+
<span
107+
className="text-3xl font-semibold tabular-nums"
108+
title={query.isPending ? undefined : total.toLocaleString()}
109+
>
110+
{query.isPending ? '—' : compact(total)}
105111
</span>
106112
<span className="text-sm text-muted-foreground">{t('threatIntel.overview.totalIndicators')}</span>
107113
</div>
@@ -116,8 +122,11 @@ export function MatchOverviewCard({ body, interval }: MatchOverviewCardProps) {
116122
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">
117123
{t(`threatIntel.overview.families.${family}`, { defaultValue: family })}
118124
</div>
119-
<div className="text-lg font-semibold tabular-nums">
120-
{query.isPending ? '—' : familyCounts[family].toLocaleString()}
125+
<div
126+
className="text-lg font-semibold tabular-nums"
127+
title={query.isPending ? undefined : familyCounts[family].toLocaleString()}
128+
>
129+
{query.isPending ? '—' : compact(familyCounts[family])}
121130
</div>
122131
</div>
123132
))}

frontend/src/features/threat-intel/components/ThreatIntelHeader.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,39 @@
11
import { useTranslation } from 'react-i18next'
2-
import { Download } from 'lucide-react'
2+
import { Download, Info } from 'lucide-react'
33
import { Button } from '@/shared/components/ui/button'
44

55
export interface ThreatIntelHeaderProps {
66
matchedCount?: number
77
onRefresh?: () => void
88
onExport?: () => void
99
isExporting?: boolean
10+
noInstanceIocs?: boolean
1011
}
1112

12-
export function ThreatIntelHeader({ matchedCount, onExport, isExporting }: ThreatIntelHeaderProps) {
13+
export function ThreatIntelHeader({ matchedCount, onExport, isExporting, noInstanceIocs }: ThreatIntelHeaderProps) {
1314
const { t } = useTranslation()
1415
const canExport = !!onExport && !!matchedCount && !isExporting
1516
return (
1617
<header className="flex flex-wrap items-center justify-between gap-3">
17-
<div className="text-xs text-muted-foreground">
18-
<span className="font-medium text-foreground">{matchedCount?.toLocaleString() || 0}</span> {t('threatIntel.header.matchedInEnv')}
18+
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
19+
{!noInstanceIocs && (
20+
<span>
21+
<span className="font-medium text-foreground">{matchedCount?.toLocaleString() || 0}</span> {t('threatIntel.header.matchedInEnv')}
22+
</span>
23+
)}
24+
{noInstanceIocs && (
25+
<span
26+
className="inline-flex items-center gap-1 rounded-md border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[11px] font-medium text-amber-600 dark:text-amber-400"
27+
title={t('threatIntel.header.noInstanceIocsHint', {
28+
defaultValue: 'No indicators observed in your alerts — showing generic feed data.',
29+
})}
30+
>
31+
<Info size={12} />
32+
{t('threatIntel.header.noInstanceIocs', {
33+
defaultValue: 'No instance indicators — showing generic data',
34+
})}
35+
</span>
36+
)}
1937
</div>
2038
<div className="flex items-center gap-2">
2139
<Button variant="default" size="sm" onClick={onExport} disabled={!canExport}>

frontend/src/features/threat-intel/hooks/use-alert-iocs.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,9 @@ export function useAlertIocs() {
5555
})
5656
}
5757

58-
export function alertIocsFragment(iocs: AlertIocs): AdvancedSearchRequest {
58+
export function alertIocsFragment(iocs: AlertIocs): AdvancedSearchRequest | undefined {
5959
const entries = Object.entries(iocs.byAttr).filter(([, v]) => v.length > 0)
60-
if (entries.length === 0) {
61-
return { query: { must: [{ terms: { id: ['__no_observed_iocs__'] } }] } }
62-
}
60+
if (entries.length === 0) return undefined
6361
return {
6462
query: {
6563
should: entries.map(([attr, values]) => ({
@@ -69,7 +67,17 @@ export function alertIocsFragment(iocs: AlertIocs): AdvancedSearchRequest {
6967
}
7068
}
7169

72-
export function useAlertIocsFragment(): AdvancedSearchRequest | undefined {
70+
export interface AlertIocsFragmentResult {
71+
ready: boolean
72+
fragment: AdvancedSearchRequest | undefined
73+
hasInstanceIocs: boolean
74+
}
75+
76+
export function useAlertIocsFragment(): AlertIocsFragmentResult {
7377
const q = useAlertIocs()
74-
return useMemo(() => (q.data ? alertIocsFragment(q.data) : undefined), [q.data])
78+
return useMemo<AlertIocsFragmentResult>(() => {
79+
if (!q.data) return { ready: false, fragment: undefined, hasInstanceIocs: false }
80+
const fragment = alertIocsFragment(q.data)
81+
return { ready: true, fragment, hasInstanceIocs: !!fragment }
82+
}, [q.data])
7583
}

frontend/src/features/threat-intel/pages/ThreatIntelPage.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ export function ThreatIntelPage() {
102102

103103
useEffect(() => {
104104
if (!isConfigured) return
105-
if (!observedFragment) return
105+
if (!observedFragment.ready) return
106106
const my = ++seqRef.current
107107
const mode = modeRef.current
108-
const body = composeBody(filtersToRequest(filters), query, timeRange, observedFragment)
108+
const body = composeBody(filtersToRequest(filters), query, timeRange, observedFragment.fragment)
109109
setLastBody(body)
110110
searchAdvancedMutation.mutate(
111111
{ body, limit: size, page: page + 1 },
@@ -129,7 +129,7 @@ export function ThreatIntelPage() {
129129
},
130130
}
131131
)
132-
}, [query, page, size, isConfigured, timeRange, observedFragment])
132+
}, [query, page, size, isConfigured, timeRange, observedFragment.ready, observedFragment.fragment])
133133

134134
if (configLoading) return null
135135
if (isConfigured === false) return <NotConfiguredState />
@@ -162,7 +162,7 @@ export function ThreatIntelPage() {
162162
}
163163

164164
const handleFiltersApply = (request: AdvancedSearchRequest) => {
165-
const body = composeBody(request, query, timeRange, observedFragment)
165+
const body = composeBody(request, query, timeRange, observedFragment.fragment)
166166
modeRef.current = 'replace'
167167
setPage(0)
168168
setLastBody(body)
@@ -232,11 +232,12 @@ export function ThreatIntelPage() {
232232
matchedCount={totalItems}
233233
onExport={handleExport}
234234
isExporting={isExporting}
235+
noInstanceIocs={observedFragment.ready && !observedFragment.hasInstanceIocs}
235236
/>
236237

237238
<div className="mt-5">
238239
<MatchOverviewCard
239-
body={observedFragment ? lastBody : undefined}
240+
body={observedFragment.ready ? lastBody : undefined}
240241
interval={TIME_RANGE_OPTIONS.find((o) => o.value === timeRange)?.interval ?? 'hour'}
241242
/>
242243
</div>

frontend/src/shared/i18n/locales/de.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5047,7 +5047,9 @@
50475047
"header": {
50485048
"matchedInEnv": "Treffer in Ihrer Umgebung",
50495049
"export": "IOCs exportieren",
5050-
"exporting": "Wird exportiert…"
5050+
"exporting": "Wird exportiert…",
5051+
"noInstanceIocs": "Keine Instanz-Indikatoren – generische Daten werden angezeigt",
5052+
"noInstanceIocsHint": "In Ihren Warnmeldungen wurden keine Indikatoren beobachtet – generische Feed-Daten werden angezeigt."
50515053
},
50525054
"overview": {
50535055
"title": "IOC-Treffer",

frontend/src/shared/i18n/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,9 @@
689689
"header": {
690690
"matchedInEnv": "matched in your env",
691691
"export": "Export IOCs",
692-
"exporting": "Exporting…"
692+
"exporting": "Exporting…",
693+
"noInstanceIocs": "No instance indicators — showing generic data",
694+
"noInstanceIocsHint": "No indicators observed in your alerts — showing generic feed data."
693695
},
694696
"overview": {
695697
"title": "IOC matches",

frontend/src/shared/i18n/locales/es.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5009,7 +5009,9 @@
50095009
"header": {
50105010
"matchedInEnv": "coincidencias en tu entorno",
50115011
"export": "Exportar IOCs",
5012-
"exporting": "Exportando…"
5012+
"exporting": "Exportando…",
5013+
"noInstanceIocs": "Sin indicadores de instancia — mostrando datos genéricos",
5014+
"noInstanceIocsHint": "No se observaron indicadores en tus alertas — se muestran datos genéricos de feeds."
50135015
},
50145016
"overview": {
50155017
"title": "Coincidencias de IOCs",

frontend/src/shared/i18n/locales/fr.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5047,7 +5047,9 @@
50475047
"header": {
50485048
"matchedInEnv": "correspondances détectées dans votre environnement",
50495049
"export": "Exporter les IOCs",
5050-
"exporting": "Exportation…"
5050+
"exporting": "Exportation…",
5051+
"noInstanceIocs": "Aucun indicateur d'instance — affichage de données génériques",
5052+
"noInstanceIocsHint": "Aucun indicateur observé dans vos alertes — affichage des données génériques des flux."
50515053
},
50525054
"overview": {
50535055
"title": "Correspondances IOC",

frontend/src/shared/i18n/locales/it.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5047,7 +5047,9 @@
50475047
"header": {
50485048
"matchedInEnv": "corrispondenze nel tuo ambiente",
50495049
"export": "Esporta IOCs",
5050-
"exporting": "Esportazione…"
5050+
"exporting": "Esportazione…",
5051+
"noInstanceIocs": "Nessun indicatore d'istanza — visualizzazione di dati generici",
5052+
"noInstanceIocsHint": "Nessun indicatore osservato nei tuoi avvisi — vengono mostrati i dati generici dei feed."
50515053
},
50525054
"overview": {
50535055
"title": "Corrispondenze IOC",

frontend/src/shared/i18n/locales/pt.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5009,7 +5009,9 @@
50095009
"header": {
50105010
"matchedInEnv": "coincidências em seu ambiente",
50115011
"export": "Exportar IOCs",
5012-
"exporting": "Exportando…"
5012+
"exporting": "Exportando…",
5013+
"noInstanceIocs": "Sem indicadores de instância — exibindo dados genéricos",
5014+
"noInstanceIocsHint": "Nenhum indicador observado em seus alertas — exibindo dados genéricos dos feeds."
50135015
},
50145016
"overview": {
50155017
"title": "Correspondências de IOC",

0 commit comments

Comments
 (0)