diff --git a/public/pages/LogTypes/components/DeleteLogTypeModal.tsx b/public/pages/LogTypes/components/DeleteLogTypeModal.tsx new file mode 100644 index 000000000..be9749a03 --- /dev/null +++ b/public/pages/LogTypes/components/DeleteLogTypeModal.tsx @@ -0,0 +1,115 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiCallOut, + EuiConfirmModal, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiLoadingSpinner, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiSpacer, +} from '@elastic/eui'; +import React from 'react'; +import { useState } from 'react'; + +export interface DeleteLogTypeModalProps { + logTypeName: string; + detectionRulesCount: number; + loading?: boolean; + closeModal: () => void; + onConfirm: () => void; +} + +export const DeleteLogTypeModal: React.FC = ({ + detectionRulesCount, + logTypeName, + loading, + closeModal, + onConfirm, +}) => { + const [confirmDeleteText, setConfirmDeleteText] = useState(''); + + if (loading) { + return ( + + + + + + ); + } + + const onConfirmClick = () => { + onConfirm(); + closeModal(); + }; + + return ( + + {detectionRulesCount > 0 ? ( + + + +

This log type can't be deleted

+
+
+ + 1 ? 'rules' : 'rule' + }.`} + iconType={'iInCircle'} + color="warning" + /> + +

+ Only log types that don’t have any associated rules can be deleted. Consider editing + log type or deleting associated detection rules. +

+
+ + + Close + + +
+ ) : ( + + +

The log type will be permanently deleted. This action is irreversible.

+ +

+ Type {logTypeName} to confirm +

+ + setConfirmDeleteText(e.target.value)} + /> + +
+
+ )} +
+ ); +}; diff --git a/public/pages/LogTypes/components/LogTypeDetails.tsx b/public/pages/LogTypes/components/LogTypeDetails.tsx new file mode 100644 index 000000000..5b4090a20 --- /dev/null +++ b/public/pages/LogTypes/components/LogTypeDetails.tsx @@ -0,0 +1,72 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiButton, EuiDescriptionList } from '@elastic/eui'; +import { ContentPanel } from '../../../components/ContentPanel'; +import React from 'react'; +import { LogTypeItem } from '../../../../types'; +import { DataStore } from '../../../store/DataStore'; +import { LogTypeForm } from './LogTypeForm'; +import { NotificationsStart } from 'opensearch-dashboards/public'; + +export interface LogTypeDetailsProps { + initialLogTypeDetails: LogTypeItem; + logTypeDetails: LogTypeItem; + isEditMode: boolean; + notifications: NotificationsStart; + setIsEditMode: (isEdit: boolean) => void; + setLogTypeDetails: (logType: LogTypeItem) => void; +} + +export const LogTypeDetails: React.FC = ({ + initialLogTypeDetails, + logTypeDetails, + isEditMode, + notifications, + setIsEditMode, + setLogTypeDetails, +}) => { + const onUpdateLogType = async () => { + const success = await DataStore.logTypes.updateLogType(logTypeDetails); + if (success) { + setIsEditMode(false); + } + }; + + return ( + setIsEditMode(true)}>Edit, + ] + } + > + { + setLogTypeDetails(initialLogTypeDetails); + setIsEditMode(false); + }} + onConfirm={onUpdateLogType} + /> + ), + }, + ]} + /> + + ); +}; diff --git a/public/pages/LogTypes/components/LogTypeDetectionRules.tsx b/public/pages/LogTypes/components/LogTypeDetectionRules.tsx index 765f03c37..8d2b4fd90 100644 --- a/public/pages/LogTypes/components/LogTypeDetectionRules.tsx +++ b/public/pages/LogTypes/components/LogTypeDetectionRules.tsx @@ -3,8 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ +import React from 'react'; +import { RulesTable } from '../../Rules/components/RulesTable/RulesTable'; +import { RuleTableItem } from '../../Rules/utils/helpers'; +import { ContentPanel } from '../../../components/ContentPanel'; +import { EuiButton } from '@elastic/eui'; + export interface LogTypeDetectionRulesProps { - logTypeId: string; + rules: RuleTableItem[]; + loadingRules: boolean; + refreshRules: () => void; } -export const LogTypeDetectionRules = () => {}; +export const LogTypeDetectionRules: React.FC = ({ + rules, + loadingRules, + refreshRules, +}) => { + return ( + Refresh]} + > + {}} /> + + ); +}; diff --git a/public/pages/LogTypes/components/LogTypeForm.tsx b/public/pages/LogTypes/components/LogTypeForm.tsx new file mode 100644 index 000000000..ce0b6ebb4 --- /dev/null +++ b/public/pages/LogTypes/components/LogTypeForm.tsx @@ -0,0 +1,124 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiBottomBar, + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiTextArea, +} from '@elastic/eui'; +import { LogTypeItem } from '../../../../types'; +import React from 'react'; +import { validateName } from '../../../utils/validation'; +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { useState } from 'react'; + +export interface LogTypeFormProps { + logTypeDetails: LogTypeItem; + isEditMode: boolean; + confirmButtonText: string; + notifications: NotificationsStart; + setLogTypeDetails: (logType: LogTypeItem) => void; + onCancel: () => void; + onConfirm: () => void; +} + +export const LogTypeForm: React.FC = ({ + logTypeDetails, + isEditMode, + confirmButtonText, + notifications, + setLogTypeDetails, + onCancel, + onConfirm, +}) => { + const [nameError, setNameError] = useState(''); + + const updateErrors = (details = logTypeDetails) => { + const nameInvalid = !validateName(details.name); + setNameError(nameInvalid ? 'Invalid name' : ''); + + return { nameInvalid }; + }; + const onConfirmClicked = () => { + const { nameInvalid } = updateErrors(); + + if (nameInvalid) { + notifications?.toasts.addDanger({ + title: `Failed to ${confirmButtonText.toLowerCase()}`, + text: `Fix the marked errors.`, + toastLifeTimeMs: 3000, + }); + + return; + } + onConfirm(); + }; + + return ( + <> + + { + const newLogType = { + ...logTypeDetails!, + name: e.target.value, + }; + setLogTypeDetails(newLogType); + updateErrors(newLogType); + }} + placeholder="Enter name for the log type" + disabled={!isEditMode || !!logTypeDetails.detectionRules} + /> + + + + { + const newLogType = { + ...logTypeDetails!, + description: e.target.value, + }; + setLogTypeDetails(newLogType); + updateErrors(newLogType); + }} + placeholder="Description of the log type" + disabled={!isEditMode} + /> + + {isEditMode ? ( + + + + + Cancel + + + + + {confirmButtonText} + + + + + ) : null} + + ); +}; diff --git a/public/pages/LogTypes/containers/CreateLogType.tsx b/public/pages/LogTypes/containers/CreateLogType.tsx new file mode 100644 index 000000000..1d11974ba --- /dev/null +++ b/public/pages/LogTypes/containers/CreateLogType.tsx @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiLink } from '@elastic/eui'; +import { ContentPanel } from '../../../components/ContentPanel'; +import React, { useContext, useState } from 'react'; +import { LogTypeForm } from '../components/LogTypeForm'; +import { LogTypeBase } from '../../../../types'; +import { defaultLogType } from '../utils/constants'; +import { RouteComponentProps } from 'react-router-dom'; +import { BREADCRUMBS, ROUTES } from '../../../utils/constants'; +import { CoreServicesContext } from '../../../components/core_services'; +import { useEffect } from 'react'; +import { DataStore } from '../../../store/DataStore'; +import { successNotificationToast } from '../../../utils/helpers'; +import { NotificationsStart } from 'opensearch-dashboards/public'; + +export interface CreateLogTypeProps extends RouteComponentProps { + notifications: NotificationsStart; +} + +export const CreateLogType: React.FC = ({ history, notifications }) => { + const [logTypeDetails, setLogTypeDetails] = useState({ ...defaultLogType }); + const context = useContext(CoreServicesContext); + + useEffect(() => { + context?.chrome.setBreadcrumbs([ + BREADCRUMBS.SECURITY_ANALYTICS, + BREADCRUMBS.DETECTORS, + BREADCRUMBS.LOG_TYPES, + BREADCRUMBS.LOG_TYPE_CREATE, + ]); + }, []); + + return ( + + Create log type to categorize and identify detection rules for your data sources.  {' '} + + Learn more + +

+ } + hideHeaderBorder={true} + > + history.push(ROUTES.LOG_TYPES)} + onConfirm={async () => { + const success = await DataStore.logTypes.createLogType(logTypeDetails); + if (success) { + successNotificationToast(notifications, 'created', `log type ${logTypeDetails.name}`); + history.push(ROUTES.LOG_TYPES); + } + }} + /> +
+ ); +}; diff --git a/public/pages/LogTypes/containers/LogType.tsx b/public/pages/LogTypes/containers/LogType.tsx new file mode 100644 index 000000000..75163d2b1 --- /dev/null +++ b/public/pages/LogTypes/containers/LogType.tsx @@ -0,0 +1,218 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useContext } from 'react'; +import { useState } from 'react'; +import { useEffect } from 'react'; +import { RouteComponentProps, useParams } from 'react-router-dom'; +import { LogTypeItem } from '../../../../types'; +import { + EuiButtonIcon, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPanel, + EuiSpacer, + EuiTab, + EuiTabs, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; +import { DataStore } from '../../../store/DataStore'; +import { CoreServicesContext } from '../../../components/core_services'; +import { BREADCRUMBS, ROUTES } from '../../../utils/constants'; +import { logTypeDetailsTabs } from '../utils/constants'; +import { LogTypeDetails } from '../components/LogTypeDetails'; +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { LogTypeDetectionRules } from '../components/LogTypeDetectionRules'; +import { RuleTableItem } from '../../Rules/utils/helpers'; +import { useCallback } from 'react'; +import { DeleteLogTypeModal } from '../components/DeleteLogTypeModal'; +import { errorNotificationToast, successNotificationToast } from '../../../utils/helpers'; + +export interface LogTypeProps extends RouteComponentProps { + notifications: NotificationsStart; +} + +export const LogType: React.FC = ({ notifications, history }) => { + const context = useContext(CoreServicesContext); + const { logTypeId } = useParams<{ logTypeId: string }>(); + const [selectedTabId, setSelectedTabId] = useState('details'); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [infoText, setInfoText] = useState( + <> + Loading details   + + + ); + const [logTypeDetails, setLogTypeDetails] = useState(undefined); + const [initialLogTypeDetails, setInitialLogTypeDetails] = useState( + undefined + ); + + const [isEditMode, setIsEditMode] = useState(false); + const [rules, setRules] = useState([]); + const [loadingRules, setLoadingRules] = useState(true); + + const updateRules = useCallback(async (details: LogTypeItem, intialDetails: LogTypeItem) => { + const rulesRes = await DataStore.rules.getAllRules({ + 'rule.category': [logTypeId], + }); + const ruleItems = rulesRes.map((rule) => ({ + title: rule._source.title, + level: rule._source.level, + category: rule._source.category, + description: rule._source.description, + source: rule.prePackaged ? 'Sigma' : 'Custom', + ruleInfo: rule, + ruleId: rule._id, + })); + setRules(ruleItems); + setLoadingRules(false); + setLogTypeDetails({ + ...details, + detectionRules: ruleItems.length, + }); + setInitialLogTypeDetails({ + ...intialDetails, + detectionRules: ruleItems.length, + }); + }, []); + + useEffect(() => { + context?.chrome.setBreadcrumbs([ + BREADCRUMBS.SECURITY_ANALYTICS, + BREADCRUMBS.DETECTORS, + BREADCRUMBS.LOG_TYPES, + { text: logTypeId }, + ]); + + const getLogTypeDetails = async () => { + const details = await DataStore.logTypes.getLogType(logTypeId); + + if (!details) { + setInfoText('Log type not found!'); + return; + } + + updateRules(details, details); + }; + + getLogTypeDetails(); + }, []); + + const refreshRules = useCallback(() => { + updateRules(logTypeDetails!, initialLogTypeDetails!); + }, [logTypeDetails]); + + const renderTabContent = () => { + switch (selectedTabId) { + case 'detection_rules': + return ( + + ); + case 'details': + default: + return ( + + ); + } + }; + + const deleteLogType = async () => { + const deleteSucceeded = await DataStore.logTypes.deleteLogType(logTypeDetails!.id); + if (deleteSucceeded) { + successNotificationToast(notifications, 'deleted', 'log type'); + history.push(ROUTES.LOG_TYPES); + } else { + errorNotificationToast(notifications, 'delete', 'log type'); + } + }; + + return !logTypeDetails ? ( + +

{infoText}

+
+ ) : ( + <> + {showDeleteModal && ( + setShowDeleteModal(false)} + onConfirm={deleteLogType} + /> + )} + + + +

{logTypeDetails.name}

+
+
+ + + setShowDeleteModal(true)} + /> + + +
+ + + + + + + + + + + + + + + + + + + + {logTypeDetailsTabs.map((tab, index) => { + return ( + { + setSelectedTabId(tab.id); + }} + key={index} + isSelected={selectedTabId === tab.id} + > + {tab.name} + + ); + })} + + {renderTabContent()} + + ); +}; diff --git a/public/pages/LogTypes/containers/LogTypes.tsx b/public/pages/LogTypes/containers/LogTypes.tsx index 59a2a5f47..e57e8799f 100644 --- a/public/pages/LogTypes/containers/LogTypes.tsx +++ b/public/pages/LogTypes/containers/LogTypes.tsx @@ -4,7 +4,7 @@ */ import React, { useContext, useEffect, useState } from 'react'; -import { EuiInMemoryTable } from '@elastic/eui'; +import { EuiButton, EuiInMemoryTable } from '@elastic/eui'; import { ContentPanel } from '../../../components/ContentPanel'; import { CoreServicesContext } from '../../../components/core_services'; import { BREADCRUMBS, ROUTES } from '../../../utils/constants'; @@ -13,12 +13,33 @@ import { DataStore } from '../../../store/DataStore'; import { getLogTypesTableColumns } from '../utils/helpers'; import { RouteComponentProps } from 'react-router-dom'; import { useCallback } from 'react'; +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { successNotificationToast } from '../../../utils/helpers'; +import { DeleteLogTypeModal } from '../components/DeleteLogTypeModal'; -export interface LogTypesProps extends RouteComponentProps {} +export interface LogTypesProps extends RouteComponentProps { + notifications: NotificationsStart; +} -export const LogTypes: React.FC = ({ history }) => { +export const LogTypes: React.FC = ({ history, notifications }) => { const context = useContext(CoreServicesContext); const [logTypes, setLogTypes] = useState([]); + const [logTypeToDelete, setLogTypeItemToDelete] = useState(undefined); + const [deletionDetails, setDeletionDetails] = useState< + { detectionRulesCount: number } | undefined + >(undefined); + const getLogTypes = async () => { + const logTypes = await DataStore.logTypes.getLogTypes(); + setLogTypes(logTypes); + }; + + const deleteLogType = async (id: string) => { + const deleteSucceeded = await DataStore.logTypes.deleteLogType(id); + if (deleteSucceeded) { + successNotificationToast(notifications, 'deleted', 'log type'); + getLogTypes(); + } + }; useEffect(() => { context?.chrome.setBreadcrumbs([ @@ -26,10 +47,6 @@ export const LogTypes: React.FC = ({ history }) => { BREADCRUMBS.DETECTORS, BREADCRUMBS.LOG_TYPES, ]); - const getLogTypes = async () => { - const logTypes = await DataStore.logTypes.getLogTypes(); - setLogTypes(logTypes); - }; getLogTypes(); }, []); @@ -38,24 +55,46 @@ export const LogTypes: React.FC = ({ history }) => { history.push(`${ROUTES.LOG_TYPES}/${id}`); }, []); + const onDeleteClick = async (item: LogType) => { + setLogTypeItemToDelete(item); + const rules = await DataStore.rules.getAllRules({ + 'rule.category': [item.id], + }); + setDeletionDetails({ detectionRulesCount: rules.length }); + }; + return ( - - - alert(`Deleted ${id}`) - )} - pagination={{ - initialPageSize: 25, - }} - sorting={true} - /> - + <> + {logTypeToDelete && ( + setLogTypeItemToDelete(undefined)} + onConfirm={() => deleteLogType(logTypeToDelete.id)} + /> + )} + history.push(ROUTES.LOG_TYPES_CREATE)}> + Create log type + , + ]} + > + + + ); }; diff --git a/public/pages/LogTypes/utils/constants.ts b/public/pages/LogTypes/utils/constants.ts index f7a7837f3..865e35e64 100644 --- a/public/pages/LogTypes/utils/constants.ts +++ b/public/pages/LogTypes/utils/constants.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { LogTypeBase } from '../../../../types'; + export const logTypeDetailsTabs = [ { id: 'details', @@ -13,3 +15,10 @@ export const logTypeDetailsTabs = [ name: 'Detection rules', }, ]; + +export const defaultLogType: LogTypeBase = { + name: '', + description: '', + source: 'Custom', + tags: null, +}; diff --git a/public/pages/LogTypes/utils/helpers.tsx b/public/pages/LogTypes/utils/helpers.tsx index 2ad78d793..a651d2a12 100644 --- a/public/pages/LogTypes/utils/helpers.tsx +++ b/public/pages/LogTypes/utils/helpers.tsx @@ -10,7 +10,7 @@ import { capitalize } from 'lodash'; export const getLogTypesTableColumns = ( showDetails: (id: string) => void, - deleteLogType: (logTypeId: string) => void + deleteLogType: (logType: LogType) => void ) => [ { field: 'name', @@ -41,7 +41,7 @@ export const getLogTypesTableColumns = ( aria-label={'Delete log type'} iconType={'trash'} color="danger" - onClick={() => deleteLogType(item.id)} + onClick={() => deleteLogType(item)} /> ); diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx index aab5ec1ca..df6adfc5e 100644 --- a/public/pages/Main/Main.tsx +++ b/public/pages/Main/Main.tsx @@ -48,7 +48,8 @@ import FindingDetailsFlyout, { FindingDetailsFlyoutBaseProps, } from '../Findings/components/FindingDetailsFlyout'; import { LogTypes } from '../LogTypes/containers/LogTypes'; -import { LogTypeDetails } from '../LogTypes/containers/LogTypeDetails'; +import { LogType } from '../LogTypes/containers/LogType'; +import { CreateLogType } from '../LogTypes/containers/CreateLogType'; enum Navigation { SecurityAnalytics = 'Security Analytics', @@ -76,6 +77,7 @@ const HIDDEN_NAV_ROUTES: string[] = [ ROUTES.EDIT_FIELD_MAPPINGS, ROUTES.EDIT_DETECTOR_ALERT_TRIGGERS, `${ROUTES.LOG_TYPES}/.+`, + ROUTES.LOG_TYPES_CREATE, ]; interface MainProps extends RouteComponentProps { @@ -559,12 +561,20 @@ export default class Main extends Component { /> } + render={(props: RouteComponentProps) => ( + + )} /> ) => { - return ; + return ; + }} + /> + ) => { + return ; }} /> diff --git a/public/pages/Rules/containers/Rules/Rules.tsx b/public/pages/Rules/containers/Rules/Rules.tsx index e5ee1ca67..60c2cb412 100644 --- a/public/pages/Rules/containers/Rules/Rules.tsx +++ b/public/pages/Rules/containers/Rules/Rules.tsx @@ -13,7 +13,6 @@ import { BREADCRUMBS, ROUTES } from '../../../../utils/constants'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { CoreServicesContext } from '../../../../components/core_services'; import { DataStore } from '../../../../store/DataStore'; -import { RuleItemInfoBase } from '../../../../../types'; export interface RulesProps extends RouteComponentProps { notifications?: NotificationsStart; @@ -22,7 +21,7 @@ export interface RulesProps extends RouteComponentProps { export const Rules: React.FC = (props) => { const context = useContext(CoreServicesContext); - const [allRules, setAllRules] = useState([]); + const [allRules, setAllRules] = useState([]); const [flyoutData, setFlyoutData] = useState(undefined); const [loading, setLoading] = useState(false); diff --git a/public/store/LogTypeStore.ts b/public/store/LogTypeStore.ts index 1eebdc8ae..8f9d6cfa1 100644 --- a/public/store/LogTypeStore.ts +++ b/public/store/LogTypeStore.ts @@ -72,4 +72,13 @@ export class LogTypeStore { return updateRes.ok; } + + public async deleteLogType(id: string) { + const deleteRes = await this.service.deleteLogType(id); + if (!deleteRes.ok) { + errorNotificationToast(this.notifications, 'delete', 'log type', deleteRes.error); + } + + return deleteRes.ok; + } } diff --git a/public/store/RulesStore.ts b/public/store/RulesStore.ts index ba735767d..edb392877 100644 --- a/public/store/RulesStore.ts +++ b/public/store/RulesStore.ts @@ -65,6 +65,7 @@ export class RulesStore implements IRulesStore { * @returns {Promise} */ public async getAllRules(terms?: { [key: string]: string[] }): Promise { + this.invalidateCache(); let customRules = await this.getCustomRules(terms); let prePackagedRules = await this.getPrePackagedRules(terms); diff --git a/types/Rule.ts b/types/Rule.ts index 82f617267..4b57f9d75 100644 --- a/types/Rule.ts +++ b/types/Rule.ts @@ -25,7 +25,7 @@ export type RuleSource = Rule & { rule: string; last_update_time: string; queries: { value: string }[]; - query_field_names: string[]; + query_field_names: { value: string }[]; }; export interface RuleInfo {