From e83a24f7633076053fe79af0c5567bd6bce3c34c Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Thu, 16 Feb 2023 09:07:47 +0100 Subject: [PATCH] Unit tests for public components #383 [BUG] Detector Edit | Custom rule are not selected on update rules #406 Signed-off-by: Jovan Cvetkovic --- public/models/interfaces.ts | 2 + public/pages/Main/Main.tsx | 72 ++++++ .../components/Widgets/TableWidget.tsx | 4 +- public/pages/Overview/models/types.ts | 11 +- .../RecentWidgets/RecentAggregators.tsx | 99 ++++++++ .../RecentWidgets/RecentInferences.tsx | 90 +++++++ .../pages/Ueba/components/Summary/Summary.tsx | 18 ++ .../CreateAggregator/CreateAggregator.tsx | 26 ++ .../Ueba/containers/CreateAggregator/index.ts | 0 .../CreateInference/CreateInference.tsx | 26 ++ .../Ueba/containers/CreateInference/index.ts | 0 public/pages/Ueba/containers/Ueba/Ueba.tsx | 228 ++++++++++++++++++ public/pages/Ueba/containers/Ueba/index.ts | 0 .../ViewAggregators/AggregatorFlyout.tsx | 95 ++++++++ .../ViewAggregators/ViewAggregators.tsx | 25 ++ .../Ueba/containers/ViewAggregators/index.ts | 0 .../ViewInferences/InferenceFlyout.tsx | 82 +++++++ .../ViewInferences/ViewInferences.tsx | 26 ++ .../Ueba/containers/ViewInferences/index.ts | 0 public/pages/Ueba/index.ts | 0 .../pages/Ueba/models/UebaViewModelActor.ts | 44 ++++ public/pages/Ueba/models/interfaces.ts | 17 ++ public/pages/Ueba/utils/helpers.ts | 57 +++++ public/security_analytics_app.tsx | 3 + public/services/UebaService.ts | 24 ++ public/services/index.ts | 2 + public/utils/constants.ts | 22 ++ server/models/interfaces/Ueba.ts | 32 +++ server/models/interfaces/index.ts | 3 + server/plugin.ts | 4 + server/routes/UebaRoutes.ts | 28 +++ server/routes/index.ts | 1 + server/services/UebaService.ts | 123 ++++++++++ server/services/index.ts | 2 + server/utils/constants.ts | 7 + 35 files changed, 1170 insertions(+), 3 deletions(-) create mode 100644 public/pages/Ueba/components/RecentWidgets/RecentAggregators.tsx create mode 100644 public/pages/Ueba/components/RecentWidgets/RecentInferences.tsx create mode 100644 public/pages/Ueba/components/Summary/Summary.tsx create mode 100644 public/pages/Ueba/containers/CreateAggregator/CreateAggregator.tsx create mode 100644 public/pages/Ueba/containers/CreateAggregator/index.ts create mode 100644 public/pages/Ueba/containers/CreateInference/CreateInference.tsx create mode 100644 public/pages/Ueba/containers/CreateInference/index.ts create mode 100644 public/pages/Ueba/containers/Ueba/Ueba.tsx create mode 100644 public/pages/Ueba/containers/Ueba/index.ts create mode 100644 public/pages/Ueba/containers/ViewAggregators/AggregatorFlyout.tsx create mode 100644 public/pages/Ueba/containers/ViewAggregators/ViewAggregators.tsx create mode 100644 public/pages/Ueba/containers/ViewAggregators/index.ts create mode 100644 public/pages/Ueba/containers/ViewInferences/InferenceFlyout.tsx create mode 100644 public/pages/Ueba/containers/ViewInferences/ViewInferences.tsx create mode 100644 public/pages/Ueba/containers/ViewInferences/index.ts create mode 100644 public/pages/Ueba/index.ts create mode 100644 public/pages/Ueba/models/UebaViewModelActor.ts create mode 100644 public/pages/Ueba/models/interfaces.ts create mode 100644 public/pages/Ueba/utils/helpers.ts create mode 100644 public/services/UebaService.ts create mode 100644 server/models/interfaces/Ueba.ts create mode 100644 server/routes/UebaRoutes.ts create mode 100644 server/services/UebaService.ts diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index 2cea676f1..ea8552ebe 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -13,6 +13,7 @@ import { RuleService, NotificationsService, IndexPatternsService, + UebaService, } from '../services'; export interface BrowserServices { @@ -25,6 +26,7 @@ export interface BrowserServices { ruleService: RuleService; notificationsService: NotificationsService; indexPatternsService: IndexPatternsService; + uebaService: UebaService; } export interface RuleOptions { diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx index 734240fe5..8177bcf34 100644 --- a/public/pages/Main/Main.tsx +++ b/public/pages/Main/Main.tsx @@ -35,6 +35,11 @@ import { EditRule } from '../Rules/containers/EditRule/EditRule'; import { ImportRule } from '../Rules/containers/ImportRule/ImportRule'; import { DuplicateRule } from '../Rules/containers/DuplicateRule/DuplicateRule'; import { DateTimeFilter } from '../Overview/models/interfaces'; +import { Ueba } from '../Ueba/containers/Ueba/Ueba'; +import { ViewAggregators } from '../Ueba/containers/ViewAggregators/ViewAggregators'; +import { ViewInferences } from '../Ueba/containers/ViewInferences/ViewInferences'; +import { CreateInference } from '../Ueba/containers/CreateInference/CreateInference'; +import { CreateAggregator } from '../Ueba/containers/CreateAggregator/CreateAggregator'; enum Navigation { SecurityAnalytics = 'Security Analytics', @@ -43,6 +48,7 @@ enum Navigation { Rules = 'Rules', Overview = 'Overview', Alerts = 'Alerts', + Ueba = 'UEBA', } /** @@ -76,6 +82,7 @@ const navItemIndexByRoute: { [route: string]: number } = { [ROUTES.ALERTS]: 3, [ROUTES.DETECTORS]: 4, [ROUTES.RULES]: 5, + [ROUTES.UEBA]: 6, }; export default class Main extends Component { @@ -211,6 +218,15 @@ export default class Main extends Component { }, isSelected: this.state.selectedNavItemIndex === 5, }, + { + name: Navigation.Ueba, + id: 6, + onClick: () => { + this.setState({ selectedNavItemIndex: 6 }); + history.push(ROUTES.UEBA); + }, + isSelected: this.state.selectedNavItemIndex === 6, + }, ], }, ]; @@ -407,6 +423,62 @@ export default class Main extends Component { /> )} /> + ) => ( + + )} + /> + ) => ( + + )} + /> + ) => ( + + )} + /> + ) => ( + + )} + /> + ) => ( + + )} + /> diff --git a/public/pages/Overview/components/Widgets/TableWidget.tsx b/public/pages/Overview/components/Widgets/TableWidget.tsx index f59bd4da9..02aefeb0a 100644 --- a/public/pages/Overview/components/Widgets/TableWidget.tsx +++ b/public/pages/Overview/components/Widgets/TableWidget.tsx @@ -9,8 +9,7 @@ import { TableWidgetItem, TableWidgetProps } from '../../models/types'; export class TableWidget extends React.Component> { render() { - const { columns, items, loading = false } = this.props; - + const { columns, items, loading = false, search = undefined } = this.props; return ( compressed @@ -19,6 +18,7 @@ export class TableWidget extends React.Component `${item.id}`} pagination={{ pageSize: 10, pageSizeOptions: [10] }} loading={loading} + search={search} /> ); } diff --git a/public/pages/Overview/models/types.ts b/public/pages/Overview/models/types.ts index d5d6b7786..a3e588e62 100644 --- a/public/pages/Overview/models/types.ts +++ b/public/pages/Overview/models/types.ts @@ -5,11 +5,20 @@ import { EuiBasicTableColumn } from '@elastic/eui'; import { AlertItem, DetectorItem, FindingItem } from './interfaces'; +import { AggregatorItem, InferenceItem } from '../../Ueba/models/interfaces'; +import { DocumentsItem } from '../../Ueba/containers/Ueba/Ueba'; -export type TableWidgetItem = FindingItem | AlertItem | DetectorItem; +export type TableWidgetItem = + | FindingItem + | AlertItem + | DetectorItem + | AggregatorItem + | DocumentsItem + | InferenceItem; export type TableWidgetProps = { columns: EuiBasicTableColumn[]; items: T[]; loading?: boolean; + search?: any; }; diff --git a/public/pages/Ueba/components/RecentWidgets/RecentAggregators.tsx b/public/pages/Ueba/components/RecentWidgets/RecentAggregators.tsx new file mode 100644 index 000000000..7439ce2d2 --- /dev/null +++ b/public/pages/Ueba/components/RecentWidgets/RecentAggregators.tsx @@ -0,0 +1,99 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiBasicTableColumn, EuiButton, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { ROUTES } from '../../../../utils/constants'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { NotificationsStart } from 'opensearch-dashboards/public'; + +import { WidgetContainer } from '../../../Overview/components/Widgets/WidgetContainer'; +import { TableWidget } from '../../../Overview/components/Widgets/TableWidget'; +import { ServicesContext } from '../../../../services'; +import { AggregatorItem } from '../../models/interfaces'; +import { UebaViewModelActor } from '../../models/UebaViewModelActor'; +import { BrowserServices } from '../../../../models/interfaces'; + +export interface AggregatorsProps { + loading?: boolean; + openFlyout: Function; + services: BrowserServices; + notifications: NotificationsStart; +} + +export const RecentAggregators: React.FC = ({ + loading = false, + openFlyout, + notifications, +}) => { + const services = useContext(ServicesContext); + const uebaViewModelActor = services && new UebaViewModelActor(services, notifications); + + const [aggregatorItems, setAggregatorItems] = useState([]); + + const actions = React.useMemo( + () => [View aggregators], + [] + ); + + const columns: EuiBasicTableColumn[] = [ + { + field: 'name', + name: 'Name', + sortable: true, + align: 'left', + }, + { + field: 'description', + name: 'Description', + sortable: false, + align: 'left', + }, + { + field: 'source_index', + name: 'Source index', + sortable: true, + align: 'left', + }, + { + field: 'page_size', + name: 'Page size', + sortable: true, + align: 'left', + }, + { + name: 'Details', + sortable: false, + actions: [ + { + render: (aggregator: AggregatorItem) => ( + + openFlyout(aggregator)} + /> + + ), + }, + ], + }, + ]; + + const getAggregators = useCallback(async () => { + const aggregators = await uebaViewModelActor?.getAggregators(); + aggregators && setAggregatorItems(aggregators); + }, [services]); + + useEffect(() => { + getAggregators(); + }, [getAggregators]); + + return ( + + + + ); +}; diff --git a/public/pages/Ueba/components/RecentWidgets/RecentInferences.tsx b/public/pages/Ueba/components/RecentWidgets/RecentInferences.tsx new file mode 100644 index 000000000..f7f7de9fc --- /dev/null +++ b/public/pages/Ueba/components/RecentWidgets/RecentInferences.tsx @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiBasicTableColumn, EuiButton, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { ROUTES } from '../../../../utils/constants'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; + +import { WidgetContainer } from '../../../Overview/components/Widgets/WidgetContainer'; +import { TableWidget } from '../../../Overview/components/Widgets/TableWidget'; +import { InferenceItem } from '../../models/interfaces'; +import { ServicesContext } from '../../../../services'; + +export interface InferenceProps { + loading?: boolean; + openFlyout: Function; +} + +export const RecentInferences: React.FC = ({ loading = false, openFlyout }) => { + const services = useContext(ServicesContext); + const [inferenceItems, setInferenceItems] = useState([]); + + const actions = React.useMemo( + () => [View inferences], + [] + ); + + const columns: EuiBasicTableColumn[] = [ + { + field: 'name', + name: 'Name', + sortable: true, + align: 'left', + }, + { + field: 'description', + name: 'Description', + sortable: false, + align: 'left', + }, + { + field: 'type', + name: 'Type', + sortable: false, + align: 'left', + }, + { + field: 'schedule', + name: 'Schedule', + sortable: false, + align: 'left', + }, + { + name: 'Details', + sortable: false, + actions: [ + { + render: (inference) => ( + + openFlyout(inference)} + /> + + ), + }, + ], + }, + ]; + + const getInferences = useCallback(async () => { + const inferencesResponse = await services?.uebaService.getInferences(); + if (inferencesResponse?.ok) { + setInferenceItems(inferencesResponse?.response.hits.hits); + } + }, [services]); + + useEffect(() => { + getInferences(); + }, [getInferences]); + + return ( + + + + ); +}; diff --git a/public/pages/Ueba/components/Summary/Summary.tsx b/public/pages/Ueba/components/Summary/Summary.tsx new file mode 100644 index 000000000..28d562af6 --- /dev/null +++ b/public/pages/Ueba/components/Summary/Summary.tsx @@ -0,0 +1,18 @@ +import React, { useState } from 'react'; +import { EuiFlexItem } from '@elastic/eui'; + +import { ChartContainer } from '../../../../components/Charts/ChartContainer'; +import { WidgetContainer } from '../../../Overview/components/Widgets/WidgetContainer'; +export interface SummaryProps { + loading: boolean; +} +export const Summary: React.FC = () => { + const [loading, setLoading] = useState(true); + return ( + + + + + + ); +}; diff --git a/public/pages/Ueba/containers/CreateAggregator/CreateAggregator.tsx b/public/pages/Ueba/containers/CreateAggregator/CreateAggregator.tsx new file mode 100644 index 000000000..daccde311 --- /dev/null +++ b/public/pages/Ueba/containers/CreateAggregator/CreateAggregator.tsx @@ -0,0 +1,26 @@ +import React, { useContext, useEffect } from 'react'; + +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { BrowserServices } from '../../../../models/interfaces'; +import { CoreServicesContext } from '../../../../components/core_services'; +import { BREADCRUMBS } from '../../../../utils/constants'; +import * as H from 'history'; + +export interface UebaProps { + services: BrowserServices; + notifications?: NotificationsStart; + history: H.History; +} + +export const CreateAggregator: React.FC = (props) => { + const context = useContext(CoreServicesContext); + useEffect(() => { + context?.chrome.setBreadcrumbs([ + BREADCRUMBS.SECURITY_ANALYTICS, + BREADCRUMBS.UEBA, + BREADCRUMBS.UEBA_CREATE_AGGREGATOR, + ]); + }); + + return <>Create aggregator; +}; diff --git a/public/pages/Ueba/containers/CreateAggregator/index.ts b/public/pages/Ueba/containers/CreateAggregator/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/public/pages/Ueba/containers/CreateInference/CreateInference.tsx b/public/pages/Ueba/containers/CreateInference/CreateInference.tsx new file mode 100644 index 000000000..7d991618d --- /dev/null +++ b/public/pages/Ueba/containers/CreateInference/CreateInference.tsx @@ -0,0 +1,26 @@ +import React, { useContext, useEffect } from 'react'; + +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { BrowserServices } from '../../../../models/interfaces'; +import { CoreServicesContext } from '../../../../components/core_services'; +import { BREADCRUMBS } from '../../../../utils/constants'; +import * as H from 'history'; + +export interface UebaProps { + services: BrowserServices; + notifications?: NotificationsStart; + history: H.History; +} + +export const CreateInference: React.FC = (props) => { + const context = useContext(CoreServicesContext); + useEffect(() => { + context?.chrome.setBreadcrumbs([ + BREADCRUMBS.SECURITY_ANALYTICS, + BREADCRUMBS.UEBA, + BREADCRUMBS.UEBA_CREATE_INFERENCE, + ]); + }); + + return <>Create inference; +}; diff --git a/public/pages/Ueba/containers/CreateInference/index.ts b/public/pages/Ueba/containers/CreateInference/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/public/pages/Ueba/containers/Ueba/Ueba.tsx b/public/pages/Ueba/containers/Ueba/Ueba.tsx new file mode 100644 index 000000000..882d7218e --- /dev/null +++ b/public/pages/Ueba/containers/Ueba/Ueba.tsx @@ -0,0 +1,228 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSuperDatePicker, + EuiFlexGrid, + EuiBasicTableColumn, + EuiButton, + EuiSpacer, +} from '@elastic/eui'; + +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { BrowserServices } from '../../../../models/interfaces'; +import { CoreServicesContext } from '../../../../components/core_services'; +import { + BREADCRUMBS, + DEFAULT_DATE_RANGE, + MAX_RECENTLY_USED_TIME_RANGES, + ROUTES, +} from '../../../../utils/constants'; +import * as H from 'history'; +import { Summary } from '../../components/Summary/Summary'; +import { DateTimeFilter } from '../../../Overview/models/interfaces'; +import { getChartTimeUnit, TimeUnit } from '../../../Overview/utils/helpers'; +import { RecentAggregators } from '../../components/RecentWidgets/RecentAggregators'; +import { RecentInferences } from '../../components/RecentWidgets/RecentInferences'; +import { TableWidget } from '../../../Overview/components/Widgets/TableWidget'; +import { WidgetContainer } from '../../../Overview/components/Widgets/WidgetContainer'; +import { renderVisualization } from '../../../../utils/helpers'; +import { getUebaVisualization } from '../../utils/helpers'; +import { AggregatorFlyout } from '../ViewAggregators/AggregatorFlyout'; +import { InferenceFlyout } from '../ViewInferences/InferenceFlyout'; +import { AggregatorItem, InferenceItem } from '../../models/interfaces'; + +export interface UebaProps { + services: BrowserServices; + notifications?: NotificationsStart; + history: H.History; + setDateTimeFilter?: Function; + dateTimeFilter?: DateTimeFilter; +} + +export interface DocumentsItem { + id?: string; +} + +export const Ueba: React.FC = (props) => { + const { + dateTimeFilter = { + startTime: DEFAULT_DATE_RANGE.start, + endTime: DEFAULT_DATE_RANGE.end, + }, + services, + notifications, + } = props; + + const context = useContext(CoreServicesContext); + + const [aggregator, setAggregator] = useState(); + const [inference, setInference] = useState(); + + const [loading, setLoading] = useState(true); + const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([DEFAULT_DATE_RANGE]); + + const timeUnits = getChartTimeUnit(dateTimeFilter.startTime, dateTimeFilter.endTime); + const [timeUnit, setTimeUnit] = useState(timeUnits.timeUnit); + + const [documents, setDocuments] = useState([]); + const [inferences, setInferences] = useState([]); + + const columns: EuiBasicTableColumn[] = [ + { + field: 'id', + name: 'ID', + sortable: true, + align: 'left', + }, + { + field: 'inference_model', + name: 'Inference model', + sortable: true, + align: 'left', + }, + { + field: 'score', + name: 'Score', + sortable: true, + align: 'left', + }, + ]; + + const inferenceModels: any = []; + + const getTableSearchConfig = () => { + return { + box: { + placeholder: 'Search documents', + schema: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'inference_model', + name: 'Inference model', + multiSelect: 'or', + options: inferenceModels.map((model: any) => ({ + value: model.id, + name: model.name, + })), + }, + ], + }; + }; + + const actions = React.useMemo( + () => [ + Create Aggregator, + Create inference, + ], + [] + ); + + useEffect(() => { + context?.chrome.setBreadcrumbs([BREADCRUMBS.SECURITY_ANALYTICS, BREADCRUMBS.UEBA]); + renderVisualization(getUebaVisualization([], ''), 'ueba-view'); + setLoading(false); + }); + + const onTimeChange = async ({ start, end }: { start: string; end: string }) => { + let usedRanges = recentlyUsedRanges.filter( + (range) => !(range.start === start && range.end === end) + ); + usedRanges.unshift({ start: start, end: end }); + if (usedRanges.length > MAX_RECENTLY_USED_TIME_RANGES) + usedRanges = usedRanges.slice(0, MAX_RECENTLY_USED_TIME_RANGES); + + const endTime = start === end ? DEFAULT_DATE_RANGE.end : end; + const timeUnits = getChartTimeUnit(start, endTime); + + props.setDateTimeFilter && + props.setDateTimeFilter({ + startTime: start, + endTime: endTime, + }); + setTimeUnit(timeUnits.timeUnit); + setRecentlyUsedRanges(usedRanges); + }; + + const onRefresh = async () => { + setLoading(true); + // await overviewViewModelActor.onRefresh(dateTimeFilter.startTime, dateTimeFilter.endTime); + }; + + const openAggregatorFlyout = useCallback((aggregator: AggregatorItem) => { + setAggregator(aggregator); + }, []); + + const openInferenceFlyout = useCallback((inference: InferenceItem) => { + setInference(inference); + }, []); + + const hideAggregatorFlyout = useCallback(() => { + setAggregator(undefined); + }, []); + + const hideInferenceFlyout = useCallback(() => { + setInference(undefined); + }, []); + + return ( + <> + {aggregator ? ( + + ) : null} + {inference ? ( + + ) : null} + + + + + +

UEBA Overview

+
+
+ + + + +
+
+ + + + + + + + + + + + + + + + + ); +}; diff --git a/public/pages/Ueba/containers/Ueba/index.ts b/public/pages/Ueba/containers/Ueba/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/public/pages/Ueba/containers/ViewAggregators/AggregatorFlyout.tsx b/public/pages/Ueba/containers/ViewAggregators/AggregatorFlyout.tsx new file mode 100644 index 000000000..3615a02d9 --- /dev/null +++ b/public/pages/Ueba/containers/ViewAggregators/AggregatorFlyout.tsx @@ -0,0 +1,95 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiButtonIcon, + EuiFormLabel, + EuiText, + EuiCodeBlock, + EuiSpacer, +} from '@elastic/eui'; +import React from 'react'; +import { AggregatorItem } from '../../models/interfaces'; + +export interface AggregatorFlyoutProps { + aggregator: AggregatorItem; + hideFlyout: () => void; +} + +export const AggregatorFlyout: React.FC = ({ aggregator, hideFlyout }) => { + return ( + + + + + +

Aggregator details

+
+
+ + hideFlyout()} + data-test-subj={`close-aggregator-details-flyout`} + /> + +
+
+ + + + Aggregator name + {aggregator.name} + + + Source index + + {aggregator.source_index} + + + + + + + Description + + {aggregator.description} + + + + Page size + {aggregator.page_size} + + + + + + + + Aggregator script + + {aggregator.aggregator_script} + + + + +
+ ); +}; diff --git a/public/pages/Ueba/containers/ViewAggregators/ViewAggregators.tsx b/public/pages/Ueba/containers/ViewAggregators/ViewAggregators.tsx new file mode 100644 index 000000000..306526f88 --- /dev/null +++ b/public/pages/Ueba/containers/ViewAggregators/ViewAggregators.tsx @@ -0,0 +1,25 @@ +import React, { useContext, useEffect } from 'react'; + +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { BrowserServices } from '../../../../models/interfaces'; +import { CoreServicesContext } from '../../../../components/core_services'; +import { BREADCRUMBS } from '../../../../utils/constants'; +import * as H from 'history'; + +export interface UebaProps { + services: BrowserServices; + notifications?: NotificationsStart; + history: H.History; +} + +export const ViewAggregators: React.FC = (props) => { + const context = useContext(CoreServicesContext); + useEffect(() => { + context?.chrome.setBreadcrumbs([ + BREADCRUMBS.SECURITY_ANALYTICS, + BREADCRUMBS.UEBA, + BREADCRUMBS.UEBA_VIEW_AGGREGATORS, + ]); + }); + return <>View aggregators; +}; diff --git a/public/pages/Ueba/containers/ViewAggregators/index.ts b/public/pages/Ueba/containers/ViewAggregators/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/public/pages/Ueba/containers/ViewInferences/InferenceFlyout.tsx b/public/pages/Ueba/containers/ViewInferences/InferenceFlyout.tsx new file mode 100644 index 000000000..a867c197c --- /dev/null +++ b/public/pages/Ueba/containers/ViewInferences/InferenceFlyout.tsx @@ -0,0 +1,82 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiButtonIcon, + EuiFormLabel, + EuiText, +} from '@elastic/eui'; +import React from 'react'; +import { InferenceItem } from '../../models/interfaces'; + +export interface InferenceFlyoutProps { + inference: InferenceItem; + hideFlyout: () => void; +} + +export const InferenceFlyout: React.FC = ({ inference, hideFlyout }) => { + return ( + + + + + +

Inference details

+
+
+ + hideFlyout()} + data-test-subj={`close-inference-details-flyout`} + /> + +
+
+ + + + Inference name + {inference.name} + + + Inference type + {inference.type} + + + + + + Description + + {inference.description} + + + + Schedule + + {JSON.stringify(inference.schedule)} + + + + +
+ ); +}; diff --git a/public/pages/Ueba/containers/ViewInferences/ViewInferences.tsx b/public/pages/Ueba/containers/ViewInferences/ViewInferences.tsx new file mode 100644 index 000000000..17141997f --- /dev/null +++ b/public/pages/Ueba/containers/ViewInferences/ViewInferences.tsx @@ -0,0 +1,26 @@ +import React, { useContext, useEffect } from 'react'; + +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { BrowserServices } from '../../../../models/interfaces'; +import { CoreServicesContext } from '../../../../components/core_services'; +import { BREADCRUMBS } from '../../../../utils/constants'; +import * as H from 'history'; + +export interface UebaProps { + services: BrowserServices; + notifications?: NotificationsStart; + history: H.History; +} + +export const ViewInferences: React.FC = (props) => { + const context = useContext(CoreServicesContext); + useEffect(() => { + context?.chrome.setBreadcrumbs([ + BREADCRUMBS.SECURITY_ANALYTICS, + BREADCRUMBS.UEBA, + BREADCRUMBS.UEBA_VIEW_INFERENCES, + ]); + }); + + return <>View inferences; +}; diff --git a/public/pages/Ueba/containers/ViewInferences/index.ts b/public/pages/Ueba/containers/ViewInferences/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/public/pages/Ueba/index.ts b/public/pages/Ueba/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/public/pages/Ueba/models/UebaViewModelActor.ts b/public/pages/Ueba/models/UebaViewModelActor.ts new file mode 100644 index 000000000..d905e2b6a --- /dev/null +++ b/public/pages/Ueba/models/UebaViewModelActor.ts @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { errorNotificationToast } from '../../../utils/helpers'; +import { AggregatorItem } from './interfaces'; +import { BrowserServices } from '../../../models/interfaces'; + +export interface UebaViewModel { + aggregators: AggregatorItem[]; +} + +export class UebaViewModelActor { + private readonly uebaViewModel: UebaViewModel; + + constructor(private services: BrowserServices, private notifications: NotificationsStart | null) { + this.uebaViewModel = { + aggregators: [], + }; + } + + public async getAggregators(pageSize: number = 10): Promise { + try { + const response = await this.services.uebaService.getAggregators(pageSize); + + if (response.ok) { + return response?.response.hits.hits; + } else { + errorNotificationToast(this.notifications, 'retrieve', 'ueba aggregators', response.error); + } + + return []; + } catch (error: any) { + errorNotificationToast(this.notifications, 'retrieve', 'ueba aggregators', error); + return []; + } + } + + public getUebaViewModel() { + return this.uebaViewModel; + } +} diff --git a/public/pages/Ueba/models/interfaces.ts b/public/pages/Ueba/models/interfaces.ts new file mode 100644 index 000000000..3a62eb93a --- /dev/null +++ b/public/pages/Ueba/models/interfaces.ts @@ -0,0 +1,17 @@ +export interface AggregatorItem { + id?: string; + name: string; + description: string; + source_index: string; + page_size: number; + aggregator_script: string; +} + +export interface InferenceItem { + id?: string; + name: string; + description: string; + type: string; + schedule: any; + aggregators: string[]; +} diff --git a/public/pages/Ueba/utils/helpers.ts b/public/pages/Ueba/utils/helpers.ts new file mode 100644 index 000000000..9ac6a90ac --- /dev/null +++ b/public/pages/Ueba/utils/helpers.ts @@ -0,0 +1,57 @@ +import { + addInteractiveLegends, + DateOpts, + defaultDateFormat, + defaultScaleDomain, + defaultTimeUnit, + getTimeTooltip, + getVisualizationSpec, + getXAxis, + getYAxis, +} from '../../Overview/utils/helpers'; + +import { euiPaletteColorBlind, euiPaletteForStatus } from '@elastic/eui'; + +export function getUebaVisualization( + visualizationData: any[], + groupBy: string, + dateOpts: DateOpts = { + timeUnit: defaultTimeUnit, + dateFormat: defaultDateFormat, + domain: defaultScaleDomain, + } +) { + const severities = ['info', 'low', 'medium', 'high', 'critical']; + const isGroupedByLogType = groupBy === 'logType'; + const logTitle = 'Log type'; + const severityTitle = 'Rule severity'; + const title = isGroupedByLogType ? logTitle : severityTitle; + return getVisualizationSpec('Findings data overview', visualizationData, [ + addInteractiveLegends({ + mark: { + type: 'bar', + clip: true, + }, + encoding: { + tooltip: [ + getYAxis('finding', 'Findings'), + getTimeTooltip(dateOpts), + { + field: groupBy, + title: title, + }, + ], + x: getXAxis(dateOpts), + y: getYAxis('finding', 'Count'), + color: { + field: groupBy, + title: title, + scale: { + domain: isGroupedByLogType ? undefined : severities, + range: groupBy === 'logType' ? euiPaletteColorBlind() : euiPaletteForStatus(5), + }, + }, + }, + }), + ]); +} diff --git a/public/security_analytics_app.tsx b/public/security_analytics_app.tsx index bf8628e6c..fa637bb83 100644 --- a/public/security_analytics_app.tsx +++ b/public/security_analytics_app.tsx @@ -12,6 +12,7 @@ import { NotificationsService, ServicesContext, IndexPatternsService, + UebaService, } from './services'; import { DarkModeContext } from './components/DarkMode'; import Main from './pages/Main'; @@ -43,6 +44,7 @@ export function renderApp( const ruleService = new RuleService(http); const notificationsService = new NotificationsService(http); const indexPatternsService = new IndexPatternsService(depsStart.data.indexPatterns); + const uebaService = new UebaService(http); const services: BrowserServices = { detectorsService, @@ -54,6 +56,7 @@ export function renderApp( alertService: alertsService, notificationsService, indexPatternsService, + uebaService, }; const isDarkMode = coreStart.uiSettings.get('theme:darkMode') || false; diff --git a/public/services/UebaService.ts b/public/services/UebaService.ts new file mode 100644 index 000000000..89a12c832 --- /dev/null +++ b/public/services/UebaService.ts @@ -0,0 +1,24 @@ +import { HttpSetup } from 'opensearch-dashboards/public'; +import { ServerResponse } from '../../server/models/types'; +import { API } from '../../server/utils/constants'; +import { GetAggregatorsResponse, GetInferencesResponse } from '../../server/models/interfaces/Ueba'; + +export default class UebaService { + httpClient: HttpSetup; + + constructor(httpClient: HttpSetup) { + this.httpClient = httpClient; + } + + getAggregators = async ( + pageSize: number = 10 + ): Promise> => { + const url = `..${API.UEBA_BASE}/aggregator`; + return (await this.httpClient.get(url, {})) as ServerResponse; + }; + + getInferences = async (pageSize: number = 10): Promise> => { + const url = `..${API.UEBA_BASE}/inference`; + return (await this.httpClient.get(url, {})) as ServerResponse; + }; +} diff --git a/public/services/index.ts b/public/services/index.ts index 9964f84a5..41d3f7e55 100644 --- a/public/services/index.ts +++ b/public/services/index.ts @@ -13,6 +13,7 @@ import RuleService from './RuleService'; import IndexService from './IndexService'; import NotificationsService from './NotificationsService'; import IndexPatternsService from './IndexPatternsService'; +import UebaService from './UebaService'; export { ServicesConsumer, @@ -26,4 +27,5 @@ export { IndexService, NotificationsService, IndexPatternsService, + UebaService, }; diff --git a/public/utils/constants.ts b/public/utils/constants.ts index df7f10d49..fab3605e1 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -25,6 +25,7 @@ export const ROUTES = Object.freeze({ FINDINGS: '/findings', OVERVIEW: '/overview', RULES: '/rules', + UEBA: '/ueba', RULES_CREATE: '/create-rule', RULES_EDIT: '/edit-rule', RULES_IMPORT: '/import-rule', @@ -35,6 +36,10 @@ export const ROUTES = Object.freeze({ EDIT_DETECTOR_RULES: '/edit-detector-rules', EDIT_FIELD_MAPPINGS: '/edit-field-mappings', EDIT_DETECTOR_ALERT_TRIGGERS: '/edit-alert-triggers', + UEBA_CREATE_AGGREGATOR: '/ueba/create-aggregator', + UEBA_CREATE_INFERENCE: '/ueba/create-inference-model', + UEBA_VIEW_AGGREGATORS: '/ueba/view-aggregators', + UEBA_VIEW_INFERENCES: '/ueba/view-inference-models', get LANDING_PAGE(): string { return this.OVERVIEW; @@ -61,6 +66,23 @@ export const BREADCRUMBS = Object.freeze({ RULES_EDIT: { text: 'Edit rule', href: `#${ROUTES.RULES_EDIT}` }, RULES_DUPLICATE: { text: 'Duplicate rule', href: `#${ROUTES.RULES_DUPLICATE}` }, RULES_IMPORT: { text: 'Import rule', href: `#${ROUTES.RULES_IMPORT}` }, + UEBA: { text: 'UEBA', href: `#${ROUTES.UEBA}` }, + UEBA_CREATE_AGGREGATOR: { + text: 'Create UEBA aggregator', + href: `#${ROUTES.UEBA}/${ROUTES.UEBA_CREATE_AGGREGATOR}`, + }, + UEBA_CREATE_INFERENCE: { + text: 'Create UEBA inference model', + href: `#${ROUTES.UEBA}/${ROUTES.UEBA_CREATE_INFERENCE}`, + }, + UEBA_VIEW_AGGREGATORS: { + text: 'View aggregators', + href: `#${ROUTES.UEBA}/${ROUTES.UEBA_VIEW_AGGREGATORS}`, + }, + UEBA_VIEW_INFERENCES: { + text: 'View inference models', + href: `#${ROUTES.UEBA}/${ROUTES.UEBA_VIEW_INFERENCES}`, + }, }); export enum SortDirection { diff --git a/server/models/interfaces/Ueba.ts b/server/models/interfaces/Ueba.ts new file mode 100644 index 000000000..4386a343d --- /dev/null +++ b/server/models/interfaces/Ueba.ts @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AggregatorItem, InferenceItem } from '../../../public/pages/Ueba/models/interfaces'; + +export interface GetAggregators { + body: string; +} +export interface GetAggregatorsResponse { + hits: { + hits: AggregatorItem[]; + total: { + value: number; + }; + timed_out: boolean; + }; +} + +export interface GetInferences { + body: string; +} +export interface GetInferencesResponse { + hits: { + hits: InferenceItem[]; + total: { + value: number; + }; + timed_out: boolean; + }; +} diff --git a/server/models/interfaces/index.ts b/server/models/interfaces/index.ts index 2edcc9c78..436ac8746 100644 --- a/server/models/interfaces/index.ts +++ b/server/models/interfaces/index.ts @@ -10,6 +10,7 @@ import { FieldMappingService, DetectorService, NotificationsService, + UebaService, } from '../../services'; import AlertService from '../../services/AlertService'; import RulesService from '../../services/RuleService'; @@ -28,6 +29,7 @@ export interface SecurityAnalyticsApi { readonly CHANNELS: string; readonly PLUGINS: string; readonly ACKNOWLEDGE_ALERTS: string; + readonly UEBA_BASE: string; } export interface NodeServices { @@ -39,6 +41,7 @@ export interface NodeServices { alertService: AlertService; rulesService: RulesService; notificationsService: NotificationsService; + uebaServices: UebaService; } export interface GetIndicesResponse { diff --git a/server/plugin.ts b/server/plugin.ts index 2d5e84d62..f6e3ebb9c 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -15,6 +15,7 @@ import { setupIndexRoutes, setupAlertsRoutes, setupNotificationsRoutes, + setupUebaRoutes, } from './routes'; import { setupRulesRoutes } from './routes/RuleRoutes'; import { @@ -26,6 +27,7 @@ import { AlertService, RulesService, NotificationsService, + UebaService, } from './services'; export class SecurityAnalyticsPlugin @@ -44,6 +46,7 @@ export class SecurityAnalyticsPlugin alertService: new AlertService(osDriver), rulesService: new RulesService(osDriver), notificationsService: new NotificationsService(osDriver), + uebaServices: new UebaService(osDriver), }; // Create router @@ -58,6 +61,7 @@ export class SecurityAnalyticsPlugin setupAlertsRoutes(services, router); setupRulesRoutes(services, router); setupNotificationsRoutes(services, router); + setupUebaRoutes(services, router); return {}; } diff --git a/server/routes/UebaRoutes.ts b/server/routes/UebaRoutes.ts new file mode 100644 index 000000000..ccddaa69c --- /dev/null +++ b/server/routes/UebaRoutes.ts @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IRouter } from 'opensearch-dashboards/server'; +import { NodeServices } from '../models/interfaces'; +import { API } from '../utils/constants'; + +export function setupUebaRoutes(services: NodeServices, router: IRouter) { + const { uebaServices } = services; + + router.get( + { + path: `${API.UEBA_BASE}/aggregator`, + validate: {}, + }, + uebaServices.getAggregators + ); + + router.get( + { + path: `${API.UEBA_BASE}/inference`, + validate: {}, + }, + uebaServices.getInferences + ); +} diff --git a/server/routes/index.ts b/server/routes/index.ts index 576af1375..23230013d 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -10,3 +10,4 @@ export * from './FieldMappingRoutes'; export * from './IndexRoutes'; export * from './AlertRoutes'; export * from './NotificationsRoutes'; +export * from './UebaRoutes'; diff --git a/server/services/UebaService.ts b/server/services/UebaService.ts new file mode 100644 index 000000000..89e838a46 --- /dev/null +++ b/server/services/UebaService.ts @@ -0,0 +1,123 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + OpenSearchDashboardsRequest, + OpenSearchDashboardsResponseFactory, + IOpenSearchDashboardsResponse, + ResponseError, + RequestHandlerContext, + ILegacyCustomClusterClient, +} from 'opensearch-dashboards/server'; +import { ServerResponse } from '../models/types'; +import { GetAggregatorsResponse, GetInferencesResponse } from '../models/interfaces/Ueba'; + +export default class UebaService { + osDriver: ILegacyCustomClusterClient; + + constructor(osDriver: ILegacyCustomClusterClient) { + this.osDriver = osDriver; + } + + getAggregators = async ( + _context: RequestHandlerContext, + request: OpenSearchDashboardsRequest<{}>, + response: OpenSearchDashboardsResponseFactory + ): Promise< + IOpenSearchDashboardsResponse | ResponseError> + > => { + try { + // const { callAsCurrentUser: callWithRequest } = this.osDriver.asScoped(request); + // const getAggregatorsResponse: GetRulesResponse = await callWithRequest( + // CLIENT_UEBA_METHODS.GET_AGGREGATORS + // ); + + const getAggregatorsResponse: GetAggregatorsResponse = { + hits: { + hits: [ + { + name: 'Aggregator 1', + description: 'Any text can go here.', + source_index: 'cypress-dns-index', + page_size: 10, + aggregator_script: 'Not implemented', + }, + ], + total: { + value: 1, + }, + timed_out: false, + }, + }; + + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: getAggregatorsResponse, + }, + }); + } catch (error: any) { + console.error('Security Analytics - UebaServices - getAggregators:', error); + return response.custom({ + statusCode: 200, + body: { + ok: false, + error: error.message, + }, + }); + } + }; + + getInferences = async ( + _context: RequestHandlerContext, + request: OpenSearchDashboardsRequest<{}>, + response: OpenSearchDashboardsResponseFactory + ): Promise< + IOpenSearchDashboardsResponse | ResponseError> + > => { + try { + // const { callAsCurrentUser: callWithRequest } = this.osDriver.asScoped(request); + // const getAggregatorsResponse: GetRulesResponse = await callWithRequest( + // CLIENT_UEBA_METHODS.GET_AGGREGATORS + // ); + + const getInferencesResponse: GetInferencesResponse = { + hits: { + hits: [ + { + name: 'Inference model I', + description: 'Any text can go here.', + type: 'itt', + schedule: {}, + aggregators: ['Aggregator 1'], + }, + ], + total: { + value: 1, + }, + timed_out: false, + }, + }; + + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: getInferencesResponse, + }, + }); + } catch (error: any) { + console.error('Security Analytics - UebaServices - getAggregators:', error); + return response.custom({ + statusCode: 200, + body: { + ok: false, + error: error.message, + }, + }); + } + }; +} diff --git a/server/services/index.ts b/server/services/index.ts index 90f050a8b..f50d1e2eb 100644 --- a/server/services/index.ts +++ b/server/services/index.ts @@ -11,6 +11,7 @@ import FieldMappingService from './FieldMappingService'; import AlertService from './AlertService'; import RulesService from './RuleService'; import NotificationsService from './NotificationsService'; +import UebaService from './UebaService'; export { DetectorService, @@ -21,4 +22,5 @@ export { AlertService, RulesService, NotificationsService, + UebaService, }; diff --git a/server/utils/constants.ts b/server/utils/constants.ts index ba8baa955..8eceda6d2 100644 --- a/server/utils/constants.ts +++ b/server/utils/constants.ts @@ -27,6 +27,7 @@ export const API: SecurityAnalyticsApi = { CHANNELS: `${BASE_API_PATH}/_notifications/channels`, PLUGINS: `${BASE_API_PATH}/_notifications/plugins`, ACKNOWLEDGE_ALERTS: `${BASE_API_PATH}/detectors/{detector_id}/_acknowledge/alerts`, + UEBA_BASE: `${BASE_API_PATH}/ueba`, }; /** @@ -65,6 +66,8 @@ export const METHOD_NAMES = { // Notifications methods GET_CHANNEl: 'getChannel', GET_CHANNElS: 'getChannels', + + GET_AGGREGATORS: 'getAggregators', }; /** @@ -104,3 +107,7 @@ export const CLIENT_NOTIFICATIONS_METHODS = { GET_CHANNEL: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.GET_CHANNEl}`, GET_CHANNELS: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.GET_CHANNElS}`, }; + +export const CLIENT_UEBA_METHODS = { + GET_AGGREGATORS: `${PLUGIN_PROPERTY_NAME}.${METHOD_NAMES.GET_CHANNEl}`, +};