Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const TYPE_FAMILIES: Record<'ip' | 'url' | 'domain' | 'signatures', string[]> =
],
}

const compact = (n: number) =>
n.toLocaleString(undefined, { notation: 'compact', maximumFractionDigits: 1 })

// Show time-of-day for sub-day intervals, date for day+ intervals.
function formatTs(ts: number, interval: string): string {
const perDay = /min|hour|^[0-9]+[mh]$/i.test(interval)
Expand Down Expand Up @@ -100,8 +103,11 @@ export function MatchOverviewCard({ body, interval }: MatchOverviewCardProps) {
{t('threatIntel.overview.title')}
</div>
<div className="mt-1 flex items-baseline gap-3">
<span className="text-3xl font-semibold tabular-nums">
{query.isPending ? '—' : total.toLocaleString()}
<span
className="text-3xl font-semibold tabular-nums"
title={query.isPending ? undefined : total.toLocaleString()}
>
{query.isPending ? '—' : compact(total)}
</span>
<span className="text-sm text-muted-foreground">{t('threatIntel.overview.totalIndicators')}</span>
</div>
Expand All @@ -116,8 +122,11 @@ export function MatchOverviewCard({ body, interval }: MatchOverviewCardProps) {
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">
{t(`threatIntel.overview.families.${family}`, { defaultValue: family })}
</div>
<div className="text-lg font-semibold tabular-nums">
{query.isPending ? '—' : familyCounts[family].toLocaleString()}
<div
className="text-lg font-semibold tabular-nums"
title={query.isPending ? undefined : familyCounts[family].toLocaleString()}
>
{query.isPending ? '—' : compact(familyCounts[family])}
</div>
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
import { useTranslation } from 'react-i18next'
import { Download } from 'lucide-react'
import { Download, Info } from 'lucide-react'
import { Button } from '@/shared/components/ui/button'

export interface ThreatIntelHeaderProps {
matchedCount?: number
onRefresh?: () => void
onExport?: () => void
isExporting?: boolean
noInstanceIocs?: boolean
}

export function ThreatIntelHeader({ matchedCount, onExport, isExporting }: ThreatIntelHeaderProps) {
export function ThreatIntelHeader({ matchedCount, onExport, isExporting, noInstanceIocs }: ThreatIntelHeaderProps) {
const { t } = useTranslation()
const canExport = !!onExport && !!matchedCount && !isExporting
return (
<header className="flex flex-wrap items-center justify-between gap-3">
<div className="text-xs text-muted-foreground">
<span className="font-medium text-foreground">{matchedCount?.toLocaleString() || 0}</span> {t('threatIntel.header.matchedInEnv')}
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
{!noInstanceIocs && (
<span>
<span className="font-medium text-foreground">{matchedCount?.toLocaleString() || 0}</span> {t('threatIntel.header.matchedInEnv')}
</span>
)}
{noInstanceIocs && (
<span
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"
title={t('threatIntel.header.noInstanceIocsHint', {
defaultValue: 'No indicators observed in your alerts — showing generic feed data.',
})}
>
<Info size={12} />
{t('threatIntel.header.noInstanceIocs', {
defaultValue: 'No instance indicators — showing generic data',
})}
</span>
)}
</div>
<div className="flex items-center gap-2">
<Button variant="default" size="sm" onClick={onExport} disabled={!canExport}>
Expand Down
20 changes: 14 additions & 6 deletions frontend/src/features/threat-intel/hooks/use-alert-iocs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,9 @@ export function useAlertIocs() {
})
}

export function alertIocsFragment(iocs: AlertIocs): AdvancedSearchRequest {
export function alertIocsFragment(iocs: AlertIocs): AdvancedSearchRequest | undefined {
const entries = Object.entries(iocs.byAttr).filter(([, v]) => v.length > 0)
if (entries.length === 0) {
return { query: { must: [{ terms: { id: ['__no_observed_iocs__'] } }] } }
}
if (entries.length === 0) return undefined
return {
query: {
should: entries.map(([attr, values]) => ({
Expand All @@ -69,7 +67,17 @@ export function alertIocsFragment(iocs: AlertIocs): AdvancedSearchRequest {
}
}

export function useAlertIocsFragment(): AdvancedSearchRequest | undefined {
export interface AlertIocsFragmentResult {
ready: boolean
fragment: AdvancedSearchRequest | undefined
hasInstanceIocs: boolean
}

export function useAlertIocsFragment(): AlertIocsFragmentResult {
const q = useAlertIocs()
return useMemo(() => (q.data ? alertIocsFragment(q.data) : undefined), [q.data])
return useMemo<AlertIocsFragmentResult>(() => {
if (!q.data) return { ready: false, fragment: undefined, hasInstanceIocs: false }
const fragment = alertIocsFragment(q.data)
return { ready: true, fragment, hasInstanceIocs: !!fragment }
}, [q.data])
}
11 changes: 6 additions & 5 deletions frontend/src/features/threat-intel/pages/ThreatIntelPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ export function ThreatIntelPage() {

useEffect(() => {
if (!isConfigured) return
if (!observedFragment) return
if (!observedFragment.ready) return
const my = ++seqRef.current
const mode = modeRef.current
const body = composeBody(filtersToRequest(filters), query, timeRange, observedFragment)
const body = composeBody(filtersToRequest(filters), query, timeRange, observedFragment.fragment)
setLastBody(body)
searchAdvancedMutation.mutate(
{ body, limit: size, page: page + 1 },
Expand All @@ -129,7 +129,7 @@ export function ThreatIntelPage() {
},
}
)
}, [query, page, size, isConfigured, timeRange, observedFragment])
}, [query, page, size, isConfigured, timeRange, observedFragment.ready, observedFragment.fragment])

if (configLoading) return null
if (isConfigured === false) return <NotConfiguredState />
Expand Down Expand Up @@ -162,7 +162,7 @@ export function ThreatIntelPage() {
}

const handleFiltersApply = (request: AdvancedSearchRequest) => {
const body = composeBody(request, query, timeRange, observedFragment)
const body = composeBody(request, query, timeRange, observedFragment.fragment)
modeRef.current = 'replace'
setPage(0)
setLastBody(body)
Expand Down Expand Up @@ -232,11 +232,12 @@ export function ThreatIntelPage() {
matchedCount={totalItems}
onExport={handleExport}
isExporting={isExporting}
noInstanceIocs={observedFragment.ready && !observedFragment.hasInstanceIocs}
/>

<div className="mt-5">
<MatchOverviewCard
body={observedFragment ? lastBody : undefined}
body={observedFragment.ready ? lastBody : undefined}
interval={TIME_RANGE_OPTIONS.find((o) => o.value === timeRange)?.interval ?? 'hour'}
/>
</div>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/shared/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -5047,7 +5047,9 @@
"header": {
"matchedInEnv": "Treffer in Ihrer Umgebung",
"export": "IOCs exportieren",
"exporting": "Wird exportiert…"
"exporting": "Wird exportiert…",
"noInstanceIocs": "Keine Instanz-Indikatoren – generische Daten werden angezeigt",
"noInstanceIocsHint": "In Ihren Warnmeldungen wurden keine Indikatoren beobachtet – generische Feed-Daten werden angezeigt."
},
"overview": {
"title": "IOC-Treffer",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/shared/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,9 @@
"header": {
"matchedInEnv": "matched in your env",
"export": "Export IOCs",
"exporting": "Exporting…"
"exporting": "Exporting…",
"noInstanceIocs": "No instance indicators — showing generic data",
"noInstanceIocsHint": "No indicators observed in your alerts — showing generic feed data."
},
"overview": {
"title": "IOC matches",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/shared/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -5009,7 +5009,9 @@
"header": {
"matchedInEnv": "coincidencias en tu entorno",
"export": "Exportar IOCs",
"exporting": "Exportando…"
"exporting": "Exportando…",
"noInstanceIocs": "Sin indicadores de instancia — mostrando datos genéricos",
"noInstanceIocsHint": "No se observaron indicadores en tus alertas — se muestran datos genéricos de feeds."
},
"overview": {
"title": "Coincidencias de IOCs",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/shared/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -5047,7 +5047,9 @@
"header": {
"matchedInEnv": "correspondances détectées dans votre environnement",
"export": "Exporter les IOCs",
"exporting": "Exportation…"
"exporting": "Exportation…",
"noInstanceIocs": "Aucun indicateur d'instance — affichage de données génériques",
"noInstanceIocsHint": "Aucun indicateur observé dans vos alertes — affichage des données génériques des flux."
},
"overview": {
"title": "Correspondances IOC",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/shared/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -5047,7 +5047,9 @@
"header": {
"matchedInEnv": "corrispondenze nel tuo ambiente",
"export": "Esporta IOCs",
"exporting": "Esportazione…"
"exporting": "Esportazione…",
"noInstanceIocs": "Nessun indicatore d'istanza — visualizzazione di dati generici",
"noInstanceIocsHint": "Nessun indicatore osservato nei tuoi avvisi — vengono mostrati i dati generici dei feed."
},
"overview": {
"title": "Corrispondenze IOC",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/shared/i18n/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -5009,7 +5009,9 @@
"header": {
"matchedInEnv": "coincidências em seu ambiente",
"export": "Exportar IOCs",
"exporting": "Exportando…"
"exporting": "Exportando…",
"noInstanceIocs": "Sem indicadores de instância — exibindo dados genéricos",
"noInstanceIocsHint": "Nenhum indicador observado em seus alertas — exibindo dados genéricos dos feeds."
},
"overview": {
"title": "Correspondências de IOC",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/shared/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -5073,7 +5073,9 @@
"header": {
"matchedInEnv": "совпадения в вашей среде",
"export": "Экспортировать IOCs",
"exporting": "Экспортирование…"
"exporting": "Экспортирование…",
"noInstanceIocs": "Нет индикаторов среды — показаны общие данные",
"noInstanceIocsHint": "В ваших оповещениях не обнаружено индикаторов — отображаются общие данные из фидов."
},
"overview": {
"title": "Совпадения IOC",
Expand Down
Loading