From d7b2d04c6b5c0db7f16d1690e5ad5c1edf9efd35 Mon Sep 17 00:00:00 2001 From: Amardeepsingh <114732919+amsiglan@users.noreply.github.com> Date: Thu, 3 Nov 2022 11:59:53 -0700 Subject: [PATCH] added detectors view/edit ux (#40) Signed-off-by: Amardeepsingh Siglani Signed-off-by: Amardeepsingh Siglani (cherry picked from commit c2858ffffbcaa2d28bbaba39fc3f24f677533092) --- .../AlertTriggerView/AlertTriggerView.tsx | 111 +++++++ .../DetectorBasicDetailsView.tsx | 63 ++++ .../DetectorRulesView/DetectorRulesView.tsx | 155 +++++++++ .../FieldMappingsView/FieldMappingsView.tsx | 84 +++++ .../UpdateAlertConditions.tsx | 190 +++++++++++ .../UpdateBasicDetails/UpdateBasicDetails.tsx | 190 +++++++++++ .../UpdateFieldMappings.tsx | 138 ++++++++ .../components/UpdateRules/UpdateRules.tsx | 162 +++++++++ .../AlertTriggersView/AlertTriggersView.tsx | 125 +++++++ .../containers/Detector/Detector.tsx | 312 ++++++++++++++++++ .../DetectorDetailsView.tsx | 62 ++++ .../containers/Detectors/Detectors.tsx | 281 +++++++++------- public/pages/Detectors/utils/constants.ts | 16 +- public/pages/Detectors/utils/helpers.ts | 10 +- 14 files changed, 1772 insertions(+), 127 deletions(-) create mode 100644 public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx create mode 100644 public/pages/Detectors/components/DetectorBasicDetailsView/DetectorBasicDetailsView.tsx create mode 100644 public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx create mode 100644 public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx create mode 100644 public/pages/Detectors/components/UpdateAlertConditions/UpdateAlertConditions.tsx create mode 100644 public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx create mode 100644 public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.tsx create mode 100644 public/pages/Detectors/components/UpdateRules/UpdateRules.tsx create mode 100644 public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx create mode 100644 public/pages/Detectors/containers/Detector/Detector.tsx create mode 100644 public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx diff --git a/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx b/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx new file mode 100644 index 000000000..10e987654 --- /dev/null +++ b/public/pages/Detectors/components/AlertTriggerView/AlertTriggerView.tsx @@ -0,0 +1,111 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiAccordion, + EuiHorizontalRule, + EuiLink, + EuiResizableContainer, + EuiResizablePanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { AlertCondition, Detector } from '../../../../../models/interfaces'; +import React, { useEffect } from 'react'; +import { createTextDetailsGroup } from '../../../../utils/helpers'; +import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers'; +import { DEFAULT_EMPTY_DATA, getNotificationDetailsHref } from '../../../../utils/constants'; +import { FeatureChannelList, RuleInfo } from '../../../../../server/models/interfaces'; + +export interface AlertTriggerViewProps { + alertTrigger: AlertCondition; + orderPosition: number; + detector: Detector; + notificationChannels: FeatureChannelList[]; + rules: { [key: string]: RuleInfo }; +} + +export const AlertTriggerView: React.FC = ({ + alertTrigger, + orderPosition, + detector, + notificationChannels, + rules, +}) => { + const { name, sev_levels, types, tags, ids, severity, actions } = alertTrigger; + const alertSeverity = parseAlertSeverityToOption(severity)?.label || DEFAULT_EMPTY_DATA; + const action = actions[0]; + const notificationChannelId = detector.triggers[orderPosition].actions[0].destination_id; + const notificationChannel = notificationChannels.find( + (channel) => channel.config_id === notificationChannelId + ); + const conditionRuleNames = ids.map((ruleId) => rules[ruleId]?._source.title); + return ( +
+ {orderPosition > 0 && } + + +

{`Alert on ${name}`}

+ + } + > + + {createTextDetailsGroup([{ label: 'Trigger name', content: `${name}` }])} + + + +
If any detection rule matches
+
+ + {createTextDetailsGroup( + [ + { label: 'Log type', content: `${types[0]}` || DEFAULT_EMPTY_DATA }, + { label: 'Rule names', content: conditionRuleNames.join('\n') || DEFAULT_EMPTY_DATA }, + ], + 3 + )} + {createTextDetailsGroup( + [ + { label: 'Rule severities', content: sev_levels.join('\n') || DEFAULT_EMPTY_DATA }, + { label: 'Tags', content: tags.join('\n') || DEFAULT_EMPTY_DATA }, + ], + 3 + )} + + + +
Alert and notify
+
+ + {createTextDetailsGroup([ + { + label: 'Trigger alerts with severity', + content: `${alertSeverity}` || DEFAULT_EMPTY_DATA, + }, + ])} + + {createTextDetailsGroup([ + { + label: 'Notify channel', + content: notificationChannel ? ( + + {notificationChannel.name} + + ) : ( + {DEFAULT_EMPTY_DATA} + ), + }, + ])} +
+ +
+ ); +}; diff --git a/public/pages/Detectors/components/DetectorBasicDetailsView/DetectorBasicDetailsView.tsx b/public/pages/Detectors/components/DetectorBasicDetailsView/DetectorBasicDetailsView.tsx new file mode 100644 index 000000000..80875d383 --- /dev/null +++ b/public/pages/Detectors/components/DetectorBasicDetailsView/DetectorBasicDetailsView.tsx @@ -0,0 +1,63 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiButton, EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import { ContentPanel } from '../../../../components/ContentPanel'; +import { createTextDetailsGroup, parseSchedule } from '../../../../utils/helpers'; +import moment from 'moment'; +import { Detector } from '../../../../../models/interfaces'; +import { DEFAULT_EMPTY_DATA } from '../../../../utils/constants'; + +export interface DetectorBasicDetailsViewProps { + detector: Detector; + rulesCanFold?: boolean; + enabled_time?: number; + last_update_time?: number; + onEditClicked: () => void; +} + +export const DetectorBasicDetailsView: React.FC = ({ + detector, + enabled_time, + last_update_time, + rulesCanFold, + children, + onEditClicked, +}) => { + const { name, detector_type, inputs, schedule } = detector; + const detectorSchedule = parseSchedule(schedule); + const createdAt = enabled_time ? moment(enabled_time).format('YYYY-MM-DDTHH:mm') : undefined; + const lastUpdated = last_update_time + ? moment(last_update_time).format('YYYY-MM-DDTHH:mm') + : undefined; + + return ( + Edit]} + > + + {createTextDetailsGroup( + [ + { label: 'Detector name', content: name }, + { label: 'Log type', content: detector_type.toLowerCase() }, + { label: 'Data source', content: inputs[0].detector_input.indices[0] }, + ], + 4 + )} + {createTextDetailsGroup( + [ + { label: 'Description', content: inputs[0].detector_input.description }, + { label: 'Detector schedule', content: detectorSchedule }, + { label: 'Created at', content: createdAt || DEFAULT_EMPTY_DATA }, + { label: 'Last updated time', content: lastUpdated || DEFAULT_EMPTY_DATA }, + ], + 4 + )} + {rulesCanFold ? children : null} + + ); +}; diff --git a/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx b/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx new file mode 100644 index 000000000..26311c90d --- /dev/null +++ b/public/pages/Detectors/components/DetectorRulesView/DetectorRulesView.tsx @@ -0,0 +1,155 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ContentPanel } from '../../../../components/ContentPanel'; +import React, { useContext, useEffect, useState } from 'react'; +import { EuiAccordion, EuiButton, EuiInMemoryTable, EuiSpacer, EuiText } from '@elastic/eui'; +import { + RuleItem, + RuleItemInfo, +} from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces'; +import { getRulesColumns } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/utils/constants'; +import { ServicesContext } from '../../../../services'; +import { ruleItemInfosToItems } from '../../../../utils/helpers'; +import { Detector } from '../../../../../models/interfaces'; +import { RuleInfo } from '../../../../../server/models/interfaces/Rules'; + +export interface DetectorRulesViewProps { + detector: Detector; + rulesCanFold?: boolean; + onEditClicked: (enabledRules: RuleItem[], allRuleItems: RuleItem[]) => void; +} + +export const DetectorRulesView: React.FC = (props) => { + const totalSelected = props.detector.inputs.reduce((sum, inputObj) => { + return ( + sum + + inputObj.detector_input.custom_rules.length + + inputObj.detector_input.pre_packaged_rules.length + ); + }, 0); + + const [enabledRuleItems, setEnabledRuleItems] = useState([]); + const [allRuleItems, setAllRuleItems] = useState([]); + const actions = [ + props.onEditClicked(enabledRuleItems, allRuleItems)}>Edit, + ]; + const services = useContext(ServicesContext); + + useEffect(() => { + const getRules = async (prePackaged: boolean): Promise => { + const getRulesRes = await services?.ruleService.getRules(prePackaged, { + from: 0, + size: 5000, + query: { + nested: { + path: 'rule', + query: { + bool: { + must: [ + { match: { 'rule.category': `${props.detector.detector_type.toLowerCase()}` } }, + ], + }, + }, + }, + }, + }); + + if (getRulesRes?.ok) { + return getRulesRes.response.hits.hits; + } + + return []; + }; + + const translateToRuleItems = ( + prePackagedRules: RuleInfo[], + customRules: RuleInfo[], + isEnabled: (rule: RuleInfo) => boolean + ) => { + let ruleItemInfos: RuleItemInfo[] = prePackagedRules.map((rule) => ({ + ...rule, + enabled: isEnabled(rule), + prePackaged: true, + })); + + ruleItemInfos = ruleItemInfos.concat( + customRules.map((rule) => ({ + ...rule, + enabled: isEnabled(rule), + prePackaged: false, + })) + ); + + return ruleItemInfosToItems(props.detector.detector_type, ruleItemInfos); + }; + + const updateRulesState = async () => { + const enabledPrePackagedRuleIds = new Set( + props.detector.inputs[0].detector_input.pre_packaged_rules.map((ruleInfo) => ruleInfo.id) + ); + const enabledCustomRuleIds = new Set( + props.detector.inputs[0].detector_input.custom_rules.map((ruleInfo) => ruleInfo.id) + ); + + const prePackagedRules = await getRules(true); + const customRules = await getRules(false); + + const enabledPrePackagedRules = prePackagedRules.filter((hit: RuleInfo) => { + return enabledPrePackagedRuleIds.has(hit._id); + }); + + const enabledCustomRules = customRules.filter((hit: RuleInfo) => { + return enabledCustomRuleIds.has(hit._id); + }); + + const enabledRuleItems = translateToRuleItems( + enabledPrePackagedRules, + enabledCustomRules, + () => true + ); + const allRuleItems = translateToRuleItems( + prePackagedRules, + customRules, + (ruleInfo) => + enabledPrePackagedRuleIds.has(ruleInfo._id) || enabledCustomRuleIds.has(ruleInfo._id) + ); + setEnabledRuleItems(enabledRuleItems); + setAllRuleItems(allRuleItems); + }; + + updateRulesState().catch((error) => { + // TODO: Show error toast + }); + }, [services, props.detector]); + + const rules = ( + `${item.name}`} + pagination + /> + ); + + return props.rulesCanFold ? ( + +

View detection rules

+ + } + > + + {rules} +
+ ) : ( + + {rules} + + ); +}; diff --git a/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx new file mode 100644 index 000000000..ed926ec9c --- /dev/null +++ b/public/pages/Detectors/components/FieldMappingsView/FieldMappingsView.tsx @@ -0,0 +1,84 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ContentPanel } from '../../../../components/ContentPanel'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { EuiBasicTableColumn, EuiButton, EuiInMemoryTable } from '@elastic/eui'; +import { FieldMappingsTableItem } from '../../../CreateDetector/models/interfaces'; +import { ServicesContext } from '../../../../services'; +import { Detector, FieldMapping } from '../../../../../models/interfaces'; + +export interface FieldMappingsViewProps { + detector: Detector; + existingMappings?: FieldMapping[]; + editFieldMappings: () => void; +} + +const columns: EuiBasicTableColumn[] = [ + { + field: 'ruleFieldName', + name: 'Rule field name', + sortable: true, + }, + { + field: 'logFieldName', + name: 'Mapped index field name', + }, +]; + +export const FieldMappingsView: React.FC = ({ + detector, + existingMappings, + editFieldMappings, +}) => { + const actions = useMemo(() => [Edit], []); + const [fieldMappingItems, setFieldMappingItems] = useState([]); + const services = useContext(ServicesContext); + + const fetchFieldMappings = useCallback( + async (indexName: string) => { + const getMappingRes = await services?.fieldMappingService.getMappings(indexName); + if (getMappingRes?.ok) { + const mappings = getMappingRes.response[detector.detector_type.toLowerCase()]; + if (mappings) { + let items: FieldMappingsTableItem[] = []; + Object.entries(mappings.mappings.properties).forEach((entry) => { + items.push({ + ruleFieldName: entry[0], + logFieldName: entry[1].path, + }); + }); + + setFieldMappingItems(items); + } + } else { + // TODO: show error notification + } + }, + [services, detector] + ); + + useEffect(() => { + if (existingMappings) { + const items: FieldMappingsTableItem[] = []; + existingMappings.forEach((mapping) => { + items.push({ + ruleFieldName: mapping.ruleFieldName, + logFieldName: mapping.indexFieldName, + }); + }); + + setFieldMappingItems(items); + } else { + fetchFieldMappings(detector.inputs[0].detector_input.indices[0]); + } + }, [detector]); + + return ( + + + + ); +}; diff --git a/public/pages/Detectors/components/UpdateAlertConditions/UpdateAlertConditions.tsx b/public/pages/Detectors/components/UpdateAlertConditions/UpdateAlertConditions.tsx new file mode 100644 index 000000000..cffb99b21 --- /dev/null +++ b/public/pages/Detectors/components/UpdateAlertConditions/UpdateAlertConditions.tsx @@ -0,0 +1,190 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Component } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { DetectorHit } from '../../../../../server/models/interfaces'; +import { Detector } from '../../../../../models/interfaces'; +import ConfigureAlerts from '../../../CreateDetector/components/ConfigureAlerts'; +import { DetectorsService, NotificationsService, RuleService } from '../../../../services'; +import { RulesSharedState } from '../../../../models/interfaces'; +import { ROUTES } from '../../../../utils/constants'; + +export interface UpdateAlertConditionsProps + extends RouteComponentProps { + detectorService: DetectorsService; + ruleService: RuleService; + notificationsService: NotificationsService; +} + +export interface UpdateAlertConditionsState { + detector: Detector; + rules: object; + rulesOptions: Pick['rulesOptions']; + submitting: boolean; +} + +export default class UpdateAlertConditions extends Component< + UpdateAlertConditionsProps, + UpdateAlertConditionsState +> { + constructor(props: UpdateAlertConditionsProps) { + super(props); + this.state = { + detector: props.location.state.detectorHit._source, + rules: {}, + rulesOptions: [], + submitting: false, + }; + } + + componentDidMount() { + this.getRules(); + } + + changeDetector = (detector: Detector) => { + this.setState({ detector: detector }); + }; + + getRules = async () => { + try { + const { ruleService } = this.props; + const { detector } = this.state; + const body = { + from: 0, + size: 5000, + query: { + nested: { + path: 'rule', + query: { + bool: { + must: [{ match: { 'rule.category': detector.detector_type.toLowerCase() } }], + }, + }, + }, + }, + }; + + const prePackagedResponse = await ruleService.getRules(true, body); + const customResponse = await ruleService.getRules(false, body); + + const allRules = {}; + const rulesOptions = new Set(); + + if (prePackagedResponse.ok) { + prePackagedResponse.response.hits.hits.forEach((hit) => { + allRules[hit._id] = hit._source; + const rule = allRules[hit._id]; + rulesOptions.add({ + name: rule.title, + id: hit._id, + severity: rule.level, + tags: rule.tags.map((tag) => tag.value), + }); + }); + } else { + console.error('Failed to retrieve pre-packaged rules:', prePackagedResponse.error); + // TODO: Display toast with error details + } + + if (customResponse.ok) { + customResponse.response.hits.hits.forEach((hit) => { + allRules[hit._id] = hit._source; + const rule = allRules[hit._id]; + rulesOptions.add({ + name: rule.title, + id: hit._id, + severity: rule.level, + tags: rule.tags.map((tag) => tag.value), + }); + }); + } else { + console.error('Failed to retrieve custom rules:', customResponse.error); + // TODO: Display toast with error details + } + + this.setState({ rules: allRules, rulesOptions: Array.from(rulesOptions) }); + } catch (e) { + console.error('Failed to retrieve rule:', e); + // TODO: Display toast with error details + } + }; + + onCancel = () => { + this.props.history.replace({ + pathname: ROUTES.DETECTOR_DETAILS, + state: this.props.location.state, + }); + }; + + onSave = async () => { + this.setState({ submitting: true }); + const { + history, + location: { + state: { detectorHit }, + }, + detectorService, + } = this.props; + const { detector } = this.state; + + try { + const updateDetectorResponse = await detectorService.updateDetector( + detectorHit._id, + detector + ); + if (!updateDetectorResponse.ok) { + // TODO: show toast notification with error + console.error('Failed to update detector: ', updateDetectorResponse.error); + } + } catch (e) { + // TODO: show toast notification with error + console.error('Failed to update detector: ', e); + } + + this.setState({ submitting: false }); + history.replace({ + pathname: ROUTES.DETECTOR_DETAILS, + state: { + detectorHit: { ...detectorHit, _source: { ...detectorHit._source, ...detector } }, + }, + }); + }; + + render() { + const { detector, rulesOptions, submitting } = this.state; + return ( +
+ {}} + /> + + + + + Cancel + + + + + Save changes + + + +
+ ); + } +} diff --git a/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx b/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx new file mode 100644 index 000000000..ad9da66a1 --- /dev/null +++ b/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx @@ -0,0 +1,190 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiComboBoxOptionOption, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { Detector, PeriodSchedule } from '../../../../../models/interfaces'; +import React, { ChangeEvent, useContext, useState } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import DetectorBasicDetailsForm from '../../../CreateDetector/components/DefineDetector/components/DetectorDetails'; +import { MIN_NUM_DATA_SOURCES } from '../../utils/constants'; +import DetectorDataSource from '../../../CreateDetector/components/DefineDetector/components/DetectorDataSource'; +import { IndexService, ServicesContext } from '../../../../services'; +import { DetectorSchedule } from '../../../CreateDetector/components/DefineDetector/components/DetectorSchedule/DetectorSchedule'; +import { useCallback } from 'react'; +import { DetectorHit } from '../../../../../server/models/interfaces'; +import { ROUTES } from '../../../../utils/constants'; + +export interface UpdateDetectorBasicDetailsProps + extends RouteComponentProps {} + +export const UpdateDetectorBasicDetails: React.FC = (props) => { + const services = useContext(ServicesContext); + const [detector, setDetector] = useState(props.location.state.detectorHit._source); + const { name, inputs } = detector; + const description = inputs[0].detector_input.description; + + const updateDetectorState = useCallback( + (detector: Detector) => { + const isDataValid = + !!detector.name && + !!detector.detector_type && + detector.inputs[0].detector_input.indices.length >= MIN_NUM_DATA_SOURCES; + + if (isDataValid) { + setDetector(detector); + } + }, + [setDetector] + ); + + const onDetectorNameChange = useCallback( + (detectorName: string) => { + const newDetector: Detector = { + ...detector, + name: detectorName, + }; + + updateDetectorState(newDetector); + }, + [detector, updateDetectorState] + ); + + const onDetectorInputDescriptionChange = useCallback( + (event: ChangeEvent, index = 0) => { + const { inputs } = detector; + const newDetector: Detector = { + ...detector, + inputs: [ + { + detector_input: { + ...inputs[0].detector_input, + description: event.target.value, + }, + }, + ...inputs.slice(1), + ], + }; + + updateDetectorState(newDetector); + }, + [detector, updateDetectorState] + ); + + const onDetectorInputIndicesChange = useCallback( + (selectedOptions: EuiComboBoxOptionOption[]) => { + const detectorIndices = selectedOptions.map((selectedOption) => selectedOption.label); + + const { inputs } = detector; + const newDetector: Detector = { + ...detector, + inputs: [ + { + detector_input: { + ...inputs[0].detector_input, + indices: detectorIndices, + }, + }, + ...inputs.slice(1), + ], + }; + + updateDetectorState(newDetector); + }, + [detector, updateDetectorState] + ); + + const onDetectorScheduleChange = useCallback( + (schedule: PeriodSchedule) => { + const newDetector: Detector = { + ...detector, + schedule, + }; + + updateDetectorState(newDetector); + }, + [detector, updateDetectorState] + ); + + const onCancel = useCallback(() => { + props.history.replace({ + pathname: ROUTES.DETECTOR_DETAILS, + state: props.location.state, + }); + }, []); + + const onSave = useCallback(() => { + const detectorHit = props.location.state.detectorHit; + + const updateDetector = async () => { + const updateDetectorRes = await services?.detectorsService?.updateDetector( + detectorHit._id, + detector + ); + + if (updateDetectorRes?.ok) { + props.history.replace({ + pathname: ROUTES.DETECTOR_DETAILS, + state: { + detectorHit: { ...detectorHit, _source: { ...detectorHit._source, ...detector } }, + }, + }); + } else { + // TODO: Show error toast + } + + props.history.replace({ + pathname: ROUTES.DETECTOR_DETAILS, + state: { + detectorHit: { ...detectorHit, _source: { ...detectorHit._source, ...detector } }, + }, + }); + }; + + updateDetector(); + }, [detector]); + + return ( +
+ +

Edit detector details

+
+ + + + + + + + + + + + + + Cancel + + + Save changes + + +
+ ); +}; diff --git a/public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.tsx b/public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.tsx new file mode 100644 index 000000000..dd6942a85 --- /dev/null +++ b/public/pages/Detectors/components/UpdateFieldMappings/UpdateFieldMappings.tsx @@ -0,0 +1,138 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Component } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import ConfigureFieldMapping from '../../../CreateDetector/components/ConfigureFieldMapping'; +import { Detector, FieldMapping } from '../../../../../models/interfaces'; +import FieldMappingService from '../../../../services/FieldMappingService'; +import { DetectorHit } from '../../../../../server/models/interfaces'; +import { ROUTES } from '../../../../utils/constants'; +import { DetectorsService } from '../../../../services'; + +export interface UpdateFieldMappingsProps + extends RouteComponentProps { + detectorService: DetectorsService; + filedMappingService: FieldMappingService; +} + +export interface UpdateFieldMappingsState { + detector: Detector; + fieldMappings: FieldMapping[]; + submitting: boolean; +} + +export default class UpdateFieldMappings extends Component< + UpdateFieldMappingsProps, + UpdateFieldMappingsState +> { + constructor(props: UpdateFieldMappingsProps) { + super(props); + this.state = { + detector: props.location.state.detectorHit._source, + fieldMappings: [], + submitting: false, + }; + } + + onCancel = () => { + this.props.history.replace({ + pathname: ROUTES.DETECTOR_DETAILS, + state: this.props.location.state, + }); + }; + + onSave = async () => { + this.setState({ submitting: true }); + const { + history, + location: { + state: { detectorHit }, + }, + detectorService, + filedMappingService, + } = this.props; + const { detector, fieldMappings } = this.state; + + try { + const createMappingsResponse = await filedMappingService.createMappings( + detector.inputs[0].detector_input.indices[0], + detector.detector_type.toLowerCase(), + fieldMappings + ); + if (!createMappingsResponse.ok) { + // TODO: show toast notification with error + console.error('Failed to update field mappings: ', createMappingsResponse.error); + } + + const updateDetectorResponse = await detectorService.updateDetector( + detectorHit._id, + detector + ); + if (!updateDetectorResponse.ok) { + // TODO: show toast notification with error + console.error('Failed to update detector: ', updateDetectorResponse.error); + } + } catch (e) { + // TODO: show toast notification with error + console.error('Failed to update detector: ', e); + } + + this.setState({ submitting: false }); + history.replace({ + pathname: ROUTES.DETECTOR_DETAILS, + state: { + detectorHit: { ...detectorHit, _source: { ...detectorHit._source, ...detector } }, + }, + }); + }; + + replaceFieldMappings = (fieldMappings: FieldMapping[]): void => { + this.setState({ fieldMappings }); + }; + + render() { + const { filedMappingService } = this.props; + const { submitting, detector, fieldMappings } = this.state; + detector.detector_type = detector.detector_type.toLowerCase(); + return ( +
+ +

Edit detector details

+
+ + + {}} + /> + + + + + Cancel + + + + + Save changes + + + +
+ ); + } +} diff --git a/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx b/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx new file mode 100644 index 000000000..ce3275886 --- /dev/null +++ b/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx @@ -0,0 +1,162 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { DetectorHit } from '../../../../../server/models/interfaces'; +import React, { useCallback, useContext, useState } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { RuleItem } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces'; +import { Detector } from '../../../../../models/interfaces'; +import { MIN_NUM_DATA_SOURCES } from '../../utils/constants'; +import { DetectionRulesTable } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRulesTable'; +import { ROUTES } from '../../../../utils/constants'; +import { ServicesContext } from '../../../../services'; +import { getUpdatedEnabledRuleIds } from '../../../../utils/helpers'; + +export interface UpdateDetectorRulesProps + extends RouteComponentProps< + any, + any, + { detectorHit: DetectorHit; enabledRules?: RuleItem[]; allRules?: RuleItem[] } + > {} + +export const UpdateDetectorRules: React.FC = (props) => { + const services = useContext(ServicesContext); + const enabledRules = props.location.state.enabledRules; + + const [enabledCustomRuleIds, setEnabledCustomRuleIds] = useState>( + new Set( + enabledRules + ?.map((rule) => (rule.library === 'Custom' ? rule.id : undefined)) + .filter((id) => !!id) as string[] + ) + ); + const [enabledPrePackagedRuleIds, setEnabledPrePackagedRuleIds] = useState>( + new Set( + enabledRules + ?.map((rule) => (rule.library === 'Sigma' ? rule.id : undefined)) + .filter((id) => !!id) as string[] + ) + ); + const [detector, setDetector] = useState(props.location.state.detectorHit._source); + const [ruleItems, setRuleItems] = useState(props.location.state.allRules || []); + + const updateDetectorState = useCallback( + (detector: Detector) => { + const isDataValid = + !!detector.name && + !!detector.detector_type && + detector.inputs[0].detector_input.indices.length >= MIN_NUM_DATA_SOURCES; + + if (isDataValid) { + setDetector(detector); + } + }, + [setDetector] + ); + + const onRulesChanged = (ruleFieldname: string, enabledRuleIds: string[]) => { + const { inputs } = detector; + const newDetector: Detector = { + ...detector, + inputs: [ + { + detector_input: { + ...inputs[0].detector_input, + [ruleFieldname]: enabledRuleIds.map((id) => { + return { id }; + }), + }, + }, + ...inputs.slice(1), + ], + }; + + updateDetectorState(newDetector); + }; + + const onRuleActivationToggle = (changedItem: RuleItem, isActive: boolean) => { + const newRuleItems = ruleItems.map((item) => { + return { + ...item, + active: item.id === changedItem.id ? isActive : item.active, + }; + }); + setRuleItems(newRuleItems); + + const existingEnabledIds = + changedItem.library === 'Sigma' ? enabledPrePackagedRuleIds : enabledCustomRuleIds; + const newEnabledIds = getUpdatedEnabledRuleIds(existingEnabledIds, changedItem.id, isActive); + if (newEnabledIds) { + if (changedItem.library === 'Sigma') { + onRulesChanged('pre_packaged_rules', newEnabledIds); + setEnabledPrePackagedRuleIds(new Set(newEnabledIds)); + } else if (changedItem.library === 'Custom') { + onRulesChanged('custom_rules', newEnabledIds); + setEnabledCustomRuleIds(new Set(newEnabledIds)); + } + } + }; + + const onCancel = useCallback(() => { + props.history.replace({ + pathname: ROUTES.DETECTOR_DETAILS, + state: props.location.state, + }); + }, []); + + const onSave = useCallback(() => { + const detectorHit = props.location.state.detectorHit; + + const updateDetector = async () => { + const updateDetectorRes = await services?.detectorsService?.updateDetector( + detectorHit._id, + detector + ); + + if (updateDetectorRes?.ok) { + props.history.replace({ + pathname: ROUTES.DETECTOR_DETAILS, + state: { + detectorHit: { ...detectorHit, _source: { ...detectorHit._source, ...detector } }, + }, + }); + } else { + // TODO: Show error toast + } + + props.history.replace({ + pathname: ROUTES.DETECTOR_DETAILS, + state: { + detectorHit: { ...detectorHit, _source: { ...detectorHit._source, ...detector } }, + }, + }); + }; + + updateDetector(); + }, [detector]); + + return ( +
+ +

Edit detector rules

+
+ + + + + + + + + Cancel + + + Save changes + + +
+ ); +}; diff --git a/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx b/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx new file mode 100644 index 000000000..ec2a5f538 --- /dev/null +++ b/public/pages/Detectors/containers/AlertTriggersView/AlertTriggersView.tsx @@ -0,0 +1,125 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ContentPanel } from '../../../../components/ContentPanel'; +import React, { useMemo, useEffect, useState, useContext } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { AlertTriggerView } from '../../components/AlertTriggerView/AlertTriggerView'; +import { Detector } from '../../../../../models/interfaces'; +import { ServicesContext } from '../../../../services'; +import { ServerResponse } from '../../../../../server/models/types'; +import { + FeatureChannelList, + GetChannelsResponse, + GetRulesResponse, + RuleInfo, +} from '../../../../../server/models/interfaces'; + +export interface AlertTriggersViewProps { + detector: Detector; + editAlertTriggers: () => void; +} + +export const AlertTriggersView: React.FC = ({ + detector, + editAlertTriggers, +}) => { + const services = useContext(ServicesContext); + const [channels, setChannels] = useState([]); + const [rules, setRules] = useState<{ [key: string]: RuleInfo }>({}); + const actions = useMemo(() => [Edit], []); + + useEffect(() => { + const getNotificationChannels = async () => { + try { + const response = (await services?.notificationsService.getChannels()) as ServerResponse< + GetChannelsResponse + >; + if (response.ok) { + setChannels(response.response.channel_list); + } else { + console.error('Failed to retrieve notification channels:', response.error); + // TODO implement toast + } + } catch (e) { + console.error('Failed to retrieve notification channels:', e); + // TODO implement toast + } + }; + + const getRules = async () => { + try { + const parseRules: { [key: string]: RuleInfo } = {}; + + // Retrieve the prepackaged rules. + const prepackagedRuleIds = detector.inputs[0].detector_input.pre_packaged_rules.map( + (rule) => rule.id + ); + if (prepackagedRuleIds.length > 0) { + const prePackagedResponse = (await services?.ruleService.getRules(true, { + from: 0, + size: 5000, + query: { nested: { path: 'rule', query: { terms: { _id: prepackagedRuleIds } } } }, + })) as ServerResponse; + + if (prePackagedResponse.ok) { + prePackagedResponse.response.hits.hits.forEach((rule) => (parseRules[rule._id] = rule)); + } else { + console.error('Failed to retrieve prepackaged rules:', prePackagedResponse.error); + // TODO implement toast + } + } + + // Retrieve the custom rules. + const customRuleIds = detector.inputs[0].detector_input.custom_rules.map((rule) => rule.id); + if (customRuleIds.length > 0) { + const customResponse = (await services?.ruleService.getRules(true, { + from: 0, + size: 5000, + query: { nested: { path: 'rule', query: { terms: { _id: customRuleIds } } } }, + })) as ServerResponse; + + if (customResponse.ok) { + customResponse.response.hits.hits.forEach((rule) => (parseRules[rule._id] = rule)); + } else { + console.error('Failed to retrieve custom rules:', customResponse.error); + // TODO implement toast + } + } + + // Set all enabled rules. + setRules({ ...parseRules }); + } catch (e) { + console.error('Failed to retrieve rules:', e); + // TODO implement toast + } + }; + + const getChannelsAndRules = async () => { + await getNotificationChannels(); + await getRules(); + }; + + getChannelsAndRules().catch((e) => { + console.error('Failed to retrieve rules and notification channels:', e); + // TODO implement toast + }); + }, [services, detector]); + + return ( + + {detector.triggers.map((alertTrigger, index) => ( + + ))} + + ); +}; diff --git a/public/pages/Detectors/containers/Detector/Detector.tsx b/public/pages/Detectors/containers/Detector/Detector.tsx new file mode 100644 index 000000000..bea23ff49 --- /dev/null +++ b/public/pages/Detectors/containers/Detector/Detector.tsx @@ -0,0 +1,312 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiPopover, + EuiSpacer, + EuiTab, + EuiTabs, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { CoreServicesContext } from '../../../../components/core_services'; +import { BREADCRUMBS, ROUTES } from '../../../../utils/constants'; +import { DetectorHit } from '../../../../../server/models/interfaces'; +import { DetectorDetailsView } from '../DetectorDetailsView/DetectorDetailsView'; +import { FieldMappingsView } from '../../components/FieldMappingsView/FieldMappingsView'; +import { AlertTriggersView } from '../AlertTriggersView/AlertTriggersView'; +import { RuleItem } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces'; +import { DetectorsService } from '../../../../services'; + +export interface DetectorDetailsProps + extends RouteComponentProps< + {}, + any, + { detectorHit: DetectorHit; enabledRules?: RuleItem[]; allRules?: RuleItem[] } + > { + detectorService: DetectorsService; +} + +export interface DetectorDetailsState { + isActionsMenuOpen: boolean; + selectedTabId: TabId; + selectedTabContent: React.ReactNode; + detectorHit: DetectorHit; +} + +enum TabId { + DetectorDetails = 'detector-config-tab', + FieldMappings = 'field-mappings-tab', + AlertTriggers = 'alert-triggers-tab', +} + +export class DetectorDetails extends React.Component { + static contextType = CoreServicesContext; + private tabs: any[]; + + private get detectorHit(): DetectorHit { + return this.state.detectorHit; + } + + private set detectorHit(hit: DetectorHit) { + this.setState({ detectorHit: hit }); + } + + editDetectorBasicDetails = () => { + this.props.history.push({ + pathname: ROUTES.EDIT_DETECTOR_DETAILS, + state: { detectorHit: this.detectorHit }, + }); + }; + + editDetectorRules = (enabledRules: RuleItem[], allRules: RuleItem[]) => { + this.props.history.push({ + pathname: ROUTES.EDIT_DETECTOR_RULES, + state: { detectorHit: this.detectorHit, enabledRules, allRules }, + }); + }; + + editFieldMappings = () => { + this.props.history.push({ + pathname: ROUTES.EDIT_FIELD_MAPPINGS, + state: { detectorHit: this.detectorHit }, + }); + }; + + editAlertTriggers = () => { + this.props.history.push({ + pathname: ROUTES.EDIT_DETECTOR_ALERT_TRIGGERS, + state: { detectorHit: this.detectorHit }, + }); + }; + + private getTabs() { + return [ + { + id: TabId.DetectorDetails, + name: 'Detector configuration', + content: ( + + ), + }, + { + id: TabId.FieldMappings, + name: 'Field mappings', + content: ( + + ), + }, + { + id: TabId.AlertTriggers, + name: 'Alert triggers', + content: ( + + ), + }, + ]; + } + + constructor(props: DetectorDetailsProps) { + super(props); + this.state = { + isActionsMenuOpen: false, + selectedTabId: TabId.DetectorDetails, + selectedTabContent: null, + detectorHit: this.props.location.state.detectorHit, + }; + this.tabs = this.getTabs(); + } + + componentDidMount(): void { + const { name } = this.detectorHit._source; + this.context.chrome.setBreadcrumbs([ + BREADCRUMBS.SECURITY_ANALYTICS, + BREADCRUMBS.DETECTORS, + BREADCRUMBS.DETECTORS_DETAILS(name), + ]); + this.setState({ selectedTabContent: this.tabs[0].content }); + } + + toggleActionsMenu = () => { + const { isActionsMenuOpen } = this.state; + this.setState({ isActionsMenuOpen: !isActionsMenuOpen }); + }; + + closeActionsPopover = () => { + this.setState({ isActionsMenuOpen: false }); + }; + + onDelete = async () => { + const detectorId = this.detectorHit._id; + const deleteRes = await this.props.detectorService.deleteDetector(detectorId); + + if (!deleteRes.ok) { + // TODO: Show error + } else { + this.props.history.push(ROUTES.DETECTORS); + } + }; + + toggleDetector = async () => { + const detectorId = this.detectorHit._id; + const detector = this.detectorHit._source; + const updateRes = await this.props.detectorService.updateDetector(detectorId, { + ...detector, + enabled: !this.detectorHit._source.enabled, + }); + + if (!updateRes.ok) { + // TODO: show error + } else { + this.detectorHit = { + ...this.detectorHit, + _source: { + ...this.detectorHit._source, + enabled: !this.detectorHit._source.enabled, + }, + }; + } + }; + + createHeaderActions(): React.ReactNode[] { + const onClickActions = [ + { name: 'View Alerts', onClick: this.onViewAlertsClick }, + { name: 'View Findings', onClick: this.onViewFindingsClick }, + ]; + const { isActionsMenuOpen } = this.state; + return [ + ...onClickActions.map((action) => ( + {action.name} + )), + + Actions + + } + isOpen={isActionsMenuOpen} + closePopover={this.closeActionsPopover} + panelPaddingSize={'none'} + anchorPosition={'downLeft'} + data-test-subj={'detectorsActionsPopover'} + > + { + this.closeActionsPopover(); + this.onDelete(); + }} + data-test-subj={'editButton'} + > + Delete + , + { + this.closeActionsPopover(); + this.toggleDetector(); + }} + data-test-subj={'deleteButton'} + > + {`${this.detectorHit._source.enabled ? 'Stop' : 'Start'} detector`} + , + ]} + /> + , + ]; + } + + onViewAlertsClick = () => { + this.props.history.push(ROUTES.ALERTS); + }; + + onViewFindingsClick = () => { + this.props.history.push(ROUTES.FINDINGS); + }; + + renderTabs() { + return this.tabs.map((tab, index) => ( + this.setState({ selectedTabId: tab.id, selectedTabContent: tab.content })} + isSelected={this.state.selectedTabId === tab.id} + > + {tab.name} + + )); + } + + render() { + const { _source: detector } = this.detectorHit; + + return ( + + + + + + +

{detector.name}

+
+
+ + + + {detector.enabled ? 'Active' : 'Inactive'} + + + +
+
+ + + {this.createHeaderActions().map( + (action: React.ReactNode, idx: number): React.ReactNode => ( + + {action} + + ) + )} + + +
+ + {this.renderTabs()} + + {this.state.selectedTabContent} +
+ ); + } +} diff --git a/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx b/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx new file mode 100644 index 000000000..b7523354c --- /dev/null +++ b/public/pages/Detectors/containers/DetectorDetailsView/DetectorDetailsView.tsx @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { DetectorBasicDetailsView } from '../../components/DetectorBasicDetailsView/DetectorBasicDetailsView'; +import { DetectorRulesView } from '../../components/DetectorRulesView/DetectorRulesView'; +import { EuiSpacer } from '@elastic/eui'; +import { Detector } from '../../../../../models/interfaces'; +import { RuleItem } from '../../../CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces'; + +export interface DetectorDetailsViewProps { + detector: Detector; + enabled_time?: number; + last_update_time?: number; + rulesCanFold?: boolean; + editBasicDetails: () => void; + editDetectorRules: (enabledRules: RuleItem[], allRuleItems: RuleItem[]) => void; +} + +export interface DetectorDetailsViewState {} + +export class DetectorDetailsView extends React.Component< + DetectorDetailsViewProps, + DetectorDetailsViewState +> { + render() { + const { + detector, + enabled_time, + last_update_time, + rulesCanFold, + editBasicDetails, + editDetectorRules, + } = this.props; + const detectorRules = ( + + ); + + return ( + <> + + {rulesCanFold ? detectorRules : null} + + + + {rulesCanFold ? null : detectorRules} + + ); + } +} diff --git a/public/pages/Detectors/containers/Detectors/Detectors.tsx b/public/pages/Detectors/containers/Detectors/Detectors.tsx index af34d1033..9c0537851 100644 --- a/public/pages/Detectors/containers/Detectors/Detectors.tsx +++ b/public/pages/Detectors/containers/Detectors/Detectors.tsx @@ -5,35 +5,39 @@ import React, { Component } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { ContentPanel } from '../../../../components/ContentPanel'; import { EuiBasicTableColumn, EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, EuiInMemoryTable, + EuiLink, + EuiPanel, EuiPopover, EuiSpacer, EuiText, + EuiTitle, } from '@elastic/eui'; import { BREADCRUMBS, DEFAULT_EMPTY_DATA, PLUGIN_NAME, ROUTES } from '../../../../utils/constants'; import DeleteModal from '../../../../components/DeleteModal'; import { getDetectorNames } from '../../utils/helpers'; -import { renderTime } from '../../../../utils/helpers'; +import { capitalizeFirstLetter, renderTime } from '../../../../utils/helpers'; import { CoreServicesContext } from '../../../../components/core_services'; -import { Detector } from '../../../../../models/interfaces'; import { FieldValueSelectionFilterConfigType } from '@elastic/eui/src/components/search_bar/filters/field_value_selection_filter'; import { DetectorsService } from '../../../../services'; +import { DetectorHit } from '../../../../../server/models/interfaces'; interface DetectorsProps extends RouteComponentProps { detectorService: DetectorsService; } interface DetectorsState { - detectors: Detector[]; + detectorHits: DetectorHit[]; loadingDetectors: boolean; - selectedItems: Detector[]; + selectedItems: DetectorHit[]; isDeleteModalVisible: boolean; isPopoverOpen: boolean; } @@ -45,7 +49,7 @@ export default class Detectors extends Component super(props); this.state = { - detectors: [], + detectorHits: [], loadingDetectors: false, selectedItems: [], isDeleteModalVisible: false, @@ -63,20 +67,24 @@ export default class Detectors extends Component const res = await this.props.detectorService.getDetectors(); if (res.ok) { - this.setState({ detectors: res.response.hits.hits.map((hit) => hit._source) }); + const detectors = res.response.hits.hits.map((detector) => { + const { custom_rules, pre_packaged_rules } = detector._source.inputs[0].detector_input; + const rulesCount = custom_rules.length + pre_packaged_rules.length; + return { + ...detector, + detectorName: detector._source.name, + lastUpdatedTime: detector._source.last_update_time, + logType: detector._source.detector_type, + rulesCount: rulesCount, + status: detector._source.enabled ? 'Active' : 'Inactive', + }; + }); + this.setState({ detectorHits: detectors }); } this.setState({ loadingDetectors: false }); }; - onClickCreate = () => { - // TODO: Implement once API is available - }; - - onClickEdit = () => { - // TODO: Implement once API is available - }; - openDeleteModal = () => { this.setState({ isDeleteModalVisible: true }); }; @@ -85,19 +93,38 @@ export default class Detectors extends Component this.setState({ isDeleteModalVisible: false }); }; + toggleDetector = async (detector: DetectorHit, shouldStart: boolean) => { + const updateRes = await this.props.detectorService.updateDetector(detector._id, { + ...detector._source, + enabled: shouldStart, + }); + + if (!updateRes.ok) { + // TODO: show error + } else { + this.getDetectors(); + } + }; + onClickDelete = async () => { const { selectedItems } = this.state; + for (let item of selectedItems) { - await this.deleteDetector(item.name); + await this.deleteDetector(item._id); } - await this.getDetectors(); + + this.getDetectors(); }; deleteDetector = async (id: string) => { - // TODO: Implement once API is available + const deleteRes = await this.props.detectorService.deleteDetector(id); + + if (!deleteRes.ok) { + // TODO: Show error + } }; - onSelectionChange = (selectedItems: Detector[]) => { + onSelectionChange = (selectedItems: DetectorHit[]) => { this.setState({ selectedItems: selectedItems }); }; @@ -110,9 +137,52 @@ export default class Detectors extends Component this.setState({ isPopoverOpen: false }); }; + showDetectorDetails = (detectorHit: DetectorHit) => { + this.props.history.push({ + pathname: ROUTES.DETECTOR_DETAILS, + state: { detectorHit }, + }); + }; + + getActionItems = (selectedItems: DetectorHit[]) => { + const actionItems = [ + { + this.closeActionsPopover(); + this.openDeleteModal(); + }} + data-test-subj={'deleteButton'} + > + Delete + , + ]; + + if (selectedItems.length === 1) { + actionItems.push( + { + this.closeActionsPopover(); + this.toggleDetector(selectedItems[0], !selectedItems[0]._source.enabled); + }} + data-test-subj={'toggleDetectorButton'} + > + {`${selectedItems[0]?._source.enabled ? 'Stop' : 'Start'} detector`} + + ); + } + + return actionItems; + }; + render() { const { - detectors, + detectorHits, isDeleteModalVisible, isPopoverOpen, loadingDetectors, @@ -146,126 +216,88 @@ export default class Detectors extends Component anchorPosition={'downLeft'} data-test-subj={'detectorsActionsPopover'} > - { - this.closeActionsPopover(); - this.onClickEdit(); - }} - data-test-subj={'editButton'} - > - Edit - , - { - this.closeActionsPopover(); - this.openDeleteModal(); - }} - data-test-subj={'deleteButton'} - > - Delete - , - { - this.closeActionsPopover(); - this.openDeleteModal(); - }} - data-test-subj={'startDetectorButton'} - > - Start detector - , - { - this.closeActionsPopover(); - this.openDeleteModal(); - }} - data-test-subj={'stopDetectorButton'} - > - Stop detector - , - ]} - /> + , Create detector , ]; - const columns: EuiBasicTableColumn[] = [ + const columns: EuiBasicTableColumn[] = [ { - field: 'name', + field: 'detectorName', name: 'Detector name', sortable: true, dataType: 'string', - render: (name: string) => name || DEFAULT_EMPTY_DATA, + render: (name: string, item: DetectorHit) => ( + this.showDetectorDetails(item)}>{name} + ), }, { - field: 'enabled', + field: 'status', name: 'Status', sortable: true, dataType: 'string', - render: (enabled: boolean) => (enabled ? 'ACTIVE' : 'INACTIVE'), }, { - field: 'type', + field: 'logType', name: 'Log type', sortable: true, dataType: 'string', - render: (type: string) => type || DEFAULT_EMPTY_DATA, + render: (detector_type: string) => + capitalizeFirstLetter(detector_type) || DEFAULT_EMPTY_DATA, }, { - field: 'rules', + field: 'rulesCount', name: 'Rules', sortable: true, - dataType: 'string', - render: (rules: string) => rules || DEFAULT_EMPTY_DATA, + dataType: 'number', + align: 'left', + render: (count) => count || DEFAULT_EMPTY_DATA, }, { - field: 'last_update_time', + field: 'lastUpdatedTime', name: 'Last updated time', sortable: true, dataType: 'date', - render: (time: number) => renderTime(time) || DEFAULT_EMPTY_DATA, + render: (last_update_time) => renderTime(last_update_time) || DEFAULT_EMPTY_DATA, }, ]; const statuses = [ - ...new Set(detectors.map((detector) => (detector.enabled ? 'Active' : 'InActive'))), + ...new Set( + detectorHits.map((detector) => (detector._source.enabled ? 'Active' : 'Inactive')) + ), ]; - const logType = [...new Set(detectors.map((detector) => detector.detector_type))]; + const logType = [...new Set(detectorHits.map((detector) => detector._source.detector_type))]; const search = { - box: { placeholder: 'Search threat detectors' }, + box: { + placeholder: 'Search threat detectors', + schema: true, + }, filters: [ { type: 'field_value_selection', field: 'status', name: 'Status', - options: statuses.map((status) => ({ value: status })), + options: statuses.map((status) => ({ + value: status, + name: capitalizeFirstLetter(status), + })), multiSelect: 'or', } as FieldValueSelectionFilterConfigType, { type: 'field_value_selection', - field: 'type', + field: 'logType', name: 'Log type', - options: logType.map((logType) => ({ value: logType })), + options: logType.map((logType) => ({ + value: logType, + name: capitalizeFirstLetter(logType), + })), multiSelect: 'or', } as FieldValueSelectionFilterConfigType, ], @@ -278,30 +310,51 @@ export default class Detectors extends Component }, }; return ( - - - `${item.type}:${item.name}`} - columns={columns} - pagination={true} - sorting={sorting} - isSelectable={true} - selection={{ onSelectionChange: this.onSelectionChange }} - search={search} - loading={loadingDetectors} - message={ - -

There are no existing detectors.

- + + + + + +

Threat detectors

+
+
+ + + {actions[0]} + {actions[1]} + {actions[2]} + + +
+ +
+ + + + `${item._id}`} + columns={columns} + pagination={true} + sorting={sorting} + isSelectable={true} + selection={{ onSelectionChange: this.onSelectionChange }} + search={search} + loading={loadingDetectors} + message={ + +

There are no existing detectors.

+ + } + actions={[actions[3]]} + /> } - actions={[actions[3]]} /> - } - /> +
+
{isDeleteModalVisible && ( type={selectedItems.length > 1 ? 'detectors' : 'detector'} /> )} -
+ ); } } diff --git a/public/pages/Detectors/utils/constants.ts b/public/pages/Detectors/utils/constants.ts index 863f8d608..a48a680aa 100644 --- a/public/pages/Detectors/utils/constants.ts +++ b/public/pages/Detectors/utils/constants.ts @@ -27,14 +27,12 @@ export const EMPTY_DEFAULT_DETECTOR_INPUT = { }; export const DETECTOR_TYPES = { - APPLICATION: { id: 'application', label: 'Application logs' }, - APT: { id: 'apt', label: 'APT logs' }, - CLOUD: { id: 'cloud', label: 'Cloud logs' }, - COMPLIANCE: { id: 'compliance', label: 'Compliance logs' }, - LINUX: { id: 'linux', label: 'Linux logs' }, - MACOS: { id: 'macos', label: 'MacOS logs' }, - NETWORK: { id: 'network', label: 'Network logs' }, - PROXY: { id: 'proxy', label: 'Proxy logs' }, - WEB: { id: 'web', label: 'Web logs' }, + NETFLOW: { id: 'network', label: 'Netflow' }, + DNS: { id: 'dns', label: 'DNS logs' }, + APACHE_ACCESS: { id: 'apache_access', label: 'Apache access logs' }, WINDOWS: { id: 'windows', label: 'Windows logs' }, + AD_LDAP: { id: 'ad_ldap', label: 'AD/LDAP' }, + SYSTEM: { id: 'linux', label: 'System logs' }, + CLOUD_TRAIL: { id: 'cloudtrail', label: 'Cloud Trail logs' }, + S3: { id: 's3', label: 'S3 access logs' }, }; diff --git a/public/pages/Detectors/utils/helpers.ts b/public/pages/Detectors/utils/helpers.ts index 0b7deaf48..6be7e84a2 100644 --- a/public/pages/Detectors/utils/helpers.ts +++ b/public/pages/Detectors/utils/helpers.ts @@ -3,10 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -export function getDetectorIds(detectors: object[]) { - return detectors.map((detector) => detector.id).join(', '); +import { DetectorHit } from '../../../../server/models/interfaces'; + +export function getDetectorIds(detectors: DetectorHit[]) { + return detectors.map((detector) => detector._id).join(', '); } -export function getDetectorNames(detectors: object[]) { - return detectors.map((detector) => detector.name).join(', '); +export function getDetectorNames(detectors: DetectorHit[]) { + return detectors.map((detector) => detector._source.name).join(', '); }