From 95560a8f3dde22cbd5f494d332123f81e8df1b73 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Wed, 7 Feb 2024 05:55:39 +0530 Subject: [PATCH] Show aliases in data source options for detector and correlation rule creation (#864) * show aliases under data source dropdowns Signed-off-by: Amardeepsingh Siglani * add manage rules entrypoint in creation UI Signed-off-by: Amardeepsingh Siglani * fixed integ test Signed-off-by: Amardeepsingh Siglani --------- Signed-off-by: Amardeepsingh Siglani --- cypress/integration/1_detectors.spec.js | 12 +++- .../containers/CreateCorrelationRule.tsx | 35 ++++------ .../DetectionRules/DetectionRules.tsx | 5 ++ .../DetectorDataSource/DetectorDataSource.tsx | 46 +++++-------- public/pages/Detectors/models/interfaces.ts | 1 + public/services/IndexService.ts | 9 ++- public/utils/helpers.tsx | 66 ++++++++++++++++++- server/models/interfaces/index.ts | 10 +++ server/routes/IndexRoutes.ts | 8 +++ server/services/IndexService.ts | 43 +++++++++--- server/utils/constants.ts | 1 + 11 files changed, 174 insertions(+), 62 deletions(-) diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js index 180fff919..bbf1ecf8d 100644 --- a/cypress/integration/1_detectors.spec.js +++ b/cypress/integration/1_detectors.spec.js @@ -113,9 +113,15 @@ const validatePendingFieldMappingsPanel = (mappings) => { }); }; -const fillDetailsForm = (detectorName, dataSource) => { +const fillDetailsForm = (detectorName, dataSource, isCustomDataSource = false) => { getNameField().type(detectorName); - getDataSourceField().selectComboboxItem(dataSource); + if (isCustomDataSource) { + getDataSourceField() + .focus() + .type(dataSource + '{enter}'); + } else { + getDataSourceField().selectComboboxItem(dataSource); + } getDataSourceField().focus().blur(); getLogTypeField().selectComboboxItem(getLogTypeLabel(cypressLogTypeDns)); getLogTypeField().focus().blur(); @@ -124,7 +130,7 @@ const fillDetailsForm = (detectorName, dataSource) => { const createDetector = (detectorName, dataSource, expectFailure) => { getCreateDetectorButton().click({ force: true }); - fillDetailsForm(detectorName, dataSource); + fillDetailsForm(detectorName, dataSource, expectFailure); cy.getElementByText('.euiAccordion .euiTitle', 'Selected detection rules (14)') .click({ force: true, timeout: 5000 }) diff --git a/public/pages/Correlations/containers/CreateCorrelationRule.tsx b/public/pages/Correlations/containers/CreateCorrelationRule.tsx index b7e490b42..f6823a7dc 100644 --- a/public/pages/Correlations/containers/CreateCorrelationRule.tsx +++ b/public/pages/Correlations/containers/CreateCorrelationRule.tsx @@ -43,7 +43,7 @@ import { CoreServicesContext } from '../../../components/core_services'; import { RouteComponentProps, useParams } from 'react-router-dom'; import { validateName } from '../../../utils/validation'; import { FieldMappingService, IndexService } from '../../../services'; -import { errorNotificationToast, getLogTypeOptions } from '../../../utils/helpers'; +import { errorNotificationToast, getDataSources, getLogTypeOptions } from '../../../utils/helpers'; export interface CreateCorrelationRuleProps { indexService: IndexService; @@ -56,9 +56,11 @@ export interface CreateCorrelationRuleProps { notifications: NotificationsStart | null; } -export interface CorrelationOptions { +export interface CorrelationOption { label: string; - value: string; + value?: string; + index?: string; + options?: CorrelationOption[]; } const parseTime = (time: number) => { @@ -87,9 +89,9 @@ export const CreateCorrelationRule: React.FC = ( props: CreateCorrelationRuleProps ) => { const correlationStore = DataStore.correlations; - const [indices, setIndices] = useState([]); + const [indices, setIndices] = useState([]); const [logFieldsByIndex, setLogFieldsByIndex] = useState<{ - [index: string]: CorrelationOptions[]; + [index: string]: CorrelationOption[]; }>({}); const params = useParams<{ ruleId: string }>(); const [initialValues, setInitialValues] = useState({ @@ -229,26 +231,14 @@ export const CreateCorrelationRule: React.FC = ( }; const context = useContext(CoreServicesContext); - const parseOptions = (indices: string[]) => { - return indices.map( - (index: string): CorrelationOptions => ({ - label: index, - value: index, - }) - ); - }; - const getIndices = useCallback(async () => { try { - const indicesResponse = await props.indexService.getIndices(); - if (indicesResponse.ok) { - const indicesNames = parseOptions( - indicesResponse.response.indices.map((index) => index.index) - ); - setIndices(indicesNames); + const dataSourcesRes = await getDataSources(props.indexService, props.notifications); + if (dataSourcesRes.ok) { + setIndices(dataSourcesRes.dataSources); } } catch (error: any) {} - }, [props.indexService.getIndices]); + }, [props.indexService, props.notifications]); useEffect(() => { getIndices(); @@ -423,6 +413,9 @@ export const CreateCorrelationRule: React.FC = ( ); updateLogFieldsForIndex(e[0]?.value || ''); }} + renderOption={(option: CorrelationOption) => { + return option.index ? `${option.label} (${option.index})` : option.label; + }} onBlur={props.handleBlur(`queries[${queryIdx}].index`)} selectedOptions={ query.index ? [{ value: query.index, label: query.index }] : [] diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRules.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRules.tsx index 2305359c3..ed5e6de5b 100644 --- a/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRules.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/DetectionRules.tsx @@ -105,6 +105,11 @@ export const DetectionRules: React.FC = ({ } + extraAction={ + + Manage + + } id={'detectorRulesAccordion'} initialIsOpen={false} isLoading={loading} diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx index 80f96cfa6..eaa453d56 100644 --- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx @@ -18,7 +18,7 @@ import { IndexOption } from '../../../../../Detectors/models/interfaces'; import { MIN_NUM_DATA_SOURCES } from '../../../../../Detectors/utils/constants'; import IndexService from '../../../../../../services/IndexService'; import { NotificationsStart } from 'opensearch-dashboards/public'; -import { errorNotificationToast } from '../../../../../../utils/helpers'; +import { getDataSources } from '../../../../../../utils/helpers'; import _ from 'lodash'; import { FieldMappingService } from '../../../../../../services'; @@ -57,41 +57,28 @@ export default class DetectorDataSource extends Component< } componentDidMount = async () => { - this.getIndices(); + this.getDataSources(); }; - getIndices = async () => { + getDataSources = async () => { this.setState({ loading: true }); - try { - const indicesResponse = await this.props.indexService.getIndices(); - if (indicesResponse.ok) { - const indices = indicesResponse.response.indices; - const indicesNames = indices.map((index) => index.index); - - this.setState({ - loading: false, - indexOptions: this.parseOptions(indicesNames), - }); - } else { - errorNotificationToast( - this.props.notifications, - 'retrieve', - 'indices', - indicesResponse.error - ); - this.setState({ errorMessage: indicesResponse.error }); - } - } catch (error: any) { - errorNotificationToast(this.props.notifications, 'retrieve', 'indices', error); + const res = await getDataSources(this.props.indexService, this.props.notifications); + + if (res.ok) { + this.setState({ + loading: false, + indexOptions: res.dataSources, + }); + } else { + this.setState({ loading: false, errorMessage: res.error }); } - this.setState({ loading: false }); }; - parseOptions = (indices: string[]) => { - return indices.map((index) => ({ label: index })); + parseOptions = (options: string[]) => { + return options.map((option) => ({ label: option })); }; - onCreateOption = (searchValue: string, options: EuiComboBoxOptionOption[]) => { + onCreateOption = (searchValue: string) => { const parsedOptions = this.parseOptions(this.props.detectorIndices); parsedOptions.push({ label: searchValue }); this.onSelectionChange(parsedOptions); @@ -174,6 +161,9 @@ export default class DetectorDataSource extends Component< isInvalid={!!errorMessage} isClearable={true} data-test-subj={'define-detector-select-data-source'} + renderOption={(option: IndexOption) => { + return option.index ? `${option.label} (${option.index})` : option.label; + }} /> {differentLogTypesDetected ? ( diff --git a/public/pages/Detectors/models/interfaces.ts b/public/pages/Detectors/models/interfaces.ts index 190e3833c..8acbb97d5 100644 --- a/public/pages/Detectors/models/interfaces.ts +++ b/public/pages/Detectors/models/interfaces.ts @@ -5,4 +5,5 @@ export interface IndexOption { label: string; + index?: string; } diff --git a/public/services/IndexService.ts b/public/services/IndexService.ts index 8253a0cc6..123d4ab4c 100644 --- a/public/services/IndexService.ts +++ b/public/services/IndexService.ts @@ -5,7 +5,7 @@ import { HttpSetup } from 'opensearch-dashboards/public'; import { ServerResponse } from '../../server/models/types'; -import { GetIndicesResponse } from '../../server/models/interfaces'; +import { GetAliasesResponse, GetIndicesResponse } from '../../server/models/interfaces'; import { API } from '../../server/utils/constants'; import { IIndexService } from '../../types'; @@ -32,6 +32,13 @@ export default class IndexService implements IIndexService { return response; }; + getAliases = async (): Promise> => { + const url = `..${API.ALIASES_BASE}`; + const response = (await this.httpClient.get(url)) as ServerResponse; + + return response; + }; + updateAliases = async (actions: any): Promise> => { const url = `..${API.UPDATE_ALIASES}`; const response = (await this.httpClient.post(url, { diff --git a/public/utils/helpers.tsx b/public/utils/helpers.tsx index 5baa727cc..28c9e7979 100644 --- a/public/utils/helpers.tsx +++ b/public/utils/helpers.tsx @@ -33,7 +33,7 @@ import { parse, View } from 'vega/build-es5/vega.js'; import { expressionInterpreter as vegaExpressionInterpreter } from 'vega-interpreter/build/vega-interpreter'; import { RuleInfo } from '../../server/models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; -import { OpenSearchService } from '../services'; +import { IndexService, OpenSearchService } from '../services'; import { ruleSeverity, ruleTypes } from '../pages/Rules/utils/constants'; import { Handler } from 'vega-tooltip'; import _ from 'lodash'; @@ -432,3 +432,67 @@ export function addDetectionType( export function isThreatIntelQuery(queryId: string) { return queryId?.startsWith('threat_intel_'); } + +export async function getDataSources( + indexService: IndexService, + notifications: any +): Promise< + | { + ok: true; + dataSources: { label: string; options: { label: string; value: string; index?: string }[] }[]; + } + | { ok: false; error: string } +> { + const dataSourceOptions = []; + try { + const aliasesResponse = await indexService.getAliases(); + const indicesResponse = await indexService.getIndices(); + + if (aliasesResponse.ok) { + const aliases = aliasesResponse.response.aliases.filter( + ({ index }) => !index.startsWith('.') + ); + const aliasOptions = aliases.map(({ alias, index }) => ({ + label: alias, + index: index, + value: alias, + })); + + dataSourceOptions.push({ + label: 'Aliases', + options: aliasOptions, + }); + } else { + errorNotificationToast(notifications, 'retrieve', 'aliases', aliasesResponse.error); + return { ok: false, error: aliasesResponse.error }; + } + + if (indicesResponse.ok) { + const indices = indicesResponse.response.indices; + const indexOptions = indices + .map(({ index }) => ({ label: index, value: index })) + .filter(({ label }) => !label.startsWith('.')); + + dataSourceOptions.push({ + label: 'Indices', + options: indexOptions, + }); + } else { + errorNotificationToast(notifications, 'retrieve', 'indices', indicesResponse.error); + + return { ok: false, error: indicesResponse.error }; + } + + return { + ok: true, + dataSources: dataSourceOptions, + }; + } catch (error: any) { + errorNotificationToast(notifications, 'retrieve', 'indices', error); + + return { + ok: false, + error, + }; + } +} diff --git a/server/models/interfaces/index.ts b/server/models/interfaces/index.ts index 412cf0ce0..d9655a01b 100644 --- a/server/models/interfaces/index.ts +++ b/server/models/interfaces/index.ts @@ -22,6 +22,7 @@ export interface SecurityAnalyticsApi { readonly CORRELATION_BASE: string; readonly SEARCH_DETECTORS: string; readonly INDICES_BASE: string; + readonly ALIASES_BASE: string; readonly FINDINGS_BASE: string; readonly GET_FINDINGS: string; readonly DOCUMENT_IDS_QUERY: string; @@ -57,6 +58,10 @@ export interface GetIndicesResponse { indices: CatIndex[]; } +export interface GetAliasesResponse { + aliases: CatAlias[]; +} + // Default _cat index response export interface CatIndex { 'docs.count': string; @@ -72,6 +77,11 @@ export interface CatIndex { data_stream: string | null; } +export interface CatAlias { + alias: string; + index: string; +} + export interface SearchResponse { hits: { total: { value: number }; diff --git a/server/routes/IndexRoutes.ts b/server/routes/IndexRoutes.ts index 26dfd4423..95de0226c 100644 --- a/server/routes/IndexRoutes.ts +++ b/server/routes/IndexRoutes.ts @@ -19,6 +19,14 @@ export function setupIndexRoutes(services: NodeServices, router: IRouter) { indexService.getIndices ); + router.get( + { + path: API.ALIASES_BASE, + validate: {}, + }, + indexService.getAliases + ); + router.post( { path: `${API.INDICES_BASE}`, diff --git a/server/services/IndexService.ts b/server/services/IndexService.ts index 1ff3882ae..fc1bbfff6 100644 --- a/server/services/IndexService.ts +++ b/server/services/IndexService.ts @@ -11,7 +11,7 @@ import { RequestHandlerContext, ILegacyCustomClusterClient, } from 'opensearch-dashboards/server'; -import { GetIndicesResponse } from '../models/interfaces'; +import { GetAliasesResponse, GetIndicesResponse } from '../models/interfaces'; import { ServerResponse } from '../models/types'; export default class IndexService { @@ -52,9 +52,6 @@ export default class IndexService { } }; - /** - * Calls backend POST Detectors API. - */ getIndices = async ( _context: RequestHandlerContext, request: OpenSearchDashboardsRequest, @@ -88,11 +85,44 @@ export default class IndexService { } }; + getAliases = async ( + _context: RequestHandlerContext, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory + ): Promise | ResponseError>> => { + try { + const { callAsCurrentUser } = this.osDriver.asScoped(request); + const aliases = await callAsCurrentUser('cat.aliases', { + format: 'json', + h: 'alias,index', + }); + + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: { + aliases, + }, + }, + }); + } catch (err: any) { + console.error('Security Analytcis - IndexService - getAliases:', err); + return response.custom({ + statusCode: 200, + body: { + ok: false, + error: err.message, + }, + }); + } + }; + updateAliases = async ( _context: RequestHandlerContext, request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory - ): Promise | ResponseError>> => { + ): Promise | ResponseError>> => { try { const actions = request.body; const params = { body: actions }; @@ -101,9 +131,6 @@ export default class IndexService { return response.custom({ statusCode: 200, - body: { - ok: true, - }, }); } catch (error: any) { console.error('Security Analytics - IndexService - createAliases:', error); diff --git a/server/utils/constants.ts b/server/utils/constants.ts index 166eb79ae..8522d3e95 100644 --- a/server/utils/constants.ts +++ b/server/utils/constants.ts @@ -19,6 +19,7 @@ export const API: SecurityAnalyticsApi = { CORRELATION_BASE: `${BASE_API_PATH}/correlation/rules`, SEARCH_DETECTORS: `${BASE_API_PATH}/detectors/_search`, INDICES_BASE: `${BASE_API_PATH}/indices`, + ALIASES_BASE: `${BASE_API_PATH}/aliases`, FINDINGS_BASE: `${BASE_API_PATH}/findings`, GET_FINDINGS: `${BASE_API_PATH}/findings/_search`, DOCUMENT_IDS_QUERY: `${BASE_API_PATH}/document_ids_query`,