From 103869509c9e2253499c662a22001e38b06e0088 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Tue, 12 Oct 2021 12:12:34 -0400 Subject: [PATCH] [Security Solution] [Platform] Utilize SO resolve api for reading rules by `id` (#112478) * added outcome to backend routes * adds so resolved property alias_target_id to response * adds UI portion * working URL redirect on aliasMatch - todo -> update rule details page refresh button to use SO resolve. * cleanup * fix integration tests * fix jest tests * cleanup types * fix eslint.. I think vs code formatted this * WIP - undo me, working index.test.ts function * WIP - also undo me, probably * working test for aliasMatch, need to add test for outcome = conflict * add conflict callout when SO resolve yields conflict outcome * code cleanup * fix type issues * small cleanup, fix jest test after undoing changes for getFailingRuleStatus * cleanup tests * add alias_target_id to response validation too * unit test changes * update tests again * add all dependencies to useEffect and prefer useMemo * add type cast * adds integration tests for different outcomes after mocking a migrated rule leading to an aliasMatch and a migrated rule + accidental inserted rule to lead to a conflict. Also removes the outcome property if it is an exactMatch * remove unused import * fix test * functional WIP * cleanup * cleanup * finishing touches to address PR review comments * remove console.error * fix bug where spaces was not typed correctly in the plugin start method here https://github.com/elastic/kibana/pull/113983 --- .../schemas/common/schemas.ts | 12 + .../schemas/request/rule_schemas.ts | 4 + .../schemas/response/rules_schema.ts | 4 + .../detection_engine/rules/types.ts | 2 + .../rules/details/failure_history.test.tsx | 68 ++- .../rules/details/failure_history.tsx | 12 +- .../rules/details/index.test.tsx | 200 +++++++-- .../detection_engine/rules/details/index.tsx | 50 +++ .../use_hosts_risk_score.ts | 2 +- .../plugins/security_solution/public/types.ts | 2 +- .../routes/__mocks__/request_responses.ts | 20 +- .../routes/rules/delete_rules_route.test.ts | 6 +- .../routes/rules/read_rules_route.test.ts | 41 ++ .../detection_engine/rules/read_rules.test.ts | 18 +- .../lib/detection_engine/rules/read_rules.ts | 12 +- .../lib/detection_engine/rules/types.ts | 4 +- .../rules/update_rules.test.ts | 14 +- .../schemas/rule_converters.ts | 9 +- .../basic/tests/index.ts | 2 +- .../security_and_spaces/tests/index.ts | 1 + .../tests/resolve_read_rules.ts | 160 +++++++ .../detection_engine_api_integration/utils.ts | 33 +- .../resolve_read_rules/7_14/data.json | 101 +++++ .../resolve_read_rules/7_14/mappings.json | 397 ++++++++++++++++++ 24 files changed, 1101 insertions(+), 73 deletions(-) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts create mode 100644 x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/data.json create mode 100644 x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/mappings.json diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index a9f7d96f1eb2e..3933d7e39275e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -35,6 +35,18 @@ export type Description = t.TypeOf; export const descriptionOrUndefined = t.union([description, t.undefined]); export type DescriptionOrUndefined = t.TypeOf; +// outcome is a property of the saved object resolve api +// will tell us info about the rule after 8.0 migrations +export const outcome = t.union([ + t.literal('exactMatch'), + t.literal('aliasMatch'), + t.literal('conflict'), +]); +export type Outcome = t.TypeOf; + +export const alias_target_id = t.string; +export type AliasTargetId = t.TypeOf; + export const enabled = t.boolean; export type Enabled = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 719337a231c1c..12e72fb6fc697 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -58,6 +58,8 @@ import { tags, interval, enabled, + outcome, + alias_target_id, updated_at, updated_by, created_at, @@ -150,6 +152,8 @@ const baseParams = { building_block_type, note, license, + outcome, + alias_target_id, output_index, timeline_id, timeline_title, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index 247829d5b9e7a..ac9329c3870f1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -70,6 +70,8 @@ import { last_failure_message, filters, meta, + outcome, + alias_target_id, note, building_block_type, license, @@ -174,6 +176,8 @@ export const partialRulesSchema = t.partial({ last_failure_message, filters, meta, + outcome, + alias_target_id, index, namespace, note, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 9faed2d0646e0..ecf68fa207b70 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -108,6 +108,8 @@ export const RuleSchema = t.intersection([ throttle: t.union([t.string, t.null]), }), t.partial({ + outcome: t.union([t.literal('exactMatch'), t.literal('aliasMatch'), t.literal('conflict')]), + alias_target_id: t.string, building_block_type, anomaly_threshold: t.number, filters: t.array(t.unknown), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx index d95b6ca9f3435..c91aade50cbae 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx @@ -6,19 +6,79 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; - -import { TestProviders } from '../../../../../common/mock'; +import { shallow, mount } from 'enzyme'; +import { + TestProviders, + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, +} from '../../../../../common/mock'; import { FailureHistory } from './failure_history'; import { useRuleStatus } from '../../../../containers/detection_engine/rules'; jest.mock('../../../../containers/detection_engine/rules'); +import { waitFor } from '@testing-library/react'; + +import '../../../../../common/mock/match_media'; + +import { createStore, State } from '../../../../../common/store'; +import { mockHistory, Router } from '../../../../../common/mock/router'; + +const state: State = { + ...mockGlobalState, +}; +const { storage } = createSecuritySolutionStorageMock(); +const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + +describe('RuleDetailsPageComponent', () => { + beforeAll(() => { + (useRuleStatus as jest.Mock).mockReturnValue([ + false, + { + status: 'succeeded', + last_failure_at: new Date().toISOString(), + last_failure_message: 'my fake failure message', + failures: [ + { + alert_id: 'myfakeid', + status_date: new Date().toISOString(), + status: 'failed', + last_failure_at: new Date().toISOString(), + last_success_at: new Date().toISOString(), + last_failure_message: 'my fake failure message', + last_look_back_date: new Date().toISOString(), // NOTE: This is no longer used on the UI, but left here in case users are using it within the API + }, + ], + }, + ]); + }); + + it('renders reported rule failures correctly', async () => { + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + // ensure the expected error message is displayed in the table + expect(wrapper.find('EuiTableRowCell').at(2).find('div').at(1).text()).toEqual( + 'my fake failure message' + ); + }); + }); +}); + describe('FailureHistory', () => { beforeAll(() => { (useRuleStatus as jest.Mock).mockReturnValue([false, null]); }); - it('renders correctly', () => { + it('renders correctly with no statuses', () => { const wrapper = shallow(, { wrappingComponent: TestProviders, }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx index a7db7ab57f6c2..5289e34b10046 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx @@ -23,6 +23,12 @@ interface FailureHistoryProps { id?: string | null; } +const renderStatus = () => {i18n.TYPE_FAILED}; +const renderLastFailureAt = (value: string) => ( + +); +const renderLastFailureMessage = (value: string) => <>{value}; + const FailureHistoryComponent: React.FC = ({ id }) => { const [loading, ruleStatus] = useRuleStatus(id); if (loading) { @@ -36,14 +42,14 @@ const FailureHistoryComponent: React.FC = ({ id }) => { const columns: Array> = [ { name: i18n.COLUMN_STATUS_TYPE, - render: () => {i18n.TYPE_FAILED}, + render: renderStatus, truncateText: false, width: '16%', }, { field: 'last_failure_at', name: i18n.COLUMN_FAILED_AT, - render: (value: string) => , + render: renderLastFailureAt, sortable: false, truncateText: false, width: '24%', @@ -51,7 +57,7 @@ const FailureHistoryComponent: React.FC = ({ id }) => { { field: 'last_failure_message', name: i18n.COLUMN_FAILED_MSG, - render: (value: string) => <>{value}, + render: renderLastFailureMessage, sortable: false, truncateText: false, width: '60%', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index 0c67a19e59e32..9c1667e7b4910 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -20,22 +20,51 @@ import { import { RuleDetailsPage } from './index'; import { createStore, State } from '../../../../../common/store'; import { useUserData } from '../../../../components/user_info'; +import { useRuleStatus } from '../../../../containers/detection_engine/rules'; +import { useRuleWithFallback } from '../../../../containers/detection_engine/rules/use_rule_with_fallback'; + import { useSourcererScope } from '../../../../../common/containers/sourcerer'; import { useParams } from 'react-router-dom'; import { mockHistory, Router } from '../../../../../common/mock/router'; -import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin'; + +import { useKibana } from '../../../../../common/lib/kibana'; + +import { fillEmptySeverityMappings } from '../helpers'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar jest.mock('../../../../../common/components/search_bar', () => ({ SiemSearchBar: () => null, })); +jest.mock('../helpers', () => { + const original = jest.requireActual('../helpers'); + return { + ...original, + fillEmptySeverityMappings: jest.fn().mockReturnValue([]), + }; +}); jest.mock('../../../../../common/components/query_bar', () => ({ QueryBar: () => null, })); jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); +jest.mock('../../../../containers/detection_engine/rules', () => { + const original = jest.requireActual('../../../../containers/detection_engine/rules'); + return { + ...original, + useRuleStatus: jest.fn(), + }; +}); +jest.mock('../../../../containers/detection_engine/rules/use_rule_with_fallback', () => { + const original = jest.requireActual( + '../../../../containers/detection_engine/rules/use_rule_with_fallback' + ); + return { + ...original, + useRuleWithFallback: jest.fn(), + }; +}); jest.mock('../../../../../common/containers/sourcerer'); jest.mock('../../../../../common/containers/use_global_time', () => ({ useGlobalTime: jest.fn().mockReturnValue({ @@ -55,41 +84,42 @@ jest.mock('react-router-dom', () => { }; }); -jest.mock('../../../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../../../common/lib/kibana'); +jest.mock('../../../../../common/lib/kibana'); - return { - ...original, - useUiSetting$: jest.fn().mockReturnValue([]), - useKibana: () => ({ - services: { - application: { - ...original.useKibana().services.application, - navigateToUrl: jest.fn(), - capabilities: { - actions: jest.fn().mockReturnValue({}), - siem: { crud_alerts: true, read_alerts: true }, - }, - }, - timelines: { ...mockTimelines }, - data: { - query: { - filterManager: jest.fn().mockReturnValue({}), - }, - }, - }, - }), - useToasts: jest.fn().mockReturnValue({ - addError: jest.fn(), - addSuccess: jest.fn(), - addWarning: jest.fn(), - }), - }; -}); +const mockRedirectLegacyUrl = jest.fn(); +const mockGetLegacyUrlConflict = jest.fn(); const state: State = { ...mockGlobalState, }; + +const mockRule = { + id: 'myfakeruleid', + author: [], + severity_mapping: [], + risk_score_mapping: [], + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', + references: [], + actions: [], + enabled: false, + false_positives: [], + max_signals: 100, + tags: [], + threat: [], + throttle: null, + version: 1, + exceptions_list: [], +}; const { storage } = createSecuritySolutionStorageMock(); const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); @@ -101,9 +131,108 @@ describe('RuleDetailsPageComponent', () => { indicesExist: true, indexPattern: {}, }); + (useRuleStatus as jest.Mock).mockReturnValue([ + false, + { + status: 'succeeded', + last_failure_at: new Date().toISOString(), + last_failure_message: 'my fake failure message', + failures: [], + }, + ]); + (useRuleWithFallback as jest.Mock).mockReturnValue({ + error: null, + loading: false, + isExistingRule: true, + refresh: jest.fn(), + rule: { ...mockRule }, + }); + (fillEmptySeverityMappings as jest.Mock).mockReturnValue([]); + }); + + async function setup() { + const useKibanaMock = useKibana as jest.Mocked; + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.spaces = { + ui: { + // @ts-expect-error + components: { getLegacyUrlConflict: mockGetLegacyUrlConflict }, + redirectLegacyUrl: mockRedirectLegacyUrl, + }, + }; + } + + it('renders correctly with no outcome property on rule', async () => { + await setup(); + + const wrapper = mount( + + + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="header-page-title"]').exists()).toBe(true); + expect(mockRedirectLegacyUrl).not.toHaveBeenCalled(); + }); + }); + + it('renders correctly with outcome === "exactMatch"', async () => { + await setup(); + (useRuleWithFallback as jest.Mock).mockReturnValue({ + error: null, + loading: false, + isExistingRule: true, + refresh: jest.fn(), + rule: { ...mockRule, outcome: 'exactMatch' }, + }); + + const wrapper = mount( + + + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="header-page-title"]').exists()).toBe(true); + expect(mockRedirectLegacyUrl).not.toHaveBeenCalled(); + }); }); - it('renders correctly', async () => { + it('renders correctly with outcome === "aliasMatch"', async () => { + await setup(); + (useRuleWithFallback as jest.Mock).mockReturnValue({ + error: null, + loading: false, + isExistingRule: true, + refresh: jest.fn(), + rule: { ...mockRule, outcome: 'aliasMatch' }, + }); + const wrapper = mount( + + + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="header-page-title"]').exists()).toBe(true); + expect(mockRedirectLegacyUrl).toHaveBeenCalledWith(`rules/id/myfakeruleid`, `rule`); + }); + }); + + it('renders correctly when outcome = conflict', async () => { + await setup(); + (useRuleWithFallback as jest.Mock).mockReturnValue({ + error: null, + loading: false, + isExistingRule: true, + refresh: jest.fn(), + rule: { ...mockRule, outcome: 'conflict', alias_target_id: 'aliased_rule_id' }, + }); const wrapper = mount( @@ -113,6 +242,13 @@ describe('RuleDetailsPageComponent', () => { ); await waitFor(() => { expect(wrapper.find('[data-test-subj="header-page-title"]').exists()).toBe(true); + expect(mockRedirectLegacyUrl).toHaveBeenCalledWith(`rules/id/myfakeruleid`, `rule`); + expect(mockGetLegacyUrlConflict).toHaveBeenCalledWith({ + currentObjectId: 'myfakeruleid', + objectNoun: 'rule', + otherObjectId: 'aliased_rule_id', + otherObjectPath: `rules/id/aliased_rule_id`, + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 70d7faa47b9ee..492b8e461fb60 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -19,6 +19,8 @@ import { EuiToolTip, EuiWindowEvent, } from '@elastic/eui'; +import { i18n as i18nTranslate } from '@kbn/i18n'; + import { FormattedMessage } from '@kbn/i18n/react'; import { noop } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -261,6 +263,7 @@ const RuleDetailsPageComponent: React.FC = ({ capabilities: { actions }, }, timelines: timelinesUi, + spaces: spacesApi, }, } = useKibana(); const hasActionsPrivileges = useMemo(() => { @@ -277,6 +280,52 @@ const RuleDetailsPageComponent: React.FC = ({ } }, [maybeRule]); + useEffect(() => { + if (rule) { + const outcome = rule.outcome; + if (spacesApi && outcome === 'aliasMatch') { + // This rule has been resolved from a legacy URL - redirect the user to the new URL and display a toast. + const path = `rules/id/${rule.id}${window.location.search}${window.location.hash}`; + spacesApi.ui.redirectLegacyUrl( + path, + i18nTranslate.translate( + 'xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', + { + defaultMessage: 'rule', + } + ) + ); + } + } + }, [rule, spacesApi]); + + const getLegacyUrlConflictCallout = useMemo(() => { + const outcome = rule?.outcome; + if (rule != null && spacesApi && outcome === 'conflict') { + const aliasTargetId = rule?.alias_target_id!; // This is always defined if outcome === 'conflict' + // We have resolved to one rule, but there is another one with a legacy URL associated with this page. Display a + // callout with a warning for the user, and provide a way for them to navigate to the other rule. + const otherRulePath = `rules/id/${aliasTargetId}${window.location.search}${window.location.hash}`; + return ( + <> + + {spacesApi.ui.components.getLegacyUrlConflict({ + objectNoun: i18nTranslate.translate( + 'xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', + { + defaultMessage: 'rule', + } + ), + currentObjectId: rule.id, + otherObjectId: aliasTargetId, + otherObjectPath: otherRulePath, + })} + + ); + } + return null; + }, [rule, spacesApi]); + useEffect(() => { if (!hasIndexRead) { setTabs(ruleDetailTabs.filter(({ id }) => id !== RuleDetailTabs.alerts)); @@ -721,6 +770,7 @@ const RuleDetailsPageComponent: React.FC = ({ {ruleError} + {getLegacyUrlConflictCallout} diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts index af663bb74f54a..15cb7ef7b1c46 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts @@ -98,7 +98,7 @@ export const useHostsRiskScore = ({ useEffect(() => { if (riskyHostsFeatureEnabled && (hostName || timerange)) { - spaces.getActiveSpace().then((space) => { + spaces?.getActiveSpace().then((space) => { start({ data, timerange: timerange diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 1cec87fd35d1f..e595b905b998e 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -68,7 +68,7 @@ export interface StartPlugins { timelines: TimelinesUIStart; uiActions: UiActionsStart; ml?: MlPluginStart; - spaces: SpacesPluginStart; + spaces?: SpacesPluginStart; } export type StartServices = CoreStart & diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index c3c3ac47baf9a..200246ba1a367 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -34,7 +34,7 @@ import { getFinalizeSignalsMigrationSchemaMock } from '../../../../../common/det import { EqlSearchResponse } from '../../../../../common/detection_engine/types'; import { getSignalsMigrationStatusSchemaMock } from '../../../../../common/detection_engine/schemas/request/get_signals_migration_status_schema.mock'; import { RuleParams } from '../../schemas/rule_schemas'; -import { Alert } from '../../../../../../alerting/common'; +import { SanitizedAlert, ResolvedSanitizedRule } from '../../../../../../alerting/common'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; import { getPerformBulkActionSchemaMock } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock'; import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; @@ -87,6 +87,13 @@ export const getReadRequest = () => query: { rule_id: 'rule-1' }, }); +export const getReadRequestWithId = (id: string) => + requestMock.create({ + method: 'get', + path: DETECTION_ENGINE_RULES_URL, + query: { id }, + }); + export const getFindRequest = () => requestMock.create({ method: 'get', @@ -362,7 +369,7 @@ export const nonRuleAlert = (isRuleRegistryEnabled: boolean) => ({ export const getAlertMock = ( isRuleRegistryEnabled: boolean, params: T -): Alert => ({ +): SanitizedAlert => ({ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', name: 'Detect Root/Admin Users', tags: [`${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:false`], @@ -378,7 +385,6 @@ export const getAlertMock = ( notifyWhen: null, createdBy: 'elastic', updatedBy: 'elastic', - apiKey: null, apiKeyOwner: 'elastic', muteAll: false, mutedInstanceIds: [], @@ -389,6 +395,14 @@ export const getAlertMock = ( }, }); +export const resolveAlertMock = ( + isRuleRegistryEnabled: boolean, + params: T +): ResolvedSanitizedRule => ({ + outcome: 'exactMatch', + ...getAlertMock(isRuleRegistryEnabled, params), +}); + export const updateActionResult = (): ActionResult => ({ id: 'result-1', actionTypeId: 'action-id-1', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index 35b3ef3d9cf85..7c447660acb45 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -8,7 +8,7 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { getEmptyFindResult, - getAlertMock, + resolveAlertMock, getDeleteRequest, getFindResultWithSingleHit, getDeleteRequestById, @@ -45,8 +45,8 @@ describe.each([ }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { - clients.rulesClient.get.mockResolvedValue( - getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) + clients.rulesClient.resolve.mockResolvedValue( + resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) ); const response = await server.inject(getDeleteRequestById(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index d6c18088800ba..37b8228ac1e9b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -12,11 +12,14 @@ import { readRulesRoute } from './read_rules_route'; import { getEmptyFindResult, getReadRequest, + getReadRequestWithId, getFindResultWithSingleHit, nonRuleFindResult, getEmptySavedObjectsResponse, + resolveAlertMock, } from '../__mocks__/request_responses'; import { requestMock, requestContextMock, serverMock } from '../__mocks__'; +import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; describe.each([ ['Legacy', false], @@ -26,6 +29,7 @@ describe.each([ let { clients, context } = requestContextMock.createTools(); let logger: ReturnType; + const myFakeId = '99403909-ca9b-49ba-9d7a-7e5320e68d05'; beforeEach(() => { server = serverMock.create(); logger = loggingSystemMock.createLogger(); @@ -35,6 +39,12 @@ describe.each([ clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); // successful transform clients.ruleExecutionLogClient.find.mockResolvedValue([]); + clients.rulesClient.resolve.mockResolvedValue({ + ...resolveAlertMock(isRuleRegistryEnabled, { + ...getQueryRuleParams(), + }), + id: myFakeId, + }); readRulesRoute(server.router, logger, isRuleRegistryEnabled); }); @@ -44,6 +54,37 @@ describe.each([ expect(response.status).toEqual(200); }); + test('returns 200 when reading a single rule outcome === exactMatch', async () => { + const response = await server.inject(getReadRequestWithId(myFakeId), context); + expect(response.status).toEqual(200); + }); + + test('returns 200 when reading a single rule outcome === aliasMatch', async () => { + clients.rulesClient.resolve.mockResolvedValue({ + ...resolveAlertMock(isRuleRegistryEnabled, { + ...getQueryRuleParams(), + }), + id: myFakeId, + outcome: 'aliasMatch', + }); + const response = await server.inject(getReadRequestWithId(myFakeId), context); + expect(response.status).toEqual(200); + }); + + test('returns 200 when reading a single rule outcome === conflict', async () => { + clients.rulesClient.resolve.mockResolvedValue({ + ...resolveAlertMock(isRuleRegistryEnabled, { + ...getQueryRuleParams(), + }), + id: myFakeId, + outcome: 'conflict', + alias_target_id: 'myaliastargetid', + }); + const response = await server.inject(getReadRequestWithId(myFakeId), context); + expect(response.status).toEqual(200); + expect(response.body.alias_target_id).toEqual('myaliastargetid'); + }); + test('returns 404 if alertClient is not available on the route', async () => { context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getReadRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts index 6f89d725a458e..2e17b91fbcd54 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts @@ -7,7 +7,11 @@ import { readRules } from './read_rules'; import { rulesClientMock } from '../../../../../alerting/server/mocks'; -import { getAlertMock, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; +import { + resolveAlertMock, + getAlertMock, + getFindResultWithSingleHit, +} from '../routes/__mocks__/request_responses'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; export class TestError extends Error { @@ -33,7 +37,9 @@ describe.each([ describe('readRules', () => { test('should return the output from rulesClient if id is set but ruleId is undefined', async () => { const rulesClient = rulesClientMock.create(); - rulesClient.get.mockResolvedValue(getAlertMock(isRuleRegistryEnabled, getQueryRuleParams())); + rulesClient.resolve.mockResolvedValue( + resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) + ); const rule = await readRules({ isRuleRegistryEnabled, @@ -45,10 +51,10 @@ describe.each([ }); test('should return null if saved object found by alerts client given id is not alert type', async () => { const rulesClient = rulesClientMock.create(); - const result = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); + const result = resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); // @ts-expect-error delete result.alertTypeId; - rulesClient.get.mockResolvedValue(result); + rulesClient.resolve.mockResolvedValue(result); const rule = await readRules({ isRuleRegistryEnabled, @@ -61,7 +67,7 @@ describe.each([ test('should return error if alerts client throws 404 error on get', async () => { const rulesClient = rulesClientMock.create(); - rulesClient.get.mockImplementation(() => { + rulesClient.resolve.mockImplementation(() => { throw new TestError(); }); @@ -76,7 +82,7 @@ describe.each([ test('should return error if alerts client throws error on get', async () => { const rulesClient = rulesClientMock.create(); - rulesClient.get.mockImplementation(() => { + rulesClient.resolve.mockImplementation(() => { throw new Error('Test error'); }); try { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts index 9578e3d4cb6d2..2571791164b6b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SanitizedAlert } from '../../../../../alerting/common'; +import { ResolvedSanitizedRule, SanitizedAlert } from '../../../../../alerting/common'; import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { RuleParams } from '../schemas/rule_schemas'; import { findRules } from './find_rules'; @@ -24,11 +24,17 @@ export const readRules = async ({ rulesClient, id, ruleId, -}: ReadRuleOptions): Promise | null> => { +}: ReadRuleOptions): Promise< + SanitizedAlert | ResolvedSanitizedRule | null +> => { if (id != null) { try { - const rule = await rulesClient.get({ id }); + const rule = await rulesClient.resolve({ id }); if (isAlertType(isRuleRegistryEnabled, rule)) { + if (rule?.outcome === 'exactMatch') { + const { outcome, ...restOfRule } = rule; + return restOfRule; + } return rule; } else { return null; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index cceda063e987b..8adf19a53f92b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -100,14 +100,14 @@ import { } from '../../../../common/detection_engine/schemas/common/schemas'; import { RulesClient, PartialAlert } from '../../../../../alerting/server'; -import { Alert, SanitizedAlert } from '../../../../../alerting/common'; +import { SanitizedAlert } from '../../../../../alerting/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { PartialFilter } from '../types'; import { RuleParams } from '../schemas/rule_schemas'; import { IRuleExecutionLogClient } from '../rule_execution_log/types'; import { ruleTypeMappings } from '../signals/utils'; -export type RuleAlertType = Alert; +export type RuleAlertType = SanitizedAlert; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface IRuleStatusSOAttributes extends Record { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts index 74301f3665ff8..703be3bdd76bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getAlertMock } from '../routes/__mocks__/request_responses'; +import { getAlertMock, resolveAlertMock } from '../routes/__mocks__/request_responses'; import { updateRules } from './update_rules'; import { getUpdateRulesOptionsMock, getUpdateMlRulesOptionsMock } from './update_rules.mock'; import { RulesClientMock } from '../../../../../alerting/server/rules_client.mock'; @@ -18,8 +18,8 @@ describe.each([ it('should call rulesClient.disable if the rule was enabled and enabled is false', async () => { const rulesOptionsMock = getUpdateRulesOptionsMock(isRuleRegistryEnabled); rulesOptionsMock.ruleUpdate.enabled = false; - (rulesOptionsMock.rulesClient as unknown as RulesClientMock).get.mockResolvedValue( - getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) + (rulesOptionsMock.rulesClient as unknown as RulesClientMock).resolve.mockResolvedValue( + resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) ); (rulesOptionsMock.rulesClient as unknown as RulesClientMock).update.mockResolvedValue( getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) @@ -38,8 +38,8 @@ describe.each([ const rulesOptionsMock = getUpdateRulesOptionsMock(isRuleRegistryEnabled); rulesOptionsMock.ruleUpdate.enabled = true; - (rulesOptionsMock.rulesClient as unknown as RulesClientMock).get.mockResolvedValue({ - ...getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()), + (rulesOptionsMock.rulesClient as unknown as RulesClientMock).resolve.mockResolvedValue({ + ...resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()), enabled: false, }); (rulesOptionsMock.rulesClient as unknown as RulesClientMock).update.mockResolvedValue( @@ -63,8 +63,8 @@ describe.each([ getAlertMock(isRuleRegistryEnabled, getMlRuleParams()) ); - (rulesOptionsMock.rulesClient as unknown as RulesClientMock).get.mockResolvedValue( - getAlertMock(isRuleRegistryEnabled, getMlRuleParams()) + (rulesOptionsMock.rulesClient as unknown as RulesClientMock).resolve.mockResolvedValue( + resolveAlertMock(isRuleRegistryEnabled, getMlRuleParams()) ); await updateRules(rulesOptionsMock); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts index eef20af0e564d..240a226e86914 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -27,7 +27,7 @@ import { AppClient } from '../../../types'; import { addTags } from '../rules/add_tags'; import { DEFAULT_MAX_SIGNALS, SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { SanitizedAlert } from '../../../../../alerting/common'; +import { ResolvedSanitizedRule, SanitizedAlert } from '../../../../../alerting/common'; import { IRuleStatusSOAttributes } from '../rules/types'; import { transformTags } from '../routes/rules/utils'; import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -281,12 +281,17 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => { }; export const internalRuleToAPIResponse = ( - rule: SanitizedAlert, + rule: SanitizedAlert | ResolvedSanitizedRule, ruleStatus?: IRuleStatusSOAttributes, legacyRuleActions?: LegacyRuleActions | null ): FullResponseSchema => { const mergedStatus = ruleStatus ? mergeAlertWithSidecarStatus(rule, ruleStatus) : undefined; + const isResolvedRule = (obj: unknown): obj is ResolvedSanitizedRule => + (obj as ResolvedSanitizedRule).outcome != null; return { + // saved object properties + outcome: isResolvedRule(rule) ? rule.outcome : undefined, + alias_target_id: isResolvedRule(rule) ? rule.alias_target_id : undefined, // Alerting framework params id: rule.id, updated_at: rule.updatedAt.toISOString(), diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts index 802b1e78930e8..5fa4540bbe854 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { - describe('detection engine api security and spaces enabled', function () { + describe('detection engine api basic license', function () { this.tags('ciGroup1'); loadTestFile(require.resolve('./add_prepackaged_rules')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index b4bd74172920b..1b88c4fe21b49 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -33,6 +33,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./get_prepackaged_rules_status')); loadTestFile(require.resolve('./import_rules')); loadTestFile(require.resolve('./read_rules')); + loadTestFile(require.resolve('./resolve_read_rules')); loadTestFile(require.resolve('./update_rules')); loadTestFile(require.resolve('./update_rules_bulk')); loadTestFile(require.resolve('./patch_rules_bulk')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts new file mode 100644 index 0000000000000..6013398d4695d --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts @@ -0,0 +1,160 @@ +/* + * 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 expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex } from '../../utils'; + +const spaceId = '714-space'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('resolve_read_rules', () => { + describe('reading rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14' + ); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14' + ); + }); + + it('should create a "migrated" rule where querying for the new SO _id will resolve the new object and not return the outcome field when outcome === exactMatch', async () => { + // link to the new URL with migrated SO id 74f3e6d7-b7bb-477d-ac28-92ee22728e6e + const URL = `/s/${spaceId}${DETECTION_ENGINE_RULES_URL}?id=90e3ca0e-71f7-513a-b60a-ac678efd8887`; + const readRulesAliasMatchRes = await supertest.get(URL).set('kbn-xsrf', 'true').send(); + expect(readRulesAliasMatchRes.body.outcome).to.eql('aliasMatch'); + + // now that we have the migrated alias_target_id, let's attempt an 'exactMatch' query + // the result of which should have the outcome as undefined when querying the read rules api. + const exactMatchURL = `/s/${spaceId}${DETECTION_ENGINE_RULES_URL}?id=${readRulesAliasMatchRes.body.alias_target_id}`; + const readRulesExactMatchRes = await supertest + .get(exactMatchURL) + .set('kbn-xsrf', 'true') + .send(); + expect(readRulesExactMatchRes.body.outcome).to.eql(undefined); + }); + + it('should create a rule and a "conflicting rule" where the SO _id matches the sourceId (see legacy-url-alias SO) of a migrated rule', async () => { + // mimic a rule SO that was inserted accidentally + // we have to insert this outside of esArchiver otherwise kibana will migrate this + // and we won't have a conflict + await es.index({ + id: 'alert:90e3ca0e-71f7-513a-b60a-ac678efd8887', + index: '.kibana', + refresh: true, + body: { + alert: { + name: 'test 7.14', + tags: [ + '__internal_rule_id:82747bb8-bae0-4b59-8119-7f65ac564e14', + '__internal_immutable:false', + ], + alertTypeId: 'siem.signals', + consumer: 'siem', + params: { + author: [], + description: 'test', + ruleId: '82747bb8-bae0-4b59-8119-7f65ac564e14', + falsePositives: [], + from: 'now-3615s', + immutable: false, + license: '', + outputIndex: '.siem-signals-devin-hurley-714-space', + meta: { + from: '1h', + kibana_siem_app_url: 'http://0.0.0.0:5601/s/714-space/app/security', + }, + maxSignals: 100, + riskScore: 21, + riskScoreMapping: [], + severity: 'low', + severityMapping: [], + threat: [], + to: 'now', + references: [], + version: 1, + exceptionsList: [], + type: 'query', + language: 'kuery', + index: [ + 'apm-*-transaction*', + 'traces-apm*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + query: '*:*', + filters: [], + }, + schedule: { + interval: '15s', + }, + enabled: true, + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + apiKeyOwner: 'elastic', + apiKey: + 'HvwrIJ8NBshJav9vf3BSEEa2P7fXLTpmEKAx2bSyBF51N2cadFkltWLRRcFnj65RXsPzvRm3VKzAde4b1iGzsjxY/IVmfGGyiO0rk6vZVJVLeMSD+CAiflnwweypoKM8WgwXJnI0Oa/SWqKMtrDiFxCcZCwIuAhS0sjenaiEuedbAuStZv513zz/clpqRKFXBydJXKyjJUQLTA==', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: '2021-10-05T19:52:25.865Z', + updatedAt: '2021-10-05T19:52:25.865Z', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'ok', + lastExecutionDate: '2021-10-05T19:52:51.260Z', + error: null, + }, + meta: { + versionApiKeyLastmodified: '7.14.2', + }, + scheduledTaskId: 'c4005e90-2615-11ec-811e-db7211397897', + legacyId: 'c364e1e0-2615-11ec-811e-db7211397897', + }, + type: 'alert', + references: [], + namespaces: [spaceId], + originId: 'c364e1e0-2615-11ec-811e-db7211397897', + migrationVersion: { + alert: '8.0.0', + }, + coreMigrationVersion: '8.0.0', + updated_at: '2021-10-05T19:52:56.014Z', + }, + }); + + // Now that we have a rule id and a legacy-url-alias with the same id, we should have a conflict + const conflictURL = `/s/${spaceId}${DETECTION_ENGINE_RULES_URL}?id=90e3ca0e-71f7-513a-b60a-ac678efd8887`; + const readRulesConflictRes = await supertest + .get(conflictURL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(readRulesConflictRes.body.outcome).to.eql('conflict'); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index ac27f06a149d9..eeae21c3b7bad 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -369,17 +369,31 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial version: 1, }); +export const resolveSimpleRuleOutput = ( + ruleId = 'rule-1', + enabled = false +): Partial => ({ outcome: 'exactMatch', ...getSimpleRuleOutput(ruleId, enabled) }); + /** * This is the typical output of a simple rule that Kibana will output with all the defaults except * for all the server generated properties such as created_by. Useful for testing end to end tests. */ export const getSimpleRuleOutputWithoutRuleId = (ruleId = 'rule-1'): Partial => { const rule = getSimpleRuleOutput(ruleId); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { rule_id, ...ruleWithoutRuleId } = rule; + const { rule_id: rId, ...ruleWithoutRuleId } = rule; return ruleWithoutRuleId; }; +/** + * This is the typical output of a simple rule that Kibana will output with all the defaults except + * for all the server generated properties such as created_by. Useful for testing end to end tests. + */ +export const resolveSimpleRuleOutputWithoutRuleId = (ruleId = 'rule-1'): Partial => { + const rule = getSimpleRuleOutput(ruleId); + const { rule_id: rId, ...ruleWithoutRuleId } = rule; + return { outcome: 'exactMatch', ...ruleWithoutRuleId }; +}; + export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial => { const rule = getSimpleRuleOutput(ruleId); const { query, language, index, ...rest } = rule; @@ -399,12 +413,17 @@ export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial = * @param supertest The supertest agent. */ export const deleteAllAlerts = async ( - supertest: SuperTest.SuperTest + supertest: SuperTest.SuperTest, + space?: string ): Promise => { await countDownTest( async () => { const { body } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}/_find?per_page=9999`) + .get( + space + ? `/s/${space}${DETECTION_ENGINE_RULES_URL}/_find?per_page=9999` + : `${DETECTION_ENGINE_RULES_URL}/_find?per_page=9999` + ) .set('kbn-xsrf', 'true') .send(); @@ -413,7 +432,11 @@ export const deleteAllAlerts = async ( })); await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .post( + space + ? `/s/${space}${DETECTION_ENGINE_RULES_URL}/_bulk_delete` + : `${DETECTION_ENGINE_RULES_URL}/_bulk_delete` + ) .send(ids) .set('kbn-xsrf', 'true'); diff --git a/x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/data.json b/x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/data.json new file mode 100644 index 0000000000000..498367c913dc0 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/data.json @@ -0,0 +1,101 @@ +{ + "type" : "doc", + "value": { + "index" : ".kibana_1", + "id" : "space:714-space", + "source" : { + "space" : { + "name" : "714-space", + "initials" : "t", + "color" : "#B9A888", + "disabledFeatures" : [ ], + "imageUrl" : "" + }, + "type" : "space", + "references" : [ ], + "migrationVersion" : { + "space" : "6.6.0" + }, + "updated_at" : "2021-10-11T14:49:07.012Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "714-space:alert:90e3ca0e-71f7-513a-b60a-ac678efd8887", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + ], + "alertTypeId" : "siem.signals", + "consumer" : "siem", + "apiKey": "QIUT8u0/kbOakEHSj50jDpVR90MrqOxanEscboYOoa8PxQvcA5jfHash+fqH3b+KNjJ1LpnBcisGuPkufY9j1e32gKzwGZV5Bfys87imHvygJvIM8uKiFF8bQ8Y4NTaxOJO9fAmZPrFy07ZcQMCAQz+DUTgBFqs=", + "apiKeyOwner": "elastic", + "createdAt": "2020-06-17T15:35:38.497Z", + "createdBy": "elastic", + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "always-firing-alert", + "params":{ + "author": [], + "description": "test", + "ruleId": "82747bb8-bae0-4b59-8119-7f65ac564e14", + "falsePositives": [], + "from": "now-3615s", + "immutable": false, + "license": "", + "outputIndex": ".siem-signals-devin-hurley-714-space", + "meta": { + "from": "1h", + "kibana_siem_app_url": "http://0.0.0.0:5601/s/714-space/app/security" + }, + "maxSignals": 100, + "riskScore": 21, + "riskScoreMapping": [], + "severity": "low", + "severityMapping": [], + "threat": [], + "to": "now", + "references": [], + "version": 1, + "exceptionsList": [], + "type": "query", + "language": "kuery", + "index": [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query": "*:*", + "filters": [] + }, + "schedule": { + "interval": "1m" + }, + "scheduledTaskId": "329798f0-b0b0-11ea-9510-fdf248d5f2a4", + "tags": [ + ], + "throttle": null, + "updatedBy": "elastic" + }, + "migrationVersion": { + "alert": "7.8.0" + }, + "references": [ + ], + "namespace": "714-space", + "type": "alert", + "updated_at": "2020-06-17T15:35:39.839Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/mappings.json b/x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/mappings.json new file mode 100644 index 0000000000000..069f70badce4e --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/mappings.json @@ -0,0 +1,397 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": {} + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", + "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "todo": "082a2cc96a590268344d5cd74c159ac4", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "legacyId": { + "type": "keyword" + }, + "createdBy": { + "type": "keyword" + }, + "updatedAt": { + "type": "date" + }, + "executionStatus": { + "properties": { + "error": { + "properties": { + "message": { + "type": "keyword" + }, + "reason": { + "type": "keyword" + } + } + }, + "lastExecutionDate": { + "type": "date" + }, + "status": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "legacy-url-alias": { + "properties": { + "sourceId": { + "type": "text" + }, + "targetNamespace": { + "type": "keyword" + }, + "targetType": { + "type": "keyword" + }, + "targetId": { + "type": "keyword" + }, + "resolveCounter": { + "type": "integer" + }, + "lastResolved": { + "type": "date" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "alert": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "coreMigrationVersion": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +}