[RAM] Fix alert search bar for security solution (#171049)

## Summary

Bring back functionality for alert search bar for security solution.

<img width="899" alt="image"
src="13100bd3-4ba9-4cba-9702-d657ee781a4a">

<img width="911" alt="image"
src="0c586d2c-67be-4b37-8fe5-cd483e6def16">



### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Xavier Mouligneau 2023-11-23 15:54:20 -05:00 committed by GitHub
parent ce43003961
commit 68ac9bdc03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 937 additions and 219 deletions

View file

@ -782,6 +782,7 @@ describe('Create Lifecycle', () => {
"defaultScheduleInterval": undefined,
"doesSetRecoveryContext": false,
"enabledInLicense": false,
"fieldsForAAD": undefined,
"hasAlertsMappings": true,
"hasFieldsForAAD": false,
"id": "test",

View file

@ -419,6 +419,7 @@ export class RuleTypeRegistry {
name,
minimumLicenseRequired
).isValid,
fieldsForAAD,
hasFieldsForAAD: Boolean(fieldsForAAD),
hasAlertsMappings: !!alerts,
validLegacyConsumers,

View file

@ -22,6 +22,7 @@ import {
ALERT_STATUS_ACTIVE,
ALERT_CASE_IDS,
MAX_CASES_PER_ALERT,
AlertConsumers,
} from '@kbn/rule-data-utils';
import {
@ -80,6 +81,7 @@ export interface ConstructorOptions {
esClient: ElasticsearchClient;
ruleDataService: IRuleDataService;
getRuleType: RuleTypeRegistry['get'];
getRuleList: RuleTypeRegistry['list'];
getAlertIndicesAlias: AlertingStart['getAlertIndicesAlias'];
}
@ -153,6 +155,7 @@ export class AlertsClient {
private readonly spaceId: string | undefined;
private readonly ruleDataService: IRuleDataService;
private readonly getRuleType: RuleTypeRegistry['get'];
private readonly getRuleList: RuleTypeRegistry['list'];
private getAlertIndicesAlias!: AlertingStart['getAlertIndicesAlias'];
constructor(options: ConstructorOptions) {
@ -165,6 +168,7 @@ export class AlertsClient {
this.spaceId = this.authorization.getSpaceId();
this.ruleDataService = options.ruleDataService;
this.getRuleType = options.getRuleType;
this.getRuleList = options.getRuleList;
this.getAlertIndicesAlias = options.getAlertIndicesAlias;
}
@ -1076,19 +1080,31 @@ export class AlertsClient {
}
public async getBrowserFields({
featureIds,
indices,
metaFields,
allowNoIndex,
}: {
featureIds: string[];
indices: string[];
metaFields: string[];
allowNoIndex: boolean;
}): Promise<{ browserFields: BrowserFields; fields: FieldDescriptor[] }> {
const indexPatternsFetcherAsInternalUser = new IndexPatternsFetcher(this.esClient);
const ruleTypeList = this.getRuleList();
const fieldsForAAD = new Set<string>();
for (const rule of ruleTypeList) {
if (featureIds.includes(rule.producer) && rule.hasFieldsForAAD) {
(rule.fieldsForAAD ?? []).forEach((f) => {
fieldsForAAD.add(f);
});
}
}
const { fields } = await indexPatternsFetcherAsInternalUser.getFieldsForWildcard({
pattern: indices,
metaFields,
fieldCapsOptions: { allow_no_indices: allowNoIndex },
fields: [...fieldsForAAD, 'kibana.*'],
});
return {
@ -1099,11 +1115,13 @@ export class AlertsClient {
public async getAADFields({ ruleTypeId }: { ruleTypeId: string }) {
const { producer, fieldsForAAD = [] } = this.getRuleType(ruleTypeId);
if (producer === AlertConsumers.SIEM) {
throw Boom.badRequest(`Security solution rule type is not supported`);
}
const indices = await this.getAuthorizedAlertsIndices([producer]);
const o11yIndices = indices?.filter((index) => index.startsWith('.alerts-observability')) ?? [];
const indexPatternsFetcherAsInternalUser = new IndexPatternsFetcher(this.esClient);
const { fields } = await indexPatternsFetcherAsInternalUser.getFieldsForWildcard({
pattern: o11yIndices,
const { fields = [] } = await indexPatternsFetcherAsInternalUser.getFieldsForWildcard({
pattern: indices ?? [],
metaFields: ['_id', '_index'],
fieldCapsOptions: { allow_no_indices: true },
fields: [...fieldsForAAD, 'kibana.*'],

View file

@ -26,6 +26,7 @@ const alertsClientFactoryParams: AlertsClientFactoryProps = {
esClient: {} as ElasticsearchClient,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
getRuleList: jest.fn(),
getAlertIndicesAlias: jest.fn(),
};
@ -53,6 +54,7 @@ describe('AlertsClientFactory', () => {
auditLogger,
esClient: {},
ruleDataService: alertsClientFactoryParams.ruleDataService,
getRuleList: alertsClientFactoryParams.getRuleList,
getRuleType: alertsClientFactoryParams.getRuleType,
getAlertIndicesAlias: alertsClientFactoryParams.getAlertIndicesAlias,
});

View file

@ -23,6 +23,7 @@ export interface AlertsClientFactoryProps {
securityPluginSetup: SecurityPluginSetup | undefined;
ruleDataService: IRuleDataService | null;
getRuleType: RuleTypeRegistry['get'];
getRuleList: RuleTypeRegistry['list'];
getAlertIndicesAlias: AlertingStart['getAlertIndicesAlias'];
}
@ -36,6 +37,7 @@ export class AlertsClientFactory {
private securityPluginSetup!: SecurityPluginSetup | undefined;
private ruleDataService!: IRuleDataService | null;
private getRuleType!: RuleTypeRegistry['get'];
private getRuleList!: RuleTypeRegistry['list'];
private getAlertIndicesAlias!: AlertingStart['getAlertIndicesAlias'];
public initialize(options: AlertsClientFactoryProps) {
@ -53,6 +55,7 @@ export class AlertsClientFactory {
this.securityPluginSetup = options.securityPluginSetup;
this.ruleDataService = options.ruleDataService;
this.getRuleType = options.getRuleType;
this.getRuleList = options.getRuleList;
this.getAlertIndicesAlias = options.getAlertIndicesAlias;
}
@ -66,6 +69,7 @@ export class AlertsClientFactory {
esClient: this.esClient,
ruleDataService: this.ruleDataService!,
getRuleType: this.getRuleType,
getRuleList: this.getRuleList,
getAlertIndicesAlias: this.getAlertIndicesAlias,
});
}

View file

@ -31,6 +31,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
getRuleList: jest.fn(),
getAlertIndicesAlias: jest.fn(),
};

View file

@ -37,6 +37,7 @@ describe('bulkUpdateCases', () => {
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
getRuleList: jest.fn(),
getAlertIndicesAlias: jest.fn(),
};

View file

@ -30,6 +30,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
getRuleList: jest.fn(),
getAlertIndicesAlias: jest.fn(),
};

View file

@ -31,6 +31,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
getRuleList: jest.fn(),
getAlertIndicesAlias: jest.fn(),
};

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { AlertConsumers } from '@kbn/rule-data-utils';
import { AlertsClient, ConstructorOptions } from '../alerts_client';
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { alertingAuthorizationMock } from '@kbn/alerting-plugin/server/authorization/alerting_authorization.mock';
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock';
const alertingAuthMock = alertingAuthorizationMock.create();
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
const auditLogger = auditLoggerMock.create();
const getRuleTypeMock = jest.fn();
const alertsClientParams: jest.Mocked<ConstructorOptions> = {
logger: loggingSystemMock.create().get(),
authorization: alertingAuthMock,
esClient: esClientMock,
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: getRuleTypeMock,
getRuleList: jest.fn(),
getAlertIndicesAlias: jest.fn(),
};
const DEFAULT_SPACE = 'test_default_space_id';
beforeEach(() => {
jest.resetAllMocks();
alertingAuthMock.getSpaceId.mockImplementation(() => DEFAULT_SPACE);
});
describe('getAADFields()', () => {
test('should throw an error when a rule type belong to security solution', async () => {
getRuleTypeMock.mockImplementation(() => ({
producer: AlertConsumers.SIEM,
fieldsForAAD: [],
}));
const alertsClient = new AlertsClient(alertsClientParams);
await expect(
alertsClient.getAADFields({ ruleTypeId: 'security-type' })
).rejects.toThrowErrorMatchingInlineSnapshot(`"Security solution rule type is not supported"`);
});
});

View file

@ -32,6 +32,7 @@ describe('remove cases from alerts', () => {
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
getRuleList: jest.fn(),
getAlertIndicesAlias: jest.fn(),
};
@ -90,6 +91,7 @@ describe('remove cases from alerts', () => {
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
getRuleList: jest.fn(),
getAlertIndicesAlias: jest.fn(),
};

View file

@ -30,6 +30,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
getRuleList: jest.fn(),
getAlertIndicesAlias: jest.fn(),
};

View file

@ -166,6 +166,7 @@ export class RuleRegistryPlugin
securityPluginSetup: security,
ruleDataService,
getRuleType: plugins.alerting.getType,
getRuleList: plugins.alerting.listTypes,
getAlertIndicesAlias: plugins.alerting.getAlertIndicesAlias,
});

View file

@ -53,6 +53,7 @@ export const getBrowserFieldsByFeatureId = (router: IRouter<RacRequestHandlerCon
const fields = await alertsClient.getBrowserFields({
indices: o11yIndices,
featureIds: onlyO11yFeatureIds,
metaFields: ['_id', '_index'],
allowNoIndex: true,
});

View file

@ -65,6 +65,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
ruleDataService: ruleDataServiceMock.create(),
esClient: esClientMock,
getRuleType: jest.fn(),
getRuleList: jest.fn(),
getAlertIndicesAlias: getAlertIndicesAliasMock,
};

View file

@ -1,111 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { AlertConsumers } from '@kbn/rule-data-utils';
import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock';
import type { ValidFeatureId } from '@kbn/rule-data-utils';
import { act, renderHook } from '@testing-library/react-hooks';
import { useAlertDataView, UserAlertDataView } from './use_alert_data_view';
const mockUseKibanaReturnValue = createStartServicesMock();
jest.mock('@kbn/kibana-react-plugin/public', () => ({
__esModule: true,
useKibana: jest.fn(() => ({
services: mockUseKibanaReturnValue,
})),
}));
describe('useAlertDataView', () => {
const observabilityAlertFeatureIds: ValidFeatureId[] = [
AlertConsumers.APM,
AlertConsumers.INFRASTRUCTURE,
AlertConsumers.LOGS,
AlertConsumers.UPTIME,
];
beforeEach(() => {
mockUseKibanaReturnValue.http.get = jest.fn().mockReturnValue({
index_name: [
'.alerts-observability.uptime.alerts-*',
'.alerts-observability.metrics.alerts-*',
'.alerts-observability.logs.alerts-*',
'.alerts-observability.apm.alerts-*',
],
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('initially is loading and does not have data', async () => {
await act(async () => {
const mockedAsyncDataView = {
loading: true,
error: undefined,
};
const { result, waitForNextUpdate } = renderHook<ValidFeatureId[], UserAlertDataView>(() =>
useAlertDataView(observabilityAlertFeatureIds)
);
await waitForNextUpdate();
expect(result.current).toEqual(mockedAsyncDataView);
});
});
it('returns dataView for the provided featureIds', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<ValidFeatureId[], UserAlertDataView>(() =>
useAlertDataView(observabilityAlertFeatureIds)
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toMatchInlineSnapshot(`
Object {
"error": undefined,
"loading": false,
"value": Array [
Object {
"fieldFormatMap": Object {},
"fields": Array [],
"title": ".alerts-observability.uptime.alerts-*,.alerts-observability.metrics.alerts-*,.alerts-observability.logs.alerts-*,.alerts-observability.apm.alerts-*",
},
],
}
`);
});
});
it('returns error with no data when error happens', async () => {
const error = new Error('http error');
mockUseKibanaReturnValue.http.get = jest.fn().mockImplementation(async () => {
throw error;
});
await act(async () => {
const { result, waitForNextUpdate } = renderHook<ValidFeatureId[], UserAlertDataView>(() =>
useAlertDataView(observabilityAlertFeatureIds)
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toMatchInlineSnapshot(`
Object {
"error": [Error: http error],
"loading": false,
"value": undefined,
}
`);
});
});
});

View file

@ -0,0 +1,162 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { AlertConsumers } from '@kbn/rule-data-utils';
import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock';
import type { ValidFeatureId } from '@kbn/rule-data-utils';
import { act, renderHook } from '@testing-library/react-hooks';
import { useAlertDataView } from './use_alert_data_view';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
const mockUseKibanaReturnValue = createStartServicesMock();
jest.mock('@kbn/kibana-react-plugin/public', () => ({
__esModule: true,
useKibana: jest.fn(() => ({
services: mockUseKibanaReturnValue,
})),
}));
jest.mock('../lib/rule_api/alert_index', () => ({
fetchAlertIndexNames: jest.fn(),
}));
const { fetchAlertIndexNames } = jest.requireMock('../lib/rule_api/alert_index');
jest.mock('../lib/rule_api/alert_fields', () => ({
fetchAlertFields: jest.fn(),
}));
const { fetchAlertFields } = jest.requireMock('../lib/rule_api/alert_fields');
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 0,
},
},
});
const wrapper = ({ children }: { children: Node }) => (
<QueryClientProvider client={queryClient}> {children} </QueryClientProvider>
);
describe('useAlertDataView', () => {
const observabilityAlertFeatureIds: ValidFeatureId[] = [
AlertConsumers.APM,
AlertConsumers.INFRASTRUCTURE,
AlertConsumers.LOGS,
AlertConsumers.UPTIME,
];
beforeEach(() => {
fetchAlertIndexNames.mockResolvedValue([
'.alerts-observability.uptime.alerts-*',
'.alerts-observability.metrics.alerts-*',
'.alerts-observability.logs.alerts-*',
'.alerts-observability.apm.alerts-*',
]);
fetchAlertFields.mockResolvedValue([{ data: ' fields' }]);
});
afterEach(() => {
queryClient.clear();
jest.clearAllMocks();
});
it('initially is loading and does not have data', async () => {
await act(async () => {
const mockedAsyncDataView = {
loading: true,
dataview: undefined,
};
const { result, waitForNextUpdate } = renderHook(
() => useAlertDataView(observabilityAlertFeatureIds),
{
wrapper,
}
);
await waitForNextUpdate();
expect(result.current).toEqual(mockedAsyncDataView);
});
});
it('fetch index names + fields for the provided o11y featureIds', async () => {
await act(async () => {
const { waitForNextUpdate } = renderHook(
() => useAlertDataView(observabilityAlertFeatureIds),
{
wrapper,
}
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(fetchAlertIndexNames).toHaveBeenCalledTimes(1);
expect(fetchAlertFields).toHaveBeenCalledTimes(1);
});
});
it('only fetch index names for security featureId', async () => {
await act(async () => {
const { waitForNextUpdate } = renderHook(() => useAlertDataView([AlertConsumers.SIEM]), {
wrapper,
});
await waitForNextUpdate();
await waitForNextUpdate();
expect(fetchAlertIndexNames).toHaveBeenCalledTimes(1);
expect(fetchAlertFields).toHaveBeenCalledTimes(0);
});
});
it('Do not fetch anything if security and o11y featureIds are mix together', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(
() => useAlertDataView([AlertConsumers.SIEM, AlertConsumers.LOGS]),
{
wrapper,
}
);
await waitForNextUpdate();
expect(fetchAlertIndexNames).toHaveBeenCalledTimes(0);
expect(fetchAlertFields).toHaveBeenCalledTimes(0);
expect(result.current).toEqual({
loading: false,
dataview: undefined,
});
});
});
it('if fetch throw error return no data', async () => {
fetchAlertIndexNames.mockRejectedValue('error');
await act(async () => {
const { result, waitForNextUpdate } = renderHook(
() => useAlertDataView(observabilityAlertFeatureIds),
{
wrapper,
}
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({
loading: false,
dataview: undefined,
});
});
});
});

View file

@ -5,72 +5,158 @@
* 2.0.
*/
import { DataView, FieldSpec } from '@kbn/data-views-plugin/common';
import { i18n } from '@kbn/i18n';
import { DataView } from '@kbn/data-views-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common';
import type { ValidFeatureId } from '@kbn/rule-data-utils';
import useAsync from 'react-use/lib/useAsync';
import { useMemo } from 'react';
import { AlertConsumers, ValidFeatureId } from '@kbn/rule-data-utils';
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { TriggersAndActionsUiServices } from '../..';
import { fetchAlertIndexNames } from '../lib/rule_api/alert_index';
import { fetchAlertFields } from '../lib/rule_api/alert_fields';
export interface UserAlertDataView {
value?: DataView[];
dataviews?: DataView[];
loading: boolean;
error?: Error;
}
export function useAlertDataView(featureIds: ValidFeatureId[]): UserAlertDataView {
const { http } = useKibana<TriggersAndActionsUiServices>().services;
const {
http,
data: dataService,
notifications: { toasts },
} = useKibana<TriggersAndActionsUiServices>().services;
const [dataviews, setDataviews] = useState<DataView[] | undefined>(undefined);
const features = featureIds.sort().join(',');
const isOnlySecurity = featureIds.length === 1 && featureIds.includes(AlertConsumers.SIEM);
const indexNames = useAsync(async () => {
const { index_name: indexNamesStr } = await http.get<{ index_name: string[] }>(
`${BASE_RAC_ALERTS_API_PATH}/index`,
{
query: { features },
}
);
const hasSecurityAndO11yFeatureIds =
featureIds.length > 1 && featureIds.includes(AlertConsumers.SIEM);
return indexNamesStr;
}, [features]);
const hasNoSecuritySolution =
featureIds.length > 0 && !isOnlySecurity && !hasSecurityAndO11yFeatureIds;
const fields = useAsync(async () => {
const { fields: alertFields } = await http.get<{ fields: FieldSpec[] }>(
`${BASE_RAC_ALERTS_API_PATH}/browser_fields`,
{
query: { featureIds },
}
);
return alertFields;
}, [features]);
const dataview = useMemo(
() =>
!fields.loading &&
!indexNames.loading &&
fields.error === undefined &&
indexNames.error === undefined
? ([
{
title: (indexNames.value ?? []).join(','),
fieldFormatMap: {},
fields: (fields.value ?? [])?.map((field) => {
return {
...field,
...(field.esTypes && field.esTypes.includes('flattened')
? { type: 'string' }
: {}),
};
}),
},
] as unknown as DataView[])
: undefined,
[fields, indexNames]
);
return {
value: dataview,
loading: fields.loading || indexNames.loading,
error: fields.error ? fields.error : indexNames.error,
const queryIndexNameFn = () => {
return fetchAlertIndexNames({ http, features });
};
const queryAlertFieldsFn = () => {
return fetchAlertFields({ http, featureIds });
};
const onErrorFn = () => {
toasts.addDanger(
i18n.translate('xpack.triggersActionsUI.useAlertDataView.useAlertDataMessage', {
defaultMessage: 'Unable to load alert data view',
})
);
};
const {
data: indexNames,
isSuccess: isIndexNameSuccess,
isInitialLoading: isIndexNameInitialLoading,
isLoading: isIndexNameLoading,
} = useQuery({
queryKey: ['loadAlertIndexNames', features],
queryFn: queryIndexNameFn,
onError: onErrorFn,
refetchOnWindowFocus: false,
enabled: featureIds.length > 0 && !hasSecurityAndO11yFeatureIds,
});
const {
data: alertFields,
isSuccess: isAlertFieldsSuccess,
isInitialLoading: isAlertFieldsInitialLoading,
isLoading: isAlertFieldsLoading,
} = useQuery({
queryKey: ['loadAlertFields', features],
queryFn: queryAlertFieldsFn,
onError: onErrorFn,
refetchOnWindowFocus: false,
enabled: hasNoSecuritySolution,
});
useEffect(() => {
return () => {
dataviews?.map((dv) => {
dataService.dataViews.clearInstanceCache(dv.id);
});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataviews]);
// FUTURE ENGINEER this useEffect is for security solution user since
// we are using the user privilege to access the security alert index
useEffect(() => {
async function createDataView() {
const localDataview = await dataService.dataViews.create({
title: (indexNames ?? []).join(','),
allowNoIndex: true,
});
setDataviews([localDataview]);
}
if (isOnlySecurity && isIndexNameSuccess) {
createDataView();
}
}, [dataService.dataViews, indexNames, isIndexNameSuccess, isOnlySecurity]);
// FUTURE ENGINEER this useEffect is for o11y and stack solution user since
// we are using the kibana user privilege to access the alert index
useEffect(() => {
if (
indexNames &&
alertFields &&
!isOnlySecurity &&
isAlertFieldsSuccess &&
isIndexNameSuccess
) {
setDataviews([
{
title: (indexNames ?? []).join(','),
fieldFormatMap: {},
fields: (alertFields ?? [])?.map((field) => {
return {
...field,
...(field.esTypes && field.esTypes.includes('flattened') ? { type: 'string' } : {}),
};
}),
},
] as unknown as DataView[]);
}
}, [
alertFields,
dataService.dataViews,
indexNames,
isIndexNameSuccess,
isOnlySecurity,
isAlertFieldsSuccess,
]);
return useMemo(
() => ({
dataviews,
loading:
featureIds.length === 0 || hasSecurityAndO11yFeatureIds
? false
: isOnlySecurity
? isIndexNameInitialLoading || isIndexNameLoading
: isIndexNameInitialLoading ||
isIndexNameLoading ||
isAlertFieldsInitialLoading ||
isAlertFieldsLoading,
}),
[
dataviews,
featureIds.length,
hasSecurityAndO11yFeatureIds,
isOnlySecurity,
isIndexNameInitialLoading,
isIndexNameLoading,
isAlertFieldsInitialLoading,
isAlertFieldsLoading,
]
);
}

View file

@ -13,6 +13,7 @@ import { RuleType, RuleTypeIndex } from '../../types';
interface UseLoadRuleTypesQueryProps {
filteredRuleTypes: string[];
enabled?: boolean;
}
const getFilteredIndex = (data: Array<RuleType<string, string>>, filteredRuleTypes: string[]) => {
@ -32,7 +33,7 @@ const getFilteredIndex = (data: Array<RuleType<string, string>>, filteredRuleTyp
};
export const useLoadRuleTypesQuery = (props: UseLoadRuleTypesQueryProps) => {
const { filteredRuleTypes } = props;
const { filteredRuleTypes, enabled = true } = props;
const {
http,
notifications: { toasts },
@ -55,6 +56,7 @@ export const useLoadRuleTypesQuery = (props: UseLoadRuleTypesQueryProps) => {
queryFn,
onError: onErrorFn,
refetchOnWindowFocus: false,
enabled,
});
const filteredIndex = data ? getFilteredIndex(data, filteredRuleTypes) : new Map();

View file

@ -8,21 +8,67 @@
import { DataViewField } from '@kbn/data-views-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common';
import useAsync from 'react-use/lib/useAsync';
import type { AsyncState } from 'react-use/lib/useAsync';
import { HttpSetup } from '@kbn/core/public';
import { useQuery } from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import { useMemo } from 'react';
import { TriggersAndActionsUiServices } from '../..';
export function useRuleAADFields(ruleTypeId?: string): AsyncState<DataViewField[]> {
const { http } = useKibana<TriggersAndActionsUiServices>().services;
const EMPTY_AAD_FIELDS: DataViewField[] = [];
const aadFields = useAsync(async () => {
if (!ruleTypeId) return [];
const fields = await http.get<DataViewField[]>(`${BASE_RAC_ALERTS_API_PATH}/aad_fields`, {
query: { ruleTypeId },
});
return fields;
async function fetchAadFields({
http,
ruleTypeId,
}: {
http: HttpSetup;
ruleTypeId?: string;
}): Promise<DataViewField[]> {
if (!ruleTypeId) return EMPTY_AAD_FIELDS;
const fields = await http.get<DataViewField[]>(`${BASE_RAC_ALERTS_API_PATH}/aad_fields`, {
query: { ruleTypeId },
});
return aadFields;
return fields;
}
export function useRuleAADFields(ruleTypeId?: string): {
aadFields: DataViewField[];
loading: boolean;
} {
const {
http,
notifications: { toasts },
} = useKibana<TriggersAndActionsUiServices>().services;
const queryAadFieldsFn = () => {
return fetchAadFields({ http, ruleTypeId });
};
const onErrorFn = () => {
toasts.addDanger(
i18n.translate('xpack.triggersActionsUI.useRuleAADFields.errorMessage', {
defaultMessage: 'Unable to load alert fields per rule type',
})
);
};
const {
data: aadFields = EMPTY_AAD_FIELDS,
isInitialLoading,
isLoading,
} = useQuery({
queryKey: ['loadAlertAadFieldsPerRuleType', ruleTypeId],
queryFn: queryAadFieldsFn,
onError: onErrorFn,
refetchOnWindowFocus: false,
enabled: ruleTypeId !== undefined,
});
return useMemo(
() => ({
aadFields,
loading: ruleTypeId === undefined ? false : isInitialLoading || isLoading,
}),
[aadFields, isInitialLoading, isLoading, ruleTypeId]
);
}

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ValidFeatureId } from '@kbn/rule-data-utils';
import { HttpSetup } from '@kbn/core/public';
import { FieldSpec } from '@kbn/data-views-plugin/common';
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common';
export async function fetchAlertFields({
http,
featureIds,
}: {
http: HttpSetup;
featureIds: ValidFeatureId[];
}): Promise<FieldSpec[]> {
const { fields: alertFields = [] } = await http.get<{ fields: FieldSpec[] }>(
`${BASE_RAC_ALERTS_API_PATH}/browser_fields`,
{
query: { featureIds },
}
);
return alertFields;
}

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common';
import { HttpSetup } from '@kbn/core/public';
export async function fetchAlertIndexNames({
http,
features,
}: {
http: HttpSetup;
features: string;
}): Promise<string[]> {
const { index_name: indexNamesStr = [] } = await http.get<{ index_name: string[] }>(
`${BASE_RAC_ALERTS_API_PATH}/index`,
{
query: { features },
}
);
return indexNamesStr;
}

View file

@ -8,7 +8,7 @@
import React, { Suspense, useEffect, useState, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { ValidFeatureId, AlertConsumers } from '@kbn/rule-data-utils';
import { ValidFeatureId } from '@kbn/rule-data-utils';
import {
EuiFlexGroup,
EuiFlexItem,
@ -428,8 +428,7 @@ export const ActionTypeForm = ({
setActionGroupIdByIndex &&
!actionItem.frequency?.summary;
const showActionAlertsFilter =
hasFieldsForAAD || producerId === AlertConsumers.SIEM || hasAlertsMappings;
const showActionAlertsFilter = hasFieldsForAAD;
const accordionContent = checkEnabledResult.isEnabled ? (
<>

View file

@ -9,12 +9,14 @@ import React, { useCallback, useState } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { Query, TimeRange } from '@kbn/es-query';
import { SuggestionsAbstraction } from '@kbn/unified-search-plugin/public/typeahead/suggestions_component';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { NO_INDEX_PATTERNS } from './constants';
import { SEARCH_BAR_PLACEHOLDER } from './translations';
import { AlertsSearchBarProps, QueryLanguageType } from './types';
import { useAlertDataView } from '../../hooks/use_alert_data_view';
import { TriggersAndActionsUiServices } from '../../..';
import { useRuleAADFields } from '../../hooks/use_rule_aad_fields';
import { useLoadRuleTypesQuery } from '../../hooks/use_load_rule_types_query';
const SA_ALERTS = { type: 'alerts', fields: {} } as SuggestionsAbstraction;
@ -44,15 +46,22 @@ export function AlertsSearchBar({
} = useKibana<TriggersAndActionsUiServices>().services;
const [queryLanguage, setQueryLanguage] = useState<QueryLanguageType>('kuery');
const { value: dataView, loading, error } = useAlertDataView(featureIds);
const {
value: aadFields,
loading: fieldsLoading,
error: fieldsError,
} = useRuleAADFields(ruleTypeId);
const { dataviews, loading } = useAlertDataView(featureIds ?? []);
const { aadFields, loading: fieldsLoading } = useRuleAADFields(ruleTypeId);
const indexPatterns =
ruleTypeId && aadFields?.length ? [{ title: ruleTypeId, fields: aadFields }] : dataView;
ruleTypeId && aadFields?.length ? [{ title: ruleTypeId, fields: aadFields }] : dataviews;
const ruleType = useLoadRuleTypesQuery({
filteredRuleTypes: ruleTypeId !== undefined ? [ruleTypeId] : [],
enabled: ruleTypeId !== undefined,
});
const isSecurity =
(featureIds && featureIds.length === 1 && featureIds.includes(AlertConsumers.SIEM)) ||
(ruleType &&
ruleTypeId &&
ruleType.ruleTypesState.data.get(ruleTypeId)?.producer === AlertConsumers.SIEM);
const onSearchQuerySubmit = useCallback(
({ dateRange, query: nextQuery }: { dateRange: TimeRange; query?: Query }) => {
@ -86,9 +95,7 @@ export function AlertsSearchBar({
appName={appName}
disableQueryLanguageSwitcher={disableQueryLanguageSwitcher}
// @ts-expect-error - DataView fields prop and SearchBar indexPatterns props are overly broad
indexPatterns={
loading || error || fieldsLoading || fieldsError ? NO_INDEX_PATTERNS : indexPatterns
}
indexPatterns={loading || fieldsLoading ? NO_INDEX_PATTERNS : indexPatterns}
placeholder={placeholder}
query={{ query: query ?? '', language: queryLanguage }}
filters={filters}
@ -105,7 +112,7 @@ export function AlertsSearchBar({
showSubmitButton={showSubmitButton}
submitOnBlur={submitOnBlur}
onQueryChange={onSearchQueryChange}
suggestionsAbstraction={SA_ALERTS}
suggestionsAbstraction={isSecurity ? undefined : SA_ALERTS}
/>
);
}

View file

@ -305,6 +305,7 @@ const RuleAdd = ({
hideGrouping={hideGrouping}
hideInterval={hideInterval}
onChangeMetaData={onChangeMetaData}
selectedConsumer={selectedConsumer}
setConsumer={setSelectedConsumer}
useRuleProducer={useRuleProducer}
/>

View file

@ -262,6 +262,7 @@ describe('rule_form', () => {
ruleTypesOverwrite?: RuleType[];
ruleTypeModelOverwrite?: RuleTypeModel;
useRuleProducer?: boolean;
selectedConsumer?: RuleCreationValidConsumer | null;
}) {
const {
showRulesList = false,
@ -273,6 +274,7 @@ describe('rule_form', () => {
ruleTypesOverwrite,
ruleTypeModelOverwrite,
useRuleProducer = false,
selectedConsumer,
} = options || {};
const mocks = coreMock.createSetup();
@ -325,7 +327,11 @@ describe('rule_form', () => {
enabledInLicense: false,
},
];
useLoadRuleTypes.mockReturnValue({ ruleTypes });
const ruleTypeIndex = ruleTypes.reduce((acc, item) => {
acc.set(item.id, item);
return acc;
}, new Map());
useLoadRuleTypes.mockReturnValue({ ruleTypes, ruleTypeIndex });
const [
{
application: { capabilities },
@ -377,7 +383,7 @@ describe('rule_form', () => {
minimumScheduleInterval: { value: '1m', enforce: enforceMinimum },
}}
dispatch={() => {}}
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [] }}
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }}
operation="create"
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
@ -386,6 +392,7 @@ describe('rule_form', () => {
validConsumers={validConsumers}
setConsumer={mockSetConsumer}
useRuleProducer={useRuleProducer}
selectedConsumer={selectedConsumer}
/>
);
@ -666,6 +673,361 @@ describe('rule_form', () => {
expect(wrapper.find('[data-test-subj="ruleFormConsumerSelect"]').exists()).toBeFalsy();
});
it('Do not show alert query in action when multi consumer rule type does not have a consumer selected', async () => {
await setup({
initialRuleOverwrite: {
name: 'Simple rule',
consumer: 'alerts',
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
schedule: {
interval: '1h',
},
},
ruleTypesOverwrite: [
{
id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
name: 'Threshold Rule',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
enabledInLicense: true,
defaultActionGroupId: 'threshold.fired',
minimumLicenseRequired: 'basic',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
producer: ALERTS_FEATURE_ID,
authorizedConsumers: {
infrastructure: { read: true, all: true },
logs: { read: true, all: true },
},
actionVariables: {
context: [],
state: [],
params: [],
},
hasFieldsForAAD: true,
hasAlertsMappings: true,
},
],
ruleTypeModelOverwrite: {
id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
iconClass: 'test',
description: 'test',
documentationUrl: null,
validate: (): ValidationResult => {
return { errors: {} };
},
ruleParamsExpression: TestExpression,
requiresAppContext: false,
},
});
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find(ActionForm).props().hasFieldsForAAD).toEqual(false);
});
it('Do not show alert query in action when we do not have hasFieldsForAAD or hasAlertsMappings or belong to security', async () => {
await setup({
initialRuleOverwrite: {
name: 'Simple rule',
consumer: 'alerts',
ruleTypeId: 'my-rule-type',
schedule: {
interval: '1h',
},
},
ruleTypesOverwrite: [
{
id: 'my-rule-type',
name: 'Threshold Rule',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
enabledInLicense: true,
defaultActionGroupId: 'threshold.fired',
minimumLicenseRequired: 'basic',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
producer: ALERTS_FEATURE_ID,
authorizedConsumers: {
infrastructure: { read: true, all: true },
logs: { read: true, all: true },
},
actionVariables: {
context: [],
state: [],
params: [],
},
hasFieldsForAAD: false,
hasAlertsMappings: false,
},
],
ruleTypeModelOverwrite: {
id: 'my-rule-type',
iconClass: 'test',
description: 'test',
documentationUrl: null,
validate: (): ValidationResult => {
return { errors: {} };
},
ruleParamsExpression: TestExpression,
requiresAppContext: false,
},
});
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find(ActionForm).props().hasFieldsForAAD).toEqual(false);
});
it('Show alert query in action when rule type hasFieldsForAAD', async () => {
await setup({
initialRuleOverwrite: {
name: 'Simple rule',
consumer: 'alerts',
ruleTypeId: 'my-rule-type',
schedule: {
interval: '1h',
},
},
ruleTypesOverwrite: [
{
id: 'my-rule-type',
name: 'Threshold Rule',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
enabledInLicense: true,
defaultActionGroupId: 'threshold.fired',
minimumLicenseRequired: 'basic',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
producer: ALERTS_FEATURE_ID,
authorizedConsumers: {
infrastructure: { read: true, all: true },
logs: { read: true, all: true },
},
actionVariables: {
context: [],
state: [],
params: [],
},
hasFieldsForAAD: true,
hasAlertsMappings: false,
},
],
ruleTypeModelOverwrite: {
id: 'my-rule-type',
iconClass: 'test',
description: 'test',
documentationUrl: null,
validate: (): ValidationResult => {
return { errors: {} };
},
ruleParamsExpression: TestExpression,
requiresAppContext: false,
},
});
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find(ActionForm).props().hasFieldsForAAD).toEqual(true);
});
it('Show alert query in action when rule type hasAlertsMappings', async () => {
await setup({
initialRuleOverwrite: {
name: 'Simple rule',
consumer: 'alerts',
ruleTypeId: 'my-rule-type',
schedule: {
interval: '1h',
},
},
ruleTypesOverwrite: [
{
id: 'my-rule-type',
name: 'Threshold Rule',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
enabledInLicense: true,
defaultActionGroupId: 'threshold.fired',
minimumLicenseRequired: 'basic',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
producer: ALERTS_FEATURE_ID,
authorizedConsumers: {
infrastructure: { read: true, all: true },
logs: { read: true, all: true },
},
actionVariables: {
context: [],
state: [],
params: [],
},
hasFieldsForAAD: false,
hasAlertsMappings: true,
},
],
ruleTypeModelOverwrite: {
id: 'my-rule-type',
iconClass: 'test',
description: 'test',
documentationUrl: null,
validate: (): ValidationResult => {
return { errors: {} };
},
ruleParamsExpression: TestExpression,
requiresAppContext: false,
},
});
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find(ActionForm).props().hasFieldsForAAD).toEqual(true);
});
it('Show alert query in action when rule type is from security solution', async () => {
await setup({
initialRuleOverwrite: {
name: 'Simple rule',
consumer: 'siem',
ruleTypeId: 'my-rule-type',
schedule: {
interval: '1h',
},
},
ruleTypesOverwrite: [
{
id: 'my-rule-type',
name: 'Threshold Rule',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
enabledInLicense: true,
defaultActionGroupId: 'threshold.fired',
minimumLicenseRequired: 'basic',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
producer: 'siem',
authorizedConsumers: {
infrastructure: { read: true, all: true },
logs: { read: true, all: true },
},
actionVariables: {
context: [],
state: [],
params: [],
},
hasFieldsForAAD: false,
hasAlertsMappings: false,
},
],
ruleTypeModelOverwrite: {
id: 'my-rule-type',
iconClass: 'test',
description: 'test',
documentationUrl: null,
validate: (): ValidationResult => {
return { errors: {} };
},
ruleParamsExpression: TestExpression,
requiresAppContext: false,
},
});
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find(ActionForm).props().hasFieldsForAAD).toEqual(true);
});
it('show alert query in action when multi consumer rule type does not have a consumer selected', async () => {
await setup({
initialRuleOverwrite: {
name: 'Simple rule',
consumer: 'alerts',
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
schedule: {
interval: '1h',
},
},
ruleTypesOverwrite: [
{
id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
name: 'Threshold Rule',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
enabledInLicense: true,
defaultActionGroupId: 'threshold.fired',
minimumLicenseRequired: 'basic',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
producer: ALERTS_FEATURE_ID,
authorizedConsumers: {
infrastructure: { read: true, all: true },
logs: { read: true, all: true },
},
actionVariables: {
context: [],
state: [],
params: [],
},
hasFieldsForAAD: true,
hasAlertsMappings: true,
},
],
ruleTypeModelOverwrite: {
id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
iconClass: 'test',
description: 'test',
documentationUrl: null,
validate: (): ValidationResult => {
return { errors: {} };
},
ruleParamsExpression: TestExpression,
requiresAppContext: false,
},
selectedConsumer: 'logs',
});
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find(ActionForm).props().hasFieldsForAAD).toEqual(true);
});
});
describe('rule_form create rule non ruleing consumer and producer', () => {

View file

@ -153,6 +153,7 @@ interface RuleFormProps<MetaData = Record<string, any>> {
hideGrouping?: boolean;
hideInterval?: boolean;
connectorFeatureId?: string;
selectedConsumer?: RuleCreationValidConsumer | null;
validConsumers?: RuleCreationValidConsumer[];
onChangeMetaData: (metadata: MetaData) => void;
useRuleProducer?: boolean;
@ -176,6 +177,7 @@ export const RuleForm = ({
hideGrouping = false,
hideInterval,
connectorFeatureId = AlertingConnectorFeatureId,
selectedConsumer,
validConsumers,
onChangeMetaData,
useRuleProducer,
@ -643,6 +645,23 @@ export const RuleForm = ({
}
};
const hasFieldsForAAD = useMemo(() => {
const hasAlertHasData = selectedRuleType
? selectedRuleType.hasFieldsForAAD ||
selectedRuleType.producer === AlertConsumers.SIEM ||
selectedRuleType.hasAlertsMappings
: false;
if (MULTI_CONSUMER_RULE_TYPE_IDS.includes(rule?.ruleTypeId ?? '')) {
return (
(validConsumers || VALID_CONSUMERS).includes(
selectedConsumer as RuleCreationValidConsumer
) && hasAlertHasData
);
}
return hasAlertHasData;
}, [rule?.ruleTypeId, selectedConsumer, selectedRuleType, validConsumers]);
const ruleTypeDetails = (
<>
<EuiHorizontalRule />
@ -820,8 +839,12 @@ export const RuleForm = ({
defaultActionGroupId={defaultActionGroupId}
hasAlertsMappings={selectedRuleType.hasAlertsMappings}
featureId={connectorFeatureId}
producerId={selectedRuleType.producer}
hasFieldsForAAD={selectedRuleType.hasFieldsForAAD}
producerId={
MULTI_CONSUMER_RULE_TYPE_IDS.includes(rule.ruleTypeId)
? selectedConsumer ?? rule.consumer
: selectedRuleType.producer
}
hasFieldsForAAD={hasFieldsForAAD}
ruleTypeId={rule.ruleTypeId}
isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) =>
isActionGroupDisabledForActionType(selectedRuleType, actionGroupId, actionTypeId)

View file

@ -6,6 +6,7 @@
*/
import { EuiLoadingSpinner } from '@elastic/eui';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React, { lazy, Suspense } from 'react';
import type { AlertsSearchBarProps } from '../application/sections/alerts_search_bar';
@ -13,8 +14,12 @@ const AlertsSearchBarLazy: React.FC<AlertsSearchBarProps> = lazy(
() => import('../application/sections/alerts_search_bar/alerts_search_bar')
);
const queryClient = new QueryClient();
export const getAlertsSearchBarLazy = (props: AlertsSearchBarProps) => (
<Suspense fallback={<EuiLoadingSpinner />}>
<AlertsSearchBarLazy {...props} />
<QueryClientProvider client={queryClient}>
<AlertsSearchBarLazy {...props} />
</QueryClientProvider>
</Suspense>
);

View file

@ -45,9 +45,14 @@ export default ({ getService }: FtrProviderContext) => {
'logs',
'uptime',
]);
expect(Object.keys(resp.browserFields)).toEqual(
expect.arrayContaining(['base', 'event', 'kibana'])
);
expect(Object.keys(resp.browserFields)).toEqual([
'base',
'cloud',
'container',
'host',
'kibana',
'orchestrator',
]);
});
it(`${superUser.username} should be able to get browser fields for o11y featureIds`, async () => {
@ -57,21 +62,14 @@ export default ({ getService }: FtrProviderContext) => {
'logs',
'uptime',
]);
expect(Object.keys(resp.browserFields)).toEqual(
expect.arrayContaining([
'base',
'agent',
'anomaly',
'ecs',
'error',
'event',
'kibana',
'monitor',
'observer',
'tls',
'url',
])
);
expect(Object.keys(resp.browserFields)).toEqual([
'base',
'cloud',
'container',
'host',
'kibana',
'orchestrator',
]);
});
it(`${superUser.username} should NOT be able to get browser fields for siem featureId`, async () => {