From 571d88db4d7a6ecfe191ed6c45142b3167d55b29 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 15 Jun 2026 22:41:21 -0400 Subject: [PATCH] =?UTF-8?q?fix(aria):=20IOC=20Feeds=20=E2=80=94=20restore?= =?UTF-8?q?=20app,=20fix=20API=20shapes,=20canonical=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: IocFeeds.svelte called feeds.find() on a Map (backend returned Map not List) → reactive statement crash → entire Svelte tree broken, all sidebar nav dead. Backend: - /feeds/status now returns List with feedName/configured/enabled/ lastRunAt/lastRunStatus/lastIocsAdded/lastIocsUpdated fields - /feeds/runs now returns full Page (not stripped .getContent()) - IocFeedService.isConfigured(feedName) added for per-feed key check - Feed order fixed to [OTX, MALWARE_BAZAAR, THREAT_FOX] (deterministic) Frontend: - loadFeeds: Array.isArray guard prevents crash if shape is ever wrong - loadRuns: safe fallback for both Page and plain-array responses - api.ts refreshFeed URL fixed: /feeds/refresh/{name} not /feeds/{name}/refresh - IocFeedStatus type: added lastIocsUpdated - IocFeeds.svelte: Refresh All moved from header to toolbar; all buttons use canonical btn/btn-primary/btn-secondary/btn-sm classes; pollInterval now calls pollStatus (status-only poll) not full refresh trigger Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/IocFeeds.svelte | 543 ++++++------------ frontend/src/lib/api.ts | 4 +- .../aria/controller/IocFeedController.java | 40 +- .../hiveops/aria/service/IocFeedService.java | 11 +- 4 files changed, 209 insertions(+), 389 deletions(-) diff --git a/frontend/src/components/IocFeeds.svelte b/frontend/src/components/IocFeeds.svelte index 995dfc5..422d2c6 100644 --- a/frontend/src/components/IocFeeds.svelte +++ b/frontend/src/components/IocFeeds.svelte @@ -4,7 +4,6 @@ import { addToast } from '../lib/stores'; import Pagination from './common/Pagination.svelte'; - // ── State ───────────────────────────────────────────────────────────── let feeds: IocFeedStatus[] = []; let feedsLoading = true; @@ -18,26 +17,23 @@ let refreshingAll = false; let refreshingFeed: Record = {}; - // ── Feed display config ─────────────────────────────────────────────── - const FEED_META: Record = { - OTX: { label: 'AlienVault OTX', icon: '🔗', requiresKey: true }, - MALWARE_BAZAAR: { label: 'MalwareBazaar', icon: '🦠', requiresKey: false }, - THREAT_FOX: { label: 'ThreatFox', icon: '🦊', requiresKey: false }, + const FEED_META: Record = { + OTX: { label: 'AlienVault OTX', icon: '🔗' }, + MALWARE_BAZAAR: { label: 'MalwareBazaar', icon: '🦠' }, + THREAT_FOX: { label: 'ThreatFox', icon: '🦊' }, }; - // Ensure cards always render in a fixed order even if backend returns them in different order const FEED_ORDER = ['OTX', 'MALWARE_BAZAAR', 'THREAT_FOX']; - $: orderedFeeds = FEED_ORDER.map(name => feeds.find(f => f.feedName === name) ?? { - feedName: name, enabled: false, configured: false, - } as IocFeedStatus); + $: orderedFeeds = FEED_ORDER.map(name => + feeds.find(f => f.feedName === name) ?? { feedName: name, enabled: false, configured: false } as IocFeedStatus + ); - // ── Data loading ────────────────────────────────────────────────────── async function loadFeeds() { feedsLoading = true; try { const res = await ariaApi.getFeedStatus(); - feeds = res.data; + feeds = Array.isArray(res.data) ? res.data : []; } catch { addToast('error', 'Load Failed', 'Could not load feed status'); } finally { @@ -49,28 +45,31 @@ runsLoading = true; try { const res = await ariaApi.getFeedRuns({ page, size: runsPageSize }); - runs = res.data.content; - runsTotalElements = res.data.page.totalElements; - runsTotalPages = res.data.page.totalPages; - runsPage = res.data.page.number; + const d = res.data as any; + runs = d.content ?? (Array.isArray(d) ? d : []); + runsTotalElements = d.page?.totalElements ?? runs.length; + runsTotalPages = d.page?.totalPages ?? 1; + runsPage = d.page?.number ?? page; } catch { - addToast('error', 'Load Failed', 'Could not load feed run history'); + addToast('error', 'Load Failed', 'Could not load run history'); } finally { runsLoading = false; } } - async function refresh() { - await Promise.all([loadFeeds(), loadRuns(runsPage)]); + async function pollStatus() { + try { + const res = await ariaApi.getFeedStatus(); + feeds = Array.isArray(res.data) ? res.data : []; + } catch { /* silent */ } } - // ── Refresh actions ─────────────────────────────────────────────────── async function refreshAll() { refreshingAll = true; try { - const res = await ariaApi.refreshAllFeeds(); - addToast('success', 'Refresh Triggered', res.data.message || 'All feeds are refreshing'); - setTimeout(() => refresh(), 2000); + await ariaApi.refreshAllFeeds(); + addToast('success', 'Refresh Triggered', 'All feeds are refreshing'); + setTimeout(() => { loadFeeds(); loadRuns(runsPage); }, 3000); } catch { addToast('error', 'Refresh Failed', 'Could not trigger feed refresh'); } finally { @@ -81,54 +80,48 @@ async function refreshSingle(feedName: string) { refreshingFeed = { ...refreshingFeed, [feedName]: true }; try { - const res = await ariaApi.refreshFeed(feedName); - addToast('success', 'Refresh Triggered', res.data.message || `${feedName} feed is refreshing`); - setTimeout(() => refresh(), 2000); + await ariaApi.refreshFeed(feedName); + addToast('success', 'Refresh Triggered', `${FEED_META[feedName]?.label ?? feedName} is refreshing`); + setTimeout(() => { loadFeeds(); loadRuns(runsPage); }, 3000); } catch { - addToast('error', 'Refresh Failed', `Could not trigger ${feedName} refresh`); + addToast('error', 'Refresh Failed', `Could not refresh ${feedName}`); } finally { refreshingFeed = { ...refreshingFeed, [feedName]: false }; } } - // ── Helpers ─────────────────────────────────────────────────────────── - function statusClass(status: string | undefined): string { - if (!status) return 'never'; - if (status === 'SUCCESS') return 'success'; - if (status === 'FAILED') return 'failed'; - if (status === 'RUNNING') return 'running'; + function statusClass(s?: string) { + if (s === 'SUCCESS') return 'success'; + if (s === 'ERROR') return 'error'; + if (s === 'RUNNING') return 'running'; return 'never'; } - function statusLabel(status: string | undefined): string { - if (!status) return 'Never run'; - if (status === 'SUCCESS') return 'Success'; - if (status === 'FAILED') return 'Failed'; - if (status === 'RUNNING') return 'Running'; - return status; + function statusLabel(s?: string) { + if (s === 'SUCCESS') return 'Success'; + if (s === 'ERROR') return 'Error'; + if (s === 'RUNNING') return 'Running'; + return 'Never run'; } - function relativeTime(iso: string | undefined): string { + function relativeTime(iso?: string) { if (!iso) return '—'; const diff = Date.now() - new Date(iso).getTime(); - const sec = Math.floor(diff / 1000); - const min = Math.floor(sec / 60); - const hr = Math.floor(min / 60); - const day = Math.floor(hr / 24); - if (sec < 60) return 'just now'; - if (min < 60) return `${min} minute${min !== 1 ? 's' : ''} ago`; - if (hr < 24) return `${hr} hour${hr !== 1 ? 's' : ''} ago`; - return `${day} day${day !== 1 ? 's' : ''} ago`; + const min = Math.floor(diff / 60000); + const hr = Math.floor(min / 60); + const day = Math.floor(hr / 24); + if (min < 1) return 'just now'; + if (min < 60) return `${min}m ago`; + if (hr < 24) return `${hr}h ago`; + return `${day}d ago`; } - function formatTs(iso: string | undefined): string { + function formatTs(iso?: string) { if (!iso) return '—'; - return new Date(iso).toLocaleString('en-US', { - month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' - }); + return new Date(iso).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } - function duration(start: string, end: string | undefined): string { + function duration(start: string, end?: string) { if (!end) return '—'; const ms = new Date(end).getTime() - new Date(start).getTime(); if (ms < 1000) return `${ms}ms`; @@ -136,18 +129,12 @@ return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; } - function fmtCount(n: number | undefined): string { - if (n == null) return '—'; - return n.toLocaleString(); - } - - // ── Auto-refresh every 30 s ─────────────────────────────────────────── let pollInterval: ReturnType; onMount(() => { loadFeeds(); loadRuns(0); - pollInterval = setInterval(refresh, 30_000); + pollInterval = setInterval(pollStatus, 30_000); }); onDestroy(() => clearInterval(pollInterval)); @@ -155,85 +142,75 @@
-

IOC Feeds

-

Automated threat intelligence ingestion — OTX, MalwareBazaar, ThreatFox

-
-
- +

Automated threat intelligence — OTX, MalwareBazaar, ThreatFox

-
+
+
+
+ {feeds.length} feed{feeds.length !== 1 ? 's' : ''} +
+
+ + +
+
+ {#if feedsLoading && feeds.length === 0} -
Loading feed status…
+
Loading feed status…
{:else}
{#each orderedFeeds as feed (feed.feedName)} - {@const meta = FEED_META[feed.feedName] ?? { label: feed.feedName, icon: '📡', requiresKey: false }} -
+ {@const meta = FEED_META[feed.feedName] ?? { label: feed.feedName, icon: '📡' }} +
-
{meta.icon}
+ {meta.icon}
{meta.label}
-
{feed.feedName}
+
{feed.feedName}
- - {statusLabel(feed.lastRunStatus)} - + {statusLabel(feed.lastRunStatus)}
-
+
{#if feed.configured} - Configured - {:else if meta.requiresKey} - Not configured — API key required + Configured {:else} - No key required + API key required {/if}
- Last run - {relativeTime(feed.lastRunAt)} + Last run + {relativeTime(feed.lastRunAt)}
- IOCs added - {fmtCount(feed.lastIocsAdded)} + IOCs added + {feed.lastIocsAdded?.toLocaleString() ?? '—'}
- Total from feed - {fmtCount(feed.totalIocsFromFeed)} + Updated + {feed.lastIocsUpdated?.toLocaleString() ?? '—'}
@@ -242,14 +219,15 @@ {/if}
- -
-
- Run History - {runsTotalElements.toLocaleString()} run{runsTotalElements !== 1 ? 's' : ''} -
+ +
+
+
+
+ {runsTotalElements.toLocaleString()} run{runsTotalElements !== 1 ? 's' : ''} +
+
-
{#if runsLoading} -
Loading run history…
+
Loading…
{:else if runs.length === 0} -
No feed runs recorded yet. Trigger a refresh to see results here.
+
No feed runs yet. Click "Run All Feeds" to start.
{:else} @@ -271,8 +249,8 @@ - - + + @@ -285,24 +263,18 @@ {FEED_META[run.feedName]?.label ?? run.feedName} - - + + + + + - - - @@ -317,275 +289,106 @@ diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 697cc37..c68e460 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -122,7 +122,7 @@ export type IocFeedStatus = { lastRunAt?: string; lastRunStatus?: string; lastIocsAdded?: number; - totalIocsFromFeed?: number; + lastIocsUpdated?: number; }; export type IocFeedRun = { @@ -190,5 +190,5 @@ export const ariaApi = { api.post<{ message: string }>('/api/aria/feeds/refresh'), refreshFeed: (feedName: string) => - api.post<{ message: string }>(`/api/aria/feeds/${feedName}/refresh`), + api.post<{ message: string }>(`/api/aria/feeds/refresh/${feedName}`), }; diff --git a/src/main/java/com/hiveops/aria/controller/IocFeedController.java b/src/main/java/com/hiveops/aria/controller/IocFeedController.java index 98e84a7..f29d6a5 100644 --- a/src/main/java/com/hiveops/aria/controller/IocFeedController.java +++ b/src/main/java/com/hiveops/aria/controller/IocFeedController.java @@ -5,14 +5,13 @@ import com.hiveops.aria.repository.IocFeedRunRepository; import com.hiveops.aria.service.IocFeedService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; @RestController @RequestMapping("/api/aria/feeds") @@ -20,6 +19,9 @@ import java.util.Optional; @Slf4j public class IocFeedController { + private final IocFeedService feedService; + private final IocFeedRunRepository feedRunRepository; + private Thread feedThread(String name, Runnable task) { Thread t = new Thread(task, name); t.setUncaughtExceptionHandler((thread, ex) -> @@ -27,30 +29,36 @@ public class IocFeedController { return t; } - private final IocFeedService feedService; - private final IocFeedRunRepository feedRunRepository; - - /** Latest run per feed — used by the status cards in the UI */ + /** One status object per feed — array, matches IocFeedStatus[] in the frontend */ @GetMapping("/status") @PreAuthorize("hasRole('BCOS_ADMIN')") - public ResponseEntity> status() { - List names = feedService.feedNames(); - Map result = new java.util.LinkedHashMap<>(); - for (String name : names) { + public ResponseEntity>> status() { + List> result = new ArrayList<>(); + for (String name : feedService.feedNames()) { Optional last = feedRunRepository.findTopByFeedNameOrderByStartedAtDesc(name); - result.put(name, last.orElse(null)); + Map entry = new LinkedHashMap<>(); + entry.put("feedName", name); + entry.put("configured", feedService.isConfigured(name)); + entry.put("enabled", true); + last.ifPresent(r -> { + entry.put("lastRunAt", r.getCompletedAt() != null ? r.getCompletedAt() : r.getStartedAt()); + entry.put("lastRunStatus", r.getStatus()); + entry.put("lastIocsAdded", r.getIocsAdded()); + entry.put("lastIocsUpdated", r.getIocsUpdated()); + }); + result.add(entry); } return ResponseEntity.ok(result); } - /** Recent run history — default 50 rows */ + /** Paginated run history — returns Spring Page so the frontend gets content + page metadata */ @GetMapping("/runs") @PreAuthorize("hasRole('BCOS_ADMIN')") - public ResponseEntity> runs( + public ResponseEntity> runs( @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "50") int size) { + @RequestParam(defaultValue = "20") int size) { return ResponseEntity.ok( - feedRunRepository.findAllByOrderByStartedAtDesc(PageRequest.of(page, size)).getContent() + feedRunRepository.findAllByOrderByStartedAtDesc(PageRequest.of(page, size)) ); } diff --git a/src/main/java/com/hiveops/aria/service/IocFeedService.java b/src/main/java/com/hiveops/aria/service/IocFeedService.java index 39cf322..192f644 100644 --- a/src/main/java/com/hiveops/aria/service/IocFeedService.java +++ b/src/main/java/com/hiveops/aria/service/IocFeedService.java @@ -114,6 +114,15 @@ public class IocFeedService { } public List feedNames() { - return List.copyOf(FEED_SOURCES.keySet()); + return List.of("OTX", "MALWARE_BAZAAR", "THREAT_FOX"); + } + + public boolean isConfigured(String feedName) { + return switch (feedName) { + case "OTX" -> otxClient.isConfigured(); + case "MALWARE_BAZAAR" -> malwareBazaarClient.isConfigured(); + case "THREAT_FOX" -> threatFoxClient.isConfigured(); + default -> false; + }; } }
Started Duration StatusIOCs AddedIOCs UpdatedAddedUpdated Error
{formatTs(run.startedAt)}{duration(run.startedAt, run.completedAt)}{formatTs(run.startedAt)}{duration(run.startedAt, run.completedAt)}{statusLabel(run.status)}{run.iocsAdded.toLocaleString()}{run.iocsUpdated.toLocaleString()} - - {statusLabel(run.status)} - - {run.iocsAdded.toLocaleString()}{run.iocsUpdated.toLocaleString()} {#if run.errorMessage} - {run.errorMessage.length > 60 - ? run.errorMessage.substring(0, 60) + '…' - : run.errorMessage} + {run.errorMessage.length > 60 ? run.errorMessage.substring(0, 60) + '…' : run.errorMessage} {:else} - + {/if}