/** * SavedSearches - Manage bookmarked queries and recurring scans. * Supports IOC, keyword, NLP, and correlation search types with delta tracking. */ import React, { useState, useEffect, useCallback } from 'react'; import { Box, Typography, Paper, CircularProgress, Alert, Button, Chip, Table, TableHead, TableRow, TableCell, TableBody, TableContainer, Dialog, DialogTitle, DialogContent, DialogActions, TextField, FormControl, InputLabel, Select, MenuItem, IconButton, Tooltip, } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import DeleteIcon from '@mui/icons-material/Delete'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import EditIcon from '@mui/icons-material/Edit'; import BookmarkIcon from '@mui/icons-material/Bookmark'; import { useSnackbar } from 'notistack'; import { savedSearches, SavedSearchData, SearchRunResult } from '../api/client'; const SEARCH_TYPES = [ { value: 'ioc_search', label: 'IOC Search' }, { value: 'keyword_scan', label: 'Keyword Scan' }, { value: 'nlp_query', label: 'NLP Query' }, { value: 'correlation', label: 'Correlation' }, ]; function typeColor(t: string): 'primary' | 'secondary' | 'warning' | 'info' { switch (t) { case 'ioc_search': return 'primary'; case 'keyword_scan': return 'warning'; case 'nlp_query': return 'info'; case 'correlation': return 'secondary'; default: return 'primary'; } } export default function SavedSearchesView() { const { enqueueSnackbar } = useSnackbar(); const [loading, setLoading] = useState(false); const [items, setItems] = useState([]); const [showForm, setShowForm] = useState(false); const [editing, setEditing] = useState(null); const [runResult, setRunResult] = useState(null); const [runId, setRunId] = useState(null); const [running, setRunning] = useState(null); // Form state const [name, setName] = useState(''); const [searchType, setSearchType] = useState('ioc_search'); const [queryParams, setQueryParams] = useState(''); const [huntId, setHuntId] = useState(''); const load = useCallback(async () => { setLoading(true); try { const data = await savedSearches.list(); setItems(data.searches); } catch (e: any) { enqueueSnackbar(e.message, { variant: 'error' }); } finally { setLoading(false); } }, [enqueueSnackbar]); useEffect(() => { load(); }, [load]); const openCreate = () => { setEditing(null); setName(''); setSearchType('ioc_search'); setQueryParams(''); setHuntId(''); setShowForm(true); }; const openEdit = (item: SavedSearchData) => { setEditing(item); setName(item.name); setSearchType(item.search_type); setQueryParams(JSON.stringify(item.query_params, null, 2)); setHuntId((item.query_params as any)?.hunt_id || ''); setShowForm(true); }; const save = async () => { if (!name.trim()) return; let params: Record = {}; try { params = JSON.parse(queryParams || '{}'); } catch { enqueueSnackbar('Invalid JSON in query parameters', { variant: 'error' }); return; } try { if (editing) { await savedSearches.update(editing.id, { name, search_type: searchType, query_params: params, hunt_id: huntId || undefined, }); enqueueSnackbar('Search updated', { variant: 'success' }); } else { await savedSearches.create({ name, search_type: searchType, query_params: params, hunt_id: huntId || undefined, }); enqueueSnackbar('Search saved', { variant: 'success' }); } setShowForm(false); load(); } catch (e: any) { enqueueSnackbar(e.message, { variant: 'error' }); } }; const remove = async (id: string) => { try { await savedSearches.delete(id); enqueueSnackbar('Deleted', { variant: 'success' }); load(); } catch (e: any) { enqueueSnackbar(e.message, { variant: 'error' }); } }; const runSearch = async (id: string) => { setRunning(id); try { const result = await savedSearches.run(id); setRunResult(result); setRunId(id); load(); // refresh last_run times } catch (e: any) { enqueueSnackbar(e.message, { variant: 'error' }); } finally { setRunning(null); } }; return ( Saved Searches {loading && } {!loading && items.length === 0 && ( No saved searches yet. Create one to bookmark frequently-used queries for quick re-execution. )} {items.length > 0 && ( Name Type Hunt ID Last Run Last Count Actions {items.map(item => ( {item.name} t.value === item.search_type)?.label || item.search_type} color={typeColor(item.search_type)} size="small" sx={{ fontSize: '0.7rem' }} /> {(item.query_params as any)?.hunt_id ? String((item.query_params as any).hunt_id).slice(0, 8) + '...' : 'All'} {item.last_run_at ? new Date(item.last_run_at).toLocaleString() : 'Never'} {item.last_result_count != null ? ( 0 ? 'warning' : 'default'} /> ) : ''} runSearch(item.id)} disabled={running === item.id}> {running === item.id ? : } openEdit(item)}> remove(item.id)}> ))}
)} {/* Run result dialog */} setRunResult(null)} maxWidth="sm" fullWidth> Search Results {runResult && ( Search: {items.find(i => i.id === runId)?.name} 0 ? 'warning' : 'success'} /> {runResult.delta !== undefined && runResult.delta !== null && ( = 0 ? '+' : ''}${runResult.delta} since last run`} color={runResult.delta > 0 ? 'error' : 'default'} variant="outlined" /> )} {runResult.results && runResult.results.length > 0 && ( Preview (first {runResult.results.length} results): {runResult.results.map((item: any, i: number) => ( {typeof item === 'string' ? item : JSON.stringify(item, null, 1)} ))} )} )} {/* Create/Edit dialog */} setShowForm(false)} maxWidth="sm" fullWidth> {editing ? 'Edit Search' : 'Create Saved Search'} setName(e.target.value)} sx={{ mt: 1, mb: 2 }} /> Search Type setHuntId(e.target.value)} sx={{ mb: 2 }} placeholder="Leave empty to search all hunts" /> setQueryParams(e.target.value)} placeholder='{"keywords": ["mimikatz", "lsass"]}' helperText="JSON object with search-specific parameters" />
); }