From d3548ebfa6c8c517cf375795f0ed5c1f8fa0d1e0 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Fri, 9 Aug 2024 18:28:00 -0700 Subject: [PATCH] Fix UI issues (#1107) * updated vega imports; fixed get correlated findings Signed-off-by: Amardeepsingh Siglani * fixed correlation alerts filtering; improved list iocs pagination Signed-off-by: Amardeepsingh Siglani * updated help text for data source selection Signed-off-by: Amardeepsingh Siglani * fixed active condition for threat intel source Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani --- .../pages/Alerts/containers/Alerts/Alerts.tsx | 4 +- .../DetectorDataSource/DetectorDataSource.tsx | 5 +- .../UpdateDetectorBasicDetails.test.tsx.snap | 14 ++ .../components/IoCsTable/IoCsTable.tsx | 120 ++++++++++++++++-- .../SelectThreatIntelLogSourcesForm.tsx | 13 +- .../ThreatIntelSourcesList.tsx | 16 +-- .../ThreatIntelSource/ThreatIntelSource.tsx | 37 ++---- public/store/CorrelationsStore.ts | 49 +++---- public/utils/helpers.tsx | 11 +- 9 files changed, 191 insertions(+), 78 deletions(-) diff --git a/public/pages/Alerts/containers/Alerts/Alerts.tsx b/public/pages/Alerts/containers/Alerts/Alerts.tsx index 2e22e6d4..f8706e3b 100644 --- a/public/pages/Alerts/containers/Alerts/Alerts.tsx +++ b/public/pages/Alerts/containers/Alerts/Alerts.tsx @@ -79,7 +79,7 @@ import { ThreatIntelAlertsTable } from '../../components/ThreatIntelAlertsTable/ type FilterAlertParams = | { alerts: AlertItem[]; timeField: 'last_notification_time' } - | { alerts: CorrelationAlertTableItem[]; timeField: 'end_time' } + | { alerts: CorrelationAlertTableItem[]; timeField: 'start_time' } | { alerts: ThreatIntelAlert[]; timeField: 'start_time' }; export interface AlertsProps extends RouteComponentProps, DataSourceProps { @@ -255,7 +255,7 @@ export class Alerts extends Component { const { correlationAlerts } = this.state; const filteredCorrelationAlerts = this.filterAlerts({ alerts: correlationAlerts, - timeField: 'end_time', + timeField: 'start_time', }); this.setState({ diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx index 719dd877..d28dc013 100644 --- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx @@ -12,7 +12,6 @@ import { EuiCallOut, EuiTextColor, EuiTitle, - EuiLink, } from '@elastic/eui'; import { FormFieldHeader } from '../../../../../../components/FormFieldHeader/FormFieldHeader'; import { IndexOption } from '../../../../../Detectors/models/interfaces'; @@ -163,6 +162,10 @@ export default class DetectorDataSource extends Component< Aliases + + {' and '} + + data streams {' '} are recommended for optimal functioning of detectors. diff --git a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap index d5806309..cc9ea1d7 100644 --- a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap +++ b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap @@ -718,6 +718,13 @@ exports[` spec renders the component 1`] = ` > Aliases + and + + data streams + are recommended for optimal functioning of detectors. @@ -1113,6 +1120,13 @@ exports[` spec renders the component 1`] = ` > Aliases + and + + data streams + are recommended for optimal functioning of detectors. diff --git a/public/pages/ThreatIntel/components/IoCsTable/IoCsTable.tsx b/public/pages/ThreatIntel/components/IoCsTable/IoCsTable.tsx index b0014f18..4c06d3a8 100644 --- a/public/pages/ThreatIntel/components/IoCsTable/IoCsTable.tsx +++ b/public/pages/ThreatIntel/components/IoCsTable/IoCsTable.tsx @@ -3,19 +3,43 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { EuiBasicTableColumn, EuiInMemoryTable, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; +import React, { useState, useEffect } from 'react'; +import { + EuiBasicTableColumn, + EuiPanel, + EuiSpacer, + EuiText, + EuiBasicTable, + Pagination, + CriteriaWithPagination, + EuiSearchBar, +} from '@elastic/eui'; import { ThreatIntelIocData } from '../../../../../types'; import { renderTime } from '../../../../utils/helpers'; import { IocLabel, ThreatIntelIocType } from '../../../../../common/constants'; +import { ThreatIntelService } from '../../../../services'; export interface IoCsTableProps { + threatIntelService: ThreatIntelService; sourceId?: string; - loadingIoCs: boolean; - iocs: ThreatIntelIocData[]; + registerRefreshHandler: (handler: () => void) => void; } -export const IoCsTable: React.FC = ({ sourceId, iocs, loadingIoCs }) => { +export const IoCsTable: React.FC = ({ + sourceId, + threatIntelService, + registerRefreshHandler, +}) => { + const [loading, setLoading] = useState(false); + const [searchString, setSearchString] = useState(''); + const [paginationState, setPaginationState] = useState({ + pageIndex: 0, + pageSize: 10, + totalItemCount: 0, + pageSizeOptions: [10, 25, 50], + }); + const [iocs, setIocs] = useState([]); + const columns: EuiBasicTableColumn[] = [ { name: 'Value', @@ -46,18 +70,94 @@ export const IoCsTable: React.FC = ({ sourceId, iocs, loadingIoC }, ]; + const getIocs = async () => { + if (sourceId) { + setLoading(true); + const iocsRes = await threatIntelService.getThreatIntelIocs({ + feed_ids: sourceId, + startIndex: paginationState.pageIndex * paginationState.pageSize, + size: paginationState.pageSize, + searchString, + }); + + if (iocsRes.ok) { + setIocs(iocsRes.response.iocs); + setPaginationState({ + ...paginationState, + totalItemCount: iocsRes.response.total, + }); + } + setLoading(false); + } + }; + + useEffect(() => { + registerRefreshHandler(getIocs); + }, []); + + useEffect(() => { + getIocs(); + }, [paginationState.pageIndex, paginationState.pageSize, searchString]); + + const onTableChange = ({ page }: CriteriaWithPagination) => { + if (paginationState.pageIndex !== page.index || paginationState.pageSize !== page.size) { + setPaginationState({ + ...paginationState, + pageIndex: page.index, + pageSize: page.size, + }); + } + }; + + const renderSearch = () => { + const schema = { + strict: true, + fields: { + value: { + type: 'string', + }, + type: { + type: 'string', + }, + created: { + type: 'date', + }, + modified: { + type: 'date', + }, + num_findings: { + type: 'number', + }, + }, + }; + + return ( + setSearchString(queryText)} + /> + ); + }; + return ( - {iocs.length} malicious IoCs + {paginationState.totalItemCount} malicious IoCs - + ); diff --git a/public/pages/ThreatIntel/components/SelectLogSourcesForm/SelectThreatIntelLogSourcesForm.tsx b/public/pages/ThreatIntel/components/SelectLogSourcesForm/SelectThreatIntelLogSourcesForm.tsx index 4c074ca8..4ff5d22b 100644 --- a/public/pages/ThreatIntel/components/SelectLogSourcesForm/SelectThreatIntelLogSourcesForm.tsx +++ b/public/pages/ThreatIntel/components/SelectLogSourcesForm/SelectThreatIntelLogSourcesForm.tsx @@ -187,7 +187,18 @@ export const SelectThreatIntelLogSources: React.FC + + Aliases + + {' and '} + + data streams + {' '} + are recommended for optimal threat intel scans. + + } > = ({ More details - {source.enabled && ( - <> - {' '} - Active - - )} + {' '} + {source.enabled_for_scan ? 'Active' : 'Inactive'} } > diff --git a/public/pages/ThreatIntel/containers/ThreatIntelSource/ThreatIntelSource.tsx b/public/pages/ThreatIntel/containers/ThreatIntelSource/ThreatIntelSource.tsx index 5f068dec..76ba097c 100644 --- a/public/pages/ThreatIntel/containers/ThreatIntelSource/ThreatIntelSource.tsx +++ b/public/pages/ThreatIntel/containers/ThreatIntelSource/ThreatIntelSource.tsx @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useContext, useState } from 'react'; -import { ThreatIntelIocData, ThreatIntelSourceItem } from '../../../../../types'; +import React, { useContext, useState, useRef } from 'react'; +import { ThreatIntelSourceItem } from '../../../../../types'; import { RouteComponentProps } from 'react-router-dom'; import { BREADCRUMBS, DEFAULT_EMPTY_DATA, ROUTES } from '../../../../utils/constants'; import { useEffect } from 'react'; @@ -49,8 +49,7 @@ export const ThreatIntelSource: React.FC = ({ const sourceId = source?.id ?? location.pathname.replace(`${ROUTES.THREAT_INTEL_SOURCE_DETAILS}/`, ''); const [showDeleteModal, setShowDeleteModal] = useState(false); - const [iocs, setIocs] = useState([]); - const [loadingIoCs, setLoadingIoCs] = useState(true); + const refreshHandler = useRef(() => {}); const getSource = async (sourceId: string) => { const res = await threatIntelService.getThreatIntelSource(sourceId); @@ -65,22 +64,6 @@ export const ThreatIntelSource: React.FC = ({ } }, []); - const getIocs = async () => { - if (sourceId) { - setLoadingIoCs(true); - const iocsRes = await threatIntelService.getThreatIntelIocs({ - feed_ids: sourceId, - startIndex: 0, - size: 10000, - }); - - if (iocsRes.ok) { - setIocs(iocsRes.response.iocs); - } - setLoadingIoCs(false); - } - }; - useEffect(() => { const baseCrumbs = [BREADCRUMBS.SECURITY_ANALYTICS, BREADCRUMBS.THREAT_INTEL_OVERVIEW]; @@ -89,8 +72,6 @@ export const ThreatIntelSource: React.FC = ({ ? baseCrumbs : [...baseCrumbs, BREADCRUMBS.THREAT_INTEL_SOURCE_DETAILS(source.name, sourceId)] ); - - getIocs(); }, [source]); if (!source) { @@ -105,7 +86,15 @@ export const ThreatIntelSource: React.FC = ({ { id: 'iocs', name: Indicators of Compromise, - content: , + content: ( + { + refreshHandler.current = handler; + }} + /> + ), }, { id: 'source-details', @@ -139,7 +128,7 @@ export const ThreatIntelSource: React.FC = ({ if (!res.ok) { errorNotificationToast(notifications, 'refresh', 'source', res.error); } else { - getIocs(); + refreshHandler.current?.(); } }; diff --git a/public/store/CorrelationsStore.ts b/public/store/CorrelationsStore.ts index 9a482d2a..4c0e81f9 100644 --- a/public/store/CorrelationsStore.ts +++ b/public/store/CorrelationsStore.ts @@ -80,7 +80,7 @@ export class CorrelationsStore implements ICorrelationsStore { return correlationInput; }), - trigger: correlationRule.trigger + trigger: correlationRule.trigger, }); if (!response.ok) { errorNotificationToast(this.notifications, 'create', 'correlation rule', response.error); @@ -218,24 +218,26 @@ export class CorrelationsStore implements ICorrelationsStore { const findings = await this.fetchAllFindings(findingIds); allFindings = { ...allFindings, - ...findings - } + ...findings, + }; } const maxNumberOfCorrelationsDisplayed = 10000; - allCorrelationsRes.response.findings.slice(0, maxNumberOfCorrelationsDisplayed).forEach(({ finding1, finding2 }) => { - const f1 = allFindings[finding1]; - const f2 = allFindings[finding2]; - if (f1 && f2) - result.push({ - finding1: { - ...f1, - }, - finding2: { - ...f2, - }, - }); - }); + allCorrelationsRes.response.findings + .slice(0, maxNumberOfCorrelationsDisplayed) + .forEach(({ finding1, finding2 }) => { + const f1 = allFindings[finding1]; + const f2 = allFindings[finding2]; + if (f1 && f2) + result.push({ + finding1: { + ...f1, + }, + finding2: { + ...f2, + }, + }); + }); return result; } @@ -245,13 +247,15 @@ export class CorrelationsStore implements ICorrelationsStore { public allFindings: { [id: string]: CorrelationFinding } = {}; - private async fetchAllFindings(findingIds: string[]): Promise<{ [id: string]: CorrelationFinding }> { + private async fetchAllFindings( + findingIds: string[] + ): Promise<{ [id: string]: CorrelationFinding }> { const detectorsRes = await this.detectorsService.getDetectors(); const allRules = await this.rulesStore.getAllRules(); if (detectorsRes.ok) { const detectorsMap: { [id: string]: DetectorHit } = {}; - detectorsRes.response.hits.hits.forEach(detector => { + detectorsRes.response.hits.hits.forEach((detector) => { detectorsMap[detector._id] = detector; }); let findingsMap: { [id: string]: CorrelationFinding } = {}; @@ -271,7 +275,7 @@ export class CorrelationsStore implements ICorrelationsStore { name: rule._source.title, severity: rule._source.level, tags: rule._source.tags, - } + } : { name: DEFAULT_EMPTY_DATA, severity: DEFAULT_EMPTY_DATA }, }; }); @@ -295,8 +299,8 @@ export class CorrelationsStore implements ICorrelationsStore { if (response?.ok) { const correlatedFindings: CorrelationFinding[] = []; - const allFindingIds = response.response.findings.map(f => f.finding); - const allFindings = await this.fetchAllFindings(allFindingIds); + const allFindingIds = response.response.findings.map((f) => f.finding); + const allFindings = await this.fetchAllFindings([...allFindingIds, findingId]); response.response.findings.forEach((f) => { if (allFindings[f.finding]) { correlatedFindings.push({ @@ -327,8 +331,7 @@ export class CorrelationsStore implements ICorrelationsStore { }; } - public async getAllCorrelationAlerts( - ): Promise { + public async getAllCorrelationAlerts(): Promise { const response = await this.service.getCorrelationAlerts(); if (response?.ok) { return { diff --git a/public/utils/helpers.tsx b/public/utils/helpers.tsx index 451ca59d..4d4605eb 100644 --- a/public/utils/helpers.tsx +++ b/public/utils/helpers.tsx @@ -42,6 +42,10 @@ import { getLogTypeLabel } from '../pages/LogTypes/utils/helpers'; import { euiThemeVars } from '@osd/ui-shared-deps/theme'; import dateMath from '@elastic/datemath'; import { IocLabel, ThreatIntelIocType } from '../../common/constants'; +import { parse, View } from 'vega/build-es5/vega.js'; +import { compile } from 'vega-lite'; +import { Handler } from 'vega-tooltip'; +import { expressionInterpreter as vegaExpressionInterpreter } from 'vega-interpreter/build/vega-interpreter'; export const parseStringsToOptions = (strings: string[]) => { return strings.map((str) => ({ id: str, label: str })); @@ -181,8 +185,6 @@ export function getUpdatedEnabledRuleIds( export async function renderVisualization(spec: any, containerId: string) { let view; - const { compile } = await import('vega-lite'); - try { setDefaultColors(spec); renderVegaSpec(compile({ ...spec, width: 'container', height: 400 }).spec).catch((err: Error) => @@ -194,7 +196,6 @@ export async function renderVisualization(spec: any, containerId: string) { async function renderVegaSpec(spec: {}) { let chartColoredItems: any[] = []; - const { Handler } = await import('vega-tooltip'); const handler = new Handler({ formatTooltip: (value, sanitize) => { let tooltipData = { ...value }; @@ -237,10 +238,6 @@ export async function renderVisualization(spec: any, containerId: string) { `; }, }); - const { expressionInterpreter: vegaExpressionInterpreter } = await import( - 'vega-interpreter/build/vega-interpreter' - ); - const { parse, View } = await import('vega'); view = new View(parse(spec, undefined, { expr: vegaExpressionInterpreter } as any), { renderer: 'canvas', // renderer (canvas or svg) container: `#${containerId}`, // parent DOM container