mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
ce43003961
commit
68ac9bdc03
29 changed files with 937 additions and 219 deletions
|
@ -782,6 +782,7 @@ describe('Create Lifecycle', () => {
|
|||
"defaultScheduleInterval": undefined,
|
||||
"doesSetRecoveryContext": false,
|
||||
"enabledInLicense": false,
|
||||
"fieldsForAAD": undefined,
|
||||
"hasAlertsMappings": true,
|
||||
"hasFieldsForAAD": false,
|
||||
"id": "test",
|
||||
|
|
|
@ -419,6 +419,7 @@ export class RuleTypeRegistry {
|
|||
name,
|
||||
minimumLicenseRequired
|
||||
).isValid,
|
||||
fieldsForAAD,
|
||||
hasFieldsForAAD: Boolean(fieldsForAAD),
|
||||
hasAlertsMappings: !!alerts,
|
||||
validLegacyConsumers,
|
||||
|
|
|
@ -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.*'],
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
ruleDataService: ruleDataServiceMock.create(),
|
||||
getRuleType: jest.fn(),
|
||||
getRuleList: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
};
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ describe('bulkUpdateCases', () => {
|
|||
auditLogger,
|
||||
ruleDataService: ruleDataServiceMock.create(),
|
||||
getRuleType: jest.fn(),
|
||||
getRuleList: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
};
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
ruleDataService: ruleDataServiceMock.create(),
|
||||
getRuleType: jest.fn(),
|
||||
getRuleList: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
};
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
ruleDataService: ruleDataServiceMock.create(),
|
||||
getRuleType: jest.fn(),
|
||||
getRuleList: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
};
|
||||
|
||||
|
|
|
@ -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"`);
|
||||
});
|
||||
});
|
|
@ -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(),
|
||||
};
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
ruleDataService: ruleDataServiceMock.create(),
|
||||
getRuleType: jest.fn(),
|
||||
getRuleList: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
};
|
||||
|
||||
|
|
|
@ -166,6 +166,7 @@ export class RuleRegistryPlugin
|
|||
securityPluginSetup: security,
|
||||
ruleDataService,
|
||||
getRuleType: plugins.alerting.getType,
|
||||
getRuleList: plugins.alerting.listTypes,
|
||||
getAlertIndicesAlias: plugins.alerting.getAlertIndicesAlias,
|
||||
});
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ export const getBrowserFieldsByFeatureId = (router: IRouter<RacRequestHandlerCon
|
|||
|
||||
const fields = await alertsClient.getBrowserFields({
|
||||
indices: o11yIndices,
|
||||
featureIds: onlyO11yFeatureIds,
|
||||
metaFields: ['_id', '_index'],
|
||||
allowNoIndex: true,
|
||||
});
|
||||
|
|
|
@ -65,6 +65,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
ruleDataService: ruleDataServiceMock.create(),
|
||||
esClient: esClientMock,
|
||||
getRuleType: jest.fn(),
|
||||
getRuleList: jest.fn(),
|
||||
getAlertIndicesAlias: getAlertIndicesAliasMock,
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 ? (
|
||||
<>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -305,6 +305,7 @@ const RuleAdd = ({
|
|||
hideGrouping={hideGrouping}
|
||||
hideInterval={hideInterval}
|
||||
onChangeMetaData={onChangeMetaData}
|
||||
selectedConsumer={selectedConsumer}
|
||||
setConsumer={setSelectedConsumer}
|
||||
useRuleProducer={useRuleProducer}
|
||||
/>
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue