diff --git a/public/pages/Correlations/Correlations.scss b/public/pages/Correlations/Correlations.scss index 78038d353..33c931485 100644 --- a/public/pages/Correlations/Correlations.scss +++ b/public/pages/Correlations/Correlations.scss @@ -5,4 +5,33 @@ .correlation_rule_field_condition .euiButtonGroup__buttons { box-shadow: none; -} \ No newline at end of file +} + +#sa-correlations-network .vis-tooltip { + background-color: #535353; + padding: 0px; +} + +.readonly-text-color-light-mode { + &.euiFieldText { + color: #535353; + -webkit-text-fill-color: #535353; + } + + .euiComboBoxPill.euiComboBoxPill--plainText { + color: #535353; + -webkit-text-fill-color: #535353; + } +} + +.readonly-text-color-dark-mode { + &.euiFieldText { + color: #DFE5EF; + -webkit-text-fill-color: #DFE5EF; + } + + .euiComboBoxPill.euiComboBoxPill--plainText { + color: #DFE5EF; + -webkit-text-fill-color: #DFE5EF; + } +} diff --git a/public/pages/Correlations/containers/CorrelationRules.tsx b/public/pages/Correlations/containers/CorrelationRules.tsx index e9d2a27e6..30c364295 100644 --- a/public/pages/Correlations/containers/CorrelationRules.tsx +++ b/public/pages/Correlations/containers/CorrelationRules.tsx @@ -21,45 +21,23 @@ import { getCorrelationRulesTableColumns, getCorrelationRulesTableSearchConfig, } from '../utils/helpers'; -import { - CorrelationRule, - CorrelationRuleHit, - CorrelationRuleSourceQueries, - CorrelationRuleTableItem, -} from '../../../../types'; +import { CorrelationRule } from '../../../../types'; import { RouteComponentProps } from 'react-router-dom'; import { CorrelationsExperimentalBanner } from '../components/ExperimentalBanner'; import { DeleteCorrelationRuleModal } from '../components/DeleteModal'; export const CorrelationRules: React.FC = (props: RouteComponentProps) => { const context = useContext(CoreServicesContext); - const [allRules, setAllRules] = useState([]); - const [filteredRules, setFilteredRules] = useState([]); + const [allRules, setAllRules] = useState([]); + const [filteredRules, setFilteredRules] = useState([]); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [selectedRule, setSelectedRule] = useState(undefined); - const getCorrelationRules = useCallback( - async (ruleItem?) => { - const allCorrelationRules: CorrelationRuleHit[] = await DataStore.correlations.getCorrelationRules(); - const allRuleItems: CorrelationRuleTableItem[] = allCorrelationRules.map( - (rule: CorrelationRuleHit) => ({ - ...rule, - ...rule._source, - id: rule._id, - name: rule._source?.name || '-', - queries: rule._source?.correlate?.map((correlate: CorrelationRuleSourceQueries) => ({ - ...correlate, - logType: correlate.category, - })), - logTypes: rule._source?.correlate?.map((correlate) => correlate.category).join(', '), - }) - ); - - setAllRules(allRuleItems); - setFilteredRules(allRuleItems); - }, - [DataStore.correlations.getCorrelationRules] - ); + const getCorrelationRules = useCallback(async () => { + const allRuleItems: CorrelationRule[] = await DataStore.correlations.getCorrelationRules(); + setAllRules(allRuleItems); + setFilteredRules(allRuleItems); + }, [DataStore.correlations.getCorrelationRules]); useEffect(() => { context?.chrome.setBreadcrumbs([ @@ -103,7 +81,7 @@ export const CorrelationRules: React.FC = (props: RouteComp const onRuleNameClick = useCallback((rule: CorrelationRule) => { props.history.push({ pathname: ROUTES.CORRELATION_RULE_CREATE, - state: { rule }, + state: { rule, isReadOnly: true }, }); }, []); @@ -111,7 +89,7 @@ export const CorrelationRules: React.FC = (props: RouteComp setIsDeleteModalVisible(false); }; - const onDeleteRuleConfirmed = async (rule: any) => { + const onDeleteRuleConfirmed = async () => { if (selectedRule) { const response = await DataStore.correlations.deleteCorrelationRule(selectedRule.id); @@ -163,7 +141,7 @@ export const CorrelationRules: React.FC = (props: RouteComp {allRules.length ? ( { + columns={getCorrelationRulesTableColumns(onRuleNameClick, (rule) => { setIsDeleteModalVisible(true); setSelectedRule(rule); })} diff --git a/public/pages/Correlations/containers/CreateCorrelationRule.tsx b/public/pages/Correlations/containers/CreateCorrelationRule.tsx index 6d3b1efed..aa7ed603c 100644 --- a/public/pages/Correlations/containers/CreateCorrelationRule.tsx +++ b/public/pages/Correlations/containers/CreateCorrelationRule.tsx @@ -28,8 +28,12 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { ruleTypes } from '../../Rules/utils/constants'; -import { CorrelationRuleModel, CorrelationRuleQuery } from '../../../../types'; -import { BREADCRUMBS, ROUTES } from '../../../utils/constants'; +import { + CorrelationRuleAction, + CorrelationRuleModel, + CorrelationRuleQuery, +} from '../../../../types'; +import { BREADCRUMBS, ROUTES, isDarkMode } from '../../../utils/constants'; import { CoreServicesContext } from '../../../components/core_services'; import { RouteComponentProps } from 'react-router-dom'; import { CorrelationsExperimentalBanner } from '../components/ExperimentalBanner'; @@ -40,7 +44,11 @@ import { errorNotificationToast } from '../../../utils/helpers'; export interface CreateCorrelationRuleProps { indexService: IndexService; fieldMappingService: FieldMappingService; - history: RouteComponentProps['history']; + history: RouteComponentProps< + any, + any, + { rule: CorrelationRuleModel; isReadOnly: boolean } + >['history']; notifications: NotificationsStart | null; } @@ -53,6 +61,8 @@ export const CreateCorrelationRule: React.FC = ( props: CreateCorrelationRuleProps ) => { const correlationStore = DataStore.correlations; + const [indices, setIndices] = useState([]); + const [logFields, setLogFields] = useState([]); const validateCorrelationRule = useCallback((rule: CorrelationRuleModel) => { if (!rule.name) { return 'Invalid rule name'; @@ -103,13 +113,26 @@ export const CreateCorrelationRule: React.FC = ( }; const context = useContext(CoreServicesContext); - const isEdit = !!props.history.location.state?.rule; - const initialValues = props.history.location.state?.rule || { + let action: CorrelationRuleAction = 'Create'; + let initialValues = { ...correlationRuleStateDefaultValue, }; - const [indices, setIndices] = useState([]); - const [logFields, setLogFields] = useState([]); + if (props.history.location.state?.rule) { + action = 'Edit'; + initialValues = props.history.location.state?.rule; + + if (props.history.location.state.isReadOnly) { + action = 'Readonly'; + } + } + + const disableForm = action === 'Readonly'; + const textClassName = disableForm + ? isDarkMode + ? 'readonly-text-color-dark-mode' + : 'readonly-text-color-light-mode' + : undefined; const parseOptions = (indices: string[]) => { return indices.map( @@ -224,6 +247,8 @@ export const CreateCorrelationRule: React.FC = ( query.index ? [{ value: query.index, label: query.index }] : [] } isClearable={true} + isDisabled={disableForm} + className={textClassName} /> @@ -254,6 +279,8 @@ export const CreateCorrelationRule: React.FC = ( onCreateOption={(e) => { props.handleChange(`queries[${queryIdx}].logType`)(e); }} + isDisabled={disableForm} + className={textClassName} /> @@ -286,6 +313,8 @@ export const CreateCorrelationRule: React.FC = ( )(e); }} isClearable={true} + isDisabled={disableForm} + className={textClassName} /> ); @@ -303,6 +332,8 @@ export const CreateCorrelationRule: React.FC = ( `queries[${queryIdx}].conditions[${conditionIdx}].value` )} value={condition.value} + disabled={disableForm} + className={textClassName} /> ); @@ -321,6 +352,7 @@ export const CreateCorrelationRule: React.FC = ( )(e); }} className={'correlation_rule_field_condition'} + isDisabled={disableForm} /> ); @@ -355,8 +387,8 @@ export const CreateCorrelationRule: React.FC = ( initialIsOpen={true} buttonContent={`Field ${conditionIdx + 1}`} extraAction={ - query.conditions.length > 1 ? ( - + query.conditions.length > 1 && !disableForm ? ( + = ( newCases ); }} + disabled={disableForm} /> ) : null @@ -382,18 +415,21 @@ export const CreateCorrelationRule: React.FC = ( ); })} - { - props.setFieldValue(`queries[${queryIdx}].conditions`, [ - ...query.conditions, - ...correlationRuleStateDefaultValue.queries[0].conditions, - ]); - }} - iconType={'plusInCircle'} - > - Add field - + {disableForm ? null : ( + { + props.setFieldValue(`queries[${queryIdx}].conditions`, [ + ...query.conditions, + ...correlationRuleStateDefaultValue.queries[0].conditions, + ]); + }} + iconType={'plusInCircle'} + disabled={disableForm} + > + Add field + + )} @@ -401,21 +437,21 @@ export const CreateCorrelationRule: React.FC = ( ); })} - - - { - props.setFieldValue('queries', [ - ...correlationQueries, - { ...correlationRuleStateDefaultValue.queries[0] }, - ]); - }} - iconType={'plusInCircle'} - > - Add query - - - + {disableForm ? null : ( + { + props.setFieldValue('queries', [ + ...correlationQueries, + { ...correlationRuleStateDefaultValue.queries[0] }, + ]); + }} + iconType={'plusInCircle'} + fullWidth={true} + disabled={disableForm} + > + Add query + + )} ); }; @@ -433,12 +469,14 @@ export const CreateCorrelationRule: React.FC = ( <> -

Create correlation rule

+

{action === 'Readonly' ? 'C' : `${action} c`}orrelation rule

- - Create a correlation rule to define threat scenarios of interest between different log - sources. - + {action === 'Readonly' ? null : ( + + {action === 'Create' ? 'Create a' : 'Edit'} correlation rule to define threat scenarios of + interest between different log sources. + + )} = ( } isInvalid={touched.name && !!errors?.name} error={errors.name} - helpText="Rule name must contain 5-50 characters. Valid characters are a-z, A-Z, 0-9, hyphens, spaces, and underscores." + helpText={ + disableForm + ? undefined + : 'Rule name must contain 5-50 characters. Valid characters are a-z, A-Z, 0-9, hyphens, spaces, and underscores.' + } > = ( }} onBlur={props.handleBlur('name')} value={name} + className={textClassName} + disabled={disableForm} /> @@ -494,28 +538,34 @@ export const CreateCorrelationRule: React.FC = ( {createForm(queries, touched, errors, props)} - - - - Cancel - - - { - props.handleSubmit(); - }} - fill={true} - > - {isEdit ? 'Update' : 'Create '} correlation rule - - - + {action === 'Create' || action === 'Edit' ? ( + <> + + + + Cancel + + + { + props.handleSubmit(); + }} + fill={true} + > + {action === 'Edit' ? 'Update' : 'Create '} correlation rule + + + + + ) : null} ); }} diff --git a/public/pages/Correlations/utils/helpers.tsx b/public/pages/Correlations/utils/helpers.tsx index 6577bc528..f8b4a13c1 100644 --- a/public/pages/Correlations/utils/helpers.tsx +++ b/public/pages/Correlations/utils/helpers.tsx @@ -4,27 +4,30 @@ */ import React from 'react'; -import { EuiBasicTableColumn, EuiBadge, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { EuiBasicTableColumn, EuiBadge, EuiToolTip, EuiButtonIcon, EuiLink } from '@elastic/eui'; import { ArgsWithError, ArgsWithQuery, CorrelationRule, CorrelationRuleQuery, - CorrelationRuleTableItem, } from '../../../../types'; import { Search } from '@opensearch-project/oui/src/eui_components/basic_table'; import { ruleTypes } from '../../Rules/utils/constants'; import { FieldClause } from '@opensearch-project/oui/src/eui_components/search_bar/query/ast'; export const getCorrelationRulesTableColumns = ( + onRuleNameClick: (rule: CorrelationRule) => void, _refreshRules: (ruleItem: CorrelationRule) => void -): EuiBasicTableColumn[] => { +): EuiBasicTableColumn[] => { return [ { field: 'name', name: 'Name', sortable: true, truncateText: true, + render: (name: string, ruleItem: CorrelationRule) => ( + onRuleNameClick(ruleItem)}>{name} + ), }, { name: 'Log types', diff --git a/public/plugin.ts b/public/plugin.ts index 89c02359c..d058114aa 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -3,14 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - AppMountParameters, - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext, -} from '../../../src/core/public'; -import { PLUGIN_NAME, ROUTES } from './utils/constants'; +import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; +import { PLUGIN_NAME, ROUTES, setDarkMode } from './utils/constants'; import { SecurityAnalyticsPluginSetup, SecurityAnalyticsPluginStart } from './index'; import { DataPublicPluginStart, DataPublicPluginSetup } from '../../../src/plugins/data/public'; @@ -29,13 +23,9 @@ export class SecurityAnalyticsPlugin SecurityAnalyticsPluginSetupDeps, SecurityAnalyticsPluginStartDeps > { - constructor(private readonly initializerContext: PluginInitializerContext) { - // can retrieve config from initializerContext - } - public setup( core: CoreSetup, - plugins: SecurityAnalyticsPluginSetupDeps + _plugins: SecurityAnalyticsPluginSetupDeps ): SecurityAnalyticsPluginSetup { core.application.register({ id: PLUGIN_NAME, @@ -52,10 +42,12 @@ export class SecurityAnalyticsPlugin return renderApp(coreStart, params, ROUTES.LANDING_PAGE, depsStart); }, }); + setDarkMode(core.uiSettings.get('theme:darkMode')); + return {}; } - public start(core: CoreStart): SecurityAnalyticsPluginStart { + public start(_core: CoreStart): SecurityAnalyticsPluginStart { return {}; } } diff --git a/public/store/CorrelationsStore.ts b/public/store/CorrelationsStore.ts index 9faffc2f9..e735dd709 100644 --- a/public/store/CorrelationsStore.ts +++ b/public/store/CorrelationsStore.ts @@ -4,9 +4,10 @@ */ import { + CorrelationFieldCondition, CorrelationFinding, CorrelationRule, - CorrelationRuleHit, + CorrelationRuleQuery, ICorrelationsStore, IRulesStore, } from '../../types'; @@ -67,11 +68,25 @@ export class CorrelationsStore implements ICorrelationsStore { return response.ok; } - public async getCorrelationRules(index?: string): Promise { + public async getCorrelationRules(index?: string): Promise { const response = await this.service.getCorrelationRules(index); if (response?.ok) { - return response.response.hits.hits; + return response.response.hits.hits.map((hit) => { + const queries: CorrelationRuleQuery[] = hit._source.correlate.map((queryData) => { + return { + index: queryData.index, + logType: queryData.category, + conditions: this.parseRuleQueryString(queryData.query), + }; + }); + + return { + id: hit._id, + name: hit._source.name, + queries, + }; + }); } return []; @@ -194,4 +209,25 @@ export class CorrelationsStore implements ICorrelationsStore { correlatedFindings: [], }; } + + private parseRuleQueryString(queryString: string): CorrelationFieldCondition[] { + const queries: CorrelationFieldCondition[] = []; + const orConditions = queryString.trim().split(/ OR /gi); + + orConditions.forEach((cond, conditionIndex) => { + cond.split(/ AND /gi).forEach((fieldInfo: string, index: number) => { + const s = fieldInfo.match(/(?:\\:|[^:])+/g); + if (s) { + const [name, value] = s; + queries.push({ + name, + value, + condition: index === 0 && conditionIndex !== 0 ? 'OR' : 'AND', + }); + } + }); + }); + + return queries; + } } diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 30697890b..daf2aca12 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -16,11 +16,14 @@ export const DEFAULT_DATE_RANGE = { start: 'now-24h', end: 'now' }; export const PLUGIN_NAME = 'opensearch_security_analytics_dashboards'; export const OS_NOTIFICATION_PLUGIN = 'opensearch-notifications'; -// TODO: Replace with actual documentation link once it's available -export const DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/'; - export const DEFAULT_EMPTY_DATA = '-'; +export let isDarkMode: boolean = false; + +export function setDarkMode(isDarkModeSetting: boolean) { + isDarkMode = isDarkModeSetting; +} + export const ROUTES = Object.freeze({ ALERTS: '/alerts', DETECTORS: '/detectors', diff --git a/types/Correlations.ts b/types/Correlations.ts index a5eac1320..290638459 100644 --- a/types/Correlations.ts +++ b/types/Correlations.ts @@ -13,6 +13,8 @@ export enum CorrelationsLevel { Finding = 'Finding', } +export type CorrelationRuleAction = 'Create' | 'Edit' | 'Readonly'; + export interface CorrelationGraphData { graph: { nodes: (Node & { chosen?: boolean })[]; @@ -104,10 +106,8 @@ export interface CreateCorrelationRuleResponse { export interface DeleteCorrelationRuleResponse {} -export type CorrelationRuleTableItem = CorrelationRule & { logTypes: string }; - export interface ICorrelationsStore { - getCorrelationRules(): Promise; + getCorrelationRules(): Promise; getCorrelatedFindings( finding: string, detector_type: string,