diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 0a0d9d44d80ea..1e2bc2a852ac0 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -11,9 +11,12 @@ disabled: - x-pack/test/fleet_api_integration/config.base.ts - x-pack/test/security_solution_api_integration/config/ess/config.base.ts - x-pack/test/security_solution_api_integration/config/ess/config.base.basic.ts + - x-pack/test/security_solution_api_integration/config/ess/config.base.edr_workflows.trial.ts + - x-pack/test/security_solution_api_integration/config/ess/config.base.edr_workflows.ts + - x-pack/test/security_solution_api_integration/config/ess/config.base.basic.ts - x-pack/test/security_solution_api_integration/config/serverless/config.base.ts + - x-pack/test/security_solution_api_integration/config/serverless/config.base.edr_workflows.ts - x-pack/test/security_solution_api_integration/config/serverless/config.base.essentials.ts - - x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/config.base.ts - x-pack/test/security_solution_endpoint/configs/config.base.ts # QA suites that are run out-of-band @@ -580,5 +583,17 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/investigation/timeline/security_and_spaces/configs/ess.trial.config.ts - x-pack/test/security_solution_api_integration/test_suites/sources/indices/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/sources/indices/trial_license_complete_tier/configs/serverless.config.ts - - x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/config.ts - - x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/configs/ess.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/configs/ess.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/configs/ess.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/configs/ess.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/configs/ess.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/configs/ess.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/ess.config.ts + - x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/serverless.config.ts diff --git a/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml index b880b0a5f2f02..b9ff060e706e7 100644 --- a/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml +++ b/.buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml @@ -1,18 +1,143 @@ steps: - - command: .buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/mki_security_solution_defend_workflows.sh cypress:dw:qa:serverless:run - label: "Cypress MKI - Defend Workflows " - key: test_defend_workflows - agents: - image: family/kibana-ubuntu-2004 - imageProject: elastic-images-prod - provider: gcp - enableNestedVirtualization: true - localSsds: 1 - localSsdInterface: nvme - machineType: n2-standard-4 - timeout_in_minutes: 300 - parallelism: 6 - retry: - automatic: - - exit_status: "*" - limit: 1 + - group: "Cypress MKI - Defend Workflows" + key: cypress_test_defend_workflows + steps: + - label: "Running cypress:dw:qa:serverless:run" + command: .buildkite/scripts/pipelines/security_solution_quality_gate/edr_workflows/mki_security_solution_defend_workflows.sh cypress:dw:qa:serverless:run + key: test_defend_workflows + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + enableNestedVirtualization: true + localSsds: 1 + localSsdInterface: nvme + machineType: n2-standard-4 + timeout_in_minutes: 300 + parallelism: 6 + retry: + automatic: + - exit_status: "*" + limit: 1 + + - group: "API MKI - Defend Workflows" + key: api_test_defend_workflows + steps: +# - label: "Running edr_workflows:artifacts:qa:serverless" +# command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh edr_workflows:artifacts:qa:serverless +# key: edr_workflows:artifacts:qa:serverless +# agents: +# image: family/kibana-ubuntu-2004 +# imageProject: elastic-images-prod +# provider: gcp +# enableNestedVirtualization: true +# localSsds: 1 +# localSsdInterface: nvme +# machineType: n2-standard-4 +# timeout_in_minutes: 120 +# retry: +# automatic: +# - exit_status: "1" +# limit: 1 +# +# - label: "Running edr_workflows:authentication:qa:serverless" +# command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh edr_workflows:authentication:qa:serverless +# key: edr_workflows:authentication:qa:serverless +# agents: +# image: family/kibana-ubuntu-2004 +# imageProject: elastic-images-prod +# provider: gcp +# enableNestedVirtualization: true +# localSsds: 1 +# localSsdInterface: nvme +# machineType: n2-standard-4 +# timeout_in_minutes: 120 +# retry: +# automatic: +# - exit_status: "1" +# limit: 1 +# +# - label: "Running edr_workflows:metadata:qa:serverless" +# command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh edr_workflows:metadata:qa:serverless +# key: edr_workflows:metadata:qa:serverless +# agents: +# image: family/kibana-ubuntu-2004 +# imageProject: elastic-images-prod +# provider: gcp +# enableNestedVirtualization: true +# localSsds: 1 +# localSsdInterface: nvme +# machineType: n2-standard-4 +# timeout_in_minutes: 120 +# retry: +# automatic: +# - exit_status: "1" +# limit: 1 +# +# - label: "Running edr_workflows:package:qa:serverless" +# command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh edr_workflows:package:qa:serverless +# key: edr_workflows:package:qa:serverless +# agents: +# image: family/kibana-ubuntu-2004 +# imageProject: elastic-images-prod +# provider: gcp +# enableNestedVirtualization: true +# localSsds: 1 +# localSsdInterface: nvme +# machineType: n2-standard-4 +# timeout_in_minutes: 120 +# retry: +# automatic: +# - exit_status: "1" +# limit: 1 + + - label: "Running edr_workflows:policy_response:qa:serverless" + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh edr_workflows:policy_response:qa:serverless + key: edr_workflows:policy_response:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + enableNestedVirtualization: true + localSsds: 1 + localSsdInterface: nvme + machineType: n2-standard-4 + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 1 + + - label: "Running edr_workflows:resolver:qa:serverless" + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh edr_workflows:resolver:qa:serverless + key: edr_workflows:resolver:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + enableNestedVirtualization: true + localSsds: 1 + localSsdInterface: nvme + machineType: n2-standard-4 + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 1 + + - label: "Running edr_workflows:response_actions:qa:serverless" + command: .buildkite/scripts/pipelines/security_solution_quality_gate/api_integration/api-integration-tests.sh edr_workflows:response_actions:qa:serverless + key: edr_workflows:response_actions:qa:serverless + agents: + image: family/kibana-ubuntu-2004 + imageProject: elastic-images-prod + provider: gcp + enableNestedVirtualization: true + localSsds: 1 + localSsdInterface: nvme + machineType: n2-standard-4 + timeout_in_minutes: 120 + retry: + automatic: + - exit_status: "1" + limit: 1 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d22a59072a86d..07bede6066d76 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1618,7 +1618,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/ /x-pack/plugins/security_solution/server/fleet_integration/ @elastic/security-defend-workflows /x-pack/plugins/security_solution/scripts/endpoint/ @elastic/security-defend-workflows /x-pack/test/security_solution_endpoint/ @elastic/security-defend-workflows -/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/ @elastic/security-defend-workflows +/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/ @elastic/security-defend-workflows /x-pack/test_serverless/shared/lib/security/kibana_roles/ @elastic/security-defend-workflows /x-pack/plugins/security_solution_serverless/public/upselling/sections/endpoint_management @elastic/security-defend-workflows /x-pack/plugins/security_solution_serverless/public/upselling/pages/endpoint_management @elastic/security-defend-workflows diff --git a/packages/core/http/core-http-server-internal/src/https_redirect_server.test.ts b/packages/core/http/core-http-server-internal/src/https_redirect_server.test.ts index 6050d86ded357..3123fe8bba06c 100644 --- a/packages/core/http/core-http-server-internal/src/https_redirect_server.test.ts +++ b/packages/core/http/core-http-server-internal/src/https_redirect_server.test.ts @@ -96,3 +96,14 @@ test('forwards http requests to https', async () => { expect(res.header.location).toEqual(`https://${config.host}:${config.port}/`); }); }); + +test('keeps the request host when redirecting', async () => { + await server.start(config); + + await supertest(`http://localhost:${config.ssl.redirectHttpFromPort}`) + .get('/') + .expect(302) + .then((res) => { + expect(res.header.location).toEqual(`https://localhost:${config.port}/`); + }); +}); diff --git a/packages/core/http/core-http-server-internal/src/https_redirect_server.ts b/packages/core/http/core-http-server-internal/src/https_redirect_server.ts index 2999c4aaf734e..e621b864fa075 100644 --- a/packages/core/http/core-http-server-internal/src/https_redirect_server.ts +++ b/packages/core/http/core-http-server-internal/src/https_redirect_server.ts @@ -40,7 +40,7 @@ export class HttpsRedirectServer { return responseToolkit .redirect( formatUrl({ - hostname: config.host, + hostname: request.url.hostname, pathname: request.url.pathname, port: config.port, protocol: 'https', diff --git a/packages/kbn-ftr-common-functional-ui-services/services/remote/webdriver.ts b/packages/kbn-ftr-common-functional-ui-services/services/remote/webdriver.ts index 4789a7566756d..9acb5f547acb9 100644 --- a/packages/kbn-ftr-common-functional-ui-services/services/remote/webdriver.ts +++ b/packages/kbn-ftr-common-functional-ui-services/services/remote/webdriver.ts @@ -32,26 +32,48 @@ import { preventParallelCalls } from './prevent_parallel_calls'; import { Browsers } from './browsers'; import { NetworkProfile, NETWORK_PROFILES } from './network_profiles'; -const throttleOption: string = process.env.TEST_THROTTLE_NETWORK as string; -const headlessBrowser: string = process.env.TEST_BROWSER_HEADLESS as string; -const browserBinaryPath: string = process.env.TEST_BROWSER_BINARY_PATH as string; -const remoteDebug: string = process.env.TEST_REMOTE_DEBUG as string; -const certValidation: string = process.env.NODE_TLS_REJECT_UNAUTHORIZED as string; -const noCache: string = process.env.TEST_DISABLE_CACHE as string; +interface Configuration { + throttleOption: string; + headlessBrowser: string; + browserBinaryPath: string; + remoteDebug: string; + certValidation: string; + noCache: string; + chromiumUserPrefs: Record; +} + +const now = Date.now(); const SECOND = 1000; const MINUTE = 60 * SECOND; const NO_QUEUE_COMMANDS = ['getLog', 'getStatus', 'newSession', 'quit']; const downloadDir = resolve(REPO_ROOT, 'target/functional-tests/downloads'); -const chromiumUserPrefs = { - 'download.default_directory': downloadDir, - 'download.prompt_for_download': false, - 'profile.content_settings.exceptions.clipboard': { - '[*.],*': { - last_modified: Date.now(), - setting: 1, - }, - }, -}; +let runtimeEnvVariables: Configuration | undefined; + +// ENV variables may be injected dynamically by the test runner CLI script +// so do not read on module load, rather read on demand. +function getConfiguration(): Configuration { + if (!runtimeEnvVariables) { + runtimeEnvVariables = { + throttleOption: process.env.TEST_THROTTLE_NETWORK as string, + headlessBrowser: process.env.TEST_BROWSER_HEADLESS as string, + browserBinaryPath: process.env.TEST_BROWSER_BINARY_PATH as string, + remoteDebug: process.env.TEST_REMOTE_DEBUG as string, + certValidation: process.env.NODE_TLS_REJECT_UNAUTHORIZED as string, + noCache: process.env.TEST_DISABLE_CACHE as string, + chromiumUserPrefs: { + 'download.default_directory': downloadDir, + 'download.prompt_for_download': false, + 'profile.content_settings.exceptions.clipboard': { + '[*.],*': { + last_modified: now, + setting: 1, + }, + }, + }, + }; + } + return runtimeEnvVariables; +} const sleep$ = (ms: number) => Rx.timer(ms).pipe(ignoreElements()); @@ -75,6 +97,14 @@ export interface BrowserConfig { function initChromiumOptions(browserType: Browsers, acceptInsecureCerts: boolean) { const options = browserType === Browsers.Chrome ? new chrome.Options() : new edge.Options(); + const { + headlessBrowser, + certValidation, + remoteDebug, + browserBinaryPath, + noCache, + chromiumUserPrefs, + } = getConfiguration(); options.addArguments( // Disables the sandbox for all process types that are normally sandboxed. @@ -162,6 +192,7 @@ async function attemptToCreateCommand( const attemptId = ++attemptCounter; log.debug('[webdriver] Creating session'); const remoteSessionUrl = process.env.REMOTE_SESSION_URL; + const { headlessBrowser, throttleOption, noCache } = getConfiguration(); const buildDriverInstance = async () => { switch (browserType) { diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 70e1f039bc3ba..aab4757706bd1 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -185,7 +185,7 @@ export function Histogram({ } & .lnsExpressionRenderer { - width: ${attributes.visualizationType === 'lnsMetric' ? '90$' : '100%'}; + width: ${attributes.visualizationType === 'lnsMetric' ? '90%' : '100%'}; margin: auto; box-shadow: ${attributes.visualizationType === 'lnsMetric' ? boxShadow : 'none'}; } diff --git a/test/api_integration/apis/esql/errors.ts b/test/api_integration/apis/esql/errors.ts index e74f86efcb44c..5dcc951e95484 100644 --- a/test/api_integration/apis/esql/errors.ts +++ b/test/api_integration/apis/esql/errors.ts @@ -237,7 +237,8 @@ export default function ({ getService }: FtrProviderContext) { await cleanup(); }); - it(`Checking error messages`, async () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/188109 + it.skip(`Checking error messages`, async () => { for (const { query, error } of queryToErrors) { const jsonBody = await sendESQLQuery(query); diff --git a/test/functional/apps/home/_welcome.ts b/test/functional/apps/home/_welcome.ts index d61afd879090e..6c8bda90de699 100644 --- a/test/functional/apps/home/_welcome.ts +++ b/test/functional/apps/home/_welcome.ts @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'home']); const deployment = getService('deployment'); - describe('Welcome interstitial', () => { + // FLAKY: https://github.com/elastic/kibana/issues/186168 + describe.skip('Welcome interstitial', () => { beforeEach(async () => { // Need to navigate to page first to clear storage before test can be run await PageObjects.common.navigateToUrl('home', undefined); diff --git a/x-pack/packages/kbn-elastic-assistant-common/constants.ts b/x-pack/packages/kbn-elastic-assistant-common/constants.ts index 96af59095ab87..144bbcaeb0226 100755 --- a/x-pack/packages/kbn-elastic-assistant-common/constants.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/constants.ts @@ -10,20 +10,22 @@ export const ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION = '1'; export const ELASTIC_AI_ASSISTANT_URL = '/api/security_ai_assistant'; export const ELASTIC_AI_ASSISTANT_INTERNAL_URL = '/internal/elastic_assistant'; -export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL = `${ELASTIC_AI_ASSISTANT_INTERNAL_URL}/current_user/conversations`; +export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL = `${ELASTIC_AI_ASSISTANT_URL}/current_user/conversations`; export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BY_ID = `${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/{id}`; -export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BY_ID_MESSAGES = `${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BY_ID}/messages`; + +export const ELASTIC_AI_ASSISTANT_INTERNAL_CONVERSATIONS_URL = `${ELASTIC_AI_ASSISTANT_INTERNAL_URL}/current_user/conversations`; +export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BY_ID_MESSAGES = `${ELASTIC_AI_ASSISTANT_INTERNAL_CONVERSATIONS_URL}/{id}/messages`; export const ELASTIC_AI_ASSISTANT_CHAT_COMPLETE_URL = `${ELASTIC_AI_ASSISTANT_URL}/chat/complete`; -export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION = `${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/_bulk_action`; +export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION = `${ELASTIC_AI_ASSISTANT_INTERNAL_CONVERSATIONS_URL}/_bulk_action`; export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND = `${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/_find`; -export const ELASTIC_AI_ASSISTANT_PROMPTS_URL = `${ELASTIC_AI_ASSISTANT_INTERNAL_URL}/prompts`; +export const ELASTIC_AI_ASSISTANT_PROMPTS_URL = `${ELASTIC_AI_ASSISTANT_URL}/prompts`; export const ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION = `${ELASTIC_AI_ASSISTANT_PROMPTS_URL}/_bulk_action`; export const ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND = `${ELASTIC_AI_ASSISTANT_PROMPTS_URL}/_find`; -export const ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL = `${ELASTIC_AI_ASSISTANT_INTERNAL_URL}/anonymization_fields`; +export const ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL = `${ELASTIC_AI_ASSISTANT_URL}/anonymization_fields`; export const ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION = `${ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL}/_bulk_action`; export const ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_FIND = `${ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL}/_find`; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/bulk_update_anonymization_fields.test.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/bulk_update_anonymization_fields.test.ts index 544c8c1606c3d..88e9c0febba13 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/bulk_update_anonymization_fields.test.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/bulk_update_anonymization_fields.test.ts @@ -48,7 +48,7 @@ describe('bulkUpdateAnonymizationFields', () => { ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION, { method: 'POST', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, body: JSON.stringify({ create: [], update: [], @@ -71,7 +71,7 @@ describe('bulkUpdateAnonymizationFields', () => { ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION, { method: 'POST', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, body: JSON.stringify({ create: [anonymizationField1, anonymizationField2], update: [], @@ -93,7 +93,7 @@ describe('bulkUpdateAnonymizationFields', () => { ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION, { method: 'POST', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, body: JSON.stringify({ update: [anonymizationField1, anonymizationField2], delete: { ids: [] }, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/bulk_update_anonymization_fields.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/bulk_update_anonymization_fields.ts index 72d73cc2a5929..9745e7ce38662 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/bulk_update_anonymization_fields.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/bulk_update_anonymization_fields.ts @@ -26,7 +26,7 @@ export const bulkUpdateAnonymizationFields = async ( ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION, { method: 'POST', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, body: JSON.stringify(anonymizationFieldsActions), } ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields.test.tsx index 7c10597eaed87..5352e60bcea45 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields.test.tsx @@ -45,14 +45,14 @@ describe('useFetchAnonymizationFields', () => { const { waitForNextUpdate } = renderHook(() => useFetchAnonymizationFields()); await waitForNextUpdate(); expect(http.fetch).toHaveBeenCalledWith( - '/internal/elastic_assistant/anonymization_fields/_find', + '/api/security_ai_assistant/anonymization_fields/_find', { method: 'GET', query: { page: 1, per_page: 1000, }, - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, signal: undefined, } ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields.ts index d2f07124f04b0..657216b9079cd 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/anonymization_fields/use_fetch_anonymization_fields.ts @@ -36,7 +36,7 @@ export const CACHING_KEYS = [ ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_FIND, QUERY.page, QUERY.per_page, - API_VERSIONS.internal.v1, + API_VERSIONS.public.v1, ]; export const useFetchAnonymizationFields = (payload?: UseFetchAnonymizationFieldsParams) => { @@ -50,7 +50,7 @@ export const useFetchAnonymizationFields = (payload?: UseFetchAnonymizationField async () => http.fetch(ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_FIND, { method: 'GET', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, query: QUERY, signal: payload?.signal, }), diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/conversations.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/conversations.test.tsx index 5f7d7cefaf450..7e87ca79e88ab 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/conversations.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/conversations.test.tsx @@ -36,11 +36,11 @@ describe('conversations api', () => { await waitForNextUpdate(); expect(deleteProps.http.fetch).toHaveBeenCalledWith( - '/internal/elastic_assistant/current_user/conversations/test', + '/api/security_ai_assistant/current_user/conversations/test', { method: 'DELETE', signal: undefined, - version: '1', + version: '2023-10-31', } ); expect(toasts.addError).not.toHaveBeenCalled(); @@ -62,11 +62,11 @@ describe('conversations api', () => { await waitForNextUpdate(); expect(getProps.http.fetch).toHaveBeenCalledWith( - '/internal/elastic_assistant/current_user/conversations/test', + '/api/security_ai_assistant/current_user/conversations/test', { method: 'GET', signal: undefined, - version: '1', + version: '2023-10-31', } ); expect(toasts.addError).not.toHaveBeenCalled(); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/conversations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/conversations.ts index e6c6b2925f337..54bac7e563acc 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/conversations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/conversations.ts @@ -44,7 +44,7 @@ export const getConversationById = async ({ try { const response = await http.fetch(`${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/${id}`, { method: 'GET', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, signal, }); @@ -84,7 +84,7 @@ export const getUserConversations = async ({ ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND, { method: 'GET', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, signal, } ); @@ -125,7 +125,7 @@ export const createConversation = async ({ try { const response = await http.post(ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL, { body: JSON.stringify(conversation), - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, signal, }); @@ -168,7 +168,7 @@ export const deleteConversation = async ({ try { const response = await http.fetch(`${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/${id}`, { method: 'DELETE', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, signal, }); @@ -237,7 +237,7 @@ export const updateConversation = async ({ headers: { 'Content-Type': 'application/json', }, - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, signal, } ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.test.tsx index 2c6ed953b56f3..652764212e996 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.test.tsx @@ -47,14 +47,14 @@ describe('useFetchCurrentUserConversations', () => { ); await waitForNextUpdate(); expect(defaultProps.http.fetch).toHaveBeenCalledWith( - '/internal/elastic_assistant/current_user/conversations/_find', + '/api/security_ai_assistant/current_user/conversations/_find', { method: 'GET', query: { page: 1, perPage: 100, }, - version: '1', + version: '2023-10-31', signal: undefined, } ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.ts index 58be08317d40c..68612e3e22397 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/conversations/use_fetch_current_user_conversations.ts @@ -47,7 +47,7 @@ export const CONVERSATIONS_QUERY_KEYS = [ ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND, query.page, query.perPage, - API_VERSIONS.internal.v1, + API_VERSIONS.public.v1, ]; export const useFetchCurrentUserConversations = ({ @@ -62,7 +62,7 @@ export const useFetchCurrentUserConversations = ({ async () => http.fetch(ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND, { method: 'GET', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, query, signal, }), diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.test.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.test.ts index d6ba2e726a76f..fef992bc14b26 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.test.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.test.ts @@ -49,7 +49,7 @@ describe('bulkUpdatePrompts', () => { expect(httpMock.fetch).toHaveBeenCalledWith(ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, { method: 'POST', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, body: JSON.stringify({ create: [], update: [], @@ -69,7 +69,7 @@ describe('bulkUpdatePrompts', () => { expect(httpMock.fetch).toHaveBeenCalledWith(ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, { method: 'POST', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, body: JSON.stringify({ create: [prompt1, prompt2], update: [], @@ -88,7 +88,7 @@ describe('bulkUpdatePrompts', () => { expect(httpMock.fetch).toHaveBeenCalledWith(ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, { method: 'POST', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, body: JSON.stringify({ update: [prompt1, prompt2], delete: { ids: [] }, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.ts index 2d7b053d7acbd..f9b470c46516c 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/bulk_update_prompts.ts @@ -26,7 +26,7 @@ export const bulkUpdatePrompts = async ( ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, { method: 'POST', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, body: JSON.stringify(prompts), } ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx index 3ec1586d6cb44..8e228d2787a36 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.test.tsx @@ -44,14 +44,14 @@ describe('useFetchPrompts', () => { await act(async () => { const { waitForNextUpdate } = renderHook(() => useFetchPrompts()); await waitForNextUpdate(); - expect(http.fetch).toHaveBeenCalledWith('/internal/elastic_assistant/prompts/_find', { + expect(http.fetch).toHaveBeenCalledWith('/api/security_ai_assistant/prompts/_find', { method: 'GET', query: { page: 1, per_page: 1000, filter: 'consumer:*', }, - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, signal: undefined, }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.ts index 18a229e524dc7..4c0ca9c0a9c89 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api/prompts/use_fetch_prompts.ts @@ -44,7 +44,7 @@ export const useFetchPrompts = (payload?: UseFetchPromptsParams) => { QUERY.page, QUERY.per_page, QUERY.filter, - API_VERSIONS.internal.v1, + API_VERSIONS.public.v1, ]; return useQuery( @@ -52,7 +52,7 @@ export const useFetchPrompts = (payload?: UseFetchPromptsParams) => { async () => http.fetch(ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND, { method: 'GET', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, query: QUERY, signal: payload?.signal, }), @@ -87,7 +87,7 @@ export const getPrompts = async ({ try { return await http.fetch(ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND, { method: 'GET', - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, signal, }); } catch (error) { diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 27fc64d44966f..30cbcbce75f0a 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -75,6 +75,8 @@ export const DATA_VIEW_INDEX_PATTERN = 'logs-*'; export const SECURITY_DEFAULT_DATA_VIEW_ID = 'security-solution-default'; +export const ALERTS_INDEX_PATTERN = '.alerts-security.alerts-*'; + export const CSP_INGEST_TIMESTAMP_PIPELINE = 'cloud_security_posture_add_ingest_timestamp_pipeline'; export const CSP_LATEST_FINDINGS_INGEST_TIMESTAMP_PIPELINE = 'cloud_security_posture_latest_index_add_ingest_timestamp_pipeline'; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/bulk_action.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/bulk_action.ts index a89c37d439716..31b80b880bcc9 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/bulk_action.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/bulk_action/bulk_action.ts @@ -44,6 +44,9 @@ export const defineBulkActionCspBenchmarkRulesRoute = (router: CspRouter) => .post({ access: 'internal', path: CSP_BENCHMARK_RULES_BULK_ACTION_ROUTE_PATH, + options: { + tags: ['access:cloud-security-posture-read'], + }, }) .addVersion( { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.ts index 8dc8f36554600..c1b481b01f2cd 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/find/find.ts @@ -29,6 +29,9 @@ export const defineFindCspBenchmarkRuleRoute = (router: CspRouter) => .get({ access: 'internal', path: FIND_CSP_BENCHMARK_RULE_ROUTE_PATH, + options: { + tags: ['access:cloud-security-posture-read'], + }, }) .addVersion( { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/get_states/get_states.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/get_states/get_states.ts index e7c28d345c52d..a55bcd92ab3c8 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/get_states/get_states.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmark_rules/get_states/get_states.ts @@ -16,6 +16,9 @@ export const defineGetCspBenchmarkRulesStatesRoute = (router: CspRouter) => .get({ access: 'internal', path: CSP_GET_BENCHMARK_RULES_STATE_ROUTE_PATH, + options: { + tags: ['access:cloud-security-posture-read'], + }, }) .addVersion( { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts index 4c9294248e1e7..851fa865566f7 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts @@ -65,6 +65,9 @@ export const defineGetComplianceDashboardRoute = (router: CspRouter) => .get({ access: 'internal', path: STATS_ROUTE_PATH, + options: { + tags: ['access:cloud-security-posture-read'], + }, }) .addVersion( { diff --git a/x-pack/plugins/cloud_security_posture/server/routes/detection_engine/get_detection_engine_alerts_count_by_rule_tags.ts b/x-pack/plugins/cloud_security_posture/server/routes/detection_engine/get_detection_engine_alerts_count_by_rule_tags.ts index d464563155023..026cf68819ab2 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/detection_engine/get_detection_engine_alerts_count_by_rule_tags.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/detection_engine/get_detection_engine_alerts_count_by_rule_tags.ts @@ -57,6 +57,9 @@ export const defineGetDetectionEngineAlertsStatus = (router: CspRouter) => .get({ access: 'internal', path: GET_DETECTION_RULE_ALERTS_STATUS_PATH, + options: { + tags: ['access:cloud-security-posture-read'], + }, }) .addVersion( { diff --git a/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts b/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts index 47213cb0d278e..94788d2d1d926 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts @@ -118,7 +118,7 @@ export const bulkActionAnonymizationFieldsRoute = ( ) => { router.versioned .post({ - access: 'internal', + access: 'public', path: ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION, options: { tags: ['access:securitySolution-updateAIAssistantAnonymization'], @@ -129,7 +129,7 @@ export const bulkActionAnonymizationFieldsRoute = ( }) .addVersion( { - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, validate: { request: { body: buildRouteValidationWithZod(PerformBulkActionRequestBody), diff --git a/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/find_route.ts b/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/find_route.ts index c0383b1b3b38c..904a80d6a3ea4 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/find_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/anonymization_fields/find_route.ts @@ -30,7 +30,7 @@ export const findAnonymizationFieldsRoute = ( ) => { router.versioned .get({ - access: 'internal', + access: 'public', path: ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_FIND, options: { tags: ['access:elasticAssistant'], @@ -38,7 +38,7 @@ export const findAnonymizationFieldsRoute = ( }) .addVersion( { - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, validate: { request: { query: buildRouteValidationWithZod(FindAnonymizationFieldsRequestQuery), diff --git a/x-pack/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.ts b/x-pack/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.ts index cfcd6d8cc05d6..d90b01b78cfa7 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.ts @@ -112,7 +112,7 @@ const buildBulkResponse = ( export const bulkPromptsRoute = (router: ElasticAssistantPluginRouter, logger: Logger) => { router.versioned .post({ - access: 'internal', + access: 'public', path: ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION, options: { tags: ['access:elasticAssistant'], @@ -123,7 +123,7 @@ export const bulkPromptsRoute = (router: ElasticAssistantPluginRouter, logger: L }) .addVersion( { - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, validate: { request: { body: buildRouteValidationWithZod(PerformBulkActionRequestBody), diff --git a/x-pack/plugins/elastic_assistant/server/routes/prompts/find_route.ts b/x-pack/plugins/elastic_assistant/server/routes/prompts/find_route.ts index 8838db3c44943..142b63a7d04b5 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/prompts/find_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/prompts/find_route.ts @@ -23,7 +23,7 @@ import { UPGRADE_LICENSE_MESSAGE, hasAIAssistantLicense } from '../helpers'; export const findPromptsRoute = (router: ElasticAssistantPluginRouter, logger: Logger) => { router.versioned .get({ - access: 'internal', + access: 'public', path: ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND, options: { tags: ['access:elasticAssistant'], @@ -31,7 +31,7 @@ export const findPromptsRoute = (router: ElasticAssistantPluginRouter, logger: L }) .addVersion( { - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, validate: { request: { query: buildRouteValidationWithZod(FindPromptsRequestQuery), diff --git a/x-pack/plugins/elastic_assistant/server/routes/user_conversations/create_route.ts b/x-pack/plugins/elastic_assistant/server/routes/user_conversations/create_route.ts index 9c02586a60b88..f2f469da15966 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/user_conversations/create_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/user_conversations/create_route.ts @@ -21,7 +21,7 @@ import { performChecks } from '../helpers'; export const createConversationRoute = (router: ElasticAssistantPluginRouter): void => { router.versioned .post({ - access: 'internal', + access: 'public', path: ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL, options: { @@ -30,7 +30,7 @@ export const createConversationRoute = (router: ElasticAssistantPluginRouter): v }) .addVersion( { - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, validate: { request: { body: buildRouteValidationWithZod(ConversationCreateProps), diff --git a/x-pack/plugins/elastic_assistant/server/routes/user_conversations/delete_route.ts b/x-pack/plugins/elastic_assistant/server/routes/user_conversations/delete_route.ts index b39f898eaeaa1..5d761c09f682c 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/user_conversations/delete_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/user_conversations/delete_route.ts @@ -19,7 +19,7 @@ import { UPGRADE_LICENSE_MESSAGE, hasAIAssistantLicense } from '../helpers'; export const deleteConversationRoute = (router: ElasticAssistantPluginRouter) => { router.versioned .delete({ - access: 'internal', + access: 'public', path: ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BY_ID, options: { tags: ['access:elasticAssistant'], @@ -27,7 +27,7 @@ export const deleteConversationRoute = (router: ElasticAssistantPluginRouter) => }) .addVersion( { - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, validate: { request: { params: buildRouteValidationWithZod(DeleteConversationRequestParams), diff --git a/x-pack/plugins/elastic_assistant/server/routes/user_conversations/find_route.ts b/x-pack/plugins/elastic_assistant/server/routes/user_conversations/find_route.ts index 8db36466c9bad..6eda3e37645c5 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/user_conversations/find_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/user_conversations/find_route.ts @@ -26,7 +26,7 @@ import { UPGRADE_LICENSE_MESSAGE, hasAIAssistantLicense } from '../helpers'; export const findUserConversationsRoute = (router: ElasticAssistantPluginRouter) => { router.versioned .get({ - access: 'internal', + access: 'public', path: ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND, options: { tags: ['access:elasticAssistant'], @@ -34,7 +34,7 @@ export const findUserConversationsRoute = (router: ElasticAssistantPluginRouter) }) .addVersion( { - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, validate: { request: { query: buildRouteValidationWithZod(FindConversationsRequestQuery), diff --git a/x-pack/plugins/elastic_assistant/server/routes/user_conversations/read_route.ts b/x-pack/plugins/elastic_assistant/server/routes/user_conversations/read_route.ts index 12020d7fa51d9..dd540897b0ece 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/user_conversations/read_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/user_conversations/read_route.ts @@ -21,7 +21,7 @@ import { UPGRADE_LICENSE_MESSAGE, hasAIAssistantLicense } from '../helpers'; export const readConversationRoute = (router: ElasticAssistantPluginRouter) => { router.versioned .get({ - access: 'internal', + access: 'public', path: ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BY_ID, options: { tags: ['access:elasticAssistant'], @@ -29,7 +29,7 @@ export const readConversationRoute = (router: ElasticAssistantPluginRouter) => { }) .addVersion( { - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, validate: { request: { params: buildRouteValidationWithZod(ReadConversationRequestParams), diff --git a/x-pack/plugins/elastic_assistant/server/routes/user_conversations/update_route.ts b/x-pack/plugins/elastic_assistant/server/routes/user_conversations/update_route.ts index 069c189609f23..4ad819ef0caa0 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/user_conversations/update_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/user_conversations/update_route.ts @@ -24,7 +24,7 @@ import { performChecks } from '../helpers'; export const updateConversationRoute = (router: ElasticAssistantPluginRouter) => { router.versioned .put({ - access: 'internal', + access: 'public', path: ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BY_ID, options: { tags: ['access:elasticAssistant'], @@ -32,7 +32,7 @@ export const updateConversationRoute = (router: ElasticAssistantPluginRouter) => }) .addVersion( { - version: API_VERSIONS.internal.v1, + version: API_VERSIONS.public.v1, validate: { request: { body: buildRouteValidationWithZod(ConversationUpdateProps), diff --git a/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts b/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts index 4fc4fa33ea5b0..6119a8b13e8b8 100644 --- a/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts @@ -182,7 +182,7 @@ queue: cy.contains('Name is required'); cy.contains('URL is required'); - cy.contains('Service Token is required'); + cy.contains('Service token is required'); shouldDisplayError(SETTINGS_OUTPUTS.NAME_INPUT); shouldDisplayError('serviceTokenSecretInput'); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx index c57d26603c889..e742005a37913 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx @@ -83,7 +83,7 @@ const kafkaSectionsLabels = [ 'Broker settings', ]; -const remoteEsOutputLabels = ['Hosts', 'Service Token']; +const remoteEsOutputLabels = ['Hosts', 'Service token']; describe('EditOutputFlyout', () => { const mockStartServices = (isServerlessEnabled?: boolean) => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_remote_es.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_remote_es.tsx index 354c6ce7d3821..658a42115381b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_remote_es.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_remote_es.tsx @@ -82,7 +82,7 @@ export const OutputFormRemoteEsSection: React.FunctionComponent = (props) label={ } {...inputs.serviceTokenInput.formRowProps} @@ -105,7 +105,7 @@ export const OutputFormRemoteEsSection: React.FunctionComponent = (props) { @@ -42,8 +43,8 @@ describe('SecretFormRow', () => { expect(queryByText(initialValue)).not.toBeInTheDocument(); }); - it('should not enable the replace button if the row is disabled', () => { - const { getByText } = renderReactTestingLibraryWithI18n( + it('should not enable action links if the row is disabled', () => { + const { getByText, queryByText } = renderReactTestingLibraryWithI18n( { ); - expect(getByText('Replace Test Secret').closest('button')).toBeDisabled(); + expect(getByText('The saved Test Secret is hidden.')).toBeInTheDocument(); + expect(queryByText('Replace Test Secret')).not.toBeInTheDocument(); + expect(queryByText('Click to use secret storage instead')).not.toBeInTheDocument(); + expect(queryByText('Click to use plain text storage instead')).not.toBeInTheDocument(); }); it('should call the cancelEdit function when the cancel button is clicked', () => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.tsx index 34383ae62f287..3614499905b2e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.tsx @@ -55,32 +55,45 @@ export const SecretFormRow: React.FC<{ const [editMode, setEditMode] = useState(isConvertedToSecret || !initialValue); const valueHiddenPanel = ( - - - - - setEditMode(true)} - color="primary" - iconType="refresh" - iconSide="left" - size="xs" - > - - + {disabled ? ( + + + + ) : ( + <> + + + + + setEditMode(true)} + color="primary" + iconType="refresh" + iconSide="left" + size="xs" + > + + + + )} ); @@ -134,6 +147,7 @@ export const SecretFormRow: React.FC<{ ); const helpText = useMemo(() => { + if (disabled) return null; if (isConvertedToSecret) return ( @@ -172,9 +186,9 @@ export const SecretFormRow: React.FC<{ /> ); return undefined; - }, [initialValue, isConvertedToSecret, onToggleSecretStorage]); + }, [disabled, initialValue, isConvertedToSecret, onToggleSecretStorage]); - const plainTextHelp = ( + const plainTextHelp = disabled ? null : ( + simulateResult = await retryTransientEsErrors(async () => esClient.indices.simulateTemplate({ name: await getIndexTemplate(esClient, dataStreamName), }) ); settings = simulateResult.template.settings; - mappings = simulateResult.template.mappings; - // @ts-expect-error template is not yet typed with DLM + mappings = fillConstantKeywordValues( + currentBackingIndexConfig?.mappings || {}, + simulateResult.template.mappings + ); + lifecycle = simulateResult.template.lifecycle; // for now, remove from object so as not to update stream or data stream properties of the index until type and name @@ -1063,6 +1067,7 @@ const updateExistingDataStream = async ({ subobjectsFieldChanged ) { logger.info(`Mappings update for ${dataStreamName} failed due to ${err}`); + logger.trace(`Attempted mappings: ${mappings}`); if (options?.skipDataStreamRollover === true) { logger.info( `Skipping rollover for ${dataStreamName} as "skipDataStreamRollover" is enabled` @@ -1075,6 +1080,7 @@ const updateExistingDataStream = async ({ } } logger.error(`Mappings update for ${dataStreamName} failed due to unexpected error: ${err}`); + logger.trace(`Attempted mappings: ${mappings}`); if (options?.ignoreMappingUpdateErrors === true) { logger.info(`Ignore mapping update errors as "ignoreMappingUpdateErrors" is enabled`); return; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/utils.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/utils.test.ts new file mode 100644 index 0000000000000..a411ba32d2954 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/utils.test.ts @@ -0,0 +1,226 @@ +/* + * 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 { fillConstantKeywordValues } from './utils'; + +describe('fillConstantKeywordValues', () => { + const oldMappings = { + dynamic: false, + _meta: { + managed_by: 'fleet', + managed: true, + package: { + name: 'elastic_agent', + }, + }, + dynamic_templates: [ + { + ecs_timestamp: { + match: '@timestamp', + mapping: { + ignore_malformed: false, + type: 'date', + }, + }, + }, + ], + date_detection: false, + properties: { + '@timestamp': { + type: 'date', + ignore_malformed: false, + }, + load: { + properties: { + '1': { + type: 'double', + }, + '5': { + type: 'double', + }, + '15': { + type: 'double', + }, + }, + }, + event: { + properties: { + agent_id_status: { + type: 'keyword', + ignore_above: 1024, + }, + dataset: { + type: 'constant_keyword', + value: 'elastic_agent.metricbeat', + }, + ingested: { + type: 'date', + format: 'strict_date_time_no_millis||strict_date_optional_time||epoch_millis', + ignore_malformed: false, + }, + }, + }, + message: { + type: 'match_only_text', + }, + 'dot.field': { + type: 'keyword', + }, + constant_keyword_without_value: { + type: 'constant_keyword', + }, + }, + }; + + const newMappings = { + dynamic: false, + _meta: { + managed_by: 'fleet', + managed: true, + package: { + name: 'elastic_agent', + }, + }, + dynamic_templates: [ + { + ecs_timestamp: { + match: '@timestamp', + mapping: { + ignore_malformed: false, + type: 'date', + }, + }, + }, + ], + date_detection: false, + properties: { + '@timestamp': { + type: 'date', + ignore_malformed: false, + }, + load: { + properties: { + '1': { + type: 'double', + }, + '5': { + type: 'double', + }, + '15': { + type: 'double', + }, + }, + }, + event: { + properties: { + agent_id_status: { + type: 'keyword', + ignore_above: 1024, + }, + dataset: { + type: 'constant_keyword', + }, + ingested: { + type: 'date', + format: 'strict_date_time_no_millis||strict_date_optional_time||epoch_millis', + ignore_malformed: false, + }, + }, + }, + message: { + type: 'match_only_text', + }, + 'dot.field': { + type: 'keyword', + }, + some_new_field: { + type: 'keyword', + }, + constant_keyword_without_value: { + type: 'constant_keyword', + }, + }, + }; + + it('should fill in missing constant_keyword values from old mappings correctly', () => { + // @ts-ignore + expect(fillConstantKeywordValues(oldMappings, newMappings)).toEqual({ + dynamic: false, + _meta: { + managed_by: 'fleet', + managed: true, + package: { + name: 'elastic_agent', + }, + }, + dynamic_templates: [ + { + ecs_timestamp: { + match: '@timestamp', + mapping: { + ignore_malformed: false, + type: 'date', + }, + }, + }, + ], + date_detection: false, + properties: { + '@timestamp': { + type: 'date', + ignore_malformed: false, + }, + load: { + properties: { + '1': { + type: 'double', + }, + '5': { + type: 'double', + }, + '15': { + type: 'double', + }, + }, + }, + event: { + properties: { + agent_id_status: { + type: 'keyword', + ignore_above: 1024, + }, + dataset: { + type: 'constant_keyword', + value: 'elastic_agent.metricbeat', + }, + ingested: { + type: 'date', + format: 'strict_date_time_no_millis||strict_date_optional_time||epoch_millis', + ignore_malformed: false, + }, + }, + }, + message: { + type: 'match_only_text', + }, + 'dot.field': { + type: 'keyword', + }, + some_new_field: { + type: 'keyword', + }, + constant_keyword_without_value: { + type: 'constant_keyword', + }, + }, + }); + }); + + it('should return the same mappings if old mappings are not provided', () => { + // @ts-ignore + expect(fillConstantKeywordValues({}, newMappings)).toMatchObject(newMappings); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/utils.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/utils.ts index e435e54a828df..6a34beb371082 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/utils.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/utils.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { USER_SETTINGS_TEMPLATE_SUFFIX } from '../../../../constants'; @@ -12,3 +13,34 @@ type UserSettingsTemplateName = `${TemplateBaseName}${typeof USER_SETTINGS_TEMPL export const isUserSettingsTemplate = (name: string): name is UserSettingsTemplateName => name.endsWith(USER_SETTINGS_TEMPLATE_SUFFIX); + +// For any `constant_keyword` fields in `newMappings` that don't have a `value`, access the same field in +// the `oldMappings` and fill in the value from there +export const fillConstantKeywordValues = ( + oldMappings: MappingTypeMapping, + newMappings: MappingTypeMapping +) => { + const filledMappings = JSON.parse(JSON.stringify(newMappings)) as MappingTypeMapping; + const deepGet = (obj: any, keys: string[]) => keys.reduce((xs, x) => xs?.[x] ?? undefined, obj); + + const fillEmptyConstantKeywordFields = (mappings: unknown, currentPath: string[] = []) => { + if (!mappings) return; + for (const [key, potentialField] of Object.entries(mappings)) { + const path = [...currentPath, key]; + if (typeof potentialField === 'object') { + if (potentialField.type === 'constant_keyword' && potentialField.value === undefined) { + const valueFromOldMappings = deepGet(oldMappings.properties, [...path, 'value']); + if (valueFromOldMappings !== undefined) { + potentialField.value = valueFromOldMappings; + } + } else if (potentialField.properties && typeof potentialField.properties === 'object') { + fillEmptyConstantKeywordFields(potentialField.properties, [...path, 'properties']); + } + } + } + }; + + fillEmptyConstantKeywordFields(filledMappings.properties); + + return filledMappings; +}; diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index 2096820e82dad..58385fb544a57 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -21,7 +21,7 @@ import { partition } from 'lodash'; import { getAssetFromAssetsMap, getPathParts } from '../../archive'; import { KibanaAssetType, KibanaSavedObjectType } from '../../../../types'; -import type { AssetReference, AssetParts, Installation, PackageSpecTags } from '../../../../types'; +import type { AssetReference, Installation, PackageSpecTags } from '../../../../types'; import type { KibanaAssetReference, PackageInstallContext } from '../../../../../common/types'; import { indexPatternTypes, @@ -242,7 +242,8 @@ export async function installKibanaAssetsAndReferences({ }) { const { savedObjectsImporter, savedObjectTagAssignmentService, savedObjectTagClient } = getSpaceAwareSaveobjectsClients(spaceId); - const kibanaAssets = await getKibanaAssets(packageInstallContext); + // This is where the memory consumption is rising up in the first place + const kibanaAssets = getKibanaAssets(packageInstallContext); if (installedPkg) { await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg, spaceId }); } @@ -323,9 +324,9 @@ export async function deleteKibanaAssetsAndReferencesForSpace({ await saveKibanaAssetsRefs(savedObjectsClient, pkgName, [], true); } -export async function getKibanaAssets( +export function getKibanaAssets( packageInstallContext: PackageInstallContext -): Promise> { +): Record { const kibanaAssetTypes = Object.values(KibanaAssetType); const isKibanaAssetType = (path: string) => { const parts = getPathParts(path); @@ -333,36 +334,20 @@ export async function getKibanaAssets( return parts.service === 'kibana' && (kibanaAssetTypes as string[]).includes(parts.type); }; - const filteredPaths = packageInstallContext.paths - .filter(isKibanaAssetType) - .map<[string, AssetParts]>((path) => [path, getPathParts(path)]); + const result = Object.fromEntries( + kibanaAssetTypes.map((type) => [type, []]) + ) as Record; - const assetArrays: Array> = []; - for (const assetType of kibanaAssetTypes) { - const matching = filteredPaths.filter(([path, parts]) => parts.type === assetType); + packageInstallContext.paths.filter(isKibanaAssetType).forEach((path) => { + const buffer = getAssetFromAssetsMap(packageInstallContext.assetsMap, path); + const asset = JSON.parse(buffer.toString('utf8')); - assetArrays.push( - Promise.all( - matching.map(([path]) => { - const buffer = getAssetFromAssetsMap(packageInstallContext.assetsMap, path); - - // cache values are buffers. convert to string / JSON - return JSON.parse(buffer.toString('utf8')); - }) - ) - ); - } - - const resolvedAssets = await Promise.all(assetArrays); - - const result = {} as Record; - - for (const [index, assetType] of kibanaAssetTypes.entries()) { - const expectedType = KibanaSavedObjectTypeMapping[assetType]; - const properlyTypedAssets = resolvedAssets[index].filter(({ type }) => type === expectedType); - - result[assetType] = properlyTypedAssets; - } + const assetType = getPathParts(path).type as KibanaAssetType; + const soType = KibanaSavedObjectTypeMapping[assetType]; + if (asset.type === soType) { + result[assetType].push(asset); + } + }); return result; } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 5369da9da82e2..ba781c18ad9c7 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -14,6 +14,7 @@ import { SavedObjectsClient } from '@kbn/core/server'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; import { SavedObjectsUtils, SavedObjectsErrorHelpers } from '@kbn/core/server'; +import minVersion from 'semver/ranges/min-version'; import { updateIndexSettings } from '../elasticsearch/index/update_settings'; @@ -43,6 +44,7 @@ import { auditLoggingService } from '../../audit_logging'; import { FleetError, PackageRemovalError } from '../../../errors'; import { populatePackagePolicyAssignedAgentsCount } from '../../package_policies/populate_package_policy_assigned_agents_count'; +import * as Registry from '../registry'; import { getInstallation, kibanaSavedObjectTypes } from '.'; @@ -112,7 +114,14 @@ export async function removeInstallation(options: { return installedAssets; } -async function deleteKibanaAssets( +/** + * This method resolves saved objects before deleting them. It is needed when + * deleting assets that were installed in 7.x to mitigate the breaking change + * that occurred in 8.0. This is a memory-intensive operation as it requires + * loading all the saved objects into memory. It is generally better to delete + * assets directly if the package is known to be installed in 8.x or later. + */ +async function resolveAndDeleteKibanaAssets( installedObjects: KibanaAssetReference[], spaceId: string = DEFAULT_SPACE_ID ) { @@ -152,6 +161,26 @@ async function deleteKibanaAssets( return savedObjectsClient.bulkDelete(assetsToDelete, { namespace }); } +async function deleteKibanaAssets( + assetsToDelete: KibanaAssetReference[], + spaceId: string = DEFAULT_SPACE_ID +) { + const savedObjectsClient = new SavedObjectsClient( + appContextService.getSavedObjects().createInternalRepository() + ); + const namespace = SavedObjectsUtils.namespaceStringToId(spaceId); + + for (const asset of assetsToDelete) { + auditLoggingService.writeCustomSoAuditLog({ + action: 'delete', + id: asset.id, + savedObjectType: asset.type, + }); + } + + return savedObjectsClient.bulkDelete(assetsToDelete, { namespace }); +} + function deleteESAssets( installedObjects: EsAssetReference[], esClient: ElasticsearchClient @@ -237,9 +266,9 @@ async function deleteAssets( // then the other asset types await Promise.all([ ...deleteESAssets(otherAssets, esClient), - deleteKibanaAssets(installedKibana, spaceId), + resolveAndDeleteKibanaAssets(installedKibana, spaceId), Object.entries(installedInAdditionalSpacesKibana).map(([additionalSpaceId, kibanaAssets]) => - deleteKibanaAssets(kibanaAssets, additionalSpaceId) + resolveAndDeleteKibanaAssets(kibanaAssets, additionalSpaceId) ), ]); } catch (err) { @@ -299,8 +328,25 @@ export async function deleteKibanaSavedObjectsAssets({ .filter(({ type }) => kibanaSavedObjectTypes.includes(type)) .map(({ id, type }) => ({ id, type } as KibanaAssetReference)); + const registryInfo = await Registry.fetchInfo( + installedPkg.attributes.name, + installedPkg.attributes.version + ); + + const minKibana = registryInfo.conditions?.kibana?.version + ? minVersion(registryInfo.conditions.kibana.version) + : null; + try { - await deleteKibanaAssets(assetsToDelete, spaceIdToDelete); + // Compare Kibana versions to determine if the package could been installed + // only in 8.x or later. If so, we can skip SO resolution step altogether + // and delete the assets directly. Otherwise, we need to resolve the assets + // which might create high memory pressure if a package has a lot of assets. + if (minKibana && minKibana.major >= 8) { + await deleteKibanaAssets(assetsToDelete, spaceIdToDelete); + } else { + await resolveAndDeleteKibanaAssets(assetsToDelete, spaceIdToDelete); + } } catch (err) { // in the rollback case, partial installs are likely, so missing assets are not an error if (!SavedObjectsErrorHelpers.isNotFoundError(err)) { diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 041d803baad12..9ac93b97b08d3 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -856,7 +856,7 @@ export class Embeddable this.updateInput({ attributes: attrs, savedObjectId }); } - async openConfingPanel( + async openConfigPanel( startDependencies: LensPluginStartDependencies, isNewPanel?: boolean, deletePanel?: () => void diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.test.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.test.tsx index 13c0bcb23e562..e9daa06b9ac07 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.test.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action.test.tsx @@ -95,7 +95,7 @@ describe('open config panel action', () => { }; }, getIsEditable: () => true, - openConfingPanel: jest.fn().mockResolvedValue(Lens Config Panel Component), + openConfigPanel: jest.fn().mockResolvedValue(Lens Config Panel Component), getRoot: () => { return { openOverlay: jest.fn(), diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action_helpers.ts index 20378c33bf33a..ae7bd559d9d14 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action_helpers.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/edit_action_helpers.ts @@ -6,9 +6,10 @@ */ import React from 'react'; import './helpers.scss'; -import { tracksOverlays } from '@kbn/presentation-containers'; -import { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { toMountPoint } from '@kbn/react-kibana-mount'; +import { tracksOverlays } from '@kbn/presentation-containers'; +import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; +import { ViewMode } from '@kbn/embeddable-plugin/common'; import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { isLensEmbeddable } from '../utils'; import type { LensPluginStartDependencies } from '../../plugin'; @@ -24,10 +25,60 @@ interface Context extends StartServices { export async function isEditActionCompatible(embeddable: IEmbeddable) { if (!embeddable?.getInput) return false; // display the action only if dashboard is on editable mode - const inDashboardEditMode = embeddable.getInput().viewMode === 'edit'; + const inDashboardEditMode = embeddable.getInput().viewMode === ViewMode.EDIT; return Boolean(isLensEmbeddable(embeddable) && embeddable.getIsEditable() && inDashboardEditMode); } +type PanelConfigElement = React.ReactElement void }>; + +const openInlineLensConfigEditor = ( + startServices: StartServices, + embeddable: IEmbeddable, + EmbeddableInlineConfigEditor: PanelConfigElement +) => { + const rootEmbeddable = embeddable.getRoot(); + const overlayTracker = tracksOverlays(rootEmbeddable) ? rootEmbeddable : undefined; + + const handle = startServices.overlays.openFlyout( + toMountPoint( + React.createElement(function InlineLensConfigEditor() { + React.useEffect(() => { + document.body.style.overflowY = 'hidden'; + + return () => { + document.body.style.overflowY = 'initial'; + }; + }, []); + + return React.cloneElement(EmbeddableInlineConfigEditor, { + closeFlyout: () => { + overlayTracker?.clearOverlays(); + handle.close(); + }, + }); + }), + startServices + ), + { + size: 's', + type: 'push', + paddingSize: 'm', + 'data-test-subj': 'customizeLens', + className: 'lnsConfigPanel__overlay', + hideCloseButton: true, + onClose: (overlayRef) => { + overlayTracker?.clearOverlays(); + overlayRef.close(); + }, + outsideClickCloses: true, + } + ); + + overlayTracker?.openOverlay(handle, { + focusedPanelId: embeddable.id, + }); +}; + export async function executeEditAction({ embeddable, startDependencies, @@ -39,35 +90,10 @@ export async function executeEditAction({ if (!isCompatibleAction || !isLensEmbeddable(embeddable)) { throw new IncompatibleActionError(); } - const rootEmbeddable = embeddable.getRoot(); - const overlayTracker = tracksOverlays(rootEmbeddable) ? rootEmbeddable : undefined; - const ConfigPanel = await embeddable.openConfingPanel(startDependencies, isNewPanel, deletePanel); + + const ConfigPanel = await embeddable.openConfigPanel(startDependencies, isNewPanel, deletePanel); if (ConfigPanel) { - const handle = startServices.overlays.openFlyout( - toMountPoint( - React.cloneElement(ConfigPanel, { - closeFlyout: () => { - if (overlayTracker) overlayTracker.clearOverlays(); - handle.close(); - }, - }), - startServices - ), - { - className: 'lnsConfigPanel__overlay', - size: 's', - 'data-test-subj': 'customizeLens', - type: 'push', - paddingSize: 'm', - hideCloseButton: true, - onClose: (overlayRef) => { - if (overlayTracker) overlayTracker.clearOverlays(); - overlayRef.close(); - }, - outsideClickCloses: true, - } - ); - overlayTracker?.openOverlay(handle, { focusedPanelId: embeddable.id }); + openInlineLensConfigEditor(startServices, embeddable, ConfigPanel); } } diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template.tsx index 36ab11d45851a..26fd83adec20a 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template.tsx @@ -12,6 +12,7 @@ import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-templat import React, { useContext } from 'react'; import { useLocation } from 'react-router-dom'; import { FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public'; +import { useEntityManagerEnablementContext } from '../../../context/entity_manager_context/use_entity_manager_enablement_context'; import { useDefaultAiAssistantStarterPromptsForAPM } from '../../../hooks/use_default_ai_assistant_starter_prompts_for_apm'; import { KibanaEnvironmentContext } from '../../../context/kibana_environment_context/kibana_environment_context'; import { getPathForFeedback } from '../../../utils/get_path_for_feedback'; @@ -28,6 +29,7 @@ import { EntityEnablement } from '../../shared/entity_enablement'; // Paths that must skip the no data screen const bypassNoDataScreenPaths = ['/settings', '/diagnostics']; const APM_FEEDBACK_LINK = 'https://ela.st/services-feedback'; +const APM_NEW_EXPERIENCE_FEEDBACK_LINK = 'https://ela.st/entity-services-feedback'; /* * This template contains: @@ -69,6 +71,7 @@ export function ApmMainTemplate({ const { kibanaVersion, isCloudEnv, isServerlessEnv } = kibanaEnvironment; const basePath = http?.basePath.get(); const { config } = useApmPluginContext(); + const { isEntityManagerEnabled } = useEntityManagerEnablementContext(); const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate; @@ -135,7 +138,11 @@ export function ApmMainTemplate({ {pageTemplate}; - - return pageTemplate; } diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/index.tsx index e305743243a4b..826ad22743efa 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/index.tsx @@ -25,7 +25,7 @@ import { TechnicalPreviewBadge } from '../technical_preview_badge'; import { ApmPluginStartDeps } from '../../../plugin'; import { useEntityManagerEnablementContext } from '../../../context/entity_manager_context/use_entity_manager_enablement_context'; import { FeedbackModal } from './feedback_modal'; -import { UnauthorisedModal } from './unauthorized_modal'; +import { Unauthorized } from './unauthorized_modal'; export function EntityEnablement() { const [isFeedbackModalVisible, setsIsFeedbackModalVisible] = useState(false); @@ -56,7 +56,7 @@ export function EntityEnablement() { } }; - const handleEnableblement = async () => { + const handleEnablement = async () => { setIsLoading(true); try { const response = await entityManager.entityClient.enableManagedEntityDiscovery(); @@ -75,7 +75,7 @@ export function EntityEnablement() { } }; - const handdleOnCloseFeedback = () => { + const handleOnCloseFeedback = () => { setsIsFeedbackModalVisible(false); refetch(); }; @@ -93,7 +93,7 @@ export function EntityEnablement() { {isEntityManagerEnabled ? i18n.translate('xpack.apm.eemEnablement.enabled.', { @@ -149,8 +149,8 @@ export function EntityEnablement() { {isEntityManagerEnabled && ( - - {i18n.translate('xpack.apm.eemEnablement.restoveClassicView.', { + + {i18n.translate('xpack.apm.eemEnablement.restoreClassicView.', { defaultMessage: 'Restore classic view', })} @@ -158,9 +158,9 @@ export function EntityEnablement() { )} - setsIsUnauthorizedModalVisible(false)} /> diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/unauthorized_modal.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/unauthorized_modal.tsx index f728790f8c50f..8dd3682b61ff3 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/unauthorized_modal.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/unauthorized_modal.tsx @@ -29,7 +29,7 @@ import { } from '@elastic/eui'; import { useKibanaUrl } from '../../../hooks/use_kibana_url'; -export function UnauthorisedModal({ +export function Unauthorized({ isUnauthorizedModalVisible = false, onClose, }: { diff --git a/x-pack/plugins/observability_solution/apm/public/tutorial/config_agent/opentelemetry_instructions.tsx b/x-pack/plugins/observability_solution/apm/public/tutorial/config_agent/opentelemetry_instructions.tsx index e1e02b62ead93..4584326f2f3f2 100644 --- a/x-pack/plugins/observability_solution/apm/public/tutorial/config_agent/opentelemetry_instructions.tsx +++ b/x-pack/plugins/observability_solution/apm/public/tutorial/config_agent/opentelemetry_instructions.tsx @@ -32,7 +32,7 @@ export function OpenTelemetryInstructions({ apmServerUrl, secretToken }: Props) }, { setting: 'OTEL_EXPORTER_OTLP_HEADERS', - value: `"Authorization=Bearer ${secretToken ? secretToken : ''}"`, + value: `Authorization=Bearer ${secretToken ? secretToken : ''}`, }, { setting: 'OTEL_METRICS_EXPORTER', diff --git a/x-pack/plugins/observability_solution/apm/server/assistant_functions/get_apm_downstream_dependencies.ts b/x-pack/plugins/observability_solution/apm/server/assistant_functions/get_apm_downstream_dependencies.ts index db2e58806da81..9b80d694151ff 100644 --- a/x-pack/plugins/observability_solution/apm/server/assistant_functions/get_apm_downstream_dependencies.ts +++ b/x-pack/plugins/observability_solution/apm/server/assistant_functions/get_apm_downstream_dependencies.ts @@ -38,11 +38,11 @@ export function registerGetApmDownstreamDependenciesFunction({ parameters: { type: 'object', properties: { - 'service.name': { + serviceName: { type: 'string', description: 'The name of the service', }, - 'service.environment': { + serviceEnvironment: { type: 'string', description: 'The environment that the service is running in. Leave empty to query for all environments.', @@ -56,7 +56,7 @@ export function registerGetApmDownstreamDependenciesFunction({ description: 'The end of the time range, in Elasticsearch date math, like `now-24h`.', }, }, - required: ['service.name', 'start', 'end'], + required: ['serviceName', 'start', 'end'], } as const, }, async ({ arguments: args }, signal) => { diff --git a/x-pack/plugins/observability_solution/apm/server/assistant_functions/get_apm_services_list.ts b/x-pack/plugins/observability_solution/apm/server/assistant_functions/get_apm_services_list.ts index 1ca34117f5f9d..b24c24425b413 100644 --- a/x-pack/plugins/observability_solution/apm/server/assistant_functions/get_apm_services_list.ts +++ b/x-pack/plugins/observability_solution/apm/server/assistant_functions/get_apm_services_list.ts @@ -32,7 +32,7 @@ export function registerGetApmServicesListFunction({ parameters: { type: 'object', properties: { - 'service.environment': { + serviceEnvironment: { ...NON_EMPTY_STRING, description: 'Optionally filter the services by the environments that they are running in', diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_downstream_dependencies/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_downstream_dependencies/index.ts index 1ceed34367422..6d8fae8afafe2 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_downstream_dependencies/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_downstream_dependencies/index.ts @@ -17,12 +17,12 @@ import { NodeType } from '../../../../common/connections'; export const downstreamDependenciesRouteRt = t.intersection([ t.type({ - 'service.name': t.string, + serviceName: t.string, start: t.string, end: t.string, }), t.partial({ - 'service.environment': t.string, + serviceEnvironment: t.string, }), ]); @@ -50,8 +50,8 @@ export async function getAssistantDownstreamDependencies({ end, apmEventClient, filter: [ - ...termQuery(SERVICE_NAME, args['service.name']), - ...environmentQuery(args['service.environment'] ?? ENVIRONMENT_ALL.value), + ...termQuery(SERVICE_NAME, args.serviceName), + ...environmentQuery(args.serviceEnvironment ?? ENVIRONMENT_ALL.value), ], randomSampler, }); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_service_list/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_service_list/index.ts index b05ea4fee699c..39f2a77ad9c92 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_service_list/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_apm_service_list/index.ts @@ -35,7 +35,7 @@ export async function getApmServiceList({ randomSampler, }: { arguments: { - 'service.environment'?: string | undefined; + serviceEnvironment?: string | undefined; healthStatus?: ServiceHealthStatus[] | undefined; start: string; end: string; @@ -57,7 +57,7 @@ export async function getApmServiceList({ documentType: ApmDocumentType.TransactionMetric, start, end, - environment: args['service.environment'] || ENVIRONMENT_ALL.value, + environment: args.serviceEnvironment || ENVIRONMENT_ALL.value, kuery: '', logger, randomSampler, diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/index.ts b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/index.ts index 4e9ae1b546aa7..64141a7f533ee 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/index.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assistant_functions/get_observability_alert_details_context/index.ts @@ -111,8 +111,8 @@ export const getAlertDetailsContextHandler = ( ? getAssistantDownstreamDependencies({ apmEventClient, arguments: { - 'service.name': serviceName, - 'service.environment': serviceEnvironment, + serviceName, + serviceEnvironment, start: moment(alertStartedAt).subtract(24, 'hours').toISOString(), end: alertStartedAt, }, diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/ingest_pipeline_script_processor_helpers.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/ingest_pipeline_script_processor_helpers.test.ts new file mode 100644 index 0000000000000..4dc8919ef6f05 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/ingest_pipeline_script_processor_helpers.test.ts @@ -0,0 +1,214 @@ +/* + * 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 { initializePathScript, cleanScript } from './ingest_pipeline_script_processor_helpers'; + +describe('Ingest Pipeline script processor helpers', () => { + describe('initializePathScript', () => { + it('initializes a single depth field', () => { + expect(initializePathScript('someField')).toMatchInlineSnapshot(` + " + + if (ctx.someField == null) { + ctx.someField = new HashMap(); + } + " + `); + }); + + it('initializes a multi depth field', () => { + expect(initializePathScript('some.nested.field')).toMatchInlineSnapshot(` + " + + if (ctx.some == null) { + ctx.some = new HashMap(); + } + + + if (ctx.some.nested == null) { + ctx.some.nested = new HashMap(); + } + + + if (ctx.some.nested.field == null) { + ctx.some.nested.field = new HashMap(); + } + " + `); + }); + }); + + describe('cleanScript', () => { + it('removes duplicate empty lines and does basic indentation', () => { + const mostlyCleanScript = ` + // This function will recursively collect all the values of a HashMap of HashMaps + Collection collectValues(HashMap subject) { + Collection values = new ArrayList(); + // Iterate through the values + for(Object value: subject.values()) { + // If the value is a HashMap, recurse + if (value instanceof HashMap) { + values.addAll(collectValues((HashMap) value)); + } else { + values.add(String.valueOf(value)); + } + } + return values; + } + + // Create the string builder + StringBuilder entityId = new StringBuilder(); + + if (ctx["entity"]["identity"] != null) { + // Get the values as a collection + Collection values = collectValues(ctx["entity"]["identity"]); + + // Convert to a list and sort + List sortedValues = new ArrayList(values); + Collections.sort(sortedValues); + + // Create comma delimited string + for(String instanceValue: sortedValues) { + entityId.append(instanceValue); + entityId.append(":"); + } + + // Assign the slo.instanceId + ctx["entity"]["id"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : "unknown"; + } + `; + + expect(cleanScript(mostlyCleanScript)).toMatchInlineSnapshot(` + "// This function will recursively collect all the values of a HashMap of HashMaps + Collection collectValues(HashMap subject) { + Collection values = new ArrayList(); + // Iterate through the values + for(Object value: subject.values()) { + // If the value is a HashMap, recurse + if (value instanceof HashMap) { + values.addAll(collectValues((HashMap) value)); + } else { + values.add(String.valueOf(value)); + } + } + return values; + } + // Create the string builder + StringBuilder entityId = new StringBuilder(); + if (ctx[\\"entity\\"][\\"identity\\"] != null) { + // Get the values as a collection + Collection values = collectValues(ctx[\\"entity\\"][\\"identity\\"]); + // Convert to a list and sort + List sortedValues = new ArrayList(values); + Collections.sort(sortedValues); + // Create comma delimited string + for(String instanceValue: sortedValues) { + entityId.append(instanceValue); + entityId.append(\\":\\"); + } + // Assign the slo.instanceId + ctx[\\"entity\\"][\\"id\\"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : \\"unknown\\"; + }" + `); + + const messyScript = ` + if (someThing) { + + ${initializePathScript('some.whatever')} + + ${initializePathScript('some.else.whatever')} + + ctx.some.thing.else = whatever; + + + if (nothing) { + + more.stuff = otherStuff; + + } + + + + } + `; + expect(cleanScript(messyScript)).toMatchInlineSnapshot(` + "if (someThing) { + if (ctx.some == null) { + ctx.some = new HashMap(); + } + if (ctx.some.whatever == null) { + ctx.some.whatever = new HashMap(); + } + if (ctx.some == null) { + ctx.some = new HashMap(); + } + if (ctx.some.else == null) { + ctx.some.else = new HashMap(); + } + if (ctx.some.else.whatever == null) { + ctx.some.else.whatever = new HashMap(); + } + ctx.some.thing.else = whatever; + if (nothing) { + more.stuff = otherStuff; + } + }" + `); + }); + + it('does not change any non white space character', () => { + const mostlyCleanScript = ` + // This function will recursively collect all the values of a HashMap of HashMaps + Collection collectValues(HashMap subject) { + Collection values = new ArrayList(); + // Iterate through the values + for(Object value: subject.values()) { + // If the value is a HashMap, recurse + if (value instanceof HashMap) { + values.addAll(collectValues((HashMap) value)); + } else { + values.add(String.valueOf(value)); + } + } + return values; + } + + // Create the string builder + StringBuilder entityId = new StringBuilder(); + + if (ctx["entity"]["identity"] != null) { + // Get the values as a collection + Collection values = collectValues(ctx["entity"]["identity"]); + + // Convert to a list and sort + List sortedValues = new ArrayList(values); + Collections.sort(sortedValues); + + // Create comma delimited string + for(String instanceValue: sortedValues) { + entityId.append(instanceValue); + entityId.append(":"); + } + + // Assign the slo.instanceId + ctx["entity"]["id"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : "unknown"; + } + `; + + function nonWhiteSpaceCharacters(string: string) { + return string + .split('') + .filter((character) => character !== ' ' && character !== '\t' && character !== '\n') + .join(''); + } + + expect(nonWhiteSpaceCharacters(cleanScript(mostlyCleanScript))).toEqual( + nonWhiteSpaceCharacters(mostlyCleanScript) + ); + }); + }); +}); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/ingest_pipeline_script_processor_helpers.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/ingest_pipeline_script_processor_helpers.ts new file mode 100644 index 0000000000000..990f54627ffc7 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/ingest_pipeline_script_processor_helpers.ts @@ -0,0 +1,51 @@ +/* + * 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 { first, last } from 'lodash'; + +export function initializePathScript(field: string) { + return field.split('.').reduce((acc, _part, currentIndex, parts) => { + const currentSegment = parts.slice(0, currentIndex + 1).join('.'); + const next = ` + if (ctx.${currentSegment} == null) { + ctx.${currentSegment} = new HashMap(); + } + `; + return `${acc}\n${next}`; + }, ''); +} + +export function cleanScript(script: string) { + const codeLines = script + .split('\n') + .map((line) => line.trim()) + .filter((line) => line !== ''); + + let cleanedScript = ''; + let indentLevel = 0; + + for (let i = 0; i < codeLines.length; i++) { + if (i === 0) { + cleanedScript += `${codeLines[i]}\n`; + continue; + } + + const previousLine = i === 0 ? null : codeLines[i - 1]; + const currentLine = codeLines[i]; + + if (last(previousLine) === '{') { + indentLevel++; + } else if (first(currentLine) === '}') { + indentLevel--; + } + + const indent = new Array(indentLevel).fill(' ').join(''); + cleanedScript += `${indent}${currentLine}\n`; + } + + return cleanedScript.trim(); +} diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap index eb74937a7e8c8..c0482cb4f87e2 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap @@ -34,50 +34,46 @@ Array [ }, Object { "set": Object { - "field": "entity.displayName", - "value": "{{entity.identityFields.log.logger}}{{#entity.identityFields.event.category}}:{{.}}{{/entity.identityFields.event.category}}", + "field": "entity.identityFields", + "value": Array [ + "log.logger", + "event.category", + ], }, }, Object { "script": Object { "description": "Generated the entity.id field", - "source": " - // This function will recursively collect all the values of a HashMap of HashMaps - Collection collectValues(HashMap subject) { - Collection values = new ArrayList(); - // Iterate through the values - for(Object value: subject.values()) { - // If the value is a HashMap, recurse - if (value instanceof HashMap) { - values.addAll(collectValues((HashMap) value)); - } else { - values.add(String.valueOf(value)); - } - } - return values; - } - - // Create the string builder - StringBuilder entityId = new StringBuilder(); - - if (ctx[\\"entity\\"][\\"identityFields\\"] != null) { - // Get the values as a collection - Collection values = collectValues(ctx[\\"entity\\"][\\"identityFields\\"]); - - // Convert to a list and sort - List sortedValues = new ArrayList(values); - Collections.sort(sortedValues); - - // Create comma delimited string - for(String instanceValue: sortedValues) { - entityId.append(instanceValue); - entityId.append(\\":\\"); - } - - // Assign the slo.instanceId - ctx[\\"entity\\"][\\"id\\"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : \\"unknown\\"; - } - ", + "source": "// This function will recursively collect all the values of a HashMap of HashMaps +Collection collectValues(HashMap subject) { + Collection values = new ArrayList(); + // Iterate through the values + for(Object value: subject.values()) { + // If the value is a HashMap, recurse + if (value instanceof HashMap) { + values.addAll(collectValues((HashMap) value)); + } else { + values.add(String.valueOf(value)); + } + } + return values; +} +// Create the string builder +StringBuilder entityId = new StringBuilder(); +if (ctx[\\"entity\\"][\\"identity\\"] != null) { + // Get the values as a collection + Collection values = collectValues(ctx[\\"entity\\"][\\"identity\\"]); + // Convert to a list and sort + List sortedValues = new ArrayList(values); + Collections.sort(sortedValues); + // Create comma delimited string + for(String instanceValue: sortedValues) { + entityId.append(instanceValue); + entityId.append(\\":\\"); + } + // Assign the entity.id + ctx[\\"entity\\"][\\"id\\"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : \\"unknown\\"; +}", }, }, Object { @@ -92,21 +88,38 @@ Array [ Object { "script": Object { "source": "if (ctx.entity?.metadata?.tags != null) { - ctx[\\"tags\\"] = ctx.entity.metadata.tags.keySet(); + if (ctx.tags == null) { + ctx.tags = new HashMap(); + } + ctx.tags = ctx.entity.metadata.tags.keySet(); } if (ctx.entity?.metadata?.host?.name != null) { - if(ctx.host == null) ctx[\\"host\\"] = new HashMap(); - ctx[\\"host\\"][\\"name\\"] = ctx.entity.metadata.host.name.keySet(); + if (ctx.host == null) { + ctx.host = new HashMap(); + } + if (ctx.host.name == null) { + ctx.host.name = new HashMap(); + } + ctx.host.name = ctx.entity.metadata.host.name.keySet(); } if (ctx.entity?.metadata?.host?.os?.name != null) { - if(ctx.host == null) ctx[\\"host\\"] = new HashMap(); - if(ctx.host.os == null) ctx[\\"host\\"][\\"os\\"] = new HashMap(); - ctx[\\"host\\"][\\"os\\"][\\"name\\"] = ctx.entity.metadata.host.os.name.keySet(); + if (ctx.host == null) { + ctx.host = new HashMap(); + } + if (ctx.host.os == null) { + ctx.host.os = new HashMap(); + } + if (ctx.host.os.name == null) { + ctx.host.os.name = new HashMap(); + } + ctx.host.os.name = ctx.entity.metadata.host.os.name.keySet(); } if (ctx.entity?.metadata?.sourceIndex != null) { - ctx[\\"sourceIndex\\"] = ctx.entity.metadata.sourceIndex.keySet(); -} -", + if (ctx.sourceIndex == null) { + ctx.sourceIndex = new HashMap(); + } + ctx.sourceIndex = ctx.entity.metadata.sourceIndex.keySet(); +}", }, }, Object { @@ -115,6 +128,26 @@ if (ctx.entity?.metadata?.sourceIndex != null) { "ignore_missing": true, }, }, + Object { + "set": Object { + "field": "log.logger", + "if": "ctx.entity?.identity?.log?.logger != null", + "value": "{{entity.identity.log.logger}}", + }, + }, + Object { + "set": Object { + "field": "event.category", + "if": "ctx.entity?.identity?.event?.category != null", + "value": "{{entity.identity.event.category}}", + }, + }, + Object { + "remove": Object { + "field": "entity.identity", + "ignore_missing": true, + }, + }, Object { "date_index_name": Object { "date_formats": Array [ diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap index c66bd8337da47..aed7fe8f4bf09 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap @@ -32,24 +32,50 @@ Array [ "value": "v1", }, }, + Object { + "set": Object { + "field": "entity.identityFields", + "value": Array [ + "log.logger", + "event.category", + ], + }, + }, Object { "script": Object { "source": "if (ctx.entity?.metadata?.tags.data != null) { - ctx[\\"tags\\"] = ctx.entity.metadata.tags.data.keySet(); + if (ctx.tags == null) { + ctx.tags = new HashMap(); + } + ctx.tags = ctx.entity.metadata.tags.data.keySet(); } if (ctx.entity?.metadata?.host?.name.data != null) { - if(ctx.host == null) ctx[\\"host\\"] = new HashMap(); - ctx[\\"host\\"][\\"name\\"] = ctx.entity.metadata.host.name.data.keySet(); + if (ctx.host == null) { + ctx.host = new HashMap(); + } + if (ctx.host.name == null) { + ctx.host.name = new HashMap(); + } + ctx.host.name = ctx.entity.metadata.host.name.data.keySet(); } if (ctx.entity?.metadata?.host?.os?.name.data != null) { - if(ctx.host == null) ctx[\\"host\\"] = new HashMap(); - if(ctx.host.os == null) ctx[\\"host\\"][\\"os\\"] = new HashMap(); - ctx[\\"host\\"][\\"os\\"][\\"name\\"] = ctx.entity.metadata.host.os.name.data.keySet(); + if (ctx.host == null) { + ctx.host = new HashMap(); + } + if (ctx.host.os == null) { + ctx.host.os = new HashMap(); + } + if (ctx.host.os.name == null) { + ctx.host.os.name = new HashMap(); + } + ctx.host.os.name = ctx.entity.metadata.host.os.name.data.keySet(); } if (ctx.entity?.metadata?.sourceIndex.data != null) { - ctx[\\"sourceIndex\\"] = ctx.entity.metadata.sourceIndex.data.keySet(); -} -", + if (ctx.sourceIndex == null) { + ctx.sourceIndex = new HashMap(); + } + ctx.sourceIndex = ctx.entity.metadata.sourceIndex.data.keySet(); +}", }, }, Object { @@ -58,6 +84,42 @@ if (ctx.entity?.metadata?.sourceIndex.data != null) { "ignore_missing": true, }, }, + Object { + "script": Object { + "if": "ctx.entity.identity.log?.logger != null && ctx.entity.identity.log.logger.size() != 0", + "source": "if (ctx.log == null) { + ctx.log = new HashMap(); +} +if (ctx.log.logger == null) { + ctx.log.logger = new HashMap(); +} +ctx.log.logger = ctx.entity.identity.log.logger.keySet().toArray()[0];", + }, + }, + Object { + "script": Object { + "if": "ctx.entity.identity.event?.category != null && ctx.entity.identity.event.category.size() != 0", + "source": "if (ctx.event == null) { + ctx.event = new HashMap(); +} +if (ctx.event.category == null) { + ctx.event.category = new HashMap(); +} +ctx.event.category = ctx.entity.identity.event.category.keySet().toArray()[0];", + }, + }, + Object { + "remove": Object { + "field": "entity.identity", + "ignore_missing": true, + }, + }, + Object { + "set": Object { + "field": "entity.displayName", + "value": "{{log.logger}}{{#event.category}}:{{.}}{{/event.category}}", + }, + }, Object { "set": Object { "field": "_index", diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts index b273be780910b..45ee008f9c6b5 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts @@ -7,42 +7,46 @@ import { EntityDefinition } from '@kbn/entities-schema'; import { ENTITY_SCHEMA_VERSION_V1 } from '../../../../common/constants_entities'; +import { + initializePathScript, + cleanScript, +} from '../helpers/ingest_pipeline_script_processor_helpers'; import { generateHistoryIndexName } from '../helpers/generate_component_id'; -function createIdTemplate(definition: EntityDefinition) { - return definition.identityFields.reduce((template, id) => { - return template.replaceAll(id.field, `entity.identityFields.${id.field}`); - }, definition.displayNameTemplate); -} - -function mapDestinationToPainless(destination: string) { - const fieldParts = destination.split('.'); - return fieldParts.reduce((acc, _part, currentIndex, parts) => { - if (currentIndex + 1 === parts.length) { - return `${acc}\n ctx${parts - .map((s) => `["${s}"]`) - .join('')} = ctx.entity.metadata.${destination}.keySet();`; - } - return `${acc}\n if(ctx.${parts.slice(0, currentIndex + 1).join('.')} == null) ctx${parts - .slice(0, currentIndex + 1) - .map((s) => `["${s}"]`) - .join('')} = new HashMap();`; - }, ''); +function mapDestinationToPainless(field: string) { + return ` + ${initializePathScript(field)} + ctx.${field} = ctx.entity.metadata.${field}.keySet(); + `; } function createMetadataPainlessScript(definition: EntityDefinition) { if (!definition.metadata) { return ''; } - return definition.metadata.reduce((script, def) => { + + return definition.metadata.reduce((acc, def) => { const destination = def.destination || def.source; - return `${script}if (ctx.entity?.metadata?.${destination.replaceAll( - '.', - '?.' - )} != null) {${mapDestinationToPainless(destination)}\n}\n`; + const optionalFieldPath = destination.replaceAll('.', '?.'); + const next = ` + if (ctx.entity?.metadata?.${optionalFieldPath} != null) { + ${mapDestinationToPainless(destination)} + } + `; + return `${acc}\n${next}`; }, ''); } +function liftIdentityFieldsToDocumentRoot(definition: EntityDefinition) { + return definition.identityFields.map((key) => ({ + set: { + if: `ctx.entity?.identity?.${key.field.replaceAll('.', '?.')} != null`, + field: key.field, + value: `{{entity.identity.${key.field}}}`, + }, + })); +} + export function generateHistoryProcessors(definition: EntityDefinition) { return [ { @@ -77,14 +81,14 @@ export function generateHistoryProcessors(definition: EntityDefinition) { }, { set: { - field: 'entity.displayName', - value: createIdTemplate(definition), + field: 'entity.identityFields', + value: definition.identityFields.map((identityField) => identityField.field), }, }, { script: { description: 'Generated the entity.id field', - source: ` + source: cleanScript(` // This function will recursively collect all the values of a HashMap of HashMaps Collection collectValues(HashMap subject) { Collection values = new ArrayList(); @@ -103,9 +107,9 @@ export function generateHistoryProcessors(definition: EntityDefinition) { // Create the string builder StringBuilder entityId = new StringBuilder(); - if (ctx["entity"]["identityFields"] != null) { + if (ctx["entity"]["identity"] != null) { // Get the values as a collection - Collection values = collectValues(ctx["entity"]["identityFields"]); + Collection values = collectValues(ctx["entity"]["identity"]); // Convert to a list and sort List sortedValues = new ArrayList(values); @@ -117,10 +121,10 @@ export function generateHistoryProcessors(definition: EntityDefinition) { entityId.append(":"); } - // Assign the slo.instanceId + // Assign the entity.id ctx["entity"]["id"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : "unknown"; } - `, + `), }, }, { @@ -136,7 +140,7 @@ export function generateHistoryProcessors(definition: EntityDefinition) { })) : []), ...(definition.metadata != null - ? [{ script: { source: createMetadataPainlessScript(definition) } }] + ? [{ script: { source: cleanScript(createMetadataPainlessScript(definition)) } }] : []), { remove: { @@ -144,6 +148,13 @@ export function generateHistoryProcessors(definition: EntityDefinition) { ignore_missing: true, }, }, + ...liftIdentityFieldsToDocumentRoot(definition), + { + remove: { + field: 'entity.identity', + ignore_missing: true, + }, + }, { date_index_name: { field: '@timestamp', diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts index 056fb043ab05c..22b2ac19775a1 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts @@ -7,36 +7,49 @@ import { EntityDefinition } from '@kbn/entities-schema'; import { ENTITY_SCHEMA_VERSION_V1 } from '../../../../common/constants_entities'; +import { + initializePathScript, + cleanScript, +} from '../helpers/ingest_pipeline_script_processor_helpers'; import { generateLatestIndexName } from '../helpers/generate_component_id'; -function mapDestinationToPainless(destination: string) { - const fieldParts = destination.split('.'); - return fieldParts.reduce((acc, _part, currentIndex, parts) => { - if (currentIndex + 1 === parts.length) { - return `${acc}\n ctx${parts - .map((s) => `["${s}"]`) - .join('')} = ctx.entity.metadata.${destination}.data.keySet();`; - } - return `${acc}\n if(ctx.${parts.slice(0, currentIndex + 1).join('.')} == null) ctx${parts - .slice(0, currentIndex + 1) - .map((s) => `["${s}"]`) - .join('')} = new HashMap();`; - }, ''); +function mapDestinationToPainless(field: string) { + return ` + ${initializePathScript(field)} + ctx.${field} = ctx.entity.metadata.${field}.data.keySet(); + `; } function createMetadataPainlessScript(definition: EntityDefinition) { if (!definition.metadata) { return ''; } - return definition.metadata.reduce((script, def) => { + + return definition.metadata.reduce((acc, def) => { const destination = def.destination || def.source; - return `${script}if (ctx.entity?.metadata?.${destination.replaceAll( - '.', - '?.' - )}.data != null) {${mapDestinationToPainless(destination)}\n}\n`; + const optionalFieldPath = destination.replaceAll('.', '?.'); + const next = ` + if (ctx.entity?.metadata?.${optionalFieldPath}.data != null) { + ${mapDestinationToPainless(destination)} + } + `; + return `${acc}\n${next}`; }, ''); } +function liftIdentityFieldsToDocumentRoot(definition: EntityDefinition) { + return definition.identityFields.map((identityField) => { + const optionalFieldPath = identityField.field.replaceAll('.', '?.'); + const assignValue = `ctx.${identityField.field} = ctx.entity.identity.${identityField.field}.keySet().toArray()[0];`; + return { + script: { + if: `ctx.entity.identity.${optionalFieldPath} != null && ctx.entity.identity.${identityField.field}.size() != 0`, + source: cleanScript(`${initializePathScript(identityField.field)}\n${assignValue}`), + }, + }; + }); +} + export function generateLatestProcessors(definition: EntityDefinition) { return [ { @@ -69,13 +82,19 @@ export function generateLatestProcessors(definition: EntityDefinition) { value: ENTITY_SCHEMA_VERSION_V1, }, }, + { + set: { + field: 'entity.identityFields', + value: definition.identityFields.map((identityField) => identityField.field), + }, + }, ...(definition.staticFields != null ? Object.keys(definition.staticFields).map((field) => ({ set: { field, value: definition.staticFields![field] }, })) : []), ...(definition.metadata != null - ? [{ script: { source: createMetadataPainlessScript(definition) } }] + ? [{ script: { source: cleanScript(createMetadataPainlessScript(definition)) } }] : []), { remove: { @@ -83,6 +102,20 @@ export function generateLatestProcessors(definition: EntityDefinition) { ignore_missing: true, }, }, + ...liftIdentityFieldsToDocumentRoot(definition), + { + remove: { + field: 'entity.identity', + ignore_missing: true, + }, + }, + { + // This must happen AFTER we lift the identity fields into the root of the document + set: { + field: 'entity.displayName', + value: definition.displayNameTemplate, + }, + }, { set: { field: '_index', diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap index 722084ceb1a80..b76cd81f6ecf9 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap @@ -101,13 +101,13 @@ Object { "fixed_interval": "1m", }, }, - "entity.identityFields.event.category": Object { + "entity.identity.event.category": Object { "terms": Object { "field": "event.category", "missing_bucket": true, }, }, - "entity.identityFields.log.logger": Object { + "entity.identity.log.logger": Object { "terms": Object { "field": "log.logger", "missing_bucket": false, diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap index 16a0aa7dcdcf7..9cc7f4bba438b 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap @@ -47,6 +47,18 @@ Object { "field": "@timestamp", }, }, + "entity.identity.event.category": Object { + "terms": Object { + "field": "event.category", + "size": 1, + }, + }, + "entity.identity.log.logger": Object { + "terms": Object { + "field": "log.logger", + "size": 1, + }, + }, "entity.lastSeenTimestamp": Object { "max": Object { "field": "entity.lastSeenTimestamp", @@ -138,28 +150,11 @@ Object { }, }, "group_by": Object { - "entity.displayName": Object { - "terms": Object { - "field": "entity.displayName.keyword", - }, - }, "entity.id": Object { "terms": Object { "field": "entity.id", }, }, - "entity.identityFields.event.category": Object { - "terms": Object { - "field": "entity.identityFields.event.category", - "missing_bucket": true, - }, - }, - "entity.identityFields.log.logger": Object { - "terms": Object { - "field": "entity.identityFields.log.logger", - "missing_bucket": false, - }, - }, }, }, "settings": Object { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.ts index 9881d5f3a899f..645225eaf688c 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_history_transform.ts @@ -69,7 +69,7 @@ export function generateHistoryTransform( ...definition.identityFields.reduce( (acc, id) => ({ ...acc, - [`entity.identityFields.${id.field}`]: { + [`entity.identity.${id.field}`]: { terms: { field: id.field, missing_bucket: id.optional }, }, }), diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_identity_aggregations.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_identity_aggregations.ts new file mode 100644 index 0000000000000..52e7dcd904839 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_identity_aggregations.ts @@ -0,0 +1,23 @@ +/* + * 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 { EntityDefinition } from '@kbn/entities-schema'; + +export function generateIdentityAggregations(definition: EntityDefinition) { + return definition.identityFields.reduce( + (aggs, identityField) => ({ + ...aggs, + [`entity.identity.${identityField.field}`]: { + terms: { + field: identityField.field, + size: 1, + }, + }, + }), + {} + ); +} diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_latest_transform.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_latest_transform.ts index aeb6bac9479f9..85ee57fefea2c 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_latest_transform.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/generate_latest_transform.ts @@ -5,19 +5,20 @@ * 2.0. */ -import { EntityDefinition } from '@kbn/entities-schema'; import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types'; +import { EntityDefinition } from '@kbn/entities-schema'; import { ENTITY_DEFAULT_LATEST_FREQUENCY, ENTITY_DEFAULT_LATEST_SYNC_DELAY, } from '../../../../common/constants_entities'; -import { generateLatestMetadataAggregations } from './generate_metadata_aggregations'; import { generateHistoryIndexName, - generateLatestTransformId, - generateLatestIngestPipelineId, generateLatestIndexName, + generateLatestIngestPipelineId, + generateLatestTransformId, } from '../helpers/generate_component_id'; +import { generateIdentityAggregations } from './generate_identity_aggregations'; +import { generateLatestMetadataAggregations } from './generate_metadata_aggregations'; import { generateLatestMetricAggregations } from './generate_metric_aggregations'; export function generateLatestTransform( @@ -53,22 +54,11 @@ export function generateLatestTransform( ['entity.id']: { terms: { field: 'entity.id' }, }, - ['entity.displayName']: { - terms: { field: 'entity.displayName.keyword' }, - }, - ...definition.identityFields.reduce( - (acc, id) => ({ - ...acc, - [`entity.identityFields.${id.field}`]: { - terms: { field: `entity.identityFields.${id.field}`, missing_bucket: id.optional }, - }, - }), - {} - ), }, aggs: { ...generateLatestMetricAggregations(definition), ...generateLatestMetadataAggregations(definition), + ...generateIdentityAggregations(definition), 'entity.lastSeenTimestamp': { max: { field: 'entity.lastSeenTimestamp', diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/uninstall_entity_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/uninstall_entity_definition.ts index 2d5b4bee83a6d..8642ebafa904b 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/uninstall_entity_definition.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/uninstall_entity_definition.ts @@ -10,6 +10,7 @@ import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { EntityDefinition } from '@kbn/entities-schema'; import { Logger } from '@kbn/logging'; import { deleteEntityDefinition } from './delete_entity_definition'; +import { deleteIndices } from './delete_index'; import { deleteHistoryIngestPipeline, deleteLatestIngestPipeline } from './delete_ingest_pipeline'; import { findEntityDefinitions } from './find_entity_definition'; import { @@ -22,27 +23,34 @@ export async function uninstallEntityDefinition({ esClient, soClient, logger, + deleteData = false, }: { definition: EntityDefinition; esClient: ElasticsearchClient; soClient: SavedObjectsClientContract; logger: Logger; + deleteData?: boolean; }) { await stopAndDeleteHistoryTransform(esClient, definition, logger); await stopAndDeleteLatestTransform(esClient, definition, logger); await deleteHistoryIngestPipeline(esClient, definition, logger); await deleteLatestIngestPipeline(esClient, definition, logger); await deleteEntityDefinition(soClient, definition, logger); + if (deleteData) { + await deleteIndices(esClient, definition, logger); + } } export async function uninstallBuiltInEntityDefinitions({ esClient, soClient, logger, + deleteData = false, }: { esClient: ElasticsearchClient; soClient: SavedObjectsClientContract; logger: Logger; + deleteData?: boolean; }): Promise { const definitions = await findEntityDefinitions({ soClient, @@ -52,7 +60,7 @@ export async function uninstallBuiltInEntityDefinitions({ await Promise.all( definitions.map(async (definition) => { - await uninstallEntityDefinition({ definition, esClient, soClient, logger }); + await uninstallEntityDefinition({ definition, esClient, soClient, logger, deleteData }); }) ); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts index 251c3ccfefada..fca2e1070afda 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/check.ts @@ -9,7 +9,6 @@ import { RequestHandlerContext } from '@kbn/core/server'; import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request'; import { SetupRouteOptions } from '../types'; import { ENTITY_INTERNAL_API_PREFIX } from '../../../common/constants_entities'; -import { ManagedEntityEnabledResponse } from '../../../common/types_api'; import { checkIfEntityDiscoveryAPIKeyIsValid, readEntityDiscoveryAPIKey } from '../../lib/auth'; import { ERROR_API_KEY_NOT_FOUND, @@ -25,7 +24,7 @@ export function checkEntityDiscoveryEnabledRoute) { - router.get( + router.get( { path: `${ENTITY_INTERNAL_API_PREFIX}/managed/enablement`, validate: false, diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts index 33a1b60dab293..9af237e22d597 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts @@ -7,6 +7,7 @@ import { RequestHandlerContext } from '@kbn/core/server'; import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request'; +import { schema } from '@kbn/config-schema'; import { SetupRouteOptions } from '../types'; import { ENTITY_INTERNAL_API_PREFIX } from '../../../common/constants_entities'; import { @@ -16,17 +17,20 @@ import { } from '../../lib/auth'; import { ERROR_API_KEY_NOT_FOUND, ERROR_API_KEY_NOT_VALID } from '../../../common/errors'; import { uninstallBuiltInEntityDefinitions } from '../../lib/entities/uninstall_entity_definition'; -import { DisableManagedEntityResponse } from '../../../common/types_api'; export function disableEntityDiscoveryRoute({ router, server, logger, }: SetupRouteOptions) { - router.delete( + router.delete( { path: `${ENTITY_INTERNAL_API_PREFIX}/managed/enablement`, - validate: false, + validate: { + query: schema.object({ + deleteData: schema.maybe(schema.boolean({ defaultValue: false })), + }), + }, }, async (context, req, res) => { try { @@ -48,7 +52,12 @@ export function disableEntityDiscoveryRoute({ const soClient = server.core.savedObjects.getScopedClient(fakeRequest); const esClient = server.core.elasticsearch.client.asScoped(fakeRequest).asCurrentUser; - await uninstallBuiltInEntityDefinitions({ soClient, esClient, logger }); + await uninstallBuiltInEntityDefinitions({ + soClient, + esClient, + logger, + deleteData: req.query.deleteData, + }); await deleteEntityDiscoveryAPIKey((await context.core).savedObjects.client); await server.security.authc.apiKeys.invalidateAsInternalUser({ diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts index 16c6bae805207..91ca36b1caa59 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts @@ -9,7 +9,6 @@ import { RequestHandlerContext } from '@kbn/core/server'; import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request'; import { SetupRouteOptions } from '../types'; import { ENTITY_INTERNAL_API_PREFIX } from '../../../common/constants_entities'; -import { EnableManagedEntityResponse } from '../../../common/types_api'; import { canEnableEntityDiscovery, checkIfAPIKeysAreEnabled, @@ -29,7 +28,7 @@ export function enableEntityDiscoveryRoute({ server, logger, }: SetupRouteOptions) { - router.put( + router.put( { path: `${ENTITY_INTERNAL_API_PREFIX}/managed/enablement`, validate: false, diff --git a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/delete.ts b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/delete.ts index c18d688572891..91078991f927b 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/delete.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/routes/entities/delete.ts @@ -19,13 +19,16 @@ export function deleteEntityDefinitionRoute({ router, server, }: SetupRouteOptions) { - router.delete<{ id: string }, unknown, unknown>( + router.delete<{ id: string }, { deleteData?: boolean }, unknown>( { path: `${ENTITY_INTERNAL_API_PREFIX}/definition/{id}`, validate: { params: schema.object({ id: schema.string(), }), + query: schema.object({ + deleteData: schema.maybe(schema.boolean({ defaultValue: false })), + }), }, }, async (context, req, res) => { @@ -35,7 +38,13 @@ export function deleteEntityDefinitionRoute({ const esClient = (await context.core).elasticsearch.client.asCurrentUser; const definition = await readEntityDefinition(soClient, req.params.id, logger); - await uninstallEntityDefinition({ definition, soClient, esClient, logger }); + await uninstallEntityDefinition({ + definition, + soClient, + esClient, + logger, + deleteData: req.query.deleteData, + }); return res.ok({ body: { acknowledged: true } }); } catch (e) { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/components/base_latest.ts b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/base_latest.ts index d0167eb716039..6ee48b37bff1d 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/templates/components/base_latest.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/base_latest.ts @@ -20,6 +20,22 @@ export const entitiesLatestBaseComponentTemplateConfig: ClusterPutComponentTempl template: { mappings: { properties: { + entity: { + properties: { + displayName: { + type: 'text', + fields: { + keyword: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + firstSeenTimestamp: { + type: 'date', + }, + }, + }, labels: { type: 'object', }, diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/components/entity.ts b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/entity.ts index 31486b8e1cb24..db93d0db77abf 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/templates/components/entity.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/entity.ts @@ -29,15 +29,6 @@ export const entitiesEntityComponentTemplateConfig: ClusterPutComponentTemplateR ignore_above: 1024, type: 'keyword', }, - displayName: { - type: 'text', - fields: { - keyword: { - ignore_above: 1024, - type: 'keyword', - }, - }, - }, definitionId: { ignore_above: 1024, type: 'keyword', @@ -53,8 +44,8 @@ export const entitiesEntityComponentTemplateConfig: ClusterPutComponentTemplateR lastSeenTimestamp: { type: 'date', }, - firstSeenTimestamp: { - type: 'date', + identityFields: { + type: 'keyword', }, }, }, diff --git a/x-pack/plugins/observability_solution/infra/common/constants.ts b/x-pack/plugins/observability_solution/infra/common/constants.ts index d0f48068dda64..580dcd16dec09 100644 --- a/x-pack/plugins/observability_solution/infra/common/constants.ts +++ b/x-pack/plugins/observability_solution/infra/common/constants.ts @@ -42,3 +42,5 @@ export const DEFAULT_METRICS_VIEW_ATTRIBUTES = { name: 'Metrics View', timeFieldName: TIMESTAMP_FIELD, }; + +export const SNAPSHOT_API_MAX_METRICS = 20; diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx index 7182b6f59c448..170eaaeb9d4db 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx @@ -198,6 +198,7 @@ export const CustomMetricForm = withTheme(({ theme, onCancel, onChange, metric } options={fieldOptions} onChange={handleFieldChange} isClearable={false} + data-test-subj="infraCustomMetricFieldSelect" /> diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx index 48ddf2c1d8305..4f366094e4d60 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx @@ -9,6 +9,7 @@ import { EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useState, useCallback } from 'react'; import { SnapshotMetricType } from '@kbn/metrics-data-access-plugin/common'; +import { SNAPSHOT_API_MAX_METRICS } from '../../../../../../../common/constants'; import { getCustomMetricLabel } from '../../../../../../../common/formatters/get_custom_metric_label'; import { SnapshotMetricInput, @@ -132,10 +133,13 @@ export const WaffleMetricControls = ({ return null; } + const canAdd = options.length + customMetrics.length < SNAPSHOT_API_MAX_METRICS; + const button = ( {currentLabel} @@ -190,6 +194,7 @@ export const WaffleMetricControls = ({ mode={mode} onSave={handleSaveEdit} customMetrics={customMetrics} + disableAdd={!canAdd} /> diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx index 4085b9c5b051b..3ce49b08d0e2a 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx @@ -70,5 +70,11 @@ export const MetricsContextMenu = ({ }, ]; - return ; + return ( + + ); }; diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx index 42677448c20e6..256d7827bc773 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx @@ -5,98 +5,128 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiToolTip, + useEuiTheme, +} from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiTheme, withTheme } from '@kbn/kibana-react-plugin/common'; +import { SNAPSHOT_API_MAX_METRICS } from '../../../../../../../common/constants'; import { CustomMetricMode } from './types'; import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; interface Props { - theme: EuiTheme | undefined; onEdit: () => void; onAdd: () => void; onSave: () => void; onEditCancel: () => void; mode: CustomMetricMode; customMetrics: SnapshotCustomMetricInput[]; + disableAdd?: boolean; } -export const ModeSwitcher = withTheme( - ({ onSave, onEditCancel, onEdit, onAdd, mode, customMetrics, theme }: Props) => { - if (['editMetric', 'addMetric'].includes(mode)) { - return null; - } - return ( -
- - {mode === 'edit' ? ( - <> - - - - - - - - - - - - ) : ( - <> - - - - - - +export const ModeSwitcher = ({ + onSave, + onEditCancel, + onEdit, + onAdd, + mode, + customMetrics, + disableAdd = false, +}: Props) => { + const { euiTheme } = useEuiTheme(); + + if (['editMetric', 'addMetric'].includes(mode)) { + return null; + } + return ( +
+ + {mode === 'edit' ? ( + <> + + + + + + + + + + + + ) : ( + <> + + + + + + + - - - )} - -
- ); - } -); + +
+ + )} +
+
+ ); +}; diff --git a/x-pack/plugins/observability_solution/infra/server/routes/snapshot/index.ts b/x-pack/plugins/observability_solution/infra/server/routes/snapshot/index.ts index ae1f02a04ca93..4398e7f47f281 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/snapshot/index.ts @@ -6,21 +6,18 @@ */ import Boom from '@hapi/boom'; -import { schema } from '@kbn/config-schema'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; +import { createRouteValidationFunction } from '@kbn/io-ts-utils'; +import { SNAPSHOT_API_MAX_METRICS } from '../../../common/constants'; import { InfraBackendLibs } from '../../lib/infra_types'; import { UsageCollector } from '../../usage/usage_collector'; import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api'; -import { throwErrors } from '../../../common/runtime_types'; import { createSearchClient } from '../../lib/create_search_client'; import { getNodes } from './lib/get_nodes'; import { LogQueryFields } from '../../lib/metrics/types'; -const escapeHatch = schema.object({}, { unknowns: 'allow' }); - export const initSnapshotRoute = (libs: InfraBackendLibs) => { + const validateBody = createRouteValidationFunction(SnapshotRequestRT); + const { framework } = libs; framework.registerRoute( @@ -28,34 +25,40 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { method: 'post', path: '/api/metrics/snapshot', validate: { - body: escapeHatch, + body: validateBody, }, }, async (requestContext, request, response) => { - const snapshotRequest = pipe( - SnapshotRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const snapshotRequest = request.body; + + try { + if (snapshotRequest.metrics.length > SNAPSHOT_API_MAX_METRICS) { + throw Boom.badRequest( + `'metrics' size is greater than maximum of ${SNAPSHOT_API_MAX_METRICS} allowed.` + ); + } - const soClient = (await requestContext.core).savedObjects.client; - const source = await libs.sources.getSourceConfiguration(soClient, snapshotRequest.sourceId); - const compositeSize = libs.configuration.inventory.compositeSize; - const [, { logsShared }] = await libs.getStartServices(); - const logQueryFields: LogQueryFields | undefined = await logsShared.logViews - .getScopedClient(request) - .getResolvedLogView({ - type: 'log-view-reference', - logViewId: snapshotRequest.sourceId, - }) - .then( - ({ indices }) => ({ indexPattern: indices }), - () => undefined + const soClient = (await requestContext.core).savedObjects.client; + const source = await libs.sources.getSourceConfiguration( + soClient, + snapshotRequest.sourceId ); + const compositeSize = libs.configuration.inventory.compositeSize; + const [, { logsShared }] = await libs.getStartServices(); + const logQueryFields: LogQueryFields | undefined = await logsShared.logViews + .getScopedClient(request) + .getResolvedLogView({ + type: 'log-view-reference', + logViewId: snapshotRequest.sourceId, + }) + .then( + ({ indices }) => ({ indexPattern: indices }), + () => undefined + ); - UsageCollector.countNode(snapshotRequest.nodeType); - const client = createSearchClient(requestContext, framework, request); + UsageCollector.countNode(snapshotRequest.nodeType); + const client = createSearchClient(requestContext, framework, request); - try { const snapshotResponse = await getNodes( client, snapshotRequest, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/common/connectors.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/common/connectors.ts index 2b834081a7ac9..f1b5b5567adc7 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/common/connectors.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/common/connectors.ts @@ -8,11 +8,13 @@ export enum ObservabilityAIAssistantConnectorType { Bedrock = '.bedrock', OpenAI = '.gen-ai', + Gemini = '.gemini', } export const SUPPORTED_CONNECTOR_TYPES = [ ObservabilityAIAssistantConnectorType.OpenAI, ObservabilityAIAssistantConnectorType.Bedrock, + ObservabilityAIAssistantConnectorType.Gemini, ]; export function isSupportedConnectorType( @@ -20,6 +22,7 @@ export function isSupportedConnectorType( ): type is ObservabilityAIAssistantConnectorType { return ( type === ObservabilityAIAssistantConnectorType.Bedrock || - type === ObservabilityAIAssistantConnectorType.OpenAI + type === ObservabilityAIAssistantConnectorType.OpenAI || + type === ObservabilityAIAssistantConnectorType.Gemini ); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/bedrock/bedrock_claude_adapter.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/bedrock/bedrock_claude_adapter.ts index 5c1713ffe6743..0cbe2f98514a4 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/bedrock/bedrock_claude_adapter.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/bedrock/bedrock_claude_adapter.ts @@ -17,12 +17,6 @@ import { getMessagesWithSimulatedFunctionCalling } from '../simulate_function_ca import { parseInlineFunctionCalls } from '../simulate_function_calling/parse_inline_function_calls'; import { TOOL_USE_END } from '../simulate_function_calling/constants'; -function replaceFunctionsWithTools(content: string) { - return content.replaceAll(/(function)(s|[\s*\.])?(?!\scall)/g, (match, p1, p2) => { - return `tool${p2 || ''}`; - }); -} - // Most of the work here is to re-format OpenAI-compatible functions for Claude. // See https://github.com/anthropics/anthropic-tools/blob/main/tool_use_package/prompt_constructors.py @@ -46,7 +40,7 @@ export const createBedrockClaudeAdapter: LlmApiAdapterFactory = ({ const formattedMessages = messagesWithSimulatedFunctionCalling.map((message) => { return { role: message.message.role, - content: replaceFunctionsWithTools(message.message.content ?? ''), + content: message.message.content ?? '', }; }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/gemini/gemini_adapter.test.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/gemini/gemini_adapter.test.ts new file mode 100644 index 0000000000000..df2986fdfcf8d --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/gemini/gemini_adapter.test.ts @@ -0,0 +1,357 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import dedent from 'dedent'; +import { last } from 'lodash'; +import { last as lastOperator, lastValueFrom, partition, shareReplay } from 'rxjs'; +import { Readable } from 'stream'; +import { + ChatCompletionChunkEvent, + concatenateChatCompletionChunks, + MessageRole, + StreamingChatResponseEventType, +} from '../../../../../common'; +import { TOOL_USE_END, TOOL_USE_START } from '../simulate_function_calling/constants'; +import { LlmApiAdapterFactory } from '../types'; +import { createGeminiAdapter } from './gemini_adapter'; +import { GoogleGenerateContentResponseChunk } from './types'; + +describe('createGeminiAdapter', () => { + describe('getSubAction', () => { + function callSubActionFactory(overrides?: Partial[0]>) { + const subActionParams = createGeminiAdapter({ + logger: { + debug: jest.fn(), + } as unknown as Logger, + functions: [ + { + name: 'my_tool', + description: 'My tool', + parameters: { + properties: { + myParam: { + type: 'string', + }, + }, + }, + }, + ], + messages: [ + { + '@timestamp': new Date().toString(), + message: { + role: MessageRole.System, + content: '', + }, + }, + { + '@timestamp': new Date().toString(), + message: { + role: MessageRole.User, + content: 'How can you help me?', + }, + }, + ], + ...overrides, + }).getSubAction().subActionParams as { + temperature: number; + messages: Array<{ role: string; content: string }>; + }; + + return { + ...subActionParams, + messages: subActionParams.messages.map((msg) => ({ ...msg, content: dedent(msg.content) })), + }; + } + describe('with functions', () => { + it('sets the temperature to 0', () => { + expect(callSubActionFactory().temperature).toEqual(0); + }); + + it('formats the functions', () => { + expect(callSubActionFactory().messages[0].content).toContain( + dedent( + JSON.stringify([ + { + name: 'my_tool', + description: 'My tool', + parameters: { + properties: { + myParam: { + type: 'string', + }, + }, + }, + }, + ]) + ) + ); + }); + + it('replaces mentions of functions with tools', () => { + const messages = [ + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.System, + content: + 'Call the "esql" tool. You can chain successive function calls, using the functions available.', + }, + }, + ]; + + const content = callSubActionFactory({ messages }).messages[0].content; + + expect(content).not.toContain(`"esql" function`); + expect(content).toContain(`"esql" tool`); + expect(content).not.toContain(`functions`); + expect(content).toContain(`tools`); + expect(content).toContain(`tool calls`); + }); + + it('mentions to explicitly call the specified function if given', () => { + expect(last(callSubActionFactory({ functionCall: 'my_tool' }).messages)!.content).toContain( + 'Remember, use the my_tool tool to answer this question.' + ); + }); + + it('formats the function requests as JSON', () => { + const messages = [ + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.System, + content: '', + }, + }, + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.Assistant, + function_call: { + name: 'my_tool', + arguments: JSON.stringify({ myParam: 'myValue' }), + trigger: MessageRole.User as const, + }, + }, + }, + ]; + + expect(last(callSubActionFactory({ messages }).messages)!.content).toContain( + dedent(`${TOOL_USE_START} + \`\`\`json + ${JSON.stringify({ name: 'my_tool', input: { myParam: 'myValue' } })} + \`\`\`${TOOL_USE_END}`) + ); + }); + + it('formats errors', () => { + const messages = [ + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.System, + content: '', + }, + }, + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.Assistant, + function_call: { + name: 'my_tool', + arguments: JSON.stringify({ myParam: 'myValue' }), + trigger: MessageRole.User as const, + }, + }, + }, + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.User, + name: 'my_tool', + content: JSON.stringify({ error: 'An internal server error occurred' }), + }, + }, + ]; + + expect(JSON.parse(last(callSubActionFactory({ messages }).messages)!.content)).toEqual({ + type: 'tool_result', + tool: 'my_tool', + error: 'An internal server error occurred', + is_error: true, + }); + }); + + it('formats function responses as JSON', () => { + const messages = [ + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.System, + content: '', + }, + }, + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.Assistant, + function_call: { + name: 'my_tool', + arguments: JSON.stringify({ myParam: 'myValue' }), + trigger: MessageRole.User as const, + }, + }, + }, + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.User, + name: 'my_tool', + content: JSON.stringify({ myResponse: { myParam: 'myValue' } }), + }, + }, + ]; + + expect(JSON.parse(last(callSubActionFactory({ messages }).messages)!.content)).toEqual({ + type: 'tool_result', + tool: 'my_tool', + myResponse: { myParam: 'myValue' }, + }); + }); + }); + }); + + describe('streamIntoObservable', () => { + it('correctly parses the response from Vertex/Gemini', async () => { + const chunks: GoogleGenerateContentResponseChunk[] = [ + { + candidates: [ + { + content: { + parts: [ + { + text: 'This is ', + }, + ], + }, + index: 0, + }, + ], + }, + { + candidates: [ + { + content: { + parts: [ + { + text: 'my response', + }, + ], + }, + index: 1, + }, + ], + }, + { + usageMetadata: { + candidatesTokenCount: 10, + promptTokenCount: 100, + totalTokenCount: 110, + }, + candidates: [ + { + content: { + parts: [ + { + text: '.', + }, + ], + }, + index: 2, + }, + ], + }, + ]; + + const stream = new Readable({ + read(...args) { + chunks.forEach((chunk) => this.push(`data: ${JSON.stringify(chunk)}\n\n`)); + this.push(null); + }, + }); + const response$ = createGeminiAdapter({ + logger: { + debug: jest.fn(), + } as unknown as Logger, + functions: [ + { + name: 'my_tool', + description: 'My tool', + parameters: { + properties: { + myParam: { + type: 'string', + }, + }, + }, + }, + ], + messages: [ + { + '@timestamp': new Date().toString(), + message: { + role: MessageRole.System, + content: '', + }, + }, + { + '@timestamp': new Date().toString(), + message: { + role: MessageRole.User, + content: 'How can you help me?', + }, + }, + ], + }) + .streamIntoObservable(stream) + .pipe(shareReplay()); + + const [chunkEvents$, tokenCountEvents$] = partition( + response$, + (value): value is ChatCompletionChunkEvent => + value.type === StreamingChatResponseEventType.ChatCompletionChunk + ); + + const [concatenatedMessage, tokenCount] = await Promise.all([ + lastValueFrom(chunkEvents$.pipe(concatenateChatCompletionChunks(), lastOperator())), + lastValueFrom(tokenCountEvents$), + ]); + + expect(concatenatedMessage).toEqual({ + message: { + content: 'This is my response.', + function_call: { + arguments: '', + name: '', + trigger: MessageRole.Assistant, + }, + role: MessageRole.Assistant, + }, + }); + + expect(tokenCount).toEqual({ + tokens: { + completion: 10, + prompt: 100, + total: 110, + }, + type: StreamingChatResponseEventType.TokenCount, + }); + }); + }); +}); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/gemini/gemini_adapter.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/gemini/gemini_adapter.ts new file mode 100644 index 0000000000000..fba0e3f542365 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/gemini/gemini_adapter.ts @@ -0,0 +1,60 @@ +/* + * 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 { map } from 'rxjs'; +import { processVertexStream } from './process_vertex_stream'; +import type { LlmApiAdapterFactory } from '../types'; +import { getMessagesWithSimulatedFunctionCalling } from '../simulate_function_calling/get_messages_with_simulated_function_calling'; +import { parseInlineFunctionCalls } from '../simulate_function_calling/parse_inline_function_calls'; +import { TOOL_USE_END } from '../simulate_function_calling/constants'; +import { eventsourceStreamIntoObservable } from '../../../util/eventsource_stream_into_observable'; +import { GoogleGenerateContentResponseChunk } from './types'; + +export const createGeminiAdapter: LlmApiAdapterFactory = ({ + messages, + functions, + functionCall, + logger, +}) => { + const filteredFunctions = functionCall + ? functions?.filter((fn) => fn.name === functionCall) + : functions; + return { + getSubAction: () => { + const messagesWithSimulatedFunctionCalling = getMessagesWithSimulatedFunctionCalling({ + messages, + functions: filteredFunctions, + functionCall, + }); + + const formattedMessages = messagesWithSimulatedFunctionCalling.map((message) => { + return { + role: message.message.role, + content: message.message.content ?? '', + }; + }); + + return { + subAction: 'invokeStream', + subActionParams: { + messages: formattedMessages, + temperature: 0, + stopSequences: ['\n\nHuman:', TOOL_USE_END], + }, + }; + }, + streamIntoObservable: (readable) => + eventsourceStreamIntoObservable(readable).pipe( + map((value) => { + const response = JSON.parse(value) as GoogleGenerateContentResponseChunk; + return response; + }), + processVertexStream(), + parseInlineFunctionCalls({ logger }) + ), + }; +}; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/gemini/process_vertex_stream.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/gemini/process_vertex_stream.ts new file mode 100644 index 0000000000000..903fa54d11acb --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/gemini/process_vertex_stream.ts @@ -0,0 +1,64 @@ +/* + * 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 { Observable } from 'rxjs'; +import { v4 } from 'uuid'; +import { + ChatCompletionChunkEvent, + StreamingChatResponseEventType, + TokenCountEvent, +} from '../../../../../common/conversation_complete'; +import type { GoogleGenerateContentResponseChunk } from './types'; + +export function processVertexStream() { + return (source: Observable) => + new Observable((subscriber) => { + const id = v4(); + + function handleNext(value: GoogleGenerateContentResponseChunk) { + // completion: what we eventually want to emit + if (value.usageMetadata) { + subscriber.next({ + type: StreamingChatResponseEventType.TokenCount, + tokens: { + prompt: value.usageMetadata.promptTokenCount, + completion: value.usageMetadata.candidatesTokenCount, + total: value.usageMetadata.totalTokenCount, + }, + }); + } + + const completion = value.candidates[0].content.parts[0].text; + + if (completion) { + subscriber.next({ + id, + type: StreamingChatResponseEventType.ChatCompletionChunk, + message: { + content: completion, + }, + }); + } + } + + source.subscribe({ + next: (value) => { + try { + handleNext(value); + } catch (error) { + subscriber.error(error); + } + }, + error: (err) => { + subscriber.error(err); + }, + complete: () => { + subscriber.complete(); + }, + }); + }); +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/gemini/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/gemini/types.ts new file mode 100644 index 0000000000000..9c131f1ee67b3 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/gemini/types.ts @@ -0,0 +1,51 @@ +/* + * 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. + */ + +interface GenerateContentResponseFunctionCall { + name: string; + args: Record; +} + +interface GenerateContentResponseSafetyRating { + category: string; + probability: string; +} + +interface GenerateContentResponseCandidate { + content: { + parts: Array<{ + text?: string; + functionCall?: GenerateContentResponseFunctionCall; + }>; + }; + finishReason?: string; + index: number; + safetyRatings?: GenerateContentResponseSafetyRating[]; +} + +interface GenerateContentResponsePromptFeedback { + promptFeedback: { + safetyRatings: GenerateContentResponseSafetyRating[]; + }; + usageMetadata: { + promptTokenCount: number; + candidatesTokenCount: number; + totalTokenCount: number; + }; +} + +interface GenerateContentResponseUsageMetadata { + promptTokenCount: number; + candidatesTokenCount: number; + totalTokenCount: number; +} + +export interface GoogleGenerateContentResponseChunk { + candidates: GenerateContentResponseCandidate[]; + promptFeedback?: GenerateContentResponsePromptFeedback; + usageMetadata?: GenerateContentResponseUsageMetadata; +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/simulate_function_calling/get_messages_with_simulated_function_calling.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/simulate_function_calling/get_messages_with_simulated_function_calling.ts index 2e319a076773c..3325432dc453d 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/simulate_function_calling/get_messages_with_simulated_function_calling.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/adapters/simulate_function_calling/get_messages_with_simulated_function_calling.ts @@ -9,6 +9,12 @@ import { FunctionDefinition, Message } from '../../../../../common'; import { TOOL_USE_END, TOOL_USE_START } from './constants'; import { getSystemMessageInstructions } from './get_system_message_instructions'; +function replaceFunctionsWithTools(content: string) { + return content.replaceAll(/(function)(s|[\s*\.])?(?!\scall)/g, (match, p1, p2) => { + return `tool${p2 || ''}`; + }); +} + export function getMessagesWithSimulatedFunctionCalling({ messages, functions, @@ -26,64 +32,76 @@ export function getMessagesWithSimulatedFunctionCalling({ systemMessage.message.content = (systemMessage.message.content ?? '') + '\n' + instructions; - return [systemMessage, ...otherMessages].map((message, index) => { - if (message.message.name) { - const deserialized = JSON.parse(message.message.content || '{}'); + return [systemMessage, ...otherMessages] + .map((message, index) => { + if (message.message.name) { + const deserialized = JSON.parse(message.message.content || '{}'); - const results = { - type: 'tool_result', - tool: message.message.name, - ...(message.message.content ? JSON.parse(message.message.content) : {}), - }; + const results = { + type: 'tool_result', + tool: message.message.name, + ...(message.message.content ? JSON.parse(message.message.content) : {}), + }; + + if ('error' in deserialized) { + return { + ...message, + message: { + role: message.message.role, + content: JSON.stringify({ + ...results, + is_error: true, + }), + }, + }; + } - if ('error' in deserialized) { return { ...message, message: { role: message.message.role, - content: JSON.stringify({ - ...results, - is_error: true, - }), + content: JSON.stringify(results), }, }; } + let content = message.message.content || ''; + + if (message.message.function_call?.name) { + content += + TOOL_USE_START + + '\n```json\n' + + JSON.stringify({ + name: message.message.function_call.name, + input: JSON.parse(message.message.function_call.arguments || '{}'), + }) + + '\n```' + + TOOL_USE_END; + } + + if (index === messages.length - 1 && functionCall) { + content += ` + + Remember, use the ${functionCall} tool to answer this question.`; + } + return { ...message, message: { role: message.message.role, - content: JSON.stringify(results), + content, }, }; - } - - let content = message.message.content || ''; - - if (message.message.function_call?.name) { - content += - TOOL_USE_START + - '\n```json\n' + - JSON.stringify({ - name: message.message.function_call.name, - input: JSON.parse(message.message.function_call.arguments || '{}'), - }) + - '\n```' + - TOOL_USE_END; - } - - if (index === messages.length - 1 && functionCall) { - content += ` - - Remember, use the ${functionCall} tool to answer this question.`; - } - - return { - ...message, - message: { - role: message.message.role, - content, - }, - }; - }); + }) + .map((message) => { + return { + ...message, + message: { + ...message.message, + content: message.message.content + ? replaceFunctionsWithTools(message.message.content) + : message.message.content, + }, + }; + }); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts index e0a9e3be684d3..4c3527873d01c 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts @@ -67,6 +67,7 @@ import { replaceSystemMessage } from '../util/replace_system_message'; import { withAssistantSpan } from '../util/with_assistant_span'; import { createBedrockClaudeAdapter } from './adapters/bedrock/bedrock_claude_adapter'; import { failOnNonExistingFunctionCall } from './adapters/fail_on_non_existing_function_call'; +import { createGeminiAdapter } from './adapters/gemini/gemini_adapter'; import { createOpenAiAdapter } from './adapters/openai_adapter'; import { LlmApiAdapter } from './adapters/types'; import { getContextFunctionRequestIfNeeded } from './get_context_function_request_if_needed'; @@ -514,13 +515,24 @@ export class ObservabilityAIAssistantClient { }); break; + case ObservabilityAIAssistantConnectorType.Gemini: + adapter = createGeminiAdapter({ + messages, + functions, + functionCall, + logger: this.dependencies.logger, + }); + break; + default: throw new Error(`Connector type is not supported: ${connector.actionTypeId}`); } const subAction = adapter.getSubAction(); - this.dependencies.logger.trace(JSON.stringify(subAction.subActionParams, null, 2)); + if (this.dependencies.logger.isLevelEnabled('trace')) { + this.dependencies.logger.trace(JSON.stringify(subAction.subActionParams, null, 2)); + } return from( withAssistantSpan('get_execute_result', () => diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/plugin.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/plugin.tsx index 6e188043a6a31..466cde6747990 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/plugin.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/plugin.tsx @@ -103,27 +103,29 @@ export class ObservabilityAIAssistantAppPlugin const appService = (this.appService = createAppService({ pluginsStart, })); + const isEnabled = appService.isEnabled(); + if (isEnabled) { + coreStart.chrome.navControls.registerRight({ + mount: (element) => { + ReactDOM.render( + + + , + element, + () => {} + ); - coreStart.chrome.navControls.registerRight({ - mount: (element) => { - ReactDOM.render( - - - , - element, - () => {} - ); - - return () => {}; - }, - // right before the user profile - order: 1001, - }); + return () => {}; + }, + // right before the user profile + order: 1001, + }); + } const service = pluginsStart.observabilityAIAssistant.service; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/query/correct_common_esql_mistakes.test.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/query/correct_common_esql_mistakes.test.ts index 709b93bb0165c..8c6b90bca76ab 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/query/correct_common_esql_mistakes.test.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/query/correct_common_esql_mistakes.test.ts @@ -56,6 +56,18 @@ describe('correctCommonEsqlMistakes', () => { expectQuery({ input: `FROM logs-* | LIMIT 10`, expectedOutput: 'FROM logs-*\n| LIMIT 10' }); }); + it('replaces double quotes around columns with backticks', () => { + expectQuery({ + input: `FROM logs-* | WHERE "@timestamp" <= NOW() - 15m`, + expectedOutput: `FROM logs-* \n| WHERE @timestamp <= NOW() - 15m`, + }); + + expectQuery({ + input: `FROM logs-* | EVAL date_bucket = DATE_TRUNC("@timestamp", 1 hour)`, + expectedOutput: `FROM logs-* \n| EVAL date_bucket = DATE_TRUNC(@timestamp, 1 hour)`, + }); + }); + it('replaces = as equal operator with ==', () => { expectQuery({ input: `FROM logs-*\n| WHERE service.name = "foo"`, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/query/correct_common_esql_mistakes.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/query/correct_common_esql_mistakes.ts index a2c96307fca60..0d4d13b61152a 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/query/correct_common_esql_mistakes.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/query/correct_common_esql_mistakes.ts @@ -234,6 +234,11 @@ export function correctCommonEsqlMistakes(query: string): { const formattedCommands: string[] = commands.map(({ name, command }, index) => { let formattedCommand = command; + + formattedCommand = formattedCommand + .replaceAll(/"@timestamp"/g, '@timestamp') + .replaceAll(/'@timestamp'/g, '@timestamp'); + switch (name) { case 'FROM': { formattedCommand = split(formattedCommand, ',') diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/command_snippet.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/command_snippet.tsx index be7857676427c..2ed3ff80f14bf 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/command_snippet.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/command_snippet.tsx @@ -17,6 +17,7 @@ interface Props { onboardingId: string; elasticsearchUrl: string; elasticAgentVersion: string; + isCopyPrimaryAction: boolean; } export function CommandSnippet({ @@ -24,6 +25,7 @@ export function CommandSnippet({ onboardingId, elasticsearchUrl, elasticAgentVersion, + isCopyPrimaryAction, }: Props) { const command = buildKubectlCommand({ encodedApiKey, @@ -66,7 +68,7 @@ export function CommandSnippet({ - + ); } diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/data_ingest_status.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/data_ingest_status.tsx index 3e91c20752bd4..11fe5d2982343 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/data_ingest_status.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/data_ingest_status.tsx @@ -93,7 +93,7 @@ export function DataIngestStatus({ onboardingId }: Props) { troubleshootingLink: ( diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/index.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/index.tsx index 58de3a72ca92f..41314e37460e8 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/index.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/kubernetes/index.tsx @@ -53,6 +53,7 @@ export const KubernetesPanel: React.FC = () => { onboardingId={data.onboardingId} elasticsearchUrl={data.elasticsearchUrl} elasticAgentVersion={data.elasticAgentVersion} + isCopyPrimaryAction={!isMonitoringStepActive} /> )} diff --git a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types.ts index 1c5b69a44dc0e..2ca9abdac8335 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/monitor_types.ts @@ -40,7 +40,7 @@ const ScheduleCodec = t.interface({ // TLSFields export const TLSFieldsCodec = t.partial({ - [ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: t.string, + [ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: t.union([t.string, t.array(t.string)]), [ConfigKey.TLS_CERTIFICATE]: t.string, [ConfigKey.TLS_VERIFICATION_MODE]: VerificationModeCodec, [ConfigKey.TLS_VERSION]: t.array(TLSVersionCodec), diff --git a/x-pack/plugins/search_playground/common/prompt.test.ts b/x-pack/plugins/search_playground/common/prompt.test.ts index 81d1d32beec92..d53a6b38dba45 100644 --- a/x-pack/plugins/search_playground/common/prompt.test.ts +++ b/x-pack/plugins/search_playground/common/prompt.test.ts @@ -21,8 +21,8 @@ describe('Prompt function', () => { Instructions: - Provide an explanation of the process. - - Answer questions truthfully and factually using only the information presented. - - If you don't know the answer, just say that you don't know, don't make up an answer! + - Answer questions truthfully and factually using only the context presented. + - If you don't know the answer, just say that you don't know, don't make up an answer. - You must always cite the document where the answer was extracted using inline academic citation style [], using the position. - Use markdown format for code examples. - You are correct, factual, precise, and reliable. @@ -31,8 +31,7 @@ describe('Prompt function', () => { Context: {context} - Question: {question} - Answer: + " `); }); @@ -45,8 +44,8 @@ describe('Prompt function', () => { " [INST] - Explain the significance of the findings. - - Answer questions truthfully and factually using only the information presented. - - If you don't know the answer, just say that you don't know, don't make up an answer! + - Answer questions truthfully and factually using only the context presented. + - If you don't know the answer, just say that you don't know, don't make up an answer. - Use markdown format for code examples. - You are correct, factual, precise, and reliable. @@ -55,10 +54,10 @@ describe('Prompt function', () => { [INST] Context: {context} - - Question: {question} - Answer: [/INST] + + + " `); }); @@ -72,7 +71,7 @@ describe('Prompt function', () => { - Summarize the key points of the article. - - If you don't know the answer, just say that you don't know, don't make up an answer! + - If you don't know the answer, just say that you don't know, don't make up an answer. - Use markdown format for code examples. - You are correct, factual, precise, and reliable. @@ -82,7 +81,7 @@ describe('Prompt function', () => { {context} - {question} + " `); }); diff --git a/x-pack/plugins/search_playground/common/prompt.ts b/x-pack/plugins/search_playground/common/prompt.ts index 9d9ffa6a00186..3df5b63b6f36e 100644 --- a/x-pack/plugins/search_playground/common/prompt.ts +++ b/x-pack/plugins/search_playground/common/prompt.ts @@ -5,7 +5,7 @@ * 2.0. */ -const OpenAIPrompt = (systemInstructions: string) => { +const OpenAIPrompt = (systemInstructions: string, question?: boolean) => { return ` Instructions: ${systemInstructions} @@ -13,27 +13,26 @@ const OpenAIPrompt = (systemInstructions: string) => { Context: {context} - Question: {question} - Answer: + ${question ? 'follow up question: {question}' : ''} `; }; -const MistralPrompt = (systemInstructions: string) => { +const MistralPrompt = (systemInstructions: string, question?: boolean) => { return ` [INST]${systemInstructions}[/INST] [INST] Context: {context} - - Question: {question} - Answer: [/INST] + + ${question ? '[INST]follow up question: {question}[/INST]' : ''} + `; }; // https://docs.anthropic.com/claude/docs/use-xml-tags -const AnthropicPrompt = (systemInstructions: string) => { +const AnthropicPrompt = (systemInstructions: string, question?: boolean) => { return ` ${systemInstructions} @@ -41,11 +40,11 @@ const AnthropicPrompt = (systemInstructions: string) => { {context} - {question} + ${question ? '{question}' : ''} `; }; -const GeminiPrompt = (systemInstructions: string) => { +const GeminiPrompt = (systemInstructions: string, question?: boolean) => { return ` Instructions: ${systemInstructions} @@ -53,8 +52,8 @@ const GeminiPrompt = (systemInstructions: string) => { Context: {context} - Question: {question} - Answer: + ${question ? 'follow up question: {question}' : ''} + `; }; @@ -69,10 +68,10 @@ export const Prompt = (instructions: string, options: PromptTemplateOptions): st - ${instructions} ${ options.context - ? '- Answer questions truthfully and factually using only the information presented.' + ? '- Answer questions truthfully and factually using only the context presented.' : '' } - - If you don't know the answer, just say that you don't know, don't make up an answer! + - If you don't know the answer, just say that you don't know, don't make up an answer. ${ options.citations ? '- You must always cite the document where the answer was extracted using inline academic citation style [], using the position.' @@ -87,7 +86,7 @@ export const Prompt = (instructions: string, options: PromptTemplateOptions): st mistral: MistralPrompt, anthropic: AnthropicPrompt, gemini: GeminiPrompt, - }[options.type || 'openai'](systemInstructions); + }[options.type || 'openai'](systemInstructions, false); }; interface QuestionRewritePromptOptions { @@ -95,11 +94,11 @@ interface QuestionRewritePromptOptions { } export const QuestionRewritePrompt = (options: QuestionRewritePromptOptions): string => { - const systemInstructions = `Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question. Rewrite the question in the question language. Keep the answer to a single sentence. Do not use quotes.`; + const systemInstructions = `Given the following conversation context and a follow up question, rephrase the follow up question to be a standalone question. Rewrite the question in the question language. Keep the answer to a single sentence. Do not use quotes.`; return { openai: OpenAIPrompt, mistral: MistralPrompt, anthropic: AnthropicPrompt, gemini: GeminiPrompt, - }[options.type || 'openai'](systemInstructions); + }[options.type || 'openai'](systemInstructions, true); }; diff --git a/x-pack/plugins/search_playground/common/types.ts b/x-pack/plugins/search_playground/common/types.ts index 9331599786ced..5c08222ae90cb 100644 --- a/x-pack/plugins/search_playground/common/types.ts +++ b/x-pack/plugins/search_playground/common/types.ts @@ -7,12 +7,24 @@ export type IndicesQuerySourceFields = Record; +export enum MessageRole { + 'user' = 'human', + 'assistant' = 'assistant', + 'system' = 'system', +} + interface ModelField { field: string; model_id: string; indices: string[]; } +export interface ChatMessage { + id: string; + role: MessageRole; + content: string; +} + interface SemanticField { field: string; inferenceId: string; diff --git a/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_lang_client.test.tsx.snap b/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_lang_client.test.tsx.snap index 792a7bf37bb62..e8777144ce994 100644 --- a/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_lang_client.test.tsx.snap +++ b/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_lang_client.test.tsx.snap @@ -54,8 +54,8 @@ def create_openai_prompt(question, results): Instructions: - Your prompt - - Answer questions truthfully and factually using only the information presented. - - If you don't know the answer, just say that you don't know, don't make up an answer! + - Answer questions truthfully and factually using only the context presented. + - If you don't know the answer, just say that you don't know, don't make up an answer. - You must always cite the document where the answer was extracted using inline academic citation style [], using the position. - Use markdown format for code examples. - You are correct, factual, precise, and reliable. @@ -64,18 +64,17 @@ def create_openai_prompt(question, results): Context: {context} - Question: {question} - Answer: + \\"\\"\\" return prompt -def generate_openai_completion(user_prompt): +def generate_openai_completion(user_prompt, question): response = openai_client.chat.completions.create( model=\\"gpt-3.5-turbo\\", messages=[ - {\\"role\\": \\"system\\", \\"content\\": \\"You are an assistant for question-answering tasks.\\"}, - {\\"role\\": \\"user\\", \\"content\\": user_prompt}, + {\\"role\\": \\"system\\", \\"content\\": user_prompt}, + {\\"role\\": \\"user\\", \\"content\\": question}, ] ) @@ -84,8 +83,8 @@ def generate_openai_completion(user_prompt): if __name__ == \\"__main__\\": question = \\"my question\\" elasticsearch_results = get_elasticsearch_results(question) - context_prompt = create_openai_prompt(question, elasticsearch_results) - openai_completion = generate_openai_completion(context_prompt) + context_prompt = create_openai_prompt(elasticsearch_results) + openai_completion = generate_openai_completion(context_prompt, question) print(openai_completion) " diff --git a/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_langchain_python.test.tsx.snap b/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_langchain_python.test.tsx.snap index 96d069736c897..ea9b77ee1878c 100644 --- a/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_langchain_python.test.tsx.snap +++ b/x-pack/plugins/search_playground/public/components/view_code/examples/__snapshots__/py_langchain_python.test.tsx.snap @@ -43,8 +43,8 @@ ANSWER_PROMPT = ChatPromptTemplate.from_template( Instructions: - Your prompt - - Answer questions truthfully and factually using only the information presented. - - If you don't know the answer, just say that you don't know, don't make up an answer! + - Answer questions truthfully and factually using only the context presented. + - If you don't know the answer, just say that you don't know, don't make up an answer. - You must always cite the document where the answer was extracted using inline academic citation style [], using the position. - Use markdown format for code examples. - You are correct, factual, precise, and reliable. @@ -53,8 +53,7 @@ ANSWER_PROMPT = ChatPromptTemplate.from_template( Context: {context} - Question: {question} - Answer: + \\"\\"\\" ) diff --git a/x-pack/plugins/search_playground/public/components/view_code/examples/py_lang_client.tsx b/x-pack/plugins/search_playground/public/components/view_code/examples/py_lang_client.tsx index b920d2f4a6d28..cfb218afa6d3c 100644 --- a/x-pack/plugins/search_playground/public/components/view_code/examples/py_lang_client.tsx +++ b/x-pack/plugins/search_playground/public/components/view_code/examples/py_lang_client.tsx @@ -58,12 +58,12 @@ def create_openai_prompt(question, results): return prompt -def generate_openai_completion(user_prompt): +def generate_openai_completion(user_prompt, question): response = openai_client.chat.completions.create( model="gpt-3.5-turbo", messages=[ - {"role": "system", "content": "You are an assistant for question-answering tasks."}, - {"role": "user", "content": user_prompt}, + {"role": "system", "content": user_prompt}, + {"role": "user", "content": question}, ] ) @@ -72,8 +72,8 @@ def generate_openai_completion(user_prompt): if __name__ == "__main__": question = "my question" elasticsearch_results = get_elasticsearch_results(question) - context_prompt = create_openai_prompt(question, elasticsearch_results) - openai_completion = generate_openai_completion(context_prompt) + context_prompt = create_openai_prompt(elasticsearch_results) + openai_completion = generate_openai_completion(context_prompt, question) print(openai_completion) `} diff --git a/x-pack/plugins/search_playground/public/types.ts b/x-pack/plugins/search_playground/public/types.ts index 2076ac2190b99..a8aab36d9a71e 100644 --- a/x-pack/plugins/search_playground/public/types.ts +++ b/x-pack/plugins/search_playground/public/types.ts @@ -22,7 +22,7 @@ import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui- import { AppMountParameters } from '@kbn/core/public'; import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; import type { ConsolePluginStart } from '@kbn/console-plugin/public'; -import { ChatRequestData } from '../common/types'; +import { ChatRequestData, MessageRole } from '../common/types'; import type { App } from './components/app'; import type { PlaygroundProvider as PlaygroundProviderComponent } from './providers/playground_provider'; import { PlaygroundHeaderDocs } from './components/playground_header_docs'; @@ -80,12 +80,6 @@ export interface ChatForm { [ChatFormFields.queryFields]: { [index: string]: string[] }; } -export enum MessageRole { - 'user' = 'human', - 'assistant' = 'assistant', - 'system' = 'system', -} - export interface Message { id: string; content: string | React.ReactNode; diff --git a/x-pack/plugins/search_playground/server/lib/conversational_chain.test.ts b/x-pack/plugins/search_playground/server/lib/conversational_chain.test.ts index 5885802428d5e..7cc947a089b6e 100644 --- a/x-pack/plugins/search_playground/server/lib/conversational_chain.test.ts +++ b/x-pack/plugins/search_playground/server/lib/conversational_chain.test.ts @@ -9,9 +9,10 @@ import type { Client } from '@elastic/elasticsearch'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { ChatPromptTemplate } from '@langchain/core/prompts'; import { FakeListChatModel, FakeStreamingLLM } from '@langchain/core/utils/testing'; -import { Message, experimental_StreamData } from 'ai'; +import { experimental_StreamData } from 'ai'; import { createAssist as Assist } from '../utils/assist'; import { ConversationalChain, clipContext } from './conversational_chain'; +import { ChatMessage, MessageRole } from '../types'; describe('conversational chain', () => { const createTestChain = async ({ @@ -28,7 +29,7 @@ describe('conversational chain', () => { modelLimit, }: { responses: string[]; - chat: Message[]; + chat: ChatMessage[]; expectedFinalAnswer: string; expectedDocs: any; expectedTokens: any; @@ -96,8 +97,8 @@ describe('conversational chain', () => { size: 3, inputTokensLimit: modelLimit, }, - prompt: 'you are a QA bot {question} {chat_history} {context}', - questionRewritePrompt: 'rewrite question {question} using {chat_history}"', + prompt: 'you are a QA bot {context}', + questionRewritePrompt: 'rewrite question {question} using {context}"', }); const stream = await conversationalChain.stream(aiClient, chat); @@ -146,7 +147,7 @@ describe('conversational chain', () => { chat: [ { id: '1', - role: 'user', + role: MessageRole.user, content: 'what is the work from home policy?', }, ], @@ -180,7 +181,7 @@ describe('conversational chain', () => { chat: [ { id: '1', - role: 'user', + role: MessageRole.user, content: 'what is the work from home policy?', }, ], @@ -215,7 +216,7 @@ describe('conversational chain', () => { chat: [ { id: '1', - role: 'user', + role: MessageRole.user, content: 'what is the work from home policy?', }, ], @@ -266,17 +267,17 @@ describe('conversational chain', () => { chat: [ { id: '1', - role: 'user', + role: MessageRole.user, content: 'what is the work from home policy?', }, { id: '2', - role: 'assistant', + role: MessageRole.assistant, content: 'the final answer', }, { id: '3', - role: 'user', + role: MessageRole.user, content: 'what is the work from home policy?', }, ], @@ -292,7 +293,7 @@ describe('conversational chain', () => { ], expectedTokens: [ { type: 'context_token_count', count: 15 }, - { type: 'prompt_token_count', count: 38 }, + { type: 'prompt_token_count', count: 39 }, ], expectedSearchRequest: [ { @@ -310,17 +311,17 @@ describe('conversational chain', () => { chat: [ { id: '1', - role: 'user', + role: MessageRole.user, content: 'what is the work from home policy?', }, { id: '2', - role: 'assistant', + role: MessageRole.assistant, content: 'the final answer', }, { id: '3', - role: 'user', + role: MessageRole.user, content: 'what is the work from home policy?', }, ], @@ -336,7 +337,7 @@ describe('conversational chain', () => { ], expectedTokens: [ { type: 'context_token_count', count: 15 }, - { type: 'prompt_token_count', count: 40 }, + { type: 'prompt_token_count', count: 39 }, ], expectedSearchRequest: [ { @@ -354,17 +355,17 @@ describe('conversational chain', () => { chat: [ { id: '1', - role: 'user', + role: MessageRole.user, content: 'what is the work from home policy?', }, { id: '2', - role: 'assistant', + role: MessageRole.assistant, content: 'the final answer', }, { id: '3', - role: 'user', + role: MessageRole.user, content: 'what is the work from home policy?', }, ], @@ -380,7 +381,7 @@ describe('conversational chain', () => { ], expectedTokens: [ { type: 'context_token_count', count: 15 }, - { type: 'prompt_token_count', count: 42 }, + { type: 'prompt_token_count', count: 49 }, ], expectedSearchRequest: [ { @@ -399,17 +400,17 @@ describe('conversational chain', () => { chat: [ { id: '1', - role: 'user', + role: MessageRole.user, content: 'what is the work from home policy?', }, { id: '2', - role: 'assistant', + role: MessageRole.assistant, content: 'the final answer', }, { id: '3', - role: 'user', + role: MessageRole.user, content: 'what is the work from home policy?', }, ], @@ -445,8 +446,8 @@ describe('conversational chain', () => { ], // Even with body_content of 1000, the token count should be below or equal to model limit of 100 expectedTokens: [ - { type: 'context_token_count', count: 70 }, - { type: 'prompt_token_count', count: 97 }, + { type: 'context_token_count', count: 65 }, + { type: 'prompt_token_count', count: 99 }, ], expectedHasClipped: true, expectedSearchRequest: [ diff --git a/x-pack/plugins/search_playground/server/lib/conversational_chain.ts b/x-pack/plugins/search_playground/server/lib/conversational_chain.ts index 0a8f26efca163..f7b1634dd27b1 100644 --- a/x-pack/plugins/search_playground/server/lib/conversational_chain.ts +++ b/x-pack/plugins/search_playground/server/lib/conversational_chain.ts @@ -7,16 +7,18 @@ import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { Document } from '@langchain/core/documents'; -import { ChatPromptTemplate, PromptTemplate } from '@langchain/core/prompts'; +import { + ChatPromptTemplate, + PromptTemplate, + SystemMessagePromptTemplate, +} from '@langchain/core/prompts'; import { Runnable, RunnableLambda, RunnableSequence } from '@langchain/core/runnables'; import { BytesOutputParser, StringOutputParser } from '@langchain/core/output_parsers'; -import { - createStreamDataTransformer, - experimental_StreamData, - Message as VercelChatMessage, -} from 'ai'; +import { createStreamDataTransformer, experimental_StreamData } from 'ai'; import { BaseLanguageModel } from '@langchain/core/language_models/base'; import { BaseMessage } from '@langchain/core/messages'; +import { HumanMessage, AIMessage } from '@langchain/core/messages'; +import { ChatMessage, MessageRole } from '../types'; import { ElasticsearchRetriever } from './elasticsearch_retriever'; import { renderTemplate } from '../utils/render_template'; @@ -47,19 +49,27 @@ interface ContextInputs { question: string; } -const formatVercelMessages = (chatHistory: VercelChatMessage[]) => { +const getSerialisedMessages = (chatHistory: ChatMessage[]) => { const formattedDialogueTurns = chatHistory.map((message) => { - if (message.role === 'user') { + if (message.role === MessageRole.user) { return `Human: ${message.content}`; - } else if (message.role === 'assistant') { + } else if (message.role === MessageRole.assistant) { return `Assistant: ${message.content}`; - } else { - return `${message.role}: ${message.content}`; } }); return formattedDialogueTurns.join('\n'); }; +const getMessages = (chatHistory: ChatMessage[]) => { + return chatHistory.map((message) => { + if (message.role === 'human') { + return new HumanMessage(message.content); + } else { + return new AIMessage(message.content); + } + }); +}; + const buildContext = (docs: Document[]) => { const serializedDocs = docs.map((doc, i) => renderTemplate( @@ -127,7 +137,7 @@ class ConversationalChainFn { this.options = options; } - async stream(client: AssistClient, msgs: VercelChatMessage[]) { + async stream(client: AssistClient, msgs: ChatMessage[]) { const data = new experimental_StreamData(); const messages = msgs ?? []; @@ -136,7 +146,7 @@ class ConversationalChainFn { const retrievedDocs: Document[] = []; let retrievalChain: Runnable = RunnableLambda.from(() => ''); - const chatHistory = formatVercelMessages(previousMessages); + const chatHistory = getSerialisedMessages(previousMessages); if (this.options.rag) { const retriever = new ElasticsearchRetriever({ @@ -161,8 +171,7 @@ class ConversationalChainFn { ); standaloneQuestionChain = RunnableSequence.from([ { - context: () => '', - chat_history: (input) => input.chat_history, + context: (input) => input.chat_history, question: (input) => input.question, }, questionRewritePromptTemplate, @@ -175,12 +184,15 @@ class ConversationalChainFn { }); } - const prompt = ChatPromptTemplate.fromTemplate(this.options.prompt); + const lcMessages = getMessages(messages); + const prompt = ChatPromptTemplate.fromMessages([ + SystemMessagePromptTemplate.fromTemplate(this.options.prompt), + ...lcMessages, + ]); const answerChain = RunnableSequence.from([ { context: RunnableSequence.from([(input) => input.question, retrievalChain]), - chat_history: (input) => input.chat_history, question: (input) => input.question, }, RunnableLambda.from(clipContext(this.options?.rag?.inputTokensLimit, prompt, data)), diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/index_management/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/index_management/index.ts index b64bea9e974b3..4b060113e613b 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/index_management/index.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/index_management/index.ts @@ -9,4 +9,4 @@ export * from './create_index/create_index.gen'; export * from './delete_index/delete_index.gen'; export * from './read_alerts_index_exists/read_alerts_index_exists_route'; export * from './read_index/read_index.gen'; -export * from './read_privileges/read_privileges_route'; +export * from './read_privileges/read_privileges.gen'; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_privileges/read_privileges.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_privileges/read_privileges.gen.ts new file mode 100644 index 0000000000000..f3186b3d730c7 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_privileges/read_privileges.gen.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Read privileges API endpoint + * version: 2023-10-31 + */ + +import { z } from 'zod'; + +export type GetPrivilegesResponse = z.infer; +export const GetPrivilegesResponse = z.object({ + is_authenticated: z.boolean(), + has_encryption_key: z.boolean(), +}); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_privileges/read_privileges.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_privileges/read_privileges.schema.yaml new file mode 100644 index 0000000000000..6bf04afd814d3 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_privileges/read_privileges.schema.yaml @@ -0,0 +1,43 @@ +openapi: 3.0.0 +info: + title: Read privileges API endpoint + version: '2023-10-31' +paths: + /api/detection_engine/privileges: + get: + x-labels: [serverless, ess] + operationId: GetPrivileges + x-codegen-enabled: true + summary: Returns user privileges for the Kibana space + description: | + Retrieves whether or not the user is authenticated, and the user's Kibana + space and index privileges, which determine if the user can create an + index for the Elastic Security alerts generated by + detection engine rules. + tags: + - Privileges API + responses: + 200: + description: Successful response + content: + application/json: + schema: + type: object + properties: + is_authenticated: + type: boolean + has_encryption_key: + type: boolean + required: [is_authenticated, has_encryption_key] + 401: + description: Unsuccessful authentication response + content: + application/json: + schema: + $ref: '../../../model/error_responses.schema.yaml#/components/schemas/PlatformErrorResponse' + 500: + description: Internal server error response + content: + application/json: + schema: + $ref: '../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse' diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_privileges/read_privileges_route.ts b/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_privileges/read_privileges_route.ts deleted file mode 100644 index 8645f5c4abff1..0000000000000 --- a/x-pack/plugins/security_solution/common/api/detection_engine/index_management/read_privileges/read_privileges_route.ts +++ /dev/null @@ -1,11 +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. - */ - -export interface ReadPrivilegesResponse { - is_authenticated: boolean; - has_encryption_key: boolean; -} diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.gen.ts index 7b300403f45c3..f4e51d9a4c2f5 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.gen.ts @@ -30,3 +30,15 @@ export type BulkDeleteRulesRequestBodyInput = z.input; export const BulkDeleteRulesResponse = BulkCrudRulesResponse; + +export type BulkDeleteRulesPostRequestBody = z.infer; +export const BulkDeleteRulesPostRequestBody = z.array( + z.object({ + id: RuleObjectId.optional(), + rule_id: RuleSignatureId.optional(), + }) +); +export type BulkDeleteRulesPostRequestBodyInput = z.input; + +export type BulkDeleteRulesPostResponse = z.infer; +export const BulkDeleteRulesPostResponse = BulkCrudRulesResponse; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.schema.yaml index 02f78a65fee7c..0229f28a0941d 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.schema.yaml @@ -33,3 +33,73 @@ paths: application/json: schema: $ref: '../response_schema.schema.yaml#/components/schemas/BulkCrudRulesResponse' + 400: + description: Invalid input data response + content: + application/json: + schema: + oneOf: + - $ref: '../../../model/error_responses.schema.yaml#/components/schemas/PlatformErrorResponse' + - $ref: '../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse' + 401: + description: Unsuccessful authentication response + content: + application/json: + schema: + $ref: '../../../model/error_responses.schema.yaml#/components/schemas/PlatformErrorResponse' + 500: + description: Internal server error response + content: + application/json: + schema: + $ref: '../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse' + + post: + x-labels: [ess] + x-codegen-enabled: true + operationId: BulkDeleteRulesPost + deprecated: true + description: Deletes multiple rules. + tags: + - Bulk API + requestBody: + description: A JSON array of `id` or `rule_id` fields of the rules you want to delete. + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + $ref: '../../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleObjectId' + rule_id: + $ref: '../../../model/rule_schema/common_attributes.schema.yaml#/components/schemas/RuleSignatureId' + responses: + 200: + description: Indicates a successful call. + content: + application/json: + schema: + $ref: '../response_schema.schema.yaml#/components/schemas/BulkCrudRulesResponse' + 400: + description: Invalid input data response + content: + application/json: + schema: + oneOf: + - $ref: '../../../model/error_responses.schema.yaml#/components/schemas/PlatformErrorResponse' + - $ref: '../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse' + 401: + description: Unsuccessful authentication response + content: + application/json: + schema: + $ref: '../../../model/error_responses.schema.yaml#/components/schemas/PlatformErrorResponse' + 500: + description: Internal server error response + content: + application/json: + schema: + $ref: '../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse' diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/index.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/index.ts index ce828c9819b82..008d8f3fd5488 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/index.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './preview_rules_route'; +export * from './rule_preview.gen'; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/preview_rules_route.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/preview_rules_route.ts deleted file mode 100644 index 6f0c61834b138..0000000000000 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/preview_rules_route.ts +++ /dev/null @@ -1,27 +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 * as z from 'zod'; -import { SharedCreateProps, TypeSpecificCreateProps } from '../model/rule_schema'; - -export type PreviewRulesSchema = z.infer; -export const PreviewRulesSchema = SharedCreateProps.and(TypeSpecificCreateProps).and( - z.object({ invocationCount: z.number(), timeframeEnd: z.string() }) -); - -export interface RulePreviewLogs { - errors: string[]; - warnings: string[]; - startedAt?: string; - duration: number; -} - -export interface PreviewResponse { - previewId: string | undefined; - logs: RulePreviewLogs[] | undefined; - isAborted: boolean | undefined; -} diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts new file mode 100644 index 0000000000000..ebff0affbec35 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts @@ -0,0 +1,66 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Rule preview API endpoint + * version: 2023-10-31 + */ + +import { z } from 'zod'; + +import { + EqlRuleCreateProps, + QueryRuleCreateProps, + SavedQueryRuleCreateProps, + ThresholdRuleCreateProps, + ThreatMatchRuleCreateProps, + MachineLearningRuleCreateProps, + NewTermsRuleCreateProps, + EsqlRuleCreateProps, +} from '../model/rule_schema/rule_schemas.gen'; +import { NonEmptyString } from '../../model/primitives.gen'; + +export type RulePreviewParams = z.infer; +export const RulePreviewParams = z.object({ + invocationCount: z.number().int(), + timeframeEnd: z.string().datetime(), +}); + +export type RulePreviewLogs = z.infer; +export const RulePreviewLogs = z.object({ + errors: z.array(NonEmptyString), + warnings: z.array(NonEmptyString), + /** + * Execution duration in milliseconds + */ + duration: z.number().int(), + startedAt: NonEmptyString.optional(), +}); + +export type RulePreviewRequestBody = z.infer; +export const RulePreviewRequestBody = z.discriminatedUnion('type', [ + EqlRuleCreateProps.merge(RulePreviewParams), + QueryRuleCreateProps.merge(RulePreviewParams), + SavedQueryRuleCreateProps.merge(RulePreviewParams), + ThresholdRuleCreateProps.merge(RulePreviewParams), + ThreatMatchRuleCreateProps.merge(RulePreviewParams), + MachineLearningRuleCreateProps.merge(RulePreviewParams), + NewTermsRuleCreateProps.merge(RulePreviewParams), + EsqlRuleCreateProps.merge(RulePreviewParams), +]); +export type RulePreviewRequestBodyInput = z.input; + +export type RulePreviewResponse = z.infer; +export const RulePreviewResponse = z.object({ + logs: z.array(RulePreviewLogs), + previewId: NonEmptyString.optional(), + isAborted: z.boolean().optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml new file mode 100644 index 0000000000000..933dccc0b8d65 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml @@ -0,0 +1,116 @@ +openapi: 3.0.0 +info: + title: Rule preview API endpoint + version: '2023-10-31' +paths: + /api/detection_engine/rules/preview: + post: + x-labels: [serverless, ess] + operationId: RulePreview + x-codegen-enabled: true + summary: Preview rule alerts generated on specified time range + tags: + - Rule preview API + requestBody: + description: An object containing tags to add or remove and alert ids the changes will be applied + required: true + content: + application/json: + schema: + discriminator: + propertyName: type + anyOf: + - allOf: + - $ref: '../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/EqlRuleCreateProps' + - $ref: '#/components/schemas/RulePreviewParams' + - allOf: + - $ref: '../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/QueryRuleCreateProps' + - $ref: '#/components/schemas/RulePreviewParams' + - allOf: + - $ref: '../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/SavedQueryRuleCreateProps' + - $ref: '#/components/schemas/RulePreviewParams' + - allOf: + - $ref: '../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/ThresholdRuleCreateProps' + - $ref: '#/components/schemas/RulePreviewParams' + - allOf: + - $ref: '../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/ThreatMatchRuleCreateProps' + - $ref: '#/components/schemas/RulePreviewParams' + - allOf: + - $ref: '../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/MachineLearningRuleCreateProps' + - $ref: '#/components/schemas/RulePreviewParams' + - allOf: + - $ref: '../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/NewTermsRuleCreateProps' + - $ref: '#/components/schemas/RulePreviewParams' + - allOf: + - $ref: '../model/rule_schema/rule_schemas.schema.yaml#/components/schemas/EsqlRuleCreateProps' + - $ref: '#/components/schemas/RulePreviewParams' + responses: + 200: + description: Successful response + content: + application/json: + schema: + type: object + properties: + logs: + type: array + items: + $ref: '#/components/schemas/RulePreviewLogs' + previewId: + $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + isAborted: + type: boolean + required: [logs] + 400: + description: Invalid input data response + content: + application/json: + schema: + oneOf: + - $ref: '../../../model/error_responses.schema.yaml#/components/schemas/PlatformErrorResponse' + - $ref: '../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse' + 401: + description: Unsuccessful authentication response + content: + application/json: + schema: + $ref: '../../../model/error_responses.schema.yaml#/components/schemas/PlatformErrorResponse' + 500: + description: Internal server error response + content: + application/json: + schema: + $ref: '../../../model/error_responses.schema.yaml#/components/schemas/SiemErrorResponse' + +components: + schemas: + RulePreviewParams: + type: object + properties: + invocationCount: + type: integer + timeframeEnd: + type: string + format: date-time + required: [invocationCount, timeframeEnd] + + RulePreviewLogs: + type: object + properties: + errors: + type: array + items: + $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + warnings: + type: array + items: + $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + duration: + type: integer + description: Execution duration in milliseconds + startedAt: + $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + required: + - errors + - warnings + - duration diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 03e1d324494f4..1b4e23d83211b 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -105,11 +105,6 @@ export const allowedExperimentalValues = Object.freeze({ */ alertTypeEnabled: false, - /** - * Disables expandable flyout - */ - expandableFlyoutDisabled: false, - /** * Enables new notes */ @@ -132,7 +127,6 @@ export const allowedExperimentalValues = Object.freeze({ /** * Enables the Managed User section inside the new user details flyout. - * To see this section you also need expandableFlyoutDisabled flag set to false. */ newUserDetailsFlyoutManagedUser: false, diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index 54ece0123738f..8609cdbd991e4 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -11,7 +11,6 @@ import type { CaseViewRefreshPropInterface } from '@kbn/cases-plugin/common'; import { CaseMetricsFeature } from '@kbn/cases-plugin/common'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { CaseDetailsRefreshContext } from '../../common/components/endpoint'; -import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { DocumentDetailsRightPanelKey } from '../../flyout/document_details/shared/constants/panel_keys'; import { useTourContext } from '../../common/components/guided_onboarding_tour'; import { @@ -58,7 +57,6 @@ const CaseContainerComponent: React.FC = () => { SecurityPageName.rules ); const { openFlyout } = useExpandableFlyoutApi(); - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); const getDetectionsRuleDetailsHref = useCallback( (ruleId) => detectionsFormatUrl(getRuleDetailsUrl(ruleId ?? '', detectionsUrlSearch)), @@ -69,38 +67,22 @@ const CaseContainerComponent: React.FC = () => { const showAlertDetails = useCallback( (alertId: string, index: string) => { - if (!expandableFlyoutDisabled) { - openFlyout({ - right: { - id: DocumentDetailsRightPanelKey, - params: { - id: alertId, - indexName: index, - scopeId: TimelineId.casePage, - }, + openFlyout({ + right: { + id: DocumentDetailsRightPanelKey, + params: { + id: alertId, + indexName: index, + scopeId: TimelineId.casePage, }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: TimelineId.casePage, - panel: 'right', - }); - } - // TODO remove when https://github.com/elastic/security-team/issues/7462 is merged - // support of old flyout in cases page - else { - dispatch( - timelineActions.toggleDetailPanel({ - panelView: 'eventDetail', - id: TimelineId.casePage, - params: { - eventId: alertId, - indexName: index, - }, - }) - ); - } + }, + }); + telemetry.reportDetailsFlyoutOpened({ + location: TimelineId.casePage, + panel: 'right', + }); }, - [dispatch, expandableFlyoutDisabled, openFlyout, telemetry] + [openFlyout, telemetry] ); const endpointDetailsHref = (endpointId: string) => diff --git a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx index 416dfcae71c99..5c61900876aae 100644 --- a/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/control_columns/row_action/index.tsx @@ -7,14 +7,9 @@ import type { EuiDataGridCellValueElementProps } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { dataTableActions, TableId } from '@kbn/securitysolution-data-table'; import { LeftPanelNotesTab } from '../../../../flyout/document_details/left'; -import { useRouteSpy } from '../../../utils/route/use_route_spy'; import { useKibana } from '../../../lib/kibana'; -import { timelineActions } from '../../../../timelines/store'; -import { SecurityPageName } from '../../../../../common/constants'; import { DocumentDetailsLeftPanelKey, DocumentDetailsRightPanelKey, @@ -23,12 +18,10 @@ import type { SetEventsDeleted, SetEventsLoading, ControlColumnProps, - ExpandedDetailType, } from '../../../../../common/types'; import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns'; import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; import type { ColumnHeaderOptions, OnRowSelected } from '../../../../../common/types/timeline'; -import { TimelineId } from '../../../../../common/types'; import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; import { useTourContext } from '../../guided_onboarding_tour'; import { AlertsCasesTourSteps, SecurityStepId } from '../../guided_onboarding_tour/tour_config'; @@ -79,8 +72,6 @@ const RowActionComponent = ({ const { telemetry } = useKibana().services; const { openFlyout } = useExpandableFlyoutApi(); - const dispatch = useDispatch(); - const [{ pageName }] = useRouteSpy(); const { activeStep, isTourShown } = useTourContext(); const shouldFocusOnOverviewTab = (activeStep === AlertsCasesTourSteps.expandEvent || @@ -102,72 +93,27 @@ const RowActionComponent = ({ [columnHeaders, timelineNonEcsData] ); - // TODO remove when https://github.com/elastic/security-team/issues/7462 is merged - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesEnabled' ); - const showExpandableFlyout = - pageName === SecurityPageName.attackDiscovery ? true : !expandableFlyoutDisabled; const handleOnEventDetailPanelOpened = useCallback(() => { - const updatedExpandedDetail: ExpandedDetailType = { - panelView: 'eventDetail', - params: { - eventId: eventId ?? '', - indexName: indexName ?? '', - }, - }; - - if (showExpandableFlyout) { - openFlyout({ - right: { - id: DocumentDetailsRightPanelKey, - path: shouldFocusOnOverviewTab ? { tab: 'overview' } : undefined, - params: { - id: eventId, - indexName, - scopeId: tableId, - }, + openFlyout({ + right: { + id: DocumentDetailsRightPanelKey, + path: shouldFocusOnOverviewTab ? { tab: 'overview' } : undefined, + params: { + id: eventId, + indexName, + scopeId: tableId, }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: tableId, - panel: 'right', - }); - } - // TODO remove when https://github.com/elastic/security-team/issues/7462 is merged - // support of old flyout in cases page - else if (tableId === TableId.alertsOnCasePage) { - dispatch( - timelineActions.toggleDetailPanel({ - ...updatedExpandedDetail, - id: TimelineId.casePage, - }) - ); - } - // TODO remove when https://github.com/elastic/security-team/issues/7462 is merged - // support of old flyout - else { - dispatch( - dataTableActions.toggleDetailPanel({ - ...updatedExpandedDetail, - tabType, - id: tableId, - }) - ); - } - }, [ - eventId, - indexName, - showExpandableFlyout, - tableId, - openFlyout, - shouldFocusOnOverviewTab, - telemetry, - dispatch, - tabType, - ]); + }, + }); + telemetry.reportDetailsFlyoutOpened({ + location: tableId, + panel: 'right', + }); + }, [eventId, indexName, tableId, openFlyout, shouldFocusOnOverviewTab, telemetry]); const toggleShowNotes = useCallback(() => { openFlyout({ @@ -229,14 +175,12 @@ const RowActionComponent = ({ showCheckboxes={showCheckboxes} tabType={tabType} timelineId={tableId} - toggleShowNotes={ - !expandableFlyoutDisabled && securitySolutionNotesEnabled ? toggleShowNotes : undefined - } + toggleShowNotes={securitySolutionNotesEnabled ? toggleShowNotes : undefined} width={width} setEventsLoading={setEventsLoading} setEventsDeleted={setEventsDeleted} refetch={refetch} - showNotes={!expandableFlyoutDisabled && securitySolutionNotesEnabled ? true : false} + showNotes={securitySolutionNotesEnabled ? true : false} /> )} diff --git a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx index 9ae3f3adfed80..e8ba1e8c22d05 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_actions/actions.tsx @@ -220,8 +220,6 @@ const ActionsComponent: React.FC = ({ 'securitySolutionNotesEnabled' ); - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); - /* only applicable for new event based notes */ const documentBasedNotes = useSelector((state: State) => selectNotesByDocumentId(state, eventId)); @@ -232,18 +230,15 @@ const ActionsComponent: React.FC = ({ ); const notesCount = useMemo( - () => - securitySolutionNotesEnabled && !expandableFlyoutDisabled - ? documentBasedNotes.length - : timelineNoteIds.length, - [documentBasedNotes, timelineNoteIds, securitySolutionNotesEnabled, expandableFlyoutDisabled] + () => (securitySolutionNotesEnabled ? documentBasedNotes.length : timelineNoteIds.length), + [documentBasedNotes, timelineNoteIds, securitySolutionNotesEnabled] ); const noteIds = useMemo(() => { - return securitySolutionNotesEnabled && !expandableFlyoutDisabled + return securitySolutionNotesEnabled ? documentBasedNotes.map((note) => note.noteId) : timelineNoteIds; - }, [documentBasedNotes, timelineNoteIds, securitySolutionNotesEnabled, expandableFlyoutDisabled]); + }, [documentBasedNotes, timelineNoteIds, securitySolutionNotesEnabled]); return ( diff --git a/x-pack/plugins/security_solution/public/common/hooks/flyout/use_init_flyout_url_param.ts b/x-pack/plugins/security_solution/public/common/hooks/flyout/use_init_flyout_url_param.ts deleted file mode 100644 index 659fc15ca5ea5..0000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/flyout/use_init_flyout_url_param.ts +++ /dev/null @@ -1,103 +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 { useCallback, useEffect, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { useLocation } from 'react-router-dom'; -import { - dataTableSelectors, - TableId, - tableDefaults, - dataTableActions, -} from '@kbn/securitysolution-data-table'; -import { useIsExperimentalFeatureEnabled } from '../use_experimental_features'; -import { useInitializeUrlParam } from '../../utils/global_query_string'; -import { URL_PARAM_KEY } from '../use_url_state'; -import type { FlyoutUrlState } from './types'; -import { useShallowEqualSelector } from '../use_selector'; -import { getQueryStringKeyValue } from '../timeline/use_query_timeline_by_id_on_url_change'; - -/** - * The state of the old flyout of the table in the Alerts page is stored in local storage. - * This hook was created to initialize things and populate the url with the correct param and its value. - * This is only be needed with the old flyout. - * // TODO remove this hook entirely when we delete the old flyout code - */ - -export const useInitFlyoutFromUrlParam = () => { - const { search } = useLocation(); - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); - const [urlDetails, setUrlDetails] = useState(null); - const [hasLoadedUrlDetails, updateHasLoadedUrlDetails] = useState(false); - const dispatch = useDispatch(); - const getDataTable = dataTableSelectors.getTableByIdSelector(); - - // Only allow the alerts page for now to be saved in the url state. - // Allowing only one makes the transition to the expanded flyout much easier as well - const dataTableCurrent = useShallowEqualSelector( - (state) => getDataTable(state, TableId.alertsOnAlertsPage) ?? tableDefaults - ); - - const onInitialize = useCallback( - (initialState: FlyoutUrlState | null) => { - if (expandableFlyoutDisabled && initialState != null && initialState.panelView) { - setUrlDetails(initialState); - } - }, - [expandableFlyoutDisabled] - ); - - const loadExpandedDetailFromUrl = useCallback(() => { - if (!expandableFlyoutDisabled) return; - - const { initialized, isLoading, totalCount, additionalFilters } = dataTableCurrent; - const isTableLoaded = initialized && !isLoading && totalCount > 0; - if (urlDetails) { - if (!additionalFilters || !additionalFilters.showBuildingBlockAlerts) { - // We want to show building block alerts when loading the flyout in case the alert is a building block alert - dispatch( - dataTableActions.updateShowBuildingBlockAlertsFilter({ - id: TableId.alertsOnAlertsPage, - showBuildingBlockAlerts: true, - }) - ); - } - - if (isTableLoaded) { - updateHasLoadedUrlDetails(true); - dispatch( - dataTableActions.toggleDetailPanel({ - id: TableId.alertsOnAlertsPage, - ...urlDetails, - }) - ); - } - } - }, [dataTableCurrent, dispatch, expandableFlyoutDisabled, urlDetails]); - - // The alert page creates a default dataTable slice in redux initially that is later overriden when data is retrieved - // We use the below to store the urlDetails on app load, and then set it when the table is done loading and has data - useEffect(() => { - if (expandableFlyoutDisabled && !hasLoadedUrlDetails) { - loadExpandedDetailFromUrl(); - } - }, [hasLoadedUrlDetails, expandableFlyoutDisabled, loadExpandedDetailFromUrl]); - - // We check the url for the presence of the old `evenFlyout` parameter. If it exists replace it with the new `flyout` key. - const eventFlyoutKey = getQueryStringKeyValue({ urlKey: URL_PARAM_KEY.eventFlyout, search }); - let currentKey = ''; - let newKey = ''; - if (expandableFlyoutDisabled) { - if (eventFlyoutKey) { - currentKey = URL_PARAM_KEY.eventFlyout; - newKey = URL_PARAM_KEY.flyout; - } else { - currentKey = URL_PARAM_KEY.flyout; - } - } - useInitializeUrlParam(currentKey, onInitialize, newKey); -}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/flyout/use_sync_flyout_url_param.ts b/x-pack/plugins/security_solution/public/common/hooks/flyout/use_sync_flyout_url_param.ts deleted file mode 100644 index a6f4904d37cd9..0000000000000 --- a/x-pack/plugins/security_solution/public/common/hooks/flyout/use_sync_flyout_url_param.ts +++ /dev/null @@ -1,59 +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 { useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; -import { useDispatch } from 'react-redux'; -import { - dataTableActions, - dataTableSelectors, - tableDefaults, - TableId, -} from '@kbn/securitysolution-data-table'; -import { useIsExperimentalFeatureEnabled } from '../use_experimental_features'; -import { ALERTS_PATH } from '../../../../common/constants'; -import { useUpdateUrlParam } from '../../utils/global_query_string'; -import { useShallowEqualSelector } from '../use_selector'; -import { URL_PARAM_KEY } from '../use_url_state'; -import type { FlyoutUrlState } from './types'; - -/** - * The state of the old flyout of the table in the Alerts page is stored in local storage. - * This hook was created to sync the data stored in local storage to the url. - * This is only be needed with the old flyout. - * // TODO remove this hook entirely when we delete the old flyout code - */ -export const useSyncFlyoutUrlParam = () => { - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); - const updateUrlParam = useUpdateUrlParam(URL_PARAM_KEY.flyout); - const { pathname } = useLocation(); - const dispatch = useDispatch(); - const getDataTable = dataTableSelectors.getTableByIdSelector(); - - // Only allow the alerts page for now to be saved in the url state - const { expandedDetail } = useShallowEqualSelector( - (state) => getDataTable(state, TableId.alertsOnAlertsPage) ?? tableDefaults - ); - - useEffect(() => { - if (!expandableFlyoutDisabled) return; - - const isOnAlertsPage = pathname === ALERTS_PATH; - if (isOnAlertsPage && expandedDetail != null && expandedDetail?.query) { - updateUrlParam(expandedDetail.query.panelView ? expandedDetail.query : null); - } else if (!isOnAlertsPage && expandedDetail?.query?.panelView) { - // Close the detail panel as it's stored in a top level redux store maintained across views - dispatch( - dataTableActions.toggleDetailPanel({ - id: TableId.alertsOnAlertsPage, - }) - ); - // Clear the reference from the url - updateUrlParam(null); - } - }, [dispatch, expandedDetail, expandableFlyoutDisabled, pathname, updateUrlParam]); -}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts b/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts index 597a87f6844d2..47168d76e8ec3 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts @@ -12,8 +12,6 @@ import { useUpdateTimerangeOnPageChange } from './search_bar/use_update_timerang import { useInitTimelineFromUrlParam } from './timeline/use_init_timeline_url_param'; import { useSyncTimelineUrlParam } from './timeline/use_sync_timeline_url_param'; import { useQueryTimelineByIdOnUrlChange } from './timeline/use_query_timeline_by_id_on_url_change'; -import { useInitFlyoutFromUrlParam } from './flyout/use_init_flyout_url_param'; -import { useSyncFlyoutUrlParam } from './flyout/use_sync_flyout_url_param'; export const useUrlState = () => { useSyncGlobalQueryString(); @@ -23,8 +21,6 @@ export const useUrlState = () => { useInitTimelineFromUrlParam(); useSyncTimelineUrlParam(); useQueryTimelineByIdOnUrlChange(); - useInitFlyoutFromUrlParam(); - useSyncFlyoutUrlParam(); }; export const URL_PARAM_KEY = { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts index 61c7f298bd4fa..9fb7417bca036 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts @@ -8,7 +8,10 @@ import { useEffect, useMemo, useState } from 'react'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import type { PreviewResponse, RuleCreateProps } from '../../../../../common/api/detection_engine'; +import type { + RuleCreateProps, + RulePreviewResponse, +} from '../../../../../common/api/detection_engine'; import { previewRule } from '../../../rule_management/api/api'; import { transformOutput } from '../../../../detections/containers/detection_engine/rules/transforms'; @@ -16,7 +19,7 @@ import type { TimeframePreviewOptions } from '../../../../detections/pages/detec import { usePreviewInvocationCount } from './use_preview_invocation_count'; import * as i18n from './translations'; -const emptyPreviewRule: PreviewResponse = { +const emptyPreviewRule: RulePreviewResponse = { previewId: undefined, logs: [], isAborted: false, @@ -28,7 +31,7 @@ export const usePreviewRule = ({ timeframeOptions: TimeframePreviewOptions; }) => { const [rule, setRule] = useState(null); - const [response, setResponse] = useState(emptyPreviewRule); + const [response, setResponse] = useState(emptyPreviewRule); const [isLoading, setIsLoading] = useState(false); const { addError } = useAppToasts(); const { invocationCount, interval, from } = usePreviewInvocationCount({ timeframeOptions }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index ad82a33559bbc..d18658087a86f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -59,7 +59,7 @@ import { import type { RulesReferencedByExceptionListsSchema } from '../../../../common/api/detection_engine/rule_exceptions'; import { DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL } from '../../../../common/api/detection_engine/rule_exceptions'; -import type { PreviewResponse, RuleResponse } from '../../../../common/api/detection_engine'; +import type { RulePreviewResponse, RuleResponse } from '../../../../common/api/detection_engine'; import { KibanaServices } from '../../../common/lib/kibana'; import * as i18n from '../../../detections/pages/detection_engine/rules/translations'; @@ -149,8 +149,11 @@ export const patchRule = async ({ * * @throws An error if response is not OK */ -export const previewRule = async ({ rule, signal }: PreviewRulesProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_PREVIEW, { +export const previewRule = async ({ + rule, + signal, +}: PreviewRulesProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_PREVIEW, { method: 'POST', version: '2023-10-31', body: JSON.stringify(rule), diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx index 2e838e9f2ba23..4f2c995b163d5 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_actions_column.tsx @@ -29,11 +29,10 @@ export const getUseActionColumnHook = // we only want to show the note icon if the expandable flyout and the new notes system are enabled // TODO delete most likely in 8.16 - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesEnabled' ); - if (expandableFlyoutDisabled || !securitySolutionNotesEnabled) { + if (!securitySolutionNotesEnabled) { ACTION_BUTTON_COUNT--; } diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.test.tsx index 509d59338fe4d..d1f3bfea2aafe 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.test.tsx @@ -11,9 +11,6 @@ import { AlertDetailsRedirect } from './alert_details_redirect'; import { TestProviders } from '../../../common/mock'; import { ALERTS_PATH, ALERT_DETAILS_REDIRECT_PATH } from '../../../../common/constants'; import { mockHistory } from '../../../common/utils/route/mocks'; -import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; - -jest.mock('../../../common/hooks/use_experimental_features'); jest.mock('../../../common/lib/kibana'); @@ -149,37 +146,4 @@ describe('AlertDetailsRedirect', () => { }); }); }); - - describe('When expandable flyout is enabled', () => { - beforeEach(() => { - jest.mocked(useIsExperimentalFeatureEnabled).mockReturnValue(true); - }); - - describe('when eventFlyout or flyout are not in the query', () => { - it('redirects to the expected path with the correct query parameters', () => { - const testSearch = `?index=${testIndex}×tamp=${testTimestamp}`; - const historyMock = { - ...mockHistory, - location: { - hash: '', - pathname: mockPathname, - search: testSearch, - state: '', - }, - }; - render( - - - - - - ); - - const [{ search, pathname }] = historyMock.replace.mock.lastCall; - - expect(search as string).toMatch(/flyout.*/); - expect(pathname).toEqual(ALERTS_PATH); - }); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx b/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx index 9655e6fb84f92..01e52d3ab7505 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/alert_details_redirect.tsx @@ -13,7 +13,6 @@ import moment from 'moment'; import { encode } from '@kbn/rison'; import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; import type { FilterControlConfig } from '@kbn/alerts-ui-shared'; -import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { ALERTS_PATH, DEFAULT_ALERTS_INDEX } from '../../../../common/constants'; import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state'; import { inputsSelectors } from '../../../common/store'; @@ -69,16 +68,11 @@ export const AlertDetailsRedirect = () => { const currentFlyoutParams = searchParams.get(URL_PARAM_KEY.flyout); - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); - const urlParams = new URLSearchParams({ [URL_PARAM_KEY.appQuery]: kqlAppQuery, [URL_PARAM_KEY.timerange]: timerange, [URL_PARAM_KEY.pageFilter]: pageFiltersQuery, - [URL_PARAM_KEY.flyout]: resolveFlyoutParams( - { index, alertId, expandableFlyoutDisabled }, - currentFlyoutParams - ), + [URL_PARAM_KEY.flyout]: resolveFlyoutParams({ index, alertId }, currentFlyoutParams), }); const url = `${ALERTS_PATH}?${urlParams.toString()}`; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/alerts/utils.ts index ac1d2a862b92c..4bbb386bc86dd 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/utils.ts @@ -11,7 +11,6 @@ import { expandableFlyoutStateFromEventMeta } from '../../../flyout/document_det export interface ResolveFlyoutParamsConfig { index: string; alertId: string; - expandableFlyoutDisabled: boolean; } /** @@ -21,27 +20,14 @@ export interface ResolveFlyoutParamsConfig { * with Share Button on the Expandable Flyout */ export const resolveFlyoutParams = ( - { index, alertId, expandableFlyoutDisabled }: ResolveFlyoutParamsConfig, + { index, alertId }: ResolveFlyoutParamsConfig, currentParamsString: string | null ) => { - if (expandableFlyoutDisabled) { - const legacyFlyoutString = encode({ - panelView: 'eventDetail', - params: { - eventId: alertId, - indexName: index, - }, - }); - return legacyFlyoutString; - } - if (currentParamsString) { return currentParamsString; } - const modernFlyoutString = encode( + return encode( expandableFlyoutStateFromEventMeta({ index, eventId: alertId, scopeId: 'alerts-page' }) ); - - return modernFlyoutString; }; diff --git a/x-pack/plugins/security_solution/public/explore/users/containers/users/observed_details/index.tsx b/x-pack/plugins/security_solution/public/explore/users/containers/users/observed_details/index.tsx index d1148af03a41a..1d337e9116556 100644 --- a/x-pack/plugins/security_solution/public/explore/users/containers/users/observed_details/index.tsx +++ b/x-pack/plugins/security_solution/public/explore/users/containers/users/observed_details/index.tsx @@ -13,7 +13,6 @@ import { UsersQueries } from '../../../../../../common/search_strategy/security_ import type { UserItem } from '../../../../../../common/search_strategy/security_solution/users/common'; import { NOT_EVENT_KIND_ASSET_FILTER } from '../../../../../../common/search_strategy/security_solution/users/common'; import { useSearchStrategy } from '../../../../../common/containers/use_search_strategy'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; export const OBSERVED_USER_QUERY_ID = 'observedUsersDetailsQuery'; @@ -43,7 +42,6 @@ export const useObservedUserDetails = ({ skip = false, startDate, }: UseUserDetails): [boolean, UserDetailsArgs] => { - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); const { loading, result: response, @@ -81,9 +79,9 @@ export const useObservedUserDetails = ({ from: startDate, to: endDate, }, - filterQuery: !expandableFlyoutDisabled ? NOT_EVENT_KIND_ASSET_FILTER : undefined, + filterQuery: NOT_EVENT_KIND_ASSET_FILTER, }), - [endDate, indexNames, startDate, userName, expandableFlyoutDisabled] + [endDate, indexNames, startDate, userName] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/management/common/translations.ts b/x-pack/plugins/security_solution/public/management/common/translations.ts index 469184e73a1f9..c07c78deb4c70 100644 --- a/x-pack/plugins/security_solution/public/management/common/translations.ts +++ b/x-pack/plugins/security_solution/public/management/common/translations.ts @@ -93,6 +93,12 @@ export const CONSOLE_COMMANDS = { } ), }, + processName: { + about: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.processName.arg.comment', + { defaultMessage: 'The process name to kill' } + ), + }, }, }, suspendProcess: { diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/kill_process_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/kill_process_action.test.tsx index 517bd7101393b..0ba2bfa0e32da 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/kill_process_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/kill_process_action.test.tsx @@ -13,11 +13,14 @@ import { } from '../../../console/components/console_manager/mocks'; import React from 'react'; import { getEndpointConsoleCommands } from '../../lib/console_commands_definition'; -import { enterConsoleCommand } from '../../../console/mocks'; +import { enterConsoleCommand, getConsoleSelectorsAndActionMock } from '../../../console/mocks'; import { waitFor } from '@testing-library/react'; import { responseActionsHttpMocks } from '../../../../mocks/response_actions_http_mocks'; import { getEndpointAuthzInitialState } from '../../../../../../common/endpoint/service/authz'; -import type { EndpointCapabilities } from '../../../../../../common/endpoint/service/response_actions/constants'; +import type { + EndpointCapabilities, + ResponseActionAgentType, +} from '../../../../../../common/endpoint/service/response_actions/constants'; import { ENDPOINT_CAPABILITIES } from '../../../../../../common/endpoint/service/response_actions/constants'; import type { ActionDetailsApiResponse, @@ -25,10 +28,10 @@ import type { } from '../../../../../../common/endpoint/types'; import { endpointActionResponseCodes } from '../../lib/endpoint_action_response_codes'; import { UPGRADE_AGENT_FOR_RESPONDER } from '../../../../../common/translations'; - -jest.mock('../../../../../common/experimental_features_service'); +import type { CommandDefinition } from '../../../console'; describe('When using the kill-process action from response actions console', () => { + let mockedContext: AppContextTestRender; let render: ( capabilities?: EndpointCapabilities[] ) => Promise>; @@ -37,31 +40,41 @@ describe('When using the kill-process action from response actions console', () let consoleManagerMockAccess: ReturnType< typeof getConsoleManagerMockRenderResultQueriesAndActions >; + let consoleCommands: CommandDefinition[]; + let consoleSelectors: ReturnType; + + /** Sets the console commands to the `consoleCommands` defined variable above */ + const setConsoleCommands = ( + capabilities: EndpointCapabilities[] = [...ENDPOINT_CAPABILITIES], + agentType: ResponseActionAgentType = 'endpoint' + ): void => { + consoleCommands = getEndpointConsoleCommands({ + agentType, + endpointAgentId: 'a.b.c', + endpointCapabilities: capabilities, + endpointPrivileges: { + ...getEndpointAuthzInitialState(), + loading: false, + canKillProcess: true, + canSuspendProcess: true, + canGetRunningProcesses: true, + }, + }); + }; beforeEach(() => { - const mockedContext = createAppRootMockRenderer(); - + mockedContext = createAppRootMockRenderer(); apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http); + setConsoleCommands(); - render = async (capabilities: EndpointCapabilities[] = [...ENDPOINT_CAPABILITIES]) => { + render = async () => { renderResult = mockedContext.render( { return { consoleProps: { 'data-test-subj': 'test', - commands: getEndpointConsoleCommands({ - agentType: 'endpoint', - endpointAgentId: 'a.b.c', - endpointCapabilities: [...capabilities], - endpointPrivileges: { - ...getEndpointAuthzInitialState(), - loading: false, - canKillProcess: true, - canSuspendProcess: true, - canGetRunningProcesses: true, - }, - }), + commands: consoleCommands, }, }; }} @@ -69,16 +82,22 @@ describe('When using the kill-process action from response actions console', () ); consoleManagerMockAccess = getConsoleManagerMockRenderResultQueriesAndActions(renderResult); - await consoleManagerMockAccess.clickOnRegisterNewConsole(); await consoleManagerMockAccess.openRunningConsole(); + consoleSelectors = getConsoleSelectorsAndActionMock(renderResult); return renderResult; }; }); + afterEach(() => { + // @ts-expect-error + consoleSelectors = undefined; + }); + it('should show an error if the `kill_process` capability is not present in the endpoint', async () => { - await render([]); + setConsoleCommands([]); + await render(); enterConsoleCommand(renderResult, 'kill-process --pid 123'); expect(renderResult.getByTestId('test-validationError-message').textContent).toEqual( @@ -361,4 +380,115 @@ describe('When using the kill-process action from response actions console', () expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(1); }); }); + + describe('and the agent type is `SentinelOne`', () => { + beforeEach(() => { + mockedContext.setExperimentalFlag({ + responseActionsSentinelOneKillProcessEnabled: true, + }); + setConsoleCommands(undefined, 'sentinel_one'); + }); + + it('should display correct help data', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --help'); + + await waitFor(() => { + expect(renderResult.getByTestId('test-helpOutput')).toHaveTextContent( + 'About' + + 'Kill/terminate a process' + + 'Usage' + + 'kill-process --processName [--comment]' + + 'Example' + + 'kill-process --processName="notepad" --comment="kill malware"' + + 'Required parameters--processName - The process name to kill' + + 'Optional parameters--comment - A comment to go along with the action' + ); + }); + }); + + it('should display correct entry in help panel', async () => { + await render(); + consoleSelectors.openHelpPanel(); + + expect( + renderResult.getByTestId('test-commandList-Responseactions-kill-process') + ).toHaveTextContent('kill-process --processNameKill/terminate a process'); + }); + + it('should only accept processName argument', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --pid=9'); + }); + + it.each` + description | command + ${'no argument is entered'} | ${'kill-process'} + ${'no value provided for processName'} | ${'kill-process --processName'} + ${'empty value provided to processName'} | ${'kill-process --processName=" "'} + `('should error when $description', async ({ command }) => { + await render(); + enterConsoleCommand(renderResult, command); + + expect(renderResult.getByTestId('test-badArgument')).toHaveTextContent( + 'Unsupported argument' + ); + }); + + it('should call API with correct payload for SentinelOne kill-process', async () => { + await render(); + enterConsoleCommand( + renderResult, + 'kill-process --processName="notepad" --comment="some comment"' + ); + + expect(renderResult.getByTestId('killProcess-pending')); + + await waitFor(() => { + expect(apiMocks.responseProvider.killProcess).toHaveBeenCalledWith( + expect.objectContaining({ + body: JSON.stringify({ + agent_type: 'sentinel_one', + endpoint_ids: ['a.b.c'], + comment: 'some comment', + parameters: { + process_name: 'notepad', + }, + }), + }) + ); + }); + }); + + describe('and `responseActionsSentinelOneKillProcessEnabled` feature flag is disabled', () => { + beforeEach(() => { + mockedContext.setExperimentalFlag({ responseActionsSentinelOneKillProcessEnabled: false }); + setConsoleCommands(undefined, 'sentinel_one'); + }); + + it('should error if kill-process is entered', async () => { + await render(); + enterConsoleCommand(renderResult, 'kill-process --processName=foo'); + + await waitFor(() => { + expect(renderResult.getByTestId('test-validationError')).toHaveTextContent( + 'Unsupported actionSupport for kill-process is not currently available for SentinelOne.' + ); + }); + + await waitFor(() => { + expect(apiMocks.responseProvider.killProcess).not.toHaveBeenCalled(); + }); + }); + + it('should not display kill-process in help', async () => { + await render(); + consoleSelectors.openHelpPanel(); + + expect( + renderResult.queryByTestId('test-commandList-Responseactions-kill-process') + ).toBeNull(); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/kill_process_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/kill_process_action.tsx index a6b2951615381..abfc48b78c791 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/kill_process_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/kill_process_action.tsx @@ -6,29 +6,35 @@ */ import { memo, useMemo } from 'react'; -import { parsedPidOrEntityIdParameter } from '../lib/utils'; +import { parsedKillOrSuspendParameter } from '../lib/utils'; import { useSendKillProcessRequest } from '../../../hooks/response_actions/use_send_kill_process_endpoint_request'; import type { ActionRequestComponentProps } from '../types'; import { useConsoleActionSubmitter } from '../hooks/use_console_action_submitter'; import type { KillProcessRequestBody } from '../../../../../common/endpoint/types'; export const KillProcessActionResult = memo< - ActionRequestComponentProps<{ pid?: string[]; entityId?: string[] }> + ActionRequestComponentProps<{ pid?: string[]; entityId?: string[]; processName?: string[] }> >(({ command, setStore, store, status, setStatus, ResultComponent }) => { const actionCreator = useSendKillProcessRequest(); const actionRequestBody = useMemo(() => { const endpointId = command.commandDefinition?.meta?.endpointId; - const parameters = parsedPidOrEntityIdParameter(command.args.args); + const agentType = command.commandDefinition?.meta?.agentType; + const parameters = parsedKillOrSuspendParameter(command.args.args); return endpointId ? { + agent_type: agentType, endpoint_ids: [endpointId], comment: command.args.args?.comment?.[0], parameters, } : undefined; - }, [command.args.args, command.commandDefinition?.meta?.endpointId]); + }, [ + command.args.args, + command.commandDefinition?.meta?.agentType, + command.commandDefinition?.meta?.endpointId, + ]); return useConsoleActionSubmitter({ ResultComponent, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/suspend_process_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/suspend_process_action.tsx index 70893689c0439..344ebe5da626e 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/suspend_process_action.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/suspend_process_action.tsx @@ -6,10 +6,12 @@ */ import { memo, useMemo } from 'react'; -import { parsedPidOrEntityIdParameter } from '../lib/utils'; +import { parsedKillOrSuspendParameter } from '../lib/utils'; import type { SuspendProcessActionOutputContent, SuspendProcessRequestBody, + ResponseActionParametersWithEntityId, + ResponseActionParametersWithPid, } from '../../../../../common/endpoint/types'; import { useSendSuspendProcessRequest } from '../../../hooks/response_actions/use_send_suspend_process_endpoint_request'; import type { ActionRequestComponentProps } from '../types'; @@ -22,7 +24,9 @@ export const SuspendProcessActionResult = memo< const actionRequestBody = useMemo(() => { const endpointId = command.commandDefinition?.meta?.endpointId; - const parameters = parsedPidOrEntityIdParameter(command.args.args); + const parameters = parsedKillOrSuspendParameter(command.args.args) as + | ResponseActionParametersWithPid + | ResponseActionParametersWithEntityId; return endpointId ? { diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts index b57bd57ecd85b..80dce9de15435 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/console_commands_definition.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import type { CommandArgDefinition } from '../../console/types'; import { isAgentTypeAndActionSupported } from '../../../../common/lib/endpoint'; import { getRbacControl } from '../../../../../common/endpoint/service/response_actions/utils'; import { UploadActionResult } from '../command_render_components/upload_action'; @@ -136,6 +137,16 @@ const COMMENT_ARG_ABOUT = i18n.translate( { defaultMessage: 'A comment to go along with the action' } ); +const commandCommentArgument = (): { comment: CommandArgDefinition } => { + return { + comment: { + required: false, + allowMultiples: false, + about: COMMENT_ARG_ABOUT, + }, + }; +}; + export interface GetEndpointConsoleCommandsOptions { endpointAgentId: string; agentType: ResponseActionAgentType; @@ -189,11 +200,7 @@ export const getEndpointConsoleCommands = ({ exampleInstruction: ENTER_OR_ADD_COMMENT_ARG_INSTRUCTION, validate: capabilitiesAndPrivilegesValidator(agentType), args: { - comment: { - required: false, - allowMultiples: false, - about: COMMENT_ARG_ABOUT, - }, + ...commandCommentArgument(), }, helpGroupLabel: HELP_GROUPS.responseActions.label, helpGroupPosition: HELP_GROUPS.responseActions.position, @@ -219,11 +226,7 @@ export const getEndpointConsoleCommands = ({ exampleInstruction: ENTER_OR_ADD_COMMENT_ARG_INSTRUCTION, validate: capabilitiesAndPrivilegesValidator(agentType), args: { - comment: { - required: false, - allowMultiples: false, - about: COMMENT_ARG_ABOUT, - }, + ...commandCommentArgument(), }, helpGroupLabel: HELP_GROUPS.responseActions.label, helpGroupPosition: HELP_GROUPS.responseActions.position, @@ -232,7 +235,6 @@ export const getEndpointConsoleCommands = ({ helpHidden: !getRbacControl({ commandName: 'release', privileges: endpointPrivileges }), }, { - // name: 'kill-process', about: getCommandAboutInfo({ aboutInfo: CONSOLE_COMMANDS.killProcess.about, @@ -250,11 +252,7 @@ export const getEndpointConsoleCommands = ({ validate: capabilitiesAndPrivilegesValidator(agentType), mustHaveArgs: true, args: { - comment: { - required: false, - allowMultiples: false, - about: COMMENT_ARG_ABOUT, - }, + ...commandCommentArgument(), pid: { required: false, allowMultiples: false, @@ -294,11 +292,7 @@ export const getEndpointConsoleCommands = ({ validate: capabilitiesAndPrivilegesValidator(agentType), mustHaveArgs: true, args: { - comment: { - required: false, - allowMultiples: false, - about: COMMENT_ARG_ABOUT, - }, + ...commandCommentArgument(), pid: { required: false, allowMultiples: false, @@ -351,11 +345,7 @@ export const getEndpointConsoleCommands = ({ exampleInstruction: ENTER_OR_ADD_COMMENT_ARG_INSTRUCTION, validate: capabilitiesAndPrivilegesValidator(agentType), args: { - comment: { - required: false, - allowMultiples: false, - about: COMMENT_ARG_ABOUT, - }, + ...commandCommentArgument(), }, helpGroupLabel: HELP_GROUPS.responseActions.label, helpGroupPosition: HELP_GROUPS.responseActions.position, @@ -389,11 +379,7 @@ export const getEndpointConsoleCommands = ({ return emptyArgumentValidator(argData); }, }, - comment: { - required: false, - allowMultiples: false, - about: COMMENT_ARG_ABOUT, - }, + ...commandCommentArgument(), }, helpGroupLabel: HELP_GROUPS.responseActions.label, helpGroupPosition: HELP_GROUPS.responseActions.position, @@ -435,11 +421,7 @@ export const getEndpointConsoleCommands = ({ mustHaveValue: 'non-empty-string', validate: executeTimeoutValidator, }, - comment: { - required: false, - allowMultiples: false, - about: COMMENT_ARG_ABOUT, - }, + ...commandCommentArgument(), }, helpGroupLabel: HELP_GROUPS.responseActions.label, helpGroupPosition: HELP_GROUPS.responseActions.position, @@ -529,11 +511,7 @@ export const getEndpointConsoleCommands = ({ mustHaveValue: 'non-empty-string', about: CONSOLE_COMMANDS.scan.args.path.about, }, - comment: { - required: false, - allowMultiples: false, - about: COMMENT_ARG_ABOUT, - }, + ...commandCommentArgument(), }, helpGroupLabel: HELP_GROUPS.responseActions.label, helpGroupPosition: HELP_GROUPS.responseActions.position, @@ -575,6 +553,24 @@ const adjustCommandsForSentinelOne = ({ const isKillProcessEnabled = featureFlags.responseActionsSentinelOneKillProcessEnabled; return commandList.map((command) => { + // Kill-Process: adjust command to accept only `processName` + if (command.name === 'kill-process') { + command.args = { + ...commandCommentArgument(), + processName: { + required: true, + allowMultiples: false, + about: CONSOLE_COMMANDS.killProcess.args.processName.about, + mustHaveValue: 'non-empty-string', + }, + }; + command.exampleUsage = 'kill-process --processName="notepad" --comment="kill malware"'; + command.exampleInstruction = i18n.translate( + 'xpack.securitySolution.consoleCommandsDefinition.killProcess.sentinelOne.instructions', + { defaultMessage: 'Enter a process name to execute' } + ); + } + if ( command.name === 'status' || (command.name === 'kill-process' && !isKillProcessEnabled) || diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/utils.test.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/utils.test.ts index 4d2ced7be9e61..10763bebfc65e 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/utils.test.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/utils.test.ts @@ -5,22 +5,22 @@ * 2.0. */ -import { parsedPidOrEntityIdParameter, parsedExecuteTimeout } from './utils'; +import { parsedKillOrSuspendParameter, parsedExecuteTimeout } from './utils'; describe('Endpoint Responder - Utilities', () => { - describe('when using parsedPidOrEntityIdParameter()', () => { + describe('when using parsedKillOrSuspendParameter()', () => { it('should parse a pid as a number and return proper params', () => { - const parameters = parsedPidOrEntityIdParameter({ pid: ['123'] }); + const parameters = parsedKillOrSuspendParameter({ pid: ['123'] }); expect(parameters).toEqual({ pid: 123 }); }); it('should parse an entity id correctly and return proper params', () => { - const parameters = parsedPidOrEntityIdParameter({ entityId: ['123qwe'] }); + const parameters = parsedKillOrSuspendParameter({ entityId: ['123qwe'] }); expect(parameters).toEqual({ entity_id: '123qwe' }); }); it('should return entity id with empty string if no params are defined', () => { - const parameters = parsedPidOrEntityIdParameter({}); + const parameters = parsedKillOrSuspendParameter({}); expect(parameters).toEqual({ entity_id: '' }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/utils.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/utils.ts index 41c57feb5c76c..dfd42461ecb36 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/utils.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/utils.ts @@ -7,16 +7,27 @@ import type { ResponseActionParametersWithEntityId, ResponseActionParametersWithPid, + ResponseActionParametersWithProcessName, } from '../../../../../common/endpoint/types'; -export const parsedPidOrEntityIdParameter = (parameters: { +export const parsedKillOrSuspendParameter = (parameters: { pid?: string[]; entityId?: string[]; -}): ResponseActionParametersWithPid | ResponseActionParametersWithEntityId => { + processName?: string[]; +}): + | ResponseActionParametersWithPid + | ResponseActionParametersWithEntityId + | ResponseActionParametersWithProcessName => { if (parameters.pid) { return { pid: Number(parameters.pid[0]) }; } + if (parameters.processName) { + return { + process_name: parameters.processName[0] ?? '', + }; + } + return { entity_id: parameters?.entityId?.[0] ?? '', }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx index 656c80384fe86..43e530d1bbc21 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx @@ -51,6 +51,7 @@ export interface NoteCardsProps { eventId?: string; timelineId: string; onCancel?: () => void; + showToggleEventDetailsAction?: boolean; } /** A view for entering and reviewing notes */ @@ -65,6 +66,7 @@ export const NoteCards = React.memo( eventId, timelineId, onCancel, + showToggleEventDetailsAction = true, }) => { const [newNote, setNewNote] = useState(''); @@ -109,7 +111,11 @@ export const NoteCards = React.memo(

{i18n.YOU_ARE_VIEWING_NOTES(ariaRowindex)}

- + ) : null} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx index c7873663d93d4..7f117d88bf531 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx @@ -239,6 +239,63 @@ describe('NotePreviews', () => { expect(wrapper.find('[data-test-subj="delete-note"] button').prop('disabled')).toBeFalsy(); }); + test('should render toggle event details action by default', () => { + const timeline = mockTimelineResults[0]; + (useDeepEqualSelector as jest.Mock).mockReturnValue(timeline); + + const wrapper = mountWithI18nProvider( + + + , + { + wrappingComponent: createReactQueryWrapper(), + } + ); + + expect(wrapper.find('[data-test-subj="notes-toggle-event-details"]').exists()).toBeTruthy(); + }); + + test('should not render toggle event details action when showToggleEventDetailsAction is false ', () => { + const timeline = mockTimelineResults[0]; + (useDeepEqualSelector as jest.Mock).mockReturnValue(timeline); + + const wrapper = mountWithI18nProvider( + + + , + { + wrappingComponent: createReactQueryWrapper(), + } + ); + + expect(wrapper.find('[data-test-subj="notes-toggle-event-details"]').exists()).toBeFalsy(); + }); + describe('Delete Notes', () => { it('should delete note correctly', async () => { const timeline = { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx index 116cddf36f5f5..fe53c36d3f049 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx @@ -20,7 +20,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { useKibana } from '../../../../common/lib/kibana'; import { DocumentDetailsRightPanelKey } from '../../../../flyout/document_details/shared/constants/panel_keys'; import type { TimelineResultNote } from '../types'; @@ -29,7 +28,7 @@ import { MarkdownRenderer } from '../../../../common/components/markdown_editor' import { timelineActions, timelineSelectors } from '../../../store'; import { NOTE_CONTENT_CLASS_NAME } from '../../timeline/body/helpers'; import * as i18n from './translations'; -import { TimelineTabs, TimelineId } from '../../../../../common/types/timeline'; +import { TimelineId } from '../../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { useSourcererDataView } from '../../../../sourcerer/containers'; @@ -51,56 +50,31 @@ const ToggleEventDetailsButtonComponent: React.FC eventId, timelineId, }) => { - const dispatch = useDispatch(); const { selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline); const { telemetry } = useKibana().services; const { openFlyout } = useExpandableFlyoutApi(); - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); const handleClick = useCallback(() => { - const indexName = selectedPatterns.join(','); - - if (!expandableFlyoutDisabled) { - openFlyout({ - right: { - id: DocumentDetailsRightPanelKey, - params: { - id: eventId, - indexName, - scopeId: timelineId, - }, + openFlyout({ + right: { + id: DocumentDetailsRightPanelKey, + params: { + id: eventId, + indexName: selectedPatterns.join(','), + scopeId: timelineId, }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: timelineId, - panel: 'right', - }); - } else { - dispatch( - timelineActions.toggleDetailPanel({ - panelView: 'eventDetail', - tabType: TimelineTabs.notes, - id: timelineId, - params: { - eventId, - indexName, - }, - }) - ); - } - }, [ - dispatch, - eventId, - expandableFlyoutDisabled, - openFlyout, - selectedPatterns, - telemetry, - timelineId, - ]); + }, + }); + telemetry.reportDetailsFlyoutOpened({ + location: timelineId, + panel: 'right', + }); + }, [eventId, openFlyout, selectedPatterns, telemetry, timelineId]); return ( ; -}>(({ eventId, timelineId, noteId, confirmingNoteId, eventIdToNoteIds, savedObjectId }) => { - return eventId && timelineId ? ( - <> - + showToggleEventDetailsAction?: boolean; +}>( + ({ + eventId, + timelineId, + noteId, + confirmingNoteId, + eventIdToNoteIds, + savedObjectId, + showToggleEventDetailsAction = true, + }) => { + return eventId && timelineId ? ( + <> + {showToggleEventDetailsAction ? ( + + ) : null} + + + ) : ( - - ) : ( - - ); -}); + ); + } +); NoteActions.displayName = 'NoteActions'; /** @@ -238,10 +225,11 @@ interface NotePreviewsProps { notes?: TimelineResultNote[] | null; timelineId?: string; showTimelineDescription?: boolean; + showToggleEventDetailsAction?: boolean; } export const NotePreviews = React.memo( - ({ notes, timelineId, showTimelineDescription }) => { + ({ notes, timelineId, showTimelineDescription, showToggleEventDetailsAction = true }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const getTimelineNotes = useMemo(() => getTimelineNoteSelector(), []); const timeline = useDeepEqualSelector((state) => @@ -315,6 +303,7 @@ export const NotePreviews = React.memo( savedObjectId={note.savedObjectId} confirmingNoteId={timeline?.confirmingNoteId} eventIdToNoteIds={eventIdToNoteIds} + showToggleEventDetailsAction={showToggleEventDetailsAction} /> ), timelineAvatar: ( @@ -326,7 +315,13 @@ export const NotePreviews = React.memo( ), }; }), - [eventIdToNoteIds, notes, timelineId, timeline?.confirmingNoteId] + [ + eventIdToNoteIds, + notes, + timelineId, + timeline?.confirmingNoteId, + showToggleEventDetailsAction, + ] ); const commentList = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx index f0b22e4f48359..5f6e69ce2a051 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.test.tsx @@ -8,7 +8,6 @@ import { renderHook, act } from '@testing-library/react-hooks'; import React from 'react'; import type { UseDetailPanelConfig } from './use_detail_panel'; import { useDetailPanel } from './use_detail_panel'; -import { timelineActions } from '../../../store'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { SourcererScopeName } from '../../../../sourcerer/store/model'; import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; @@ -33,14 +32,6 @@ jest.mock('../../../../common/lib/kibana', () => { jest.mock('../../../../common/hooks/use_selector'); jest.mock('../../../store'); -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); jest.mock('../../../../sourcerer/containers', () => { const mockSourcererReturn = { browserFields: {}, @@ -88,18 +79,6 @@ describe('useDetailPanel', () => { }); }); - test('should fire redux action to open event details panel', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderUseDetailPanel(); - await waitForNextUpdate(); - - result.current?.openEventDetailsPanel('123'); - - expect(mockDispatch).not.toHaveBeenCalled(); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); - }); - }); - test('should show the details panel', async () => { mockGetExpandedDetail.mockImplementation(() => ({ [TimelineTabs.session]: { diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.tsx index d66053c1128e4..4d76510c12b77 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/hooks/use_detail_panel.tsx @@ -5,15 +5,12 @@ * 2.0. */ -import React, { useMemo, useCallback, useRef } from 'react'; -import { useDispatch } from 'react-redux'; +import React, { useMemo, useCallback } from 'react'; import type { EntityType } from '@kbn/timelines-plugin/common'; import { dataTableSelectors } from '@kbn/securitysolution-data-table'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { useKibana } from '../../../../common/lib/kibana'; -import type { ExpandedDetailType } from '../../../../../common/types'; -import { getScopedActions, isInTableScope, isTimelineScope } from '../../../../helpers'; +import { isInTableScope, isTimelineScope } from '../../../../helpers'; import { timelineSelectors } from '../../../store'; import { useSourcererDataView } from '../../../../sourcerer/containers'; import type { SourcererScopeName } from '../../../../sourcerer/store/model'; @@ -45,10 +42,8 @@ export const useDetailPanel = ({ }: UseDetailPanelConfig): UseDetailPanelReturn => { const { telemetry } = useKibana().services; const { browserFields, selectedPatterns, runtimeMappings } = useSourcererDataView(sourcererScope); - const dispatch = useDispatch(); const { openFlyout } = useExpandableFlyoutApi(); - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); const getScope = useMemo(() => { if (isTimelineScope(scopeId)) { @@ -62,8 +57,6 @@ export const useDetailPanel = ({ const expandedDetail = useDeepEqualSelector( (state) => ((getScope && getScope(state, scopeId)) ?? timelineDefaults)?.expandedDetail ); - const onPanelClose = useRef(() => {}); - const noopPanelClose = () => {}; const shouldShowDetailsPanel = useMemo(() => { if ( @@ -76,68 +69,34 @@ export const useDetailPanel = ({ } return false; }, [expandedDetail, tabType]); - const scopedActions = getScopedActions(scopeId); - - // We could just surface load details panel, but rather than have users be concerned - // of the config for a panel, they can just pass the base necessary values to a panel specific function - const loadDetailsPanel = useCallback( - (panelConfig?: ExpandedDetailType) => { - if (panelConfig && scopedActions) { - dispatch( - scopedActions.toggleDetailPanel({ - ...panelConfig, - tabType, - id: scopeId, - }) - ); - } - }, - [scopedActions, scopeId, dispatch, tabType] - ); const openEventDetailsPanel = useCallback( (eventId?: string, onClose?: () => void) => { - if (!expandableFlyoutDisabled) { - openFlyout({ - right: { - id: DocumentDetailsRightPanelKey, - params: { - id: eventId, - indexName: eventDetailsIndex, - scopeId, - }, + openFlyout({ + right: { + id: DocumentDetailsRightPanelKey, + params: { + id: eventId, + indexName: eventDetailsIndex, + scopeId, }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: scopeId, - panel: 'right', - }); - } else if (eventId) { - loadDetailsPanel({ - panelView: 'eventDetail', - params: { eventId, indexName: eventDetailsIndex }, - }); - onPanelClose.current = onClose ?? noopPanelClose; - } + }, + }); + telemetry.reportDetailsFlyoutOpened({ + location: scopeId, + panel: 'right', + }); }, - [expandableFlyoutDisabled, openFlyout, eventDetailsIndex, scopeId, telemetry, loadDetailsPanel] + [openFlyout, eventDetailsIndex, scopeId, telemetry] ); - const handleOnDetailsPanelClosed = useCallback(() => { - if (!expandableFlyoutDisabled) return; - if (onPanelClose.current) onPanelClose.current(); - if (scopedActions) { - dispatch(scopedActions.toggleDetailPanel({ tabType, id: scopeId })); - } - }, [expandableFlyoutDisabled, scopedActions, dispatch, tabType, scopeId]); - const DetailsPanel = useMemo( () => shouldShowDetailsPanel ? ( {}} isFlyoutView={isFlyoutView} runtimeMappings={runtimeMappings} tabType={tabType} @@ -147,7 +106,6 @@ export const useDetailPanel = ({ [ browserFields, entityType, - handleOnDetailsPanelClosed, isFlyoutView, runtimeMappings, shouldShowDetailsPanel, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 000837ff83500..479d4d0defc1f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -11,7 +11,6 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { isEventBuildingBlockType } from '@kbn/securitysolution-data-table'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_details/shared/constants/panel_keys'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { useKibana } from '../../../../../common/lib/kibana'; @@ -43,7 +42,6 @@ import { useGetMappedNonEcsValue } from '../data_driven_columns'; import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context'; import type { ControlColumnProps, - ExpandedDetailType, SetEventsDeleted, SetEventsLoading, } from '../../../../../../common/types'; @@ -112,7 +110,6 @@ const StatefulEventComponent: React.FC = ({ const trGroupRef = useRef(null); const dispatch = useDispatch(); - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); const { openFlyout } = useExpandableFlyoutApi(); // Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created @@ -206,51 +203,21 @@ const StatefulEventComponent: React.FC = ({ ); const handleOnEventDetailPanelOpened = useCallback(() => { - const updatedExpandedDetail: ExpandedDetailType = { - panelView: 'eventDetail', - params: { - eventId, - indexName, - refetch, - }, - }; - - if (!expandableFlyoutDisabled) { - openFlyout({ - right: { - id: DocumentDetailsRightPanelKey, - params: { - id: eventId, - indexName, - scopeId: timelineId, - }, + openFlyout({ + right: { + id: DocumentDetailsRightPanelKey, + params: { + id: eventId, + indexName, + scopeId: timelineId, }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: timelineId, - panel: 'right', - }); - } else { - // opens the panel when clicking on the table row action - dispatch( - timelineActions.toggleDetailPanel({ - ...updatedExpandedDetail, - tabType, - id: timelineId, - }) - ); - } - }, [ - eventId, - indexName, - refetch, - expandableFlyoutDisabled, - openFlyout, - timelineId, - telemetry, - dispatch, - tabType, - ]); + }, + }); + telemetry.reportDetailsFlyoutOpened({ + location: timelineId, + panel: 'right', + }); + }, [eventId, indexName, openFlyout, timelineId, telemetry]); const setEventsLoading = useCallback( ({ eventIds, isLoading }) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx index 0a9c53a88d689..d00ad83b9b556 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.test.tsx @@ -11,17 +11,13 @@ import { waitFor } from '@testing-library/react'; import { HostName } from './host_name'; import { TestProviders } from '../../../../../common/mock'; import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; -import { timelineActions } from '../../../../store'; import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context'; import { createTelemetryServiceMock } from '../../../../../common/lib/telemetry/telemetry_service.mock'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; -import { dataTableActions, TableId } from '@kbn/securitysolution-data-table'; +import { TableId } from '@kbn/securitysolution-data-table'; const mockedTelemetry = createTelemetryServiceMock(); const mockOpenRightPanel = jest.fn(); -jest.mock('../../../../../common/hooks/use_experimental_features'); - jest.mock('@kbn/expandable-flyout', () => { return { useExpandableFlyoutApi: () => ({ @@ -31,14 +27,6 @@ jest.mock('@kbn/expandable-flyout', () => { }; }); -jest.mock('react-redux', () => { - const origin = jest.requireActual('react-redux'); - return { - ...origin, - useDispatch: jest.fn().mockReturnValue(jest.fn()), - }; -}); - jest.mock('../../../../../common/lib/kibana/kibana_react', () => { return { useKibana: () => ({ @@ -57,28 +45,6 @@ jest.mock('../../../../../common/components/draggables', () => ({ DefaultDraggable: () =>
, })); -jest.mock('../../../../store', () => { - const original = jest.requireActual('../../../../store'); - return { - ...original, - timelineActions: { - ...original.timelineActions, - toggleDetailPanel: jest.fn(), - }, - }; -}); - -jest.mock('@kbn/securitysolution-data-table', () => { - const original = jest.requireActual('@kbn/securitysolution-data-table'); - return { - ...original, - dataTableActions: { - ...original.dataTableActions, - toggleDetailPanel: jest.fn(), - }, - }; -}); - describe('HostName', () => { afterEach(() => { jest.clearAllMocks(); @@ -129,8 +95,6 @@ describe('HostName', () => { wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click'); await waitFor(() => { - expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); expect(mockOpenRightPanel).not.toHaveBeenCalled(); }); }); @@ -153,8 +117,6 @@ describe('HostName', () => { wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click'); await waitFor(() => { - expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); expect(mockOpenRightPanel).not.toHaveBeenCalled(); }); }); @@ -177,82 +139,11 @@ describe('HostName', () => { wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click'); await waitFor(() => { - expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(mockOpenRightPanel).not.toHaveBeenCalled(); - }); - }); - - test('should open old flyout on table', async () => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => { - if (feature === 'expandableFlyoutDisabled') return true; - }); - const context = { - enableHostDetailsFlyout: true, - enableIpDetailsFlyout: true, - timelineID: TableId.alertsOnAlertsPage, - tabType: TimelineTabs.query, - }; - const wrapper = mount( - - - - - - ); - - wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click'); - await waitFor(() => { - expect(dataTableActions.toggleDetailPanel).toHaveBeenCalledWith({ - id: context.timelineID, - panelView: 'hostDetail', - params: { - hostName: props.value, - }, - tabType: context.tabType, - }); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(mockOpenRightPanel).not.toHaveBeenCalled(); - }); - }); - - test('should open old flyout in timeline', async () => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => { - if (feature === 'expandableFlyoutDisabled') return true; - }); - const context = { - enableHostDetailsFlyout: true, - enableIpDetailsFlyout: true, - timelineID: TimelineId.active, - tabType: TimelineTabs.query, - }; - const wrapper = mount( - - - - - - ); - - wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click'); - await waitFor(() => { - expect(timelineActions.toggleDetailPanel).toHaveBeenCalledWith({ - id: context.timelineID, - panelView: 'hostDetail', - params: { - hostName: props.value, - }, - tabType: context.tabType, - }); - expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled(); expect(mockOpenRightPanel).not.toHaveBeenCalled(); }); }); test('should open expandable flyout on table', async () => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => { - if (feature === 'expandableFlyoutDisabled') return false; - }); const context = { enableHostDetailsFlyout: true, enableIpDetailsFlyout: true, @@ -278,15 +169,10 @@ describe('HostName', () => { isDraggable: false, }, }); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled(); }); }); test('should open expandable flyout in timeline', async () => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => { - if (feature === 'expandableFlyoutDisabled') return false; - }); const context = { enableHostDetailsFlyout: true, enableIpDetailsFlyout: true, @@ -312,8 +198,6 @@ describe('HostName', () => { isDraggable: false, }, }); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx index 42c5344c7445c..d1a9c3a00bf37 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx @@ -7,16 +7,11 @@ import React, { useCallback, useContext, useMemo } from 'react'; import type { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; -import { useDispatch } from 'react-redux'; import { isString } from 'lodash/fp'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { HostPanelKey } from '../../../../../flyout/entity_details/host_right'; -import type { ExpandedDetailType } from '../../../../../../common/types'; import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context'; -import { getScopedActions } from '../../../../../helpers'; import { HostDetailsLink } from '../../../../../common/components/links'; -import type { TimelineTabs } from '../../../../../../common/types/timeline'; import { DefaultDraggable } from '../../../../../common/components/draggables'; import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { TruncatableText } from '../../../../../common/components/truncatable_text'; @@ -48,10 +43,8 @@ const HostNameComponent: React.FC = ({ title, value, }) => { - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); const { openRightPanel } = useExpandableFlyoutApi(); - const dispatch = useDispatch(); const eventContext = useContext(StatefulEventContext); const hostName = `${value}`; const isInTimelineContext = @@ -69,48 +62,19 @@ const HostNameComponent: React.FC = ({ return; } - const { timelineID, tabType } = eventContext; + const { timelineID } = eventContext; - if (!expandableFlyoutDisabled) { - openRightPanel({ - id: HostPanelKey, - params: { - hostName, - contextID: contextId, - scopeId: timelineID, - isDraggable, - }, - }); - } else { - const updatedExpandedDetail: ExpandedDetailType = { - panelView: 'hostDetail', - params: { - hostName, - }, - }; - const scopedActions = getScopedActions(timelineID); - if (scopedActions) { - dispatch( - scopedActions.toggleDetailPanel({ - ...updatedExpandedDetail, - id: timelineID, - tabType: tabType as TimelineTabs, - }) - ); - } - } + openRightPanel({ + id: HostPanelKey, + params: { + hostName, + contextID: contextId, + scopeId: timelineID, + isDraggable, + }, + }); }, - [ - contextId, - dispatch, - eventContext, - expandableFlyoutDisabled, - hostName, - isDraggable, - isInTimelineContext, - onClick, - openRightPanel, - ] + [contextId, eventContext, hostName, isDraggable, isInTimelineContext, onClick, openRightPanel] ); // The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.test.tsx index 16f881fe90130..471d43dcf8462 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.test.tsx @@ -10,18 +10,14 @@ import { waitFor } from '@testing-library/react'; import { TestProviders } from '../../../../../common/mock'; import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; -import { timelineActions } from '../../../../store'; import { UserName } from './user_name'; import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context'; import { createTelemetryServiceMock } from '../../../../../common/lib/telemetry/telemetry_service.mock'; -import { dataTableActions, TableId } from '@kbn/securitysolution-data-table'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { TableId } from '@kbn/securitysolution-data-table'; const mockedTelemetry = createTelemetryServiceMock(); const mockOpenRightPanel = jest.fn(); -jest.mock('../../../../../common/hooks/use_experimental_features'); - jest.mock('@kbn/expandable-flyout', () => { return { useExpandableFlyoutApi: () => ({ @@ -31,14 +27,6 @@ jest.mock('@kbn/expandable-flyout', () => { }; }); -jest.mock('react-redux', () => { - const origin = jest.requireActual('react-redux'); - return { - ...origin, - useDispatch: jest.fn().mockReturnValue(jest.fn()), - }; -}); - jest.mock('../../../../../common/lib/kibana/kibana_react', () => { return { useKibana: () => ({ @@ -57,28 +45,6 @@ jest.mock('../../../../../common/components/draggables', () => ({ DefaultDraggable: () =>
, })); -jest.mock('../../../../store', () => { - const original = jest.requireActual('../../../../store'); - return { - ...original, - timelineActions: { - ...original.timelineActions, - toggleDetailPanel: jest.fn(), - }, - }; -}); - -jest.mock('@kbn/securitysolution-data-table', () => { - const original = jest.requireActual('@kbn/securitysolution-data-table'); - return { - ...original, - dataTableActions: { - ...original.dataTableActions, - toggleDetailPanel: jest.fn(), - }, - }; -}); - describe('UserName', () => { afterEach(() => { jest.clearAllMocks(); @@ -127,8 +93,6 @@ describe('UserName', () => { wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click'); await waitFor(() => { - expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); expect(mockOpenRightPanel).not.toHaveBeenCalled(); }); }); @@ -151,82 +115,11 @@ describe('UserName', () => { wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click'); await waitFor(() => { - expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(mockOpenRightPanel).not.toHaveBeenCalled(); - }); - }); - - test('should open old flyout on table', async () => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => { - if (feature === 'expandableFlyoutDisabled') return true; - }); - const context = { - enableHostDetailsFlyout: true, - enableIpDetailsFlyout: true, - timelineID: TableId.alertsOnAlertsPage, - tabType: TimelineTabs.query, - }; - const wrapper = mount( - - - - - - ); - - wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click'); - await waitFor(() => { - expect(dataTableActions.toggleDetailPanel).toHaveBeenCalledWith({ - id: context.timelineID, - panelView: 'userDetail', - params: { - userName: props.value, - }, - tabType: context.tabType, - }); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(mockOpenRightPanel).not.toHaveBeenCalled(); - }); - }); - - test('should open old flyout in timeline', async () => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => { - if (feature === 'expandableFlyoutDisabled') return true; - }); - const context = { - enableHostDetailsFlyout: true, - enableIpDetailsFlyout: true, - timelineID: TimelineId.active, - tabType: TimelineTabs.query, - }; - const wrapper = mount( - - - - - - ); - - wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click'); - await waitFor(() => { - expect(timelineActions.toggleDetailPanel).toHaveBeenCalledWith({ - id: context.timelineID, - panelView: 'userDetail', - params: { - userName: props.value, - }, - tabType: context.tabType, - }); - expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled(); expect(mockOpenRightPanel).not.toHaveBeenCalled(); }); }); test('should open expandable flyout on table', async () => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => { - if (feature === 'expandableFlyoutDisabled') return false; - }); const context = { enableHostDetailsFlyout: true, enableIpDetailsFlyout: true, @@ -252,15 +145,10 @@ describe('UserName', () => { isDraggable: false, }, }); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled(); }); }); test('should open expandable flyout in timeline', async () => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => { - if (feature === 'expandableFlyoutDisabled') return false; - }); const context = { enableHostDetailsFlyout: true, enableIpDetailsFlyout: true, @@ -286,8 +174,6 @@ describe('UserName', () => { isDraggable: false, }, }); - expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); - expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.tsx index 78d9ad9c3bdfd..48ce6255725d3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_name.tsx @@ -7,15 +7,10 @@ import React, { useCallback, useContext, useMemo } from 'react'; import type { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; -import { useDispatch } from 'react-redux'; import { isString } from 'lodash/fp'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { UserPanelKey } from '../../../../../flyout/entity_details/user_right'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context'; -import type { ExpandedDetailType } from '../../../../../../common/types'; -import { getScopedActions } from '../../../../../helpers'; -import type { TimelineTabs } from '../../../../../../common/types/timeline'; import { DefaultDraggable } from '../../../../../common/components/draggables'; import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { UserDetailsLink } from '../../../../../common/components/links'; @@ -48,9 +43,7 @@ const UserNameComponent: React.FC = ({ title, value, }) => { - const dispatch = useDispatch(); const eventContext = useContext(StatefulEventContext); - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); const userName = `${value}`; const isInTimelineContext = userName && eventContext?.timelineID; const { openRightPanel } = useExpandableFlyoutApi(); @@ -67,48 +60,19 @@ const UserNameComponent: React.FC = ({ return; } - const { timelineID, tabType } = eventContext; + const { timelineID } = eventContext; - if (!expandableFlyoutDisabled) { - openRightPanel({ - id: UserPanelKey, - params: { - userName, - contextID: contextId, - scopeId: timelineID, - isDraggable, - }, - }); - } else { - const updatedExpandedDetail: ExpandedDetailType = { - panelView: 'userDetail', - params: { - userName, - }, - }; - const scopedActions = getScopedActions(timelineID); - if (scopedActions) { - dispatch( - scopedActions.toggleDetailPanel({ - ...updatedExpandedDetail, - id: timelineID, - tabType: tabType as TimelineTabs, - }) - ); - } - } + openRightPanel({ + id: UserPanelKey, + params: { + userName, + contextID: contextId, + scopeId: timelineID, + isDraggable, + }, + }); }, - [ - contextId, - dispatch, - eventContext, - expandableFlyoutDisabled, - isDraggable, - isInTimelineContext, - onClick, - openRightPanel, - userName, - ] + [contextId, eventContext, isDraggable, isInTimelineContext, onClick, openRightPanel, userName] ); // The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_flyout.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_flyout.tsx index 438e04283e74a..8ff78376c7683 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_flyout.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/notes_flyout.tsx @@ -7,9 +7,10 @@ import React from 'react'; import { - EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, + EuiFlyoutResizable, + EuiOutsideClickDetector, EuiTitle, useGeneratedHtmlId, } from '@elastic/eui'; @@ -32,7 +33,7 @@ export type NotesFlyoutProps = { * z-index override is needed because otherwise NotesFlyout appears below * Timeline Modal as they both have same z-index of 1000 */ -const NotesFlyoutContainer = styled(EuiFlyout)` +const NotesFlyoutContainer = styled(EuiFlyoutResizable)` /* * We want the width of flyout to be less than 50% of screen because * otherwise it interferes with the delete notes modal @@ -55,33 +56,37 @@ export const NotesFlyout = React.memo(function NotesFlyout(props: NotesFlyoutPro } return ( - - - -

{i18n.NOTES}

-
-
- - - -
+ + + + +

{i18n.NOTES}

+
+
+ + + +
+
); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_notes_in_flyout.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_notes_in_flyout.test.tsx index d40bf849dc32b..1ca417a3f27a3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_notes_in_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_notes_in_flyout.test.tsx @@ -6,9 +6,10 @@ */ import React from 'react'; -import { TimelineId } from '../../../../../common/types'; +import { TimelineId, TimelineTabs } from '../../../../../common/types'; import { renderHook, act } from '@testing-library/react-hooks/dom'; import { createMockStore, mockGlobalState, TestProviders } from '../../../../common/mock'; +import type { UseNotesInFlyoutArgs } from './use_notes_in_flyout'; import { useNotesInFlyout } from './use_notes_in_flyout'; import { waitFor } from '@testing-library/react'; import { useDispatch } from 'react-redux'; @@ -85,11 +86,13 @@ const refetchMock = jest.fn(); const renderTestHook = () => { return renderHook( - () => + (props?: Partial) => useNotesInFlyout({ eventIdToNoteIds: mockEventIdToNoteIds, timelineId: TimelineId.test, refetch: refetchMock, + activeTab: TimelineTabs.query, + ...props, }), { wrapper: ({ children }) => ( @@ -198,4 +201,33 @@ describe('useNotesInFlyout', () => { expect(result.current.isNotesFlyoutVisible).toBe(false); }); + + it('should close the flyout when activeTab is changed', () => { + const { result, rerender, waitForNextUpdate } = renderTestHook(); + + act(() => { + result.current.setNotesEventId('event-1'); + }); + + act(() => { + result.current.showNotesFlyout(); + }); + + expect(result.current.isNotesFlyoutVisible).toBe(true); + + act(() => { + // no change in active Tab + rerender({ activeTab: TimelineTabs.query }); + }); + + expect(result.current.isNotesFlyoutVisible).toBe(true); + + act(() => { + rerender({ activeTab: TimelineTabs.eql }); + }); + + waitForNextUpdate(); + + expect(result.current.isNotesFlyoutVisible).toBe(false); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_notes_in_flyout.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_notes_in_flyout.ts index 99a2dfe3953ce..a134c806937f5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_notes_in_flyout.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_notes_in_flyout.ts @@ -5,16 +5,18 @@ * 2.0. */ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; +import type { TimelineTabs } from '../../../../../common/types'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { appSelectors } from '../../../../common/store'; import { timelineActions } from '../../../store'; -interface UseNotesInFlyoutArgs { +export interface UseNotesInFlyoutArgs { eventIdToNoteIds: Record; refetch?: () => void; timelineId: string; + activeTab: TimelineTabs; } const EMPTY_STRING_ARRAY: string[] = []; @@ -36,12 +38,19 @@ export const useNotesInFlyout = (args: UseNotesInFlyoutArgs) => { setIsNotesFlyoutVisible(true); }, []); - const { eventIdToNoteIds, refetch, timelineId } = args; + const { eventIdToNoteIds, refetch, timelineId, activeTab } = args; const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []); const notesById = useDeepEqualSelector(getNotesByIds); + useEffect(() => { + if (activeTab) { + // if activeTab changes, close the notes flyout + closeNotesFlyout(); + } + }, [activeTab, closeNotesFlyout]); + const dispatch = useDispatch(); const noteIds: string[] = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx index 7492a00a4a2de..c6dbb541e4d07 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx @@ -145,7 +145,6 @@ export const EqlTabContentComponent: React.FC = ({ timerangeKind, }); - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); const { openFlyout } = useExpandableFlyoutApi(); const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesEnabled' @@ -163,12 +162,13 @@ export const EqlTabContentComponent: React.FC = ({ eventIdToNoteIds, refetch, timelineId, + activeTab: TimelineTabs.eql, }); const onToggleShowNotes = useCallback( (eventId?: string) => { const indexName = selectedPatterns.join(','); - if (eventId && !expandableFlyoutDisabled && securitySolutionNotesEnabled) { + if (eventId && securitySolutionNotesEnabled) { openFlyout({ right: { id: DocumentDetailsRightPanelKey, @@ -205,7 +205,6 @@ export const EqlTabContentComponent: React.FC = ({ } }, [ - expandableFlyoutDisabled, openFlyout, securitySolutionNotesEnabled, selectedPatterns, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx index 76db7bcde9afe..ce0907be5c64b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/pinned/index.tsx @@ -181,7 +181,6 @@ export const PinnedTabContentComponent: React.FC = ({ timerangeKind: undefined, }); - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); const { openFlyout } = useExpandableFlyoutApi(); const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesEnabled' @@ -199,12 +198,13 @@ export const PinnedTabContentComponent: React.FC = ({ eventIdToNoteIds, refetch, timelineId, + activeTab: TimelineTabs.pinned, }); const onToggleShowNotes = useCallback( (eventId?: string) => { const indexName = selectedPatterns.join(','); - if (eventId && !expandableFlyoutDisabled && securitySolutionNotesEnabled) { + if (eventId && securitySolutionNotesEnabled) { openFlyout({ right: { id: DocumentDetailsRightPanelKey, @@ -241,7 +241,6 @@ export const PinnedTabContentComponent: React.FC = ({ } }, [ - expandableFlyoutDisabled, openFlyout, securitySolutionNotesEnabled, selectedPatterns, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx index 017031e1ffba6..7100402d6ca98 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx @@ -211,7 +211,6 @@ export const QueryTabContentComponent: React.FC = ({ timerangeKind, }); - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); const { openFlyout } = useExpandableFlyoutApi(); const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled( 'securitySolutionNotesEnabled' @@ -229,12 +228,13 @@ export const QueryTabContentComponent: React.FC = ({ eventIdToNoteIds, refetch, timelineId, + activeTab, }); const onToggleShowNotes = useCallback( (eventId?: string) => { const indexName = selectedPatterns.join(','); - if (eventId && !expandableFlyoutDisabled && securitySolutionNotesEnabled) { + if (eventId && securitySolutionNotesEnabled) { openFlyout({ right: { id: DocumentDetailsRightPanelKey, @@ -271,7 +271,6 @@ export const QueryTabContentComponent: React.FC = ({ } }, [ - expandableFlyoutDisabled, openFlyout, securitySolutionNotesEnabled, selectedPatterns, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx index 9a712a8fbeaf1..136160b1e22dc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/query_tab_unified_components.test.tsx @@ -27,7 +27,6 @@ import { createStartServicesMock } from '../../../../../common/lib/kibana/kibana import type { StartServices } from '../../../../../types'; import { useKibana } from '../../../../../common/lib/kibana'; import { useDispatch } from 'react-redux'; -import { timelineActions } from '../../../../store'; import type { ExperimentalFeatures } from '../../../../../../common'; import { allowedExperimentalValues } from '../../../../../../common'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; @@ -35,7 +34,7 @@ import { defaultUdtHeaders } from '../../unified_components/default_headers'; import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; import { useUserPrivileges } from '../../../../../common/components/user_privileges'; import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks'; -import userEvent from '@testing-library/user-event'; +import * as timelineActions from '../../../../store/actions'; jest.mock('../../../../../common/components/user_privileges'); @@ -172,6 +171,10 @@ describe('query tab with unified timeline', () => { }); beforeEach(() => { + Object.defineProperty(window, '__@hello-pangea/dnd-disable-dev-warnings', { + value: true, + writable: false, + }); useTimelineEventsMock = jest.fn(() => [ false, { @@ -776,332 +779,196 @@ describe('query tab with unified timeline', () => { describe('Leading actions - notes', () => { describe('securitySolutionNotesEnabled = true', () => { - describe('expandableFlyoutDisabled = false', () => { - beforeEach(() => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( - jest.fn((feature: keyof ExperimentalFeatures) => { - if (feature === 'unifiedComponentsInTimelineEnabled') { - return true; - } - if (feature === 'securitySolutionNotesEnabled') { - return true; - } - return allowedExperimentalValues[feature]; - }) - ); - }); - - it( - 'should have the notification dot & correct tooltip', - async () => { - renderTestComponents(); - - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - - expect(screen.getAllByTestId('timeline-notes-button-small')).toHaveLength(1); - expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); - - expect(screen.getByTestId('timeline-notes-notification-dot')).toBeVisible(); - - fireEvent.mouseOver(screen.getByTestId('timeline-notes-button-small')); - - await waitFor(() => { - expect(screen.getByTestId('timeline-notes-tool-tip')).toBeVisible(); - expect(screen.getByTestId('timeline-notes-tool-tip')).toHaveTextContent( - '1 Note available. Click to view it & add more.' - ); - }); - }, - SPECIAL_TEST_TIMEOUT - ); - it( - 'should be able to add notes through expandable flyout', - async () => { - renderTestComponents(); - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - - await waitFor(() => { - expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); - }); - - fireEvent.click(screen.getByTestId('timeline-notes-button-small')); - - await waitFor(() => { - expect(mockOpenFlyout).toHaveBeenCalled(); - }); - }, - SPECIAL_TEST_TIMEOUT + beforeEach(() => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( + jest.fn((feature: keyof ExperimentalFeatures) => { + if (feature === 'unifiedComponentsInTimelineEnabled') { + return true; + } + if (feature === 'securitySolutionNotesEnabled') { + return true; + } + return allowedExperimentalValues[feature]; + }) ); }); - describe('expandableFlyoutDisabled = true', () => { - beforeEach(() => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( - jest.fn((feature: keyof ExperimentalFeatures) => { - if (feature === 'unifiedComponentsInTimelineEnabled') { - return true; - } - if (feature === 'expandableFlyoutDisabled') { - return true; - } - if (feature === 'securitySolutionNotesEnabled') { - return true; - } - return allowedExperimentalValues[feature]; - }) - ); - }); - - it( - 'should have the notification dot & correct tooltip', - async () => { - renderTestComponents(); - - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - - expect(screen.getAllByTestId('timeline-notes-button-small')).toHaveLength(1); - expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); - - expect(screen.getByTestId('timeline-notes-notification-dot')).toBeVisible(); - - fireEvent.mouseOver(screen.getByTestId('timeline-notes-button-small')); - - await waitFor(() => { - expect(screen.getByTestId('timeline-notes-tool-tip')).toBeVisible(); - expect(screen.getByTestId('timeline-notes-tool-tip')).toHaveTextContent( - '1 Note available. Click to view it & add more.' - ); - }); - }, - SPECIAL_TEST_TIMEOUT - ); - it( - 'should be able to add notes using EuiFlyout', - async () => { - renderTestComponents(); - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - - await waitFor(() => { - expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); - }); - - fireEvent.click(screen.getByTestId('timeline-notes-button-small')); - - await waitFor(() => { - expect(screen.getByTestId('add-note-container')).toBeVisible(); - }); - }, - SPECIAL_TEST_TIMEOUT - ); + it( + 'should have the notification dot & correct tooltip', + async () => { + renderTestComponents(); - it( - 'should be cancel adding notes', - async () => { - renderTestComponents(); - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); + expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - await waitFor(() => { - expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); - }); + expect(screen.getAllByTestId('timeline-notes-button-small')).toHaveLength(1); + expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); - fireEvent.click(screen.getByTestId('timeline-notes-button-small')); + expect(screen.getByTestId('timeline-notes-notification-dot')).toBeVisible(); - await waitFor(() => { - expect(screen.getByTestId('add-note-container')).toBeVisible(); - }); + fireEvent.mouseOver(screen.getByTestId('timeline-notes-button-small')); - userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), 'Test Note 1'); + await waitFor(() => { + expect(screen.getByTestId('timeline-notes-tool-tip')).toBeVisible(); + expect(screen.getByTestId('timeline-notes-tool-tip')).toHaveTextContent( + '1 Note available. Click to view it & add more.' + ); + }); + }, + SPECIAL_TEST_TIMEOUT + ); + it( + 'should be able to add notes through expandable flyout', + async () => { + renderTestComponents(); + expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - expect(screen.getByTestId('cancel')).not.toBeDisabled(); + await waitFor(() => { + expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); + }); - fireEvent.click(screen.getByTestId('cancel')); + fireEvent.click(screen.getByTestId('timeline-notes-button-small')); - await waitFor(() => { - expect(screen.queryByTestId('add-note-container')).not.toBeInTheDocument(); - }); - }, - SPECIAL_TEST_TIMEOUT - ); - }); + await waitFor(() => { + expect(mockOpenFlyout).toHaveBeenCalled(); + }); + }, + SPECIAL_TEST_TIMEOUT + ); }); describe('securitySolutionNotesEnabled = false', () => { - describe('expandableFlyoutDisabled = false', () => { - beforeEach(() => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( - jest.fn((feature: keyof ExperimentalFeatures) => { - if (feature === 'unifiedComponentsInTimelineEnabled') { - return true; - } - if (feature === 'securitySolutionNotesEnabled') { - return false; - } - return allowedExperimentalValues[feature]; - }) - ); - }); - - it( - 'should have the notification dot & correct tooltip', - async () => { - renderTestComponents(); - - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - - expect(screen.getAllByTestId('timeline-notes-button-small')).toHaveLength(1); - expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); - - expect(screen.getByTestId('timeline-notes-notification-dot')).toBeVisible(); - - fireEvent.mouseOver(screen.getByTestId('timeline-notes-button-small')); - - await waitFor(() => { - expect(screen.getByTestId('timeline-notes-tool-tip')).toBeVisible(); - expect(screen.getByTestId('timeline-notes-tool-tip')).toHaveTextContent( - '1 Note available. Click to view it & add more.' - ); - }); - }, - SPECIAL_TEST_TIMEOUT + beforeEach(() => { + (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( + jest.fn((feature: keyof ExperimentalFeatures) => { + if (feature === 'unifiedComponentsInTimelineEnabled') { + return true; + } + if (feature === 'securitySolutionNotesEnabled') { + return false; + } + return allowedExperimentalValues[feature]; + }) ); - it( - 'should be able to add notes using EuiFlyout', - async () => { - renderTestComponents(); - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - - await waitFor(() => { - expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); - }); + }); - fireEvent.click(screen.getByTestId('timeline-notes-button-small')); + it( + 'should have the notification dot & correct tooltip', + async () => { + renderTestComponents(); - await waitFor(() => { - expect(screen.getByTestId('add-note-container')).toBeVisible(); - }); - }, - SPECIAL_TEST_TIMEOUT - ); + expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - it( - 'should be cancel adding notes', - async () => { - renderTestComponents(); - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); + expect(screen.getAllByTestId('timeline-notes-button-small')).toHaveLength(1); + expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); - await waitFor(() => { - expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); - }); + expect(screen.getByTestId('timeline-notes-notification-dot')).toBeVisible(); - fireEvent.click(screen.getByTestId('timeline-notes-button-small')); + fireEvent.mouseOver(screen.getByTestId('timeline-notes-button-small')); - await waitFor(() => { - expect(screen.getByTestId('add-note-container')).toBeVisible(); - }); + await waitFor(() => { + expect(screen.getByTestId('timeline-notes-tool-tip')).toBeVisible(); + expect(screen.getByTestId('timeline-notes-tool-tip')).toHaveTextContent( + '1 Note available. Click to view it & add more.' + ); + }); + }, + SPECIAL_TEST_TIMEOUT + ); + it( + 'should be able to add notes using EuiFlyout', + async () => { + renderTestComponents(); + expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), 'Test Note 1'); + await waitFor(() => { + expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); + }); - expect(screen.getByTestId('cancel')).not.toBeDisabled(); + fireEvent.click(screen.getByTestId('timeline-notes-button-small')); - fireEvent.click(screen.getByTestId('cancel')); + await waitFor(() => { + expect(screen.getByTestId('add-note-container')).toBeVisible(); + }); + }, + SPECIAL_TEST_TIMEOUT + ); - await waitFor(() => { - expect(screen.queryByTestId('add-note-container')).not.toBeInTheDocument(); - }); - }, - SPECIAL_TEST_TIMEOUT - ); - }); + it( + 'should cancel adding notes', + async () => { + renderTestComponents(); + expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - describe('expandableFlyoutDisabled = true', () => { - beforeEach(() => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation( - jest.fn((feature: keyof ExperimentalFeatures) => { - if (feature === 'unifiedComponentsInTimelineEnabled') { - return true; - } - if (feature === 'expandableFlyoutDisabled') { - return true; - } - if (feature === 'securitySolutionNotesEnabled') { - return true; - } - return allowedExperimentalValues[feature]; - }) - ); - }); + await waitFor(() => { + expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); + }); - it( - 'should have the notification dot & correct tooltip', - async () => { - renderTestComponents(); + fireEvent.click(screen.getByTestId('timeline-notes-button-small')); - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); + await waitFor(() => { + expect(screen.getByTestId('add-note-container')).toBeVisible(); + }); - expect(screen.getAllByTestId('timeline-notes-button-small')).toHaveLength(1); - expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); + expect(screen.getByTestId('cancel')).not.toBeDisabled(); - expect(screen.getByTestId('timeline-notes-notification-dot')).toBeVisible(); + fireEvent.click(screen.getByTestId('cancel')); - fireEvent.mouseOver(screen.getByTestId('timeline-notes-button-small')); + await waitFor(() => { + expect(screen.queryByTestId('add-note-container')).not.toBeInTheDocument(); + }); + }, + SPECIAL_TEST_TIMEOUT + ); - await waitFor(() => { - expect(screen.getByTestId('timeline-notes-tool-tip')).toBeVisible(); - expect(screen.getByTestId('timeline-notes-tool-tip')).toHaveTextContent( - '1 Note available. Click to view it & add more.' - ); - }); - }, - SPECIAL_TEST_TIMEOUT - ); - it( - 'should be able to add notes using EuiFlyout', - async () => { - renderTestComponents(); - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - - await waitFor(() => { - expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); - }); + it( + 'should be able to delete notes', + async () => { + renderTestComponents(); + expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - fireEvent.click(screen.getByTestId('timeline-notes-button-small')); + await waitFor(() => { + expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); + }); - await waitFor(() => { - expect(screen.getByTestId('add-note-container')).toBeVisible(); - }); - }, - SPECIAL_TEST_TIMEOUT - ); + fireEvent.click(screen.getByTestId('timeline-notes-button-small')); - it( - 'should be cancel adding notes', - async () => { - renderTestComponents(); - expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); + await waitFor(() => { + expect(screen.getByTestId('delete-note')).toBeVisible(); + }); - await waitFor(() => { - expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); - }); + const noteDeleteSpy = jest.spyOn(timelineActions, 'setConfirmingNoteId'); - fireEvent.click(screen.getByTestId('timeline-notes-button-small')); + fireEvent.click(screen.getByTestId('delete-note')); - await waitFor(() => { - expect(screen.getByTestId('add-note-container')).toBeVisible(); + await waitFor(() => { + expect(noteDeleteSpy).toHaveBeenCalled(); + expect(noteDeleteSpy).toHaveBeenCalledWith({ + confirmingNoteId: '1', + id: TimelineId.test, }); + }); + }, + SPECIAL_TEST_TIMEOUT + ); - userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), 'Test Note 1'); + it( + 'should not show toggle event details action', + async () => { + renderTestComponents(); + expect(await screen.findByTestId('discoverDocTable')).toBeVisible(); - expect(screen.getByTestId('cancel')).not.toBeDisabled(); + await waitFor(() => { + expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled(); + }); - fireEvent.click(screen.getByTestId('cancel')); + fireEvent.click(screen.getByTestId('timeline-notes-button-small')); - await waitFor(() => { - expect(screen.queryByTestId('add-note-container')).not.toBeInTheDocument(); - }); - }, - SPECIAL_TEST_TIMEOUT - ); - }); + await waitFor(() => { + expect(screen.queryByTestId('notes-toggle-event-details')).not.toBeInTheDocument(); + }); + }, + SPECIAL_TEST_TIMEOUT + ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx index 52cc643cbf2e4..b9807e08572b4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.test.tsx @@ -36,7 +36,6 @@ const onChangePageMock = jest.fn(); const openFlyoutMock = jest.fn(); const closeFlyoutMock = jest.fn(); -const isExpandableFlyoutDisabled = false; jest.mock('../hooks/use_unified_timeline_expandable_flyout'); @@ -99,7 +98,6 @@ describe('unified data table', () => { beforeEach(() => { (useSourcererDataView as jest.Mock).mockReturnValue(mockSourcererScope); (useUnifiedTableExpandableFlyout as jest.Mock).mockReturnValue({ - isExpandableFlyoutDisabled, openFlyout: openFlyoutMock, closeFlyout: closeFlyoutMock, }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx index b1467a4afdf4d..e4433a0fd56a1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/data_table/index.tsx @@ -19,7 +19,7 @@ import { RowRendererCount } from '../../../../../../common/api/timeline'; import { EmptyComponent } from '../../../../../common/lib/cell_actions/helpers'; import { withDataView } from '../../../../../common/components/with_data_view'; import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context'; -import type { ExpandedDetailTimeline, ExpandedDetailType } from '../../../../../../common/types'; +import type { ExpandedDetailTimeline } from '../../../../../../common/types'; import type { TimelineItem } from '../../../../../../common/search_strategy'; import { useKibana } from '../../../../../common/lib/kibana'; import type { @@ -29,12 +29,7 @@ import type { ToggleDetailPanel, TimelineTabs, } from '../../../../../../common/types/timeline'; -import { TimelineId } from '../../../../../../common/types/timeline'; import type { State, inputsModel } from '../../../../../common/store'; -import { SourcererScopeName } from '../../../../../sourcerer/store/model'; -import { useSourcererDataView } from '../../../../../sourcerer/containers'; -import { activeTimeline } from '../../../../containers/active_timeline_context'; -import { DetailsPanel } from '../../../side_panel'; import { SecurityCellActionsTrigger } from '../../../../../app/actions/constants'; import { getFormattedFields } from '../../body/renderers/formatted_field_udt'; import ToolbarAdditionalControls from './toolbar_additional_controls'; @@ -147,13 +142,9 @@ export const TimelineDataTableComponent: React.FC = memo( setExpandedDoc((prev) => (!prev ? prev : undefined)); }, []); - const { openFlyout, closeFlyout, isExpandableFlyoutDisabled } = useUnifiedTableExpandableFlyout( - { - onClose: onCloseExpandableFlyout, - } - ); - - const { browserFields, runtimeMappings } = useSourcererDataView(SourcererScopeName.timeline); + const { openFlyout, closeFlyout } = useUnifiedTableExpandableFlyout({ + onClose: onCloseExpandableFlyout, + }); const showTimeCol = useMemo(() => !!dataView && !!dataView.timeFieldName, [dataView]); @@ -168,57 +159,24 @@ export const TimelineDataTableComponent: React.FC = memo( const handleOnEventDetailPanelOpened = useCallback( (eventData: DataTableRecord & TimelineItem) => { - const updatedExpandedDetail: ExpandedDetailType = { - panelView: 'eventDetail', - params: { - eventId: eventData._id, - indexName: eventData.ecs._index ?? '', // TODO: fix type error - refetch, - }, - }; - - if (!isExpandableFlyoutDisabled) { - openFlyout({ - right: { - id: DocumentDetailsRightPanelKey, - params: { - id: eventData._id, - indexName: eventData.ecs._index ?? '', - scopeId: timelineId, - }, + openFlyout({ + right: { + id: DocumentDetailsRightPanelKey, + params: { + id: eventData._id, + indexName: eventData.ecs._index ?? '', + scopeId: timelineId, }, - }); - telemetry.reportDetailsFlyoutOpened({ - location: timelineId, - panel: 'right', - }); - } else { - dispatch( - timelineActions.toggleDetailPanel({ - ...updatedExpandedDetail, - tabType: activeTab, - id: timelineId, - }) - ); - } - - activeTimeline.toggleExpandedDetail({ ...updatedExpandedDetail }); + }, + }); + telemetry.reportDetailsFlyoutOpened({ + location: timelineId, + panel: 'right', + }); }, - [refetch, isExpandableFlyoutDisabled, openFlyout, timelineId, telemetry, dispatch, activeTab] + [openFlyout, timelineId, telemetry] ); - const onTimelineLegacyFlyoutClose = useCallback(() => { - if ( - expandedDetail[activeTab]?.panelView && - timelineId === TimelineId.active && - showExpandedDetails - ) { - activeTimeline.toggleExpandedDetail({}); - } - setExpandedDoc(undefined); - onEventClosed({ tabType: activeTab, id: timelineId }); - }, [expandedDetail, activeTab, timelineId, showExpandedDetails, onEventClosed]); - const onSetExpandedDoc = useCallback( (newDoc?: DataTableRecord) => { if (newDoc) { @@ -228,20 +186,10 @@ export const TimelineDataTableComponent: React.FC = memo( handleOnEventDetailPanelOpened(timelineDoc); } } else { - if (!isExpandableFlyoutDisabled) { - closeFlyout(); - return; - } - onTimelineLegacyFlyoutClose(); + closeFlyout(); } }, - [ - tableRows, - handleOnEventDetailPanelOpened, - onTimelineLegacyFlyoutClose, - closeFlyout, - isExpandableFlyoutDisabled, - ] + [tableRows, handleOnEventDetailPanelOpened, closeFlyout] ); const onColumnResize = useCallback( @@ -468,16 +416,6 @@ export const TimelineDataTableComponent: React.FC = memo( externalControlColumns={leadingControlColumns} cellContext={cellContext} /> - {showExpandedDetails && isExpandableFlyoutDisabled && ( - - )} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.test.tsx index 54e227c79e86b..a1b89511de6c3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.test.tsx @@ -5,14 +5,12 @@ * 2.0. */ -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { renderHook } from '@testing-library/react-hooks'; import { useUnifiedTableExpandableFlyout } from './use_unified_timeline_expandable_flyout'; import { useLocation } from 'react-router-dom'; import { URL_PARAM_KEY } from '../../../../../common/hooks/use_url_state'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; -jest.mock('../../../../../common/hooks/use_experimental_features'); jest.mock('@kbn/kibana-react-plugin/public'); jest.mock('react-router-dom', () => { return { @@ -24,60 +22,20 @@ jest.mock('@kbn/expandable-flyout'); const onFlyoutCloseMock = jest.fn(); describe('useUnifiedTimelineExpandableFlyout', () => { - it('should disable expandable flyout when expandableFlyoutDisabled flag is true', () => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true); - (useLocation as jest.Mock).mockReturnValue({ - search: `?${URL_PARAM_KEY.timelineFlyout}=(test:value)`, - }); - (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ - openFlyout: jest.fn(), - closeFlyout: jest.fn(), - }); - - const { result } = renderHook(() => - useUnifiedTableExpandableFlyout({ - onClose: onFlyoutCloseMock, - }) - ); - - expect(result.current.isExpandableFlyoutDisabled).toBe(true); - }); - describe('when expandable flyout is enabled', () => { beforeEach(() => { - (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should mark flyout as closed when location is empty', () => { - (useLocation as jest.Mock).mockReturnValue({ - search: '', + (useExpandableFlyoutApi as jest.Mock).mockReturnValue({ + openFlyout: jest.fn(), + closeFlyout: jest.fn(), }); - const { result } = renderHook(() => - useUnifiedTableExpandableFlyout({ - onClose: onFlyoutCloseMock, - }) - ); - - expect(result.current.isExpandableFlyoutDisabled).toBe(false); - }); - - it('should mark flyout as open when location has `timelineFlyout`', () => { (useLocation as jest.Mock).mockReturnValue({ search: `${URL_PARAM_KEY.timelineFlyout}=(test:value)`, }); + }); - const { result } = renderHook(() => - useUnifiedTableExpandableFlyout({ - onClose: onFlyoutCloseMock, - }) - ); - - expect(result.current.isExpandableFlyoutDisabled).toBe(false); + afterEach(() => { + jest.clearAllMocks(); }); it('should mark flyout as close when location has empty `timelineFlyout`', () => { @@ -87,8 +45,6 @@ describe('useUnifiedTimelineExpandableFlyout', () => { }) ); - expect(result.current.isExpandableFlyoutDisabled).toBe(false); - (useLocation as jest.Mock).mockReturnValue({ search: `${URL_PARAM_KEY.timelineFlyout}=()`, }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.tsx index 83050110eb090..fb28b6f44f367 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/unified_components/hooks/use_unified_timeline_expandable_flyout.tsx @@ -8,7 +8,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { useLocation } from 'react-router-dom'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; import { URL_PARAM_KEY } from '../../../../../common/hooks/use_url_state'; const EMPTY_TIMELINE_FLYOUT_SEARCH_PARAMS = '()'; @@ -20,8 +19,6 @@ interface UseUnifiedTableExpandableFlyoutArgs { export const useUnifiedTableExpandableFlyout = ({ onClose, }: UseUnifiedTableExpandableFlyoutArgs) => { - const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled'); - const location = useLocation(); const { openFlyout, closeFlyout } = useExpandableFlyoutApi(); @@ -33,7 +30,7 @@ export const useUnifiedTableExpandableFlyout = ({ const isFlyoutOpen = useMemo(() => { /** - * Currently, if new expanable flyout is closed, there is not way for + * Currently, if new expandable flyout is closed, there is no way for * consumer to trigger an effect `onClose` of flyout. So, we are using * this hack to know if flyout is open or not. * @@ -71,6 +68,5 @@ export const useUnifiedTableExpandableFlyout = ({ isTimelineExpandableFlyoutOpen, openFlyout, closeFlyout: closeFlyoutWithEffect, - isExpandableFlyoutDisabled: expandableFlyoutDisabled, }; }; diff --git a/x-pack/plugins/security_solution/server/integration_tests/__mocks__/alerts-detections-request.json b/x-pack/plugins/security_solution/server/integration_tests/__mocks__/alerts-detections-request.json new file mode 100644 index 0000000000000..f6a0e5af07922 --- /dev/null +++ b/x-pack/plugins/security_solution/server/integration_tests/__mocks__/alerts-detections-request.json @@ -0,0 +1,185 @@ +{ + "@timestamp": "2024-07-09T12:07:22.061Z", + "kibana.alert.ancestors": [ + { + "id": "yEVhkpABheYIwp45uyhA", + "type": "event", + "index": ".ds-logs-endpoint.alerts-default-2024.07.08-000001", + "depth": 0 + } + ], + "kibana.alert.depth": 1, + "kibana.alert.original_event.action": "rule_detection", + "kibana.alert.original_event.category": "behavior", + "kibana.alert.original_event.dataset": "endpoint.diagnostic.collection", + "kibana.alert.original_event.kind": "alert", + "kibana.alert.original_event.module": "endpoint", + "kibana.alert.original_event.type": "info", + "kibana.alert.original_time": "2024-07-08T12:46:42.856Z", + "kibana.alert.risk_score": 47, + "kibana.alert.rule.actions": [], + "kibana.alert.rule.category": "Custom Query Rule", + "kibana.alert.rule.consumer": "siem", + "kibana.alert.rule.created_at": "2024-07-08T12:00:22.100Z", + "kibana.alert.rule.enabled": true, + "kibana.alert.rule.exceptions_list": [ + { + "id": "endpoint_list", + "list_id": "endpoint_list", + "type": "endpoint", + "namespace_type": "agnostic" + } + ], + "kibana.alert.rule.execution.uuid": "740f5acd-6dfa-4b71-878a-2dcbf615f0d2", + "kibana.alert.rule.false_positives": [], + "kibana.alert.rule.from": "now-10m", + "kibana.alert.rule.immutable": true, + "kibana.alert.rule.interval": "5m", + "kibana.alert.rule.name": "Endpoint Security", + "kibana.alert.rule.producer": "siem", + "kibana.alert.rule.references": [], + "kibana.alert.rule.risk_score_mapping": [ + { + "field": "event.risk_score", + "operator": "equals", + "value": "" + } + ], + "kibana.alert.rule.rule_id": "9a1a2dae-0b5f-4c3d-8305-a268d404c306", + "kibana.alert.rule.rule_type_id": "siem.queryRule", + "kibana.alert.rule.severity": "medium", + "kibana.alert.rule.severity_mapping": [ + { + "field": "event.severity", + "operator": "equals", + "severity": "low", + "value": "21" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "medium", + "value": "47" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "high", + "value": "73" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "critical", + "value": "99" + } + ], + "kibana.alert.rule.tags": ["Data Source: Elastic Defend"], + "kibana.alert.rule.threat": [], + "kibana.alert.rule.timestamp_override": "event.ingested", + "kibana.alert.rule.type": "query", + "kibana.alert.rule.updated_at": "2024-07-08T12:00:22.100Z", + "kibana.alert.rule.uuid": "5623aff4-d3f2-41c8-9542-ef7e6515ce40", + "kibana.alert.rule.version": 103, + "kibana.alert.severity": "medium", + "kibana.alert.status": "active", + "kibana.alert.uuid": "76713cff0f7c8e81bd7462f94c5fc6df4d3b52d9737ccc35a38c5efa42f47c26", + "kibana.alert.workflow_status": "open", + "kibana.space_ids": ["default"], + "kibana.version": "8.14.2", + "event.ingested": "2024-07-08T12:46:36Z", + "event.kind": "signal", + "event.action": "rule_detection", + "event.id": "87f78f3b-5f84-434a-ac37-6c9e414c4df9", + "event.type": "info", + "event.category": "behavior", + "event.dataset": "endpoint.diagnostic.collection", + "event.module": "endpoint", + "agent": { + "id": "9e6f8f6a-6913-47a1-8a38-2a9ba87f8894" + }, + "destination": { + "port": 443, + "ip": "10.102.118.219" + }, + "dll": [ + { + "code_signature": { + "subject_name": "Cybereason Inc", + "trusted": true + }, + "path": "", + "hash": { + "sha256": "8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2" + } + } + ], + "host": { + "os": { + "Ext": { + "variant": "Windows Server Release 2" + }, + "name": "Windows", + "family": "windows", + "version": "6.3", + "platform": "Windows", + "full": "Windows Server 2012R2" + } + }, + "network": { + "transport": "tcp", + "type": "ipv4", + "direction": "outgoing" + }, + "process": { + "code_signature": { + "status": "trusted", + "subject_name": "Microsoft Windows" + }, + "entity_id": "5hdvz461o6", + "entry_leader": { + "name": "fake entry", + "pid": 376, + "entity_id": "jpd1z6lsu6" + }, + "executable": "C:/fake_behavior/notepad.exe", + "Ext": { + "token": { + "integrity_level_name": "high" + } + }, + "name": "notepad.exe", + "parent": { + "entity_id": "iv54turo1i", + "pid": 1 + }, + "pid": 2, + "session_leader": { + "name": "fake session", + "pid": 891, + "entity_id": "jpd1z6lsu6" + } + }, + "registry": { + "data": { + "strings": "C:/fake_behavior/notepad.exe" + }, + "path": "", + "value": "notepad.exe" + }, + "source": { + "port": 59406, + "ip": "10.43.68.40" + }, + "user": { + "domain": "qbf98z0au1" + }, + "file": { + "name": "fake_behavior.exe", + "path": "C:/fake_behavior.exe" + }, + "licence_id": "b7d16098-16fc-42fb-ab0f-40e2394c2375", + "cluster_uuid": "BldID7FMTb66oQgpvC5Uyg", + "cluster_name": "es-test-cluster", + "task_version": "1.2.0" +} diff --git a/x-pack/plugins/security_solution/server/integration_tests/__mocks__/prebuilt-rules-events.json b/x-pack/plugins/security_solution/server/integration_tests/__mocks__/prebuilt-rules-events.json new file mode 100644 index 0000000000000..d9e9c2ec137f0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/integration_tests/__mocks__/prebuilt-rules-events.json @@ -0,0 +1,734 @@ +[ + { + "kibana.alert.start": "2024-07-08T12:50:55.123Z", + "kibana.alert.last_detected": "2024-07-08T12:50:55.123Z", + "kibana.version": "8.14.2", + "kibana.alert.rule.parameters": { + "description": "Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.", + "risk_score": 47, + "severity": "medium", + "license": "Elastic License v2", + "rule_name_override": "message", + "timestamp_override": "event.ingested", + "author": ["Elastic"], + "false_positives": [], + "from": "now-10m", + "rule_id": "9a1a2dae-0b5f-4c3d-8305-a268d404c306", + "max_signals": 10000, + "risk_score_mapping": [ + { + "field": "event.risk_score", + "operator": "equals", + "value": "" + } + ], + "severity_mapping": [ + { + "field": "event.severity", + "operator": "equals", + "severity": "low", + "value": "21" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "medium", + "value": "47" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "high", + "value": "73" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "critical", + "value": "99" + } + ], + "threat": [], + "to": "now", + "references": [], + "version": 103, + "exceptions_list": [ + { + "id": "endpoint_list", + "list_id": "endpoint_list", + "type": "endpoint", + "namespace_type": "agnostic" + } + ], + "immutable": true, + "related_integrations": [ + { + "package": "endpoint", + "version": "^8.2.0" + } + ], + "required_fields": [ + { + "name": "event.kind", + "type": "keyword", + "ecs": true + }, + { + "name": "event.module", + "type": "keyword", + "ecs": true + } + ], + "setup": "" + }, + "kibana.alert.rule.category": "Custom Query Rule", + "kibana.alert.rule.consumer": "siem", + "kibana.alert.rule.execution.uuid": "740f5acd-6dfa-4b71-878a-2dcbf615f0d2", + "kibana.alert.rule.name": "Endpoint Security", + "kibana.alert.rule.producer": "siem", + "kibana.alert.rule.revision": 0, + "kibana.alert.rule.rule_type_id": "siem.queryRule", + "kibana.alert.rule.uuid": "5623aff4-d3f2-41c8-9542-ef7e6515ce40", + "kibana.space_ids": ["default"], + "kibana.alert.rule.tags": ["Data Source: Elastic Defend"], + "@timestamp": "2024-07-08T12:50:55.085Z", + "registry": { + "path": "", + "data": { + "strings": "C:/fake_behavior/notepad.exe" + }, + "value": "notepad.exe" + }, + "agent": { + "id": "9e6f8f6a-6913-47a1-8a38-2a9ba87f8894", + "type": "endpoint", + "version": "8.14.2" + }, + "process": { + "Ext": { + "ancestry": ["iv54turo1i", "dac98d002m"], + "code_signature": [ + { + "trusted": false, + "subject_name": "bad signer" + } + ], + "user": "SYSTEM", + "token": { + "integrity_level_name": "high", + "elevation_level": "full" + } + }, + "parent": { + "pid": 1, + "entity_id": "iv54turo1i" + }, + "group_leader": { + "name": "fake leader", + "pid": 687, + "entity_id": "jpd1z6lsu6" + }, + "session_leader": { + "name": "fake session", + "pid": 891, + "entity_id": "jpd1z6lsu6" + }, + "code_signature": { + "subject_name": "Microsoft Windows", + "status": "trusted" + }, + "entry_leader": { + "name": "fake entry", + "pid": 376, + "entity_id": "jpd1z6lsu6" + }, + "name": "notepad.exe", + "pid": 2, + "entity_id": "5hdvz461o6", + "executable": "C:/fake_behavior/notepad.exe" + }, + "dll": [ + { + "Ext": { + "compile_time": 1534424710, + "malware_classification": { + "identifier": "Whitelisted", + "score": 0, + "threshold": 0, + "version": "3.0.0" + }, + "mapped_address": 5362483200, + "mapped_size": 0 + }, + "path": "", + "code_signature": { + "trusted": true, + "subject_name": "Cybereason Inc" + }, + "pe": { + "architecture": "x64" + }, + "hash": { + "sha1": "ca85243c0af6a6471bdaa560685c51eefd6dbc0d", + "sha256": "8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2", + "md5": "1f2d082566b0fc5f2c238a5180db7451" + } + } + ], + "destination": { + "port": 443, + "ip": "10.102.118.219" + }, + "rule": { + "description": "Behavior rule description", + "id": "ee2b68fd-a8b4-42cb-82e3-018dd54e0d68" + }, + "source": { + "port": 59406, + "ip": "10.43.68.40" + }, + "network": { + "transport": "tcp", + "type": "ipv4", + "direction": "outgoing" + }, + "file": { + "path": "C:/fake_behavior.exe", + "name": "fake_behavior.exe" + }, + "Endpoint": { + "capabilities": [ + "isolation", + "kill_process", + "suspend_process", + "running_processes", + "get_file", + "execute", + "upload_file" + ], + "configuration": { + "isolation": true + }, + "state": { + "isolation": true + }, + "status": "enrolled", + "policy": { + "applied": { + "name": "With Eventing", + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A", + "endpoint_policy_version": 3, + "version": 5, + "status": "success" + } + } + }, + "ecs": { + "version": "1.6.0" + }, + "data_stream": { + "namespace": "default", + "type": "logs", + "dataset": "endpoint.alerts" + }, + "elastic": { + "agent": { + "id": "9e6f8f6a-6913-47a1-8a38-2a9ba87f8894" + } + }, + "host": { + "hostname": "Host-o0zw8cq8rq", + "os": { + "Ext": { + "variant": "Windows Server Release 2" + }, + "name": "Windows", + "family": "windows", + "version": "6.3", + "platform": "Windows", + "full": "Windows Server 2012R2" + }, + "ip": ["10.254.97.183"], + "name": "Host-o0zw8cq8rq", + "id": "a5977222-3dfe-4f74-9719-9347c3b01857", + "mac": ["33-e1-de-eb-d3-2e"], + "architecture": "2ok2s7qnf3" + }, + "user": { + "domain": "qbf98z0au1", + "name": "2q8d3pq1j8" + }, + "event.agent_id_status": "auth_metadata_missing", + "event.sequence": 15, + "event.ingested": "2024-07-08T12:46:36Z", + "event.code": "behavior", + "event.kind": "signal", + "event.module": "endpoint", + "event.action": "rule_detection", + "event.id": "87f78f3b-5f84-434a-ac37-6c9e414c4df9", + "event.category": "behavior", + "event.type": "info", + "event.dataset": "endpoint.diagnostic.collection", + "kibana.alert.original_time": "2024-07-08T12:46:42.856Z", + "kibana.alert.ancestors": [ + { + "id": "yEVhkpABheYIwp45uyhA", + "type": "event", + "index": ".ds-logs-endpoint.alerts-default-2024.07.08-000001", + "depth": 0 + } + ], + "kibana.alert.status": "active", + "kibana.alert.workflow_status": "open", + "kibana.alert.depth": 1, + "kibana.alert.reason": "behavior event with process notepad.exe, file fake_behavior.exe, source 10.43.68.40:59406, destination 10.102.118.219:443, by 2q8d3pq1j8 on Host-o0zw8cq8rq created medium alert Endpoint Security.", + "kibana.alert.severity": "medium", + "kibana.alert.risk_score": 47, + "kibana.alert.rule.actions": [], + "kibana.alert.rule.author": ["Elastic"], + "kibana.alert.rule.created_at": "2024-07-08T12:00:22.100Z", + "kibana.alert.rule.created_by": "elastic", + "kibana.alert.rule.description": "Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.", + "kibana.alert.rule.enabled": true, + "kibana.alert.rule.exceptions_list": [ + { + "id": "endpoint_list", + "list_id": "endpoint_list", + "type": "endpoint", + "namespace_type": "agnostic" + } + ], + "kibana.alert.rule.false_positives": [], + "kibana.alert.rule.from": "now-10m", + "kibana.alert.rule.immutable": true, + "kibana.alert.rule.interval": "5m", + "kibana.alert.rule.indices": ["logs-endpoint.alerts-*"], + "kibana.alert.rule.license": "Elastic License v2", + "kibana.alert.rule.max_signals": 10000, + "kibana.alert.rule.references": [], + "kibana.alert.rule.risk_score_mapping": [ + { + "field": "event.risk_score", + "operator": "equals", + "value": "" + } + ], + "kibana.alert.rule.rule_id": "9a1a2dae-0b5f-4c3d-8305-a268d404c306", + "kibana.alert.rule.rule_name_override": "message", + "kibana.alert.rule.severity_mapping": [ + { + "field": "event.severity", + "operator": "equals", + "severity": "low", + "value": "21" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "medium", + "value": "47" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "high", + "value": "73" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "critical", + "value": "99" + } + ], + "kibana.alert.rule.threat": [], + "kibana.alert.rule.timestamp_override": "event.ingested", + "kibana.alert.rule.to": "now", + "kibana.alert.rule.type": "query", + "kibana.alert.rule.updated_at": "2024-07-08T12:00:22.100Z", + "kibana.alert.rule.updated_by": "elastic", + "kibana.alert.rule.version": 103, + "kibana.alert.uuid": "76713cff0f7c8e81bd7462f94c5fc6df4d3b52d9737ccc35a38c5efa42f47c26", + "kibana.alert.workflow_tags": [], + "kibana.alert.workflow_assignee_ids": [], + "kibana.alert.rule.risk_score": 47, + "kibana.alert.rule.severity": "medium", + "kibana.alert.original_event.agent_id_status": "auth_metadata_missing", + "kibana.alert.original_event.sequence": 15, + "kibana.alert.original_event.ingested": "2024-07-08T12:46:36Z", + "kibana.alert.original_event.code": "behavior", + "kibana.alert.original_event.kind": "alert", + "kibana.alert.original_event.module": "endpoint", + "kibana.alert.original_event.action": "rule_detection", + "kibana.alert.original_event.id": "87f78f3b-5f84-434a-ac37-6c9e414c4df9", + "kibana.alert.original_event.category": "behavior", + "kibana.alert.original_event.type": "info", + "kibana.alert.original_event.dataset": "endpoint.diagnostic.collection" + }, + { + "kibana.alert.start": "2024-07-08T12:50:55.123Z", + "kibana.alert.last_detected": "2024-07-08T12:50:55.123Z", + "kibana.version": "8.14.2", + "kibana.alert.rule.parameters": { + "description": "Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.", + "risk_score": 47, + "severity": "medium", + "license": "Elastic License v2", + "rule_name_override": "message", + "timestamp_override": "event.ingested", + "author": ["Elastic"], + "false_positives": [], + "from": "now-10m", + "rule_id": "9a1a2dae-0b5f-4c3d-8305-a268d404c306", + "max_signals": 10000, + "risk_score_mapping": [ + { + "field": "event.risk_score", + "operator": "equals", + "value": "" + } + ], + "severity_mapping": [ + { + "field": "event.severity", + "operator": "equals", + "severity": "low", + "value": "21" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "medium", + "value": "47" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "high", + "value": "73" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "critical", + "value": "99" + } + ], + "threat": [], + "to": "now", + "references": [], + "version": 103, + "exceptions_list": [ + { + "id": "endpoint_list", + "list_id": "endpoint_list", + "type": "endpoint", + "namespace_type": "agnostic" + } + ], + "immutable": true, + "related_integrations": [ + { + "package": "endpoint", + "version": "^8.2.0" + } + ], + "required_fields": [ + { + "name": "event.kind", + "type": "keyword", + "ecs": true + }, + { + "name": "event.module", + "type": "keyword", + "ecs": true + } + ], + "setup": "" + }, + "kibana.alert.rule.category": "Custom Query Rule", + "kibana.alert.rule.consumer": "siem", + "kibana.alert.rule.execution.uuid": "740f5acd-6dfa-4b71-878a-2dcbf615f0d2", + "kibana.alert.rule.name": "Endpoint Security", + "kibana.alert.rule.producer": "siem", + "kibana.alert.rule.revision": 0, + "kibana.alert.rule.rule_type_id": "siem.queryRule", + "kibana.alert.rule.uuid": "5623aff4-d3f2-41c8-9542-ef7e6515ce40", + "kibana.space_ids": ["default"], + "kibana.alert.rule.tags": ["Data Source: Elastic Defend"], + "@timestamp": "2024-07-08T12:50:55.087Z", + "registry": { + "path": "", + "data": { + "strings": "C:/fake_behavior/explorer.exe" + }, + "value": "explorer.exe" + }, + "agent": { + "id": "9e6f8f6a-6913-47a1-8a38-2a9ba87f8894", + "type": "endpoint", + "version": "8.14.2" + }, + "process": { + "Ext": { + "ancestry": ["dac98d002m", "jpd1z6lsu6"], + "code_signature": [ + { + "trusted": false, + "subject_name": "bad signer" + } + ], + "user": "SYSTEM", + "token": { + "integrity_level_name": "high", + "elevation_level": "full" + } + }, + "parent": { + "pid": 1, + "entity_id": "dac98d002m" + }, + "group_leader": { + "name": "fake leader", + "pid": 471, + "entity_id": "jpd1z6lsu6" + }, + "session_leader": { + "name": "fake session", + "pid": 775, + "entity_id": "jpd1z6lsu6" + }, + "code_signature": { + "subject_name": "Microsoft Windows", + "status": "trusted" + }, + "entry_leader": { + "name": "fake entry", + "pid": 722, + "entity_id": "jpd1z6lsu6" + }, + "name": "explorer.exe", + "pid": 2, + "entity_id": "iv54turo1i", + "executable": "C:/fake_behavior/explorer.exe" + }, + "dll": [ + { + "Ext": { + "compile_time": 1534424710, + "malware_classification": { + "identifier": "Whitelisted", + "score": 0, + "threshold": 0, + "version": "3.0.0" + }, + "mapped_address": 5362483200, + "mapped_size": 0 + }, + "path": "", + "code_signature": { + "trusted": true, + "subject_name": "Cybereason Inc" + }, + "pe": { + "architecture": "x64" + }, + "hash": { + "sha1": "ca85243c0af6a6471bdaa560685c51eefd6dbc0d", + "sha256": "8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2", + "md5": "1f2d082566b0fc5f2c238a5180db7451" + } + } + ], + "destination": { + "port": 443, + "ip": "10.183.30.139" + }, + "rule": { + "description": "Behavior rule description", + "id": "cc1892b8-e6ee-4a1e-bef9-3e1f1f62370e" + }, + "source": { + "port": 59406, + "ip": "10.3.18.122" + }, + "network": { + "transport": "tcp", + "type": "ipv4", + "direction": "outgoing" + }, + "file": { + "path": "C:/fake_behavior.exe", + "name": "fake_behavior.exe" + }, + "Endpoint": { + "capabilities": [ + "isolation", + "kill_process", + "suspend_process", + "running_processes", + "get_file", + "execute", + "upload_file" + ], + "configuration": { + "isolation": true + }, + "state": { + "isolation": true + }, + "status": "enrolled", + "policy": { + "applied": { + "name": "With Eventing", + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A", + "endpoint_policy_version": 3, + "version": 5, + "status": "success" + } + } + }, + "ecs": { + "version": "1.6.0" + }, + "data_stream": { + "namespace": "default", + "type": "logs", + "dataset": "endpoint.alerts" + }, + "elastic": { + "agent": { + "id": "9e6f8f6a-6913-47a1-8a38-2a9ba87f8894" + } + }, + "host": { + "hostname": "Host-o0zw8cq8rq", + "os": { + "Ext": { + "variant": "Windows Server Release 2" + }, + "name": "Windows", + "family": "windows", + "version": "6.3", + "platform": "Windows", + "full": "Windows Server 2012R2" + }, + "ip": ["10.254.97.183"], + "name": "Host-o0zw8cq8rq", + "id": "a5977222-3dfe-4f74-9719-9347c3b01857", + "mac": ["33-e1-de-eb-d3-2e"], + "architecture": "2ok2s7qnf3" + }, + "user": { + "domain": "182cw5hsw7", + "name": "v0teoghxky" + }, + "event.agent_id_status": "auth_metadata_missing", + "event.sequence": 11, + "event.ingested": "2024-07-08T12:46:36Z", + "event.code": "behavior", + "event.kind": "signal", + "event.module": "endpoint", + "event.action": "rule_detection", + "event.id": "374b28d3-152e-4b80-8f80-d8c9ed42a2ef", + "event.category": "behavior", + "event.type": "info", + "event.dataset": "endpoint.diagnostic.collection", + "kibana.alert.original_time": "2024-07-08T14:53:09.856Z", + "kibana.alert.ancestors": [ + { + "id": "xEVhkpABheYIwp45uyhA", + "type": "event", + "index": ".ds-logs-endpoint.alerts-default-2024.07.08-000001", + "depth": 0 + } + ], + "kibana.alert.status": "active", + "kibana.alert.workflow_status": "open", + "kibana.alert.depth": 1, + "kibana.alert.reason": "behavior event with process explorer.exe, file fake_behavior.exe, source 10.3.18.122:59406, destination 10.183.30.139:443, by v0teoghxky on Host-o0zw8cq8rq created medium alert Endpoint Security.", + "kibana.alert.severity": "medium", + "kibana.alert.risk_score": 47, + "kibana.alert.rule.actions": [], + "kibana.alert.rule.author": ["Elastic"], + "kibana.alert.rule.created_at": "2024-07-08T12:00:22.100Z", + "kibana.alert.rule.created_by": "elastic", + "kibana.alert.rule.description": "Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.", + "kibana.alert.rule.enabled": true, + "kibana.alert.rule.exceptions_list": [ + { + "id": "endpoint_list", + "list_id": "endpoint_list", + "type": "endpoint", + "namespace_type": "agnostic" + } + ], + "kibana.alert.rule.false_positives": [], + "kibana.alert.rule.from": "now-10m", + "kibana.alert.rule.immutable": true, + "kibana.alert.rule.interval": "5m", + "kibana.alert.rule.indices": ["logs-endpoint.alerts-*"], + "kibana.alert.rule.license": "Elastic License v2", + "kibana.alert.rule.max_signals": 10000, + "kibana.alert.rule.references": [], + "kibana.alert.rule.risk_score_mapping": [ + { + "field": "event.risk_score", + "operator": "equals", + "value": "" + } + ], + "kibana.alert.rule.rule_id": "9a1a2dae-0b5f-4c3d-8305-a268d404c306", + "kibana.alert.rule.rule_name_override": "message", + "kibana.alert.rule.severity_mapping": [ + { + "field": "event.severity", + "operator": "equals", + "severity": "low", + "value": "21" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "medium", + "value": "47" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "high", + "value": "73" + }, + { + "field": "event.severity", + "operator": "equals", + "severity": "critical", + "value": "99" + } + ], + "kibana.alert.rule.threat": [], + "kibana.alert.rule.timestamp_override": "event.ingested", + "kibana.alert.rule.to": "now", + "kibana.alert.rule.type": "query", + "kibana.alert.rule.updated_at": "2024-07-08T12:00:22.100Z", + "kibana.alert.rule.updated_by": "elastic", + "kibana.alert.rule.version": 103, + "kibana.alert.uuid": "bda4832328607d81ebd65eef8abbef8f3c8b74614ea85e71a781fd7e2d79fbda", + "kibana.alert.workflow_tags": [], + "kibana.alert.workflow_assignee_ids": [], + "kibana.alert.rule.risk_score": 47, + "kibana.alert.rule.severity": "medium", + "kibana.alert.original_event.agent_id_status": "auth_metadata_missing", + "kibana.alert.original_event.sequence": 11, + "kibana.alert.original_event.ingested": "2024-07-08T12:46:36Z", + "kibana.alert.original_event.code": "behavior", + "kibana.alert.original_event.kind": "alert", + "kibana.alert.original_event.module": "endpoint", + "kibana.alert.original_event.action": "rule_detection", + "kibana.alert.original_event.id": "374b28d3-152e-4b80-8f80-d8c9ed42a2ef", + "kibana.alert.original_event.category": "behavior", + "kibana.alert.original_event.type": "info", + "kibana.alert.original_event.dataset": "endpoint.diagnostic.collection" + } +] diff --git a/x-pack/plugins/security_solution/server/integration_tests/lib/telemetry_helpers.ts b/x-pack/plugins/security_solution/server/integration_tests/lib/telemetry_helpers.ts index f697f0f793624..2daf8c732002b 100644 --- a/x-pack/plugins/security_solution/server/integration_tests/lib/telemetry_helpers.ts +++ b/x-pack/plugins/security_solution/server/integration_tests/lib/telemetry_helpers.ts @@ -44,6 +44,7 @@ import mockEndpointAlert from '../__mocks__/endpoint-alert.json'; import mockedRule from '../__mocks__/rule.json'; import fleetAgents from '../__mocks__/fleet-agents.json'; import endpointMetrics from '../__mocks__/endpoint-metrics.json'; +import prebuiltRulesEvents from '../__mocks__/prebuilt-rules-events.json'; import endpointMetadata from '../__mocks__/endpoint-metadata.json'; import endpointPolicy from '../__mocks__/endpoint-policy.json'; @@ -51,6 +52,7 @@ const fleetIndex = '.fleet-agents'; const endpointMetricsIndex = '.ds-metrics-endpoint.metrics-1'; const endpointMetricsMetadataIndex = '.ds-metrics-endpoint.metadata-1'; const endpointMetricsPolicyIndex = '.ds-metrics-endpoint.policy-1'; +const prebuiltRulesIndex = '.alerts-security.alerts'; export function getTelemetryTasks( spy: jest.SpyInstance< @@ -182,6 +184,10 @@ export async function mockEndpointData( await bulkInsert(esClient, endpointMetricsPolicyIndex, updateTimestamps(endpointPolicy)); } +export async function mockPrebuiltRulesData(esClient: ElasticsearchClient) { + await bulkInsert(esClient, prebuiltRulesIndex, updateTimestamps(prebuiltRulesEvents)); +} + export async function initEndpointIndices(esClient: ElasticsearchClient) { const mappings: object = { dynamic: false, diff --git a/x-pack/plugins/security_solution/server/integration_tests/telemetry.test.ts b/x-pack/plugins/security_solution/server/integration_tests/telemetry.test.ts index cdd984530d7aa..750a67e48cc24 100644 --- a/x-pack/plugins/security_solution/server/integration_tests/telemetry.test.ts +++ b/x-pack/plugins/security_solution/server/integration_tests/telemetry.test.ts @@ -15,7 +15,10 @@ import type { } from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_STAGING } from '@kbn/telemetry-plugin/common/constants'; -import { TELEMETRY_CHANNEL_ENDPOINT_META } from '../lib/telemetry/constants'; +import { + TELEMETRY_CHANNEL_DETECTION_ALERTS, + TELEMETRY_CHANNEL_ENDPOINT_META, +} from '../lib/telemetry/constants'; import { eventually, setupTestServers, removeFile } from './lib/helpers'; import { @@ -33,6 +36,7 @@ import { dropEndpointIndices, mockEndpointData, getTelemetryReceiver, + mockPrebuiltRulesData, } from './lib/telemetry_helpers'; import { @@ -45,9 +49,10 @@ import { type TaskManagerStartContract, } from '@kbn/task-manager-plugin/server/plugin'; import type { SecurityTelemetryTask } from '../lib/telemetry/task'; -import { TelemetryChannel } from '../lib/telemetry/types'; +import { TelemetryChannel, type TelemetryEvent } from '../lib/telemetry/types'; import type { AsyncTelemetryEventsSender } from '../lib/telemetry/async_sender'; import endpointMetaTelemetryRequest from './__mocks__/endpoint-meta-telemetry-request.json'; +import alertsDetectionsRequest from './__mocks__/alerts-detections-request.json'; import type { ITelemetryReceiver, TelemetryReceiver } from '../lib/telemetry/receiver'; import type { TaskMetric } from '../lib/telemetry/task_metrics.types'; import type { AgentPolicy } from '@kbn/fleet-plugin/common'; @@ -659,6 +664,54 @@ describe('telemetry tasks', () => { }); }); + describe('telemetry-prebuilt-rule-alerts', () => { + it('should execute when scheduled', async () => { + await mockAndSchedulePrebuiltRulesTask(); + + const alertsDetectionsRequests = await getAlertsDetectionsRequests(); + + expect(alertsDetectionsRequests.length).toBe(2); + + const body = alertsDetectionsRequests[0]; + + expect(body.dll).toStrictEqual(alertsDetectionsRequest.dll); + expect(body.process).toStrictEqual(alertsDetectionsRequest.process); + expect(body.file).toStrictEqual(alertsDetectionsRequest.file); + }); + + it('should manage runtime errors searching endpoint metrics', async () => { + const errorMessage = 'Something went wront'; + + async function* mockedGenerator( + _index: string, + _executeFrom: string, + _executeTo: string + ): AsyncGenerator { + throw Error(errorMessage); + } + + const fetchEndpointMetricsAbstract = telemetryReceiver.fetchPrebuiltRuleAlertsBatch; + deferred.push(() => { + telemetryReceiver.fetchPrebuiltRuleAlertsBatch = fetchEndpointMetricsAbstract; + }); + + telemetryReceiver.fetchPrebuiltRuleAlertsBatch = mockedGenerator; + + const task = await mockAndSchedulePrebuiltRulesTask(); + const started = performance.now(); + + const requests = await getTaskMetricsRequests(task, started); + + expect(requests.length).toBe(1); + + const metric = requests[0]; + + expect(metric).not.toBeFalsy(); + expect(metric.taskMetric.passed).toBe(false); + expect(metric.taskMetric.error_message).toBe(errorMessage); + }); + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any async function getEndpointMetaRequests(atLeast: number = 1): Promise { return eventually(async () => { @@ -681,6 +734,28 @@ describe('telemetry tasks', () => { }); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async function getAlertsDetectionsRequests(atLeast: number = 1): Promise { + return eventually(async () => { + const found = mockedAxiosPost.mock.calls.filter(([url]) => { + return url.startsWith(ENDPOINT_STAGING) && url.endsWith(TELEMETRY_CHANNEL_DETECTION_ALERTS); + }); + + expect(found).not.toBeFalsy(); + expect(found.length).toBeGreaterThanOrEqual(atLeast); + + return (found ?? []).flatMap((req) => { + const ndjson = req[1] as string; + return ndjson + .split('\n') + .filter((l) => l.trim().length > 0) + .map((l) => { + return JSON.parse(l); + }); + }); + }); + } + async function mockAndScheduleDetectionRulesTask(): Promise { const task = getTelemetryTask(tasks, 'security:telemetry-detection-rules'); @@ -717,6 +792,19 @@ describe('telemetry tasks', () => { return task; } + async function mockAndSchedulePrebuiltRulesTask(): Promise { + const task = getTelemetryTask(tasks, 'security:telemetry-prebuilt-rule-alerts'); + + await mockPrebuiltRulesData(esClient); + + // schedule task to run ASAP + await eventually(async () => { + await taskManagerPlugin.runSoon(task.getTaskId()); + }); + + return task; + } + async function mockAndScheduleEndpointDiagnosticsTask(): Promise { const task = getTelemetryTask(tasks, 'security:endpoint-diagnostics'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts index e4d9255dc2b95..9190bb873e81b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.test.ts @@ -36,7 +36,7 @@ describe('Prebuilt rule asset schema', () => { // The PrebuiltRuleAsset schema is built out of the rule schema, // but the following fields are manually omitted. // See: detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts - const omittedFields = [ + const omittedBaseFields = [ 'actions', 'throttle', 'meta', @@ -47,10 +47,24 @@ describe('Prebuilt rule asset schema', () => { 'outcome', ]; - test.each(omittedFields)('ignores %s since it`s an omitted field', (field) => { + test.each(omittedBaseFields)( + 'ignores the base %s field since it`s an omitted field', + (field) => { + const payload: Partial & Record = { + ...getPrebuiltRuleMock(), + [field]: 'some value', + }; + + const result = PrebuiltRuleAsset.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(getPrebuiltRuleMock()); + } + ); + + test('ignores the type specific response_actions field since it`s an omitted field', () => { const payload: Partial & Record = { ...getPrebuiltRuleMock(), - [field]: 'some value', + response_actions: [{ action_type_id: `.osquery`, params: {} }], }; const result = PrebuiltRuleAsset.safeParse(payload); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts index 056a3998e3b3e..c41d24660462e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/model/rule_assets/prebuilt_rule_asset.ts @@ -10,14 +10,7 @@ import { RuleSignatureId, RuleVersion, BaseCreateProps, - EqlRuleCreateFields, - EsqlRuleCreateFields, - MachineLearningRuleCreateFields, - NewTermsRuleCreateFields, - QueryRuleCreateFields, - SavedQueryRuleCreateFields, - ThreatMatchRuleCreateFields, - ThresholdRuleCreateFields, + TypeSpecificCreateProps, } from '../../../../../../common/api/detection_engine/model/rule_schema'; /** @@ -37,28 +30,26 @@ const BASE_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET = zodMaskFor( 'outcome', ]); -// `response_actions` is only part of the optional fields in QueryRuleCreateFields and SavedQueryRuleCreateFields -const TYPE_SPECIFIC_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET = zodMaskFor< - QueryRuleCreateFields | SavedQueryRuleCreateFields ->()(['response_actions']); - -const QueryRuleAssetFields = QueryRuleCreateFields.omit( - TYPE_SPECIFIC_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET -); -const SavedQueryRuleAssetFields = SavedQueryRuleCreateFields.omit( - TYPE_SPECIFIC_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET -); - -export const RuleAssetTypeSpecificCreateProps = z.discriminatedUnion('type', [ - EqlRuleCreateFields, - QueryRuleAssetFields, - SavedQueryRuleAssetFields, - ThresholdRuleCreateFields, - ThreatMatchRuleCreateFields, - MachineLearningRuleCreateFields, - NewTermsRuleCreateFields, - EsqlRuleCreateFields, -]); +/** + * Aditionally remove fields which are part only of the optional fields in the rule types that make up + * the TypeSpecificCreateProps discriminatedUnion, by using a Zod transformation which extracts out the + * necessary fields in the rules types where they exist. Fields to extract: + * - response_actions: from Query and SavedQuery rules + */ +const TypeSpecificFields = TypeSpecificCreateProps.transform((val) => { + switch (val.type) { + case 'query': { + const { response_actions: _, ...rest } = val; + return rest; + } + case 'saved_query': { + const { response_actions: _, ...rest } = val; + return rest; + } + default: + return val; + } +}); function zodMaskFor() { return function (props: U[]): Record { @@ -85,7 +76,7 @@ function zodMaskFor() { */ export type PrebuiltRuleAsset = z.infer; export const PrebuiltRuleAsset = BaseCreateProps.omit(BASE_PROPS_REMOVED_FROM_PREBUILT_RULE_ASSET) - .and(RuleAssetTypeSpecificCreateProps) + .and(TypeSpecificFields) .and( z.object({ rule_id: RuleSignatureId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 314d2c273b04a..2ca4790d3c203 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -12,7 +12,7 @@ import type { IKibanaResponse } from '@kbn/core/server'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../../common/constants'; import { buildSiemResponse } from '../utils'; -import type { ReadPrivilegesResponse } from '../../../../../common/api/detection_engine'; +import type { GetPrivilegesResponse } from '../../../../../common/api/detection_engine'; export const readPrivilegesRoute = ( router: SecuritySolutionPluginRouter, @@ -31,7 +31,7 @@ export const readPrivilegesRoute = ( version: '2023-10-31', validate: false, }, - async (context, request, response): Promise> => { + async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); try { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_delete_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_delete_rules/route.ts index 2f1ef59d0971b..f582e19d2bcad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_delete_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_delete_rules/route.ts @@ -9,8 +9,13 @@ import type { VersionedRouteConfig } from '@kbn/core-http-server'; import type { IKibanaResponse, Logger, RequestHandler } from '@kbn/core/server'; import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import type { + BulkDeleteRulesPostResponse, + BulkDeleteRulesResponse, +} from '../../../../../../../common/api/detection_engine/rule_management'; import { BulkCrudRulesResponse, + BulkDeleteRulesPostRequestBody, BulkDeleteRulesRequestBody, validateQueryRuleByIds, } from '../../../../../../../common/api/detection_engine/rule_management'; @@ -33,7 +38,7 @@ import { RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS } from '../../timeouts'; type Handler = RequestHandler< unknown, unknown, - BulkDeleteRulesRequestBody, + BulkDeleteRulesRequestBody | BulkDeleteRulesPostRequestBody, SecuritySolutionRequestHandlerContext, 'delete' | 'post' >; @@ -46,7 +51,7 @@ export const bulkDeleteRulesRoute = (router: SecuritySolutionPluginRouter, logge context, request, response - ): Promise> => { + ): Promise> => { logDeprecatedBulkEndpoint(logger, DETECTION_ENGINE_RULES_BULK_DELETE); const siemResponse = buildSiemResponse(response); @@ -111,14 +116,26 @@ export const bulkDeleteRulesRoute = (router: SecuritySolutionPluginRouter, logge }, }, }; - const versionConfig = { - version: '2023-10-31', - validate: { - request: { - body: buildRouteValidationWithZod(BulkDeleteRulesRequestBody), + router.versioned.delete(routeConfig).addVersion( + { + version: '2023-10-31', + validate: { + request: { + body: buildRouteValidationWithZod(BulkDeleteRulesRequestBody), + }, }, }, - }; - router.versioned.delete(routeConfig).addVersion(versionConfig, handler); - router.versioned.post(routeConfig).addVersion(versionConfig, handler); + handler + ); + router.versioned.post(routeConfig).addVersion( + { + version: '2023-10-31', + validate: { + request: { + body: buildRouteValidationWithZod(BulkDeleteRulesPostRequestBody), + }, + }, + }, + handler + ); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index 7b48e32bf9962..229ebf3b71d93 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -28,10 +28,10 @@ import { import { validateCreateRuleProps } from '../../../../../../common/api/detection_engine/rule_management'; import { RuleExecutionStatusEnum } from '../../../../../../common/api/detection_engine/rule_monitoring'; import type { - PreviewResponse, + RulePreviewResponse, RulePreviewLogs, } from '../../../../../../common/api/detection_engine'; -import { PreviewRulesSchema } from '../../../../../../common/api/detection_engine'; +import { RulePreviewRequestBody } from '../../../../../../common/api/detection_engine'; import type { StartPlugins, SetupPlugins } from '../../../../../plugin'; import { buildSiemResponse } from '../../../routes/utils'; @@ -92,9 +92,9 @@ export const previewRulesRoute = ( .addVersion( { version: '2023-10-31', - validate: { request: { body: buildRouteValidationWithZod(PreviewRulesSchema) } }, + validate: { request: { body: buildRouteValidationWithZod(RulePreviewRequestBody) } }, }, - async (context, request, response): Promise> => { + async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); const validationErrors = validateCreateRuleProps(request.body); const coreContext = await context.core; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index 2209c6f9dda9b..bddee1c6a1f28 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -204,6 +204,7 @@ export interface ITelemetryReceiver { }>; fetchPrebuiltRuleAlertsBatch( + index: string, executeFrom: string, executeTo: string ): AsyncGenerator; @@ -746,13 +747,17 @@ export class TelemetryReceiver implements ITelemetryReceiver { }; } - public async *fetchPrebuiltRuleAlertsBatch(executeFrom: string, executeTo: string) { + public async *fetchPrebuiltRuleAlertsBatch( + index: string, + executeFrom: string, + executeTo: string + ) { this.logger.debug('Searching prebuilt rule alerts from', { executeFrom, executeTo, } as LogMeta); - let pitId = await this.openPointInTime(DEFAULT_DIAGNOSTIC_INDEX); + let pitId = await this.openPointInTime(index); let fetchMore = true; let searchAfter: SortResults | undefined; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts index 15f84e1ff4ef1..bf1c457e9dfc9 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts @@ -66,6 +66,7 @@ export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: n } for await (const alerts of receiver.fetchPrebuiltRuleAlertsBatch( + index, taskExecutionPeriod.last ?? 'now-1h', taskExecutionPeriod.current )) { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/gemini/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/gemini/index.ts index 91cfbaf1151ea..d8add7df88177 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/gemini/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/gemini/index.ts @@ -17,6 +17,7 @@ import { import { urlAllowListValidator } from '@kbn/actions-plugin/server'; import { ValidatorServices } from '@kbn/actions-plugin/server/types'; import { assertURL } from '@kbn/actions-plugin/server/sub_action_framework/helpers/validators'; +import { GenerativeAIForObservabilityConnectorFeatureId } from '@kbn/actions-plugin/common/connector_feature_config'; import { GEMINI_CONNECTOR_ID, GEMINI_TITLE } from '../../../common/gemini/constants'; import { ConfigSchema, SecretsSchema } from '../../../common/gemini/schema'; import { Config, Secrets } from '../../../common/gemini/types'; @@ -35,6 +36,7 @@ export const getConnectorType = (): SubActionConnectorType => ( supportedFeatureIds: [ GenerativeAIForSecurityConnectorFeatureId, GenerativeAIForSearchPlaygroundConnectorFeatureId, + GenerativeAIForObservabilityConnectorFeatureId, ], minimumLicenseRequired: 'enterprise' as const, renderParameterTemplates, diff --git a/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts b/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts index 8eef4dc32df1c..8b70c5c32e9fb 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts @@ -622,5 +622,26 @@ export default function ({ getService }: FtrProviderContext) { } }); }); + + describe('request validation', () => { + it('should return 400 when requesting more than 20 metrics', async () => { + const { min, max } = DATES['8.0.0'].logs_and_metrics; + await fetchSnapshot( + { + sourceId: 'default', + timerange: { + to: max, + from: min, + interval: '1m', + }, + metrics: Array(21).fill({ type: 'cpu' }), + nodeType: 'host', + groupBy: [{ field: 'service.type' }], + includeTimeseries: true, + }, + 400 + ); + }); + }); }); } diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 7a3ef5cc1f849..31e9c38263a24 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -22,6 +22,7 @@ import { import { AlertsMigrationCleanupRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/delete_signals_migration/delete_signals_migration.gen'; import { BulkCreateRulesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_create_rules/bulk_create_rules_route.gen'; import { BulkDeleteRulesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.gen'; +import { BulkDeleteRulesPostRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_delete_rules/bulk_delete_rules_route.gen'; import { BulkPatchRulesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_patch_rules/bulk_patch_rules_route.gen'; import { BulkUpdateRulesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_update_rules_route.gen'; import { CreateAlertsMigrationRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/create_signals_migration/create_signals_migration.gen'; @@ -63,6 +64,7 @@ import { PerformBulkActionRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.gen'; import { ReadRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/read_rule/read_rule_route.gen'; +import { RulePreviewRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_preview/rule_preview.gen'; import { SearchAlertsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/query_signals/query_signals_route.gen'; import { SetAlertAssigneesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen'; import { SetAlertsStatusRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/set_signal_status/set_signals_status_route.gen'; @@ -112,6 +114,17 @@ after 30 days. It also deletes other artifacts specific to the migration impleme .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') .send(props.body as object); }, + /** + * Deletes multiple rules. + */ + bulkDeleteRulesPost(props: BulkDeleteRulesPostProps) { + return supertest + .post('/api/detection_engine/rules/_bulk_delete') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, /** * Updates multiple rules using the `PATCH` method. */ @@ -287,6 +300,20 @@ finalize it. .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + /** + * Retrieves whether or not the user is authenticated, and the user's Kibana +space and index privileges, which determine if the user can create an +index for the Elastic Security alerts generated by +detection engine rules. + + */ + getPrivileges() { + return supertest + .get('/api/detection_engine/privileges') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); + }, getProtectionUpdatesNote(props: GetProtectionUpdatesNoteProps) { return supertest .get( @@ -383,6 +410,14 @@ finalize it. .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + rulePreview(props: RulePreviewProps) { + return supertest + .post('/api/detection_engine/rules/preview') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send(props.body as object); + }, searchAlerts(props: SearchAlertsProps) { return supertest .post('/api/detection_engine/signals/search') @@ -444,6 +479,9 @@ export interface BulkCreateRulesProps { export interface BulkDeleteRulesProps { body: BulkDeleteRulesRequestBodyInput; } +export interface BulkDeleteRulesPostProps { + body: BulkDeleteRulesPostRequestBodyInput; +} export interface BulkPatchRulesProps { body: BulkPatchRulesRequestBodyInput; } @@ -519,6 +557,9 @@ export interface PerformBulkActionProps { export interface ReadRuleProps { query: ReadRuleRequestQueryInput; } +export interface RulePreviewProps { + body: RulePreviewRequestBodyInput; +} export interface SearchAlertsProps { body: SearchAlertsRequestBodyInput; } diff --git a/x-pack/test/cloud_security_posture_api/config.ts b/x-pack/test/cloud_security_posture_api/config.ts index 62a976da70669..08dfc89bf1800 100644 --- a/x-pack/test/cloud_security_posture_api/config.ts +++ b/x-pack/test/cloud_security_posture_api/config.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { resolve } from 'path'; import type { FtrConfigProviderContext } from '@kbn/test'; import { CLOUD_SECURITY_PLUGIN_VERSION } from '@kbn/cloud-security-posture-plugin/common/constants'; @@ -15,14 +15,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...xpackFunctionalConfig.getAll(), - testFiles: [ - require.resolve('./telemetry/telemetry.ts'), - require.resolve('./routes/vulnerabilities_dashboard.ts'), - require.resolve('./routes/stats.ts'), - require.resolve('./routes/csp_benchmark_rules_bulk_update.ts'), - require.resolve('./routes/csp_benchmark_rules_get_states.ts'), - require.resolve('./routes/benchmarks.ts'), - ], + testFiles: [resolve(__dirname, './routes')], junit: { reportName: 'X-Pack Cloud Security Posture API Tests', }, diff --git a/x-pack/test/cloud_security_posture_api/routes/benchmarks.ts b/x-pack/test/cloud_security_posture_api/routes/benchmarks.ts index 65a3ec42fc37d..a8b94a9f36799 100644 --- a/x-pack/test/cloud_security_posture_api/routes/benchmarks.ts +++ b/x-pack/test/cloud_security_posture_api/routes/benchmarks.ts @@ -17,18 +17,20 @@ import expect from '@kbn/expect'; import Chance from 'chance'; import { CspBenchmarkRule } from '@kbn/cloud-security-posture-plugin/common/types/latest'; import { FtrProviderContext } from '../ftr_provider_context'; +import { CspSecurityCommonProvider } from './helper/user_roles_utilites'; const chance = new Chance(); // eslint-disable-next-line import/no-default-export export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; - const retry = getService('retry'); const es = getService('es'); const kibanaServer = getService('kibanaServer'); const supertest = getService('supertest'); const log = getService('log'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const cspSecurity = CspSecurityCommonProvider(providerContext); const getCspBenchmarkRules = async (benchmarkId: string): Promise => { let cspBenchmarkRules: CspBenchmarkRule[] = []; @@ -263,5 +265,57 @@ export default function (providerContext: FtrProviderContext) { expect(scoreAfterMute.score.postureScore).to.equal(0); }); }); + + describe('Get Benchmark API', async () => { + beforeEach(async () => { + await index.removeFindings(); + await kibanaServer.savedObjects.clean({ + types: ['cloud-security-posture-settings'], + }); + await waitForPluginInitialized(); + }); + + it('Calling Benchmark API as User with no read access to Security', async () => { + const benchmark = 'cis_aws'; + const benchmarkRules = await getCspBenchmarkRules(benchmark); + + const cspmFinding1 = getMockFinding(benchmarkRules[0], 'passed'); + + await index.addFindings([cspmFinding1]); + + const { body: benchmarksResult } = await supertestWithoutAuth + .get('/internal/cloud_security_posture/benchmarks') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_no_read_user', + cspSecurity.getPasswordForUser('role_security_no_read_user') + ); + + expect(benchmarksResult.statusCode).to.equal(403); + }); + + // Blocked by https://github.com/elastic/kibana/issues/188059 + it.skip('Calling Benchmark API as User with read access to Security', async () => { + const benchmark = 'cis_aws'; + const benchmarkRules = await getCspBenchmarkRules(benchmark); + + const cspmFinding1 = getMockFinding(benchmarkRules[0], 'passed'); + + await index.addFindings([cspmFinding1]); + + const { status } = await supertestWithoutAuth + .get('/internal/cloud_security_posture/benchmarks') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_read_user', + cspSecurity.getPasswordForUser('role_security_read_user') + ); + expect(status).to.equal(200); + }); + }); }); } diff --git a/x-pack/test/cloud_security_posture_api/routes/csp_benchmark_rules_bulk_update.ts b/x-pack/test/cloud_security_posture_api/routes/csp_benchmark_rules_bulk_update.ts index 17ce5675e1319..54ae003de8698 100644 --- a/x-pack/test/cloud_security_posture_api/routes/csp_benchmark_rules_bulk_update.ts +++ b/x-pack/test/cloud_security_posture_api/routes/csp_benchmark_rules_bulk_update.ts @@ -21,13 +21,17 @@ import type { CspBenchmarkRule } from '@kbn/cloud-security-posture-plugin/common // eslint-disable @kbn/imports/no_boundary_crossing import { generateBenchmarkRuleTags } from '@kbn/cloud-security-posture-plugin/common/utils/detection_rules'; import type { FtrProviderContext } from '../ftr_provider_context'; +import { CspSecurityCommonProvider } from './helper/user_roles_utilites'; // eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; const retry = getService('retry'); const supertest = getService('supertest'); const log = getService('log'); const kibanaServer = getService('kibanaServer'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const cspSecurity = CspSecurityCommonProvider(providerContext); const generateRuleKey = (rule: CspBenchmarkRule): string => { return `${rule.metadata.benchmark.id};${rule.metadata.benchmark.version};${rule.metadata.benchmark.rule_number}`; @@ -447,5 +451,68 @@ export default function ({ getService }: FtrProviderContext) { expect(body.error).to.eql('Bad Request'); expect(body.statusCode).to.eql(400); }); + + it('users without read privileges on cloud security should not be able to mute', async () => { + const rule1 = await getRandomCspBenchmarkRule(); + const rule2 = await getRandomCspBenchmarkRule(); + + const { status } = await supertestWithoutAuth + .post(`/internal/cloud_security_posture/rules/_bulk_action`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_no_read_user', + cspSecurity.getPasswordForUser('role_security_no_read_user') + ) + .send({ + action: 'mute', + rules: [ + { + benchmark_id: rule1.metadata.benchmark.id, + benchmark_version: rule1.metadata.benchmark.version, + rule_number: rule1.metadata.benchmark.rule_number || '', + rule_id: rule1.metadata.id, + }, + { + benchmark_id: rule2.metadata.benchmark.id, + benchmark_version: rule2.metadata.benchmark.version, + rule_number: rule2.metadata.benchmark.rule_number || '', + rule_id: rule2.metadata.id, + }, + ], + }); + expect(status).to.be(403); + }); + // Blocked by https://github.com/elastic/kibana/issues/188059 + it.skip('users with read privileges on cloud security should be able to mute', async () => { + const rule1 = await getRandomCspBenchmarkRule(); + const rule2 = await getRandomCspBenchmarkRule(); + + const { status } = await supertestWithoutAuth + .post(`/internal/cloud_security_posture/rules/_bulk_action`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx') + .auth('role_security_read_user', cspSecurity.getPasswordForUser('role_security_read_user')) + .send({ + action: 'mute', + rules: [ + { + benchmark_id: rule1.metadata.benchmark.id, + benchmark_version: rule1.metadata.benchmark.version, + rule_number: rule1.metadata.benchmark.rule_number || '', + rule_id: rule1.metadata.id, + }, + { + benchmark_id: rule2.metadata.benchmark.id, + benchmark_version: rule2.metadata.benchmark.version, + rule_number: rule2.metadata.benchmark.rule_number || '', + rule_id: rule2.metadata.id, + }, + ], + }); + expect(status).to.be(200); + }); }); } diff --git a/x-pack/test/cloud_security_posture_api/routes/csp_benchmark_rules_get_states.ts b/x-pack/test/cloud_security_posture_api/routes/csp_benchmark_rules_get_states.ts index 737013950d611..3b023ca4d352b 100644 --- a/x-pack/test/cloud_security_posture_api/routes/csp_benchmark_rules_get_states.ts +++ b/x-pack/test/cloud_security_posture_api/routes/csp_benchmark_rules_get_states.ts @@ -15,13 +15,17 @@ import { import { CSP_BENCHMARK_RULE_SAVED_OBJECT_TYPE } from '@kbn/cloud-security-posture-plugin/common/constants'; import type { CspBenchmarkRule } from '@kbn/cloud-security-posture-plugin/common/types/latest'; import type { FtrProviderContext } from '../ftr_provider_context'; +import { CspSecurityCommonProvider } from './helper/user_roles_utilites'; // eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; const retry = getService('retry'); const supertest = getService('supertest'); const log = getService('log'); const kibanaServer = getService('kibanaServer'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const cspSecurity = CspSecurityCommonProvider(providerContext); const generateRuleKey = (rule: CspBenchmarkRule): string => { return `${rule.metadata.benchmark.id};${rule.metadata.benchmark.version};${rule.metadata.benchmark.rule_number}`; @@ -160,5 +164,28 @@ export default function ({ getService }: FtrProviderContext) { expectExpect(body).toEqual({}); }); + // Blocked by https://github.com/elastic/kibana/issues/188059 + it.skip('GET rules states API with user with read access', async () => { + const { status } = await supertestWithoutAuth + .get(`/internal/cloud_security_posture/rules/_get_states?tags=CIS`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx') + .auth('role_security_read_user', cspSecurity.getPasswordForUser('role_security_read_use')); + expect(status).to.be(200); + }); + + it('GET rules states API API with user without read access', async () => { + const { status } = await supertestWithoutAuth + .get(`/internal/cloud_security_posture/rules/_get_states`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_no_read_user', + cspSecurity.getPasswordForUser('role_security_no_read_user') + ); + expect(status).to.be(403); + }); }); } diff --git a/x-pack/test/cloud_security_posture_api/routes/get_detection_engine_alerts_count_by_rule_tags.ts b/x-pack/test/cloud_security_posture_api/routes/get_detection_engine_alerts_count_by_rule_tags.ts new file mode 100644 index 0000000000000..0b2ceb882ba23 --- /dev/null +++ b/x-pack/test/cloud_security_posture_api/routes/get_detection_engine_alerts_count_by_rule_tags.ts @@ -0,0 +1,70 @@ +/* + * 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 { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; +import { CspSecurityCommonProvider } from './helper/user_roles_utilites'; + +// eslint-disable-next-line import/no-default-export +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + + const retry = getService('retry'); + const supertest = getService('supertest'); + const log = getService('log'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const cspSecurity = CspSecurityCommonProvider(providerContext); + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + describe('/internal/cloud_security_posture/detection_engine_rules/alerts/_status', () => { + describe('GET detection_engine_rules API with user that has specific access', async () => { + before(async () => { + await waitForPluginInitialized(); + }); + it('GET detection_engine_rules API with user with read access', async () => { + const { status } = await supertestWithoutAuth + .get( + '/internal/cloud_security_posture/detection_engine_rules/alerts/_status?tags=["CIS"]' + ) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_read_user_alerts', + cspSecurity.getPasswordForUser('role_security_read_user_alerts') + ); + expect(status).to.be(200); + }); + + it('GET detection_engine_rules API with user without read access', async () => { + const { status } = await supertestWithoutAuth + .get( + '/internal/cloud_security_posture/detection_engine_rules/alerts/_status?tags=["CIS"]' + ) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_no_read_user_alerts', + cspSecurity.getPasswordForUser('role_security_no_read_user_alerts') + ); + expect(status).to.be(403); + }); + }); + }); +} diff --git a/x-pack/plugins/security_solution/public/common/hooks/flyout/types.ts b/x-pack/test/cloud_security_posture_api/routes/helper/index.ts similarity index 52% rename from x-pack/plugins/security_solution/public/common/hooks/flyout/types.ts rename to x-pack/test/cloud_security_posture_api/routes/helper/index.ts index 09fd6a25cc53e..e26a8f21c9ea8 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/flyout/types.ts +++ b/x-pack/test/cloud_security_posture_api/routes/helper/index.ts @@ -5,6 +5,11 @@ * 2.0. */ -import type { ExpandedDetailType } from '../../../../common/types'; +import { CspSecurityCommonProvider } from './user_roles_utilites'; -export type FlyoutUrlState = ExpandedDetailType; +export const cloudSecurityPosturePageObjects = { + cspSecurity: CspSecurityCommonProvider, +}; +export const pageObjects = { + ...cloudSecurityPosturePageObjects, +}; diff --git a/x-pack/test/cloud_security_posture_api/routes/helper/user_roles_utilites.ts b/x-pack/test/cloud_security_posture_api/routes/helper/user_roles_utilites.ts new file mode 100644 index 0000000000000..ef66b58d28311 --- /dev/null +++ b/x-pack/test/cloud_security_posture_api/routes/helper/user_roles_utilites.ts @@ -0,0 +1,201 @@ +/* + * 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 { + LATEST_FINDINGS_INDEX_PATTERN, + BENCHMARK_SCORE_INDEX_PATTERN, + LATEST_VULNERABILITIES_INDEX_PATTERN, + ALERTS_INDEX_PATTERN, + FINDINGS_INDEX_PATTERN, +} from '@kbn/cloud-security-posture-plugin/common/constants'; +import type { FtrProviderContext } from '../../ftr_provider_context'; + +const alertsSecurityUserIndices = [ + { + names: [FINDINGS_INDEX_PATTERN], + privileges: ['read'], + }, + { + names: [LATEST_FINDINGS_INDEX_PATTERN], + privileges: ['read'], + }, + { + names: [BENCHMARK_SCORE_INDEX_PATTERN], + privileges: ['read'], + }, + { + names: [LATEST_VULNERABILITIES_INDEX_PATTERN], + privileges: ['all'], + }, + { + names: [ALERTS_INDEX_PATTERN], + privileges: ['all'], + }, +]; + +const securityUserIndinces = [ + { + names: [FINDINGS_INDEX_PATTERN], + privileges: ['read'], + }, + { + names: [LATEST_FINDINGS_INDEX_PATTERN], + privileges: ['read'], + }, + { + names: [BENCHMARK_SCORE_INDEX_PATTERN], + privileges: ['read'], + }, + { + names: [LATEST_VULNERABILITIES_INDEX_PATTERN], + privileges: ['all'], + }, +]; + +export function CspSecurityCommonProvider(providerContext: FtrProviderContext) { + const { getService } = providerContext; + const security = getService('security'); + + const roles = [ + { + name: 'role_security_no_read', + elasticsearch: { + indices: securityUserIndinces, + }, + kibana: [ + { + base: [], + feature: { + fleet: ['all'], + fleetv2: ['all'], + savedObjectsManagement: ['all'], + }, + spaces: ['*'], + }, + ], + }, + { + name: 'role_security_read', + elasticsearch: { + indices: securityUserIndinces, + }, + kibana: [ + { + base: [], + feature: { + siem: ['read'], + fleet: ['all'], + fleetv2: ['all'], + savedObjectsManagement: ['all'], + }, + spaces: ['*'], + }, + ], + }, + { + name: 'role_security_read_alerts', + elasticsearch: { + indices: alertsSecurityUserIndices, + }, + kibana: [ + { + base: [], + feature: { + siem: ['read'], + fleet: ['all'], + fleetv2: ['all'], + }, + spaces: ['*'], + }, + ], + }, + { + name: 'role_security_no_read_alerts', + elasticsearch: { + indices: alertsSecurityUserIndices, + }, + kibana: [ + { + base: [], + feature: { + fleet: ['all'], + fleetv2: ['all'], + }, + spaces: ['*'], + }, + ], + }, + ]; + + const users = [ + { + name: 'role_security_read_user', + full_name: 'security read privilege user', + password: 'test123', + roles: ['role_security_read'], + }, + { + name: 'role_security_read_user_alerts', + full_name: 'user with 0 security privilege for', + password: 'csp123', + roles: ['role_security_read_alerts'], + }, + { + name: 'role_security_no_read_user', + full_name: 'user with 0 security privilege', + password: 'csp123', + roles: ['role_security_no_read'], + }, + { + name: 'role_security_no_read_user_alerts', + full_name: 'user with 0 security privilege for', + password: 'csp123', + roles: ['role_security_no_read_alerts'], + }, + ]; + + return { + async createRoles() { + for (const role of roles) { + await security.role.create(role.name, { + elasticsearch: role.elasticsearch, + kibana: role.kibana, + }); + } + }, + + async createUsers() { + for (const user of users) { + await security.user.create(user.name, { + password: user.password, + roles: user.roles, + full_name: user.full_name, + }); + } + }, + + async cleanRoles() { + for (const role of roles) { + await security.role.delete(role.name); + } + }, + + async cleanUsers() { + for (const user of users) { + await security.user.delete(user.name); + } + }, + + getPasswordForUser(user: string): string { + const userConfig = users.find((u) => u.name === user); + if (userConfig === undefined) { + throw new Error(`Can't log in user ${user} - not defined`); + } + return userConfig.password; + }, + }; +} diff --git a/x-pack/test/cloud_security_posture_api/routes/index.ts b/x-pack/test/cloud_security_posture_api/routes/index.ts new file mode 100644 index 0000000000000..adacc80fb6f7d --- /dev/null +++ b/x-pack/test/cloud_security_posture_api/routes/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; +import { CspSecurityCommonProvider } from './helper/user_roles_utilites'; + +// eslint-disable-next-line import/no-default-export +export default function (providerContext: FtrProviderContext) { + const { loadTestFile } = providerContext; + const cspSecurity = CspSecurityCommonProvider(providerContext); + describe('Cloud Security Posture', function () { + before(async () => { + await cspSecurity.createRoles(); + await cspSecurity.createUsers(); + }); + + loadTestFile(require.resolve('../telemetry/telemetry.ts')); + loadTestFile(require.resolve('./vulnerabilities_dashboard.ts')); + loadTestFile(require.resolve('./stats.ts')); + loadTestFile(require.resolve('./csp_benchmark_rules_bulk_update.ts')); + loadTestFile(require.resolve('./csp_benchmark_rules_get_states.ts')); + loadTestFile(require.resolve('./benchmarks.ts')); + loadTestFile(require.resolve('./status.ts')); + loadTestFile(require.resolve('./get_detection_engine_alerts_count_by_rule_tags')); + }); +} diff --git a/x-pack/test/cloud_security_posture_api/routes/stats.ts b/x-pack/test/cloud_security_posture_api/routes/stats.ts index f290c24a645fc..adb2952d5388e 100644 --- a/x-pack/test/cloud_security_posture_api/routes/stats.ts +++ b/x-pack/test/cloud_security_posture_api/routes/stats.ts @@ -26,6 +26,7 @@ import { cspmComplianceDashboardDataMockV2, } from './mocks/benchmark_score_mock'; import { findingsMockData } from './mocks/findings_mock'; +import { CspSecurityCommonProvider } from './helper/user_roles_utilites'; const removeRealtimeCalculatedFields = (trends: PostureTrend[]) => { return trends.map((trend: PostureTrend) => { @@ -61,6 +62,8 @@ export default function (providerContext: FtrProviderContext) { const es = getService('es'); const supertest = getService('supertest'); const log = getService('log'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const cspSecurity = CspSecurityCommonProvider(providerContext); /** * required before indexing findings @@ -295,5 +298,75 @@ export default function (providerContext: FtrProviderContext) { }).to.eql(kspmComplianceDashboardDataMockV2); }); }); + + describe('GET stats API with user that has specific access', async () => { + beforeEach(async () => { + await index.removeFindings(); + await index.removeScores(); + + await waitForPluginInitialized(); + }); + it('GET stats API V1 with user with read access', async () => { + await index.addScores(getBenchmarkScoreMockData('cspm', true)); + await index.addScores(getBenchmarkScoreMockData('cspm', false)); + await index.addFindings([findingsMockData[1]]); + + const { status } = await supertestWithoutAuth + .get(`/internal/cloud_security_posture/stats/cspm`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_read_user', + cspSecurity.getPasswordForUser('role_security_read_user') + ); + expect(status).to.be(200); + }); + it('GET stats API V1 with user with read access', async () => { + await index.addScores(getBenchmarkScoreMockData('cspm', true)); + await index.addScores(getBenchmarkScoreMockData('cspm', false)); + await index.addFindings([findingsMockData[1]]); + + const { status } = await supertestWithoutAuth + .get(`/internal/cloud_security_posture/stats/cspm`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_read_user', + cspSecurity.getPasswordForUser('role_security_read_user') + ); + expect(status).to.be(200); + }); + it('GET stats API V2 with user with read access', async () => { + await index.addScores(getBenchmarkScoreMockData('cspm', true)); + await index.addScores(getBenchmarkScoreMockData('cspm', false)); + await index.addFindings([findingsMockData[1]]); + + const { status } = await supertestWithoutAuth + .get(`/internal/cloud_security_posture/stats/cspm`) + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_read_user', + cspSecurity.getPasswordForUser('role_security_read_user') + ); + expect(status).to.be(200); + }); + + it('GET stats API V2 with user without read access', async () => { + await index.addScores(getBenchmarkScoreMockData('kspm', true)); + await index.addScores(getBenchmarkScoreMockData('kspm', false)); + await index.addFindings([findingsMockData[0]]); + + const { status } = await supertestWithoutAuth + .get(`/internal/cloud_security_posture/stats/kspm`) + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_no_read_user', + cspSecurity.getPasswordForUser('role_security_no_read_user') + ); + expect(status).to.be(403); + }); + }); }); } diff --git a/x-pack/test/cloud_security_posture_api/routes/status.ts b/x-pack/test/cloud_security_posture_api/routes/status.ts new file mode 100644 index 0000000000000..0004144575956 --- /dev/null +++ b/x-pack/test/cloud_security_posture_api/routes/status.ts @@ -0,0 +1,66 @@ +/* + * 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 { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; +import { CspSecurityCommonProvider } from './helper/user_roles_utilites'; + +// eslint-disable-next-line import/no-default-export +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + + const retry = getService('retry'); + const supertest = getService('supertest'); + const log = getService('log'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const cspSecurity = CspSecurityCommonProvider(providerContext); + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + describe('GET /internal/cloud_security_posture/status', () => { + describe('GET status API with user that has specific access', async () => { + before(async () => { + await waitForPluginInitialized(); + }); + it('GET stats API with user with read access', async () => { + const { status } = await supertestWithoutAuth + .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_read_user', + cspSecurity.getPasswordForUser('role_security_read_user') + ); + expect(status).to.be(200); + }); + + it('GET status API with user without read access', async () => { + const { status } = await supertestWithoutAuth + .get(`/internal/cloud_security_posture/status`) + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_no_read_user', + cspSecurity.getPasswordForUser('role_security_no_read_user') + ); + expect(status).to.be(403); + }); + }); + }); +} diff --git a/x-pack/test/cloud_security_posture_api/routes/vulnerabilities_dashboard.ts b/x-pack/test/cloud_security_posture_api/routes/vulnerabilities_dashboard.ts index 8f2a2fa2693d4..8ed4f91a61fea 100644 --- a/x-pack/test/cloud_security_posture_api/routes/vulnerabilities_dashboard.ts +++ b/x-pack/test/cloud_security_posture_api/routes/vulnerabilities_dashboard.ts @@ -13,6 +13,7 @@ import { vulnerabilitiesLatestMock, scoresVulnerabilitiesMock, } from './mocks/vulnerabilities_latest_mock'; +import { CspSecurityCommonProvider } from './helper/user_roles_utilites'; export interface CnvmStatistics { criticalCount?: number; @@ -106,11 +107,14 @@ const removeRealtimeCalculatedFields = ( }; // eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; const retry = getService('retry'); const es = getService('es'); const supertest = getService('supertest'); const log = getService('log'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const cspSecurity = CspSecurityCommonProvider(providerContext); /** * required before indexing findings @@ -304,5 +308,28 @@ export default function ({ getService }: FtrProviderContext) { .set(ELASTIC_HTTP_VERSION_HEADER, '1') .expect(500); }); + + it('GET vulnerabilities dashboard API with users with read access to cloud security posture', async () => { + const { status } = await supertestWithoutAuth + .get('/internal/cloud_security_posture/vulnerabilities_dashboard') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set('kbn-xsrf', 'xxxx') + .auth('role_security_read_user', cspSecurity.getPasswordForUser('role_security_read_user')); + + expect(status).to.be(200); + }); + + it('GET vulnerabilities dashboard API with users without read access to cloud security posture', async () => { + const { status } = await supertestWithoutAuth + .get('/internal/cloud_security_posture/vulnerabilities_dashboard') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .set('kbn-xsrf', 'xxxx') + .auth( + 'role_security_no_read_user', + cspSecurity.getPasswordForUser('role_security_no_read_user') + ); + + expect(status).to.be(403); + }); }); } diff --git a/x-pack/test/common/services/security_solution/endpoint_registry_helpers.ts b/x-pack/test/common/services/security_solution/endpoint_registry_helpers.ts index 0866042696eed..985d49653b262 100644 --- a/x-pack/test/common/services/security_solution/endpoint_registry_helpers.ts +++ b/x-pack/test/common/services/security_solution/endpoint_registry_helpers.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import path from 'path'; import { defineDockerServersConfig } from '@kbn/test'; diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 1681d397f932b..7751359ac30fd 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -555,6 +555,39 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); }); + + it('should not allow adding more than 20 custom metrics', async () => { + // open + await pageObjects.infraHome.clickCustomMetricDropdown(); + + const fields = [ + 'process.cpu.pct', + 'process.memory.pct', + 'system.core.total.pct', + 'system.core.user.pct', + 'system.core.nice.pct', + 'system.core.idle.pct', + 'system.core.iowait.pct', + 'system.core.irq.pct', + 'system.core.softirq.pct', + 'system.core.steal.pct', + 'system.cpu.nice.pct', + 'system.cpu.idle.pct', + 'system.cpu.iowait.pct', + 'system.cpu.irq.pct', + ]; + + for (const field of fields) { + await pageObjects.infraHome.addCustomMetric(field); + } + const metricsCount = await pageObjects.infraHome.getMetricsContextMenuItemsCount(); + // there are 6 default metrics in the context menu for hosts + expect(metricsCount).to.eql(20); + + await pageObjects.infraHome.ensureCustomMetricAddButtonIsDisabled(); + // close + await pageObjects.infraHome.clickCustomMetricDropdown(); + }); }); describe('alerts flyouts', () => { diff --git a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts index 24fac3f726bc9..e5a2a8f6a8971 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts @@ -28,7 +28,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const dashboardAddPanel = getService('dashboardAddPanel'); const filterBar = getService('filterBar'); - describe('Dashboard to TSVB to Lens', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/179307 + describe.skip('Dashboard to TSVB to Lens', function describeIndexTests() { before(async () => { await visualize.initTests(); }); diff --git a/x-pack/test/functional/page_objects/infra_home_page.ts b/x-pack/test/functional/page_objects/infra_home_page.ts index e67c19a4834c8..07efff0fc4d72 100644 --- a/x-pack/test/functional/page_objects/infra_home_page.ts +++ b/x-pack/test/functional/page_objects/infra_home_page.ts @@ -482,5 +482,27 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide async clickCloseFlyoutButton() { return testSubjects.click('euiFlyoutCloseButton'); }, + + async clickCustomMetricDropdown() { + await testSubjects.click('infraInventoryMetricDropdown'); + }, + + async addCustomMetric(field: string) { + await testSubjects.click('infraModeSwitcherAddMetricButton'); + const groupByCustomField = await testSubjects.find('infraCustomMetricFieldSelect'); + await comboBox.setElement(groupByCustomField, field); + await testSubjects.click('infraCustomMetricFormSaveButton'); + }, + + async getMetricsContextMenuItemsCount() { + const contextMenu = await testSubjects.find('infraInventoryMetricsContextMenu'); + const menuItems = await contextMenu.findAllByCssSelector('button.euiContextMenuItem'); + return menuItems.length; + }, + + async ensureCustomMetricAddButtonIsDisabled() { + const button = await testSubjects.find('infraModeSwitcherAddMetricButton'); + expect(await button.getAttribute('disabled')).to.be('true'); + }, }; } diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/config.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.edr_workflows.trial.ts similarity index 78% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/config.ts rename to x-pack/test/security_solution_api_integration/config/ess/config.base.edr_workflows.trial.ts index c46c94d91cf6b..b2ca24d142675 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/config.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.edr_workflows.trial.ts @@ -6,12 +6,12 @@ */ import { FtrConfigProviderContext } from '@kbn/test'; -import { generateConfig } from './config.base'; -import { services } from '../services'; +import { generateConfig } from './config.base.edr_workflows'; +import { services } from './services_edr_workflows'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile( - require.resolve('../../../../api_integration/config.ts') + require.resolve('../../../api_integration/config.ts') ); return generateConfig({ diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.edr_workflows.ts similarity index 88% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/config.base.ts rename to x-pack/test/security_solution_api_integration/config/ess/config.base.edr_workflows.ts index 08adb40b48646..00f4d282ac6ed 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.edr_workflows.ts @@ -6,8 +6,8 @@ */ import { Config } from '@kbn/test'; -import { SecuritySolutionEndpointRegistryHelpers } from '../../../../common/services/security_solution'; -import { SUITE_TAGS } from '../../../../security_solution_endpoint/configs/config.base'; +import { SecuritySolutionEndpointRegistryHelpers } from '../../../common/services/security_solution'; +import { SUITE_TAGS } from '../../../security_solution_endpoint/configs/config.base'; export const generateConfig = async ({ baseConfig, @@ -27,7 +27,6 @@ export const generateConfig = async ({ SecuritySolutionEndpointRegistryHelpers(); return { ...baseConfig.getAll(), - testFiles: [require.resolve('../apis')], dockerServers: createEndpointDockerConfig(), services, junit: { @@ -54,7 +53,7 @@ export const generateConfig = async ({ ...baseConfig.get('mochaOpts'), grep: target === 'serverless' - ? '/^(?!.*@(skipInServerless|brokenInServerless)).*@serverless/' + ? '/^(?!.*(^|\\s)@skipInServerless(\\s|$)).*@serverless.*/' : '/^(?!.*@skipInEss).*@ess.*/', }, }; diff --git a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts index a47e43bd426e6..49374c86daefa 100644 --- a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts @@ -7,12 +7,13 @@ import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext, kbnTestConfig, kibanaTestUser } from '@kbn/test'; -import { services } from './services'; +import { services as baseServices } from './services'; import { PRECONFIGURED_ACTION_CONNECTORS } from '../shared'; interface CreateTestConfigOptions { license: string; ssl?: boolean; + services?: any; } // test.not-enabled is specifically not enabled @@ -33,7 +34,7 @@ const enabledActionTypes = [ ]; export function createTestConfig(options: CreateTestConfigOptions, testFiles?: string[]) { - const { license = 'trial', ssl = false } = options; + const { license = 'trial', ssl = false, services = baseServices } = options; return async ({ readConfigFile }: FtrConfigProviderContext) => { const xPackApiIntegrationTestsConfig = await readConfigFile( diff --git a/x-pack/test/security_solution_api_integration/config/ess/services_edr_workflows.ts b/x-pack/test/security_solution_api_integration/config/ess/services_edr_workflows.ts new file mode 100644 index 0000000000000..eba99b3d6f6f5 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/config/ess/services_edr_workflows.ts @@ -0,0 +1,29 @@ +/* + * 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 { EndpointTestResources } from '../../../security_solution_endpoint/services/endpoint'; +import { EndpointArtifactsTestResources } from '../../../security_solution_endpoint/services/endpoint_artifacts'; +import { EndpointPolicyTestResourcesProvider } from '../../../security_solution_endpoint/services/endpoint_policy'; + +import { services as xPackAPIServices } from '../../../api_integration/services'; +import { ResolverGeneratorProvider } from '../services/security_solution_edr_workflows_resolver'; +import { RolesUsersProvider } from '../services/security_solution_edr_workflows_roles_users'; +import { + SecuritySolutionEndpointDataStreamHelpers, + SecuritySolutionEndpointRegistryHelpers, +} from '../../../common/services/security_solution'; + +export const services = { + ...xPackAPIServices, + resolverGenerator: ResolverGeneratorProvider, + endpointTestResources: EndpointTestResources, + endpointPolicyTestResources: EndpointPolicyTestResourcesProvider, + endpointArtifactTestResources: EndpointArtifactsTestResources, + rolesUsersProvider: RolesUsersProvider, + endpointDataStreamHelpers: SecuritySolutionEndpointDataStreamHelpers, + endpointRegistryHelpers: SecuritySolutionEndpointRegistryHelpers, +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/config/serverless/config.base.edr_workflows.ts similarity index 78% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/serverless.config.ts rename to x-pack/test/security_solution_api_integration/config/serverless/config.base.edr_workflows.ts index 693987c070cd8..c6050198b44c0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/serverless.config.ts +++ b/x-pack/test/security_solution_api_integration/config/serverless/config.base.edr_workflows.ts @@ -4,14 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { FtrConfigProviderContext } from '@kbn/test'; -import { generateConfig } from './config.base'; -import { svlServices } from '../services'; +import { svlServices } from './services_edr_workflows'; +import { generateConfig } from '../ess/config.base.edr_workflows'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const serverlessTestsConfig = await readConfigFile( - require.resolve('../../../../../test_serverless/shared/config.base.ts') + require.resolve('../../../../test_serverless/shared/config.base.ts') ); return generateConfig({ diff --git a/x-pack/test/security_solution_api_integration/config/serverless/services_edr_workflows.ts b/x-pack/test/security_solution_api_integration/config/serverless/services_edr_workflows.ts new file mode 100644 index 0000000000000..9123c039c25cc --- /dev/null +++ b/x-pack/test/security_solution_api_integration/config/serverless/services_edr_workflows.ts @@ -0,0 +1,19 @@ +/* + * 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 { + KibanaSupertestWithCertProvider, + KibanaSupertestWithCertWithoutAuthProvider, +} from '../../../security_solution_endpoint/services/supertest_with_cert'; +import { services as essServices } from '../ess/services_edr_workflows'; + +export const svlServices = { + ...essServices, + + supertest: KibanaSupertestWithCertProvider, + supertestWithoutAuth: KibanaSupertestWithCertWithoutAuthProvider, +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/metadata.fixtures.ts b/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_metadata.ts similarity index 100% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/metadata.fixtures.ts rename to x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_metadata.ts diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/services/resolver.ts b/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_resolver.ts similarity index 98% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/services/resolver.ts rename to x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_resolver.ts index dd97681aff7e2..b9eed4dec9fae 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/services/resolver.ts +++ b/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_resolver.ts @@ -12,7 +12,7 @@ import { Event, } from '@kbn/security-solution-plugin/common/endpoint/generate_data'; import { firstNonNullValue } from '@kbn/security-solution-plugin/common/endpoint/models/ecs_safety_helpers'; -import { FtrProviderContext } from '../configs/ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context_edr_workflows'; export const processEventsIndex = 'logs-endpoint.events.process-default'; diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/services/roles_users.ts b/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_roles_users.ts similarity index 96% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/services/roles_users.ts rename to x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_roles_users.ts index 00d3de63f237c..f364943164322 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/services/roles_users.ts +++ b/x-pack/test/security_solution_api_integration/config/services/security_solution_edr_workflows_roles_users.ts @@ -11,7 +11,7 @@ import { getAllEndpointSecurityRoles, } from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users'; -import { FtrProviderContext } from '../configs/ftr_provider_context'; +import { FtrProviderContext } from '../../ftr_provider_context_edr_workflows'; export const ROLE = ENDPOINT_SECURITY_ROLE_NAMES; diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/ftr_provider_context.d.ts b/x-pack/test/security_solution_api_integration/ftr_provider_context_edr_workflows.d.ts similarity index 85% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/ftr_provider_context.d.ts rename to x-pack/test/security_solution_api_integration/ftr_provider_context_edr_workflows.d.ts index 16c2f01eb1d72..4104343008a4d 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/configs/ftr_provider_context.d.ts +++ b/x-pack/test/security_solution_api_integration/ftr_provider_context_edr_workflows.d.ts @@ -7,6 +7,5 @@ import { GenericFtrProviderContext } from '@kbn/test'; -import { services } from '../services'; - +import { services } from './config/ess/services_edr_workflows'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/security_solution_api_integration/package.json b/x-pack/test/security_solution_api_integration/package.json index ffba287fb07cd..4ec0553f981c8 100644 --- a/x-pack/test/security_solution_api_integration/package.json +++ b/x-pack/test/security_solution_api_integration/package.json @@ -20,6 +20,7 @@ "initialize-server:dr": "node ./scripts/index.js server detections_response trial_license_complete_tier", "run-tests:dr": "node ./scripts/index.js runner detections_response trial_license_complete_tier", + "initialize-server:de": "node ./scripts/index.js server detections_response/detection_engine trial_license_complete_tier", "run-tests:de": "node ./scripts/index.js runner detections_response/detection_engine trial_license_complete_tier", @@ -35,6 +36,9 @@ "initialize-server:lists:complete": "node ./scripts/index.js server lists_and_exception_lists trial_license_complete_tier", "run-tests:lists:complete": "node ./scripts/index.js runner lists_and_exception_lists trial_license_complete_tier", + "initialize-server:edr-workflows": "node ./scripts/index.js server edr_workflows trial_license_complete_tier", + "run-tests:edr-workflows": "node ./scripts/index.js runner edr_workflows trial_license_complete_tier", + "initialize-server:investigations": "node scripts/index.js server investigation trial_license_complete_tier", "run-tests:investigations": "node scripts/index.js runner investigation trial_license_complete_tier", @@ -72,6 +76,55 @@ "entity_analytics:essentials:server:ess": "npm run initialize-server:ea:basic_essentials risk_engine ess", "entity_analytics:essentials:runner:ess": "npm run run-tests:ea:basic_essentials risk_engine ess essEnv", + "edr_workflows:artifacts:server:serverless": "npm run initialize-server:edr-workflows artifacts serverless", + "edr_workflows:artifacts:runner:serverless": "npm run run-tests:edr-workflows artifacts serverless serverlessEnv", + "edr_workflows:artifacts:qa:serverless": "npm run run-tests:edr-workflows artifacts serverless qaPeriodicEnv", + "edr_workflows:artifacts:qa:serverless:release": "npm run run-tests:edr-workflows artifacts serverless qaEnv", + "edr_workflows:artifacts:server:ess": "npm run initialize-server:edr-workflows artifacts ess", + "edr_workflows:artifacts:runner:ess": "npm run run-tests:edr-workflows artifacts ess essEnv", + + "edr_workflows:authentication:server:serverless": "npm run initialize-server:edr-workflows authentication serverless", + "edr_workflows:authentication:runner:serverless": "npm run run-tests:edr-workflows authentication serverless serverlessEnv", + "edr_workflows:authentication:qa:serverless": "npm run run-tests:edr-workflows authentication serverless qaPeriodicEnv", + "edr_workflows:authentication:qa:serverless:release": "npm run run-tests:edr-workflows authentication serverless qaEnv", + "edr_workflows:authentication:server:ess": "npm run initialize-server:edr-workflows authentication ess", + "edr_workflows:authentication:runner:ess": "npm run run-tests:edr-workflows authentication ess essEnv", + + "edr_workflows:metadata:server:serverless": "npm run initialize-server:edr-workflows metadata serverless", + "edr_workflows:metadata:runner:serverless": "npm run run-tests:edr-workflows metadata serverless serverlessEnv", + "edr_workflows:metadata:qa:serverless": "npm run run-tests:edr-workflows metadata serverless qaPeriodicEnv", + "edr_workflows:metadata:qa:serverless:release": "npm run run-tests:edr-workflows metadata serverless qaEnv", + "edr_workflows:metadata:server:ess": "npm run initialize-server:edr-workflows metadata ess", + "edr_workflows:metadata:runner:ess": "npm run run-tests:edr-workflows metadata ess essEnv", + + "edr_workflows:package:server:serverless": "npm run initialize-server:edr-workflows package serverless", + "edr_workflows:package:runner:serverless": "npm run run-tests:edr-workflows package serverless serverlessEnv", + "edr_workflows:package:qa:serverless": "npm run run-tests:edr-workflows package serverless qaPeriodicEnv", + "edr_workflows:package:qa:serverless:release": "npm run run-tests:edr-workflows package serverless qaEnv", + "edr_workflows:package:server:ess": "npm run initialize-server:edr-workflows package ess", + "edr_workflows:package:runner:ess": "npm run run-tests:edr-workflows package ess essEnv", + + "edr_workflows:policy_response:server:serverless": "npm run initialize-server:edr-workflows policy_response serverless", + "edr_workflows:policy_response:runner:serverless": "npm run run-tests:edr-workflows policy_response serverless serverlessEnv", + "edr_workflows:policy_response:qa:serverless": "npm run run-tests:edr-workflows policy_response serverless qaPeriodicEnv", + "edr_workflows:policy_response:qa:serverless:release": "npm run run-tests:edr-workflows policy_response serverless qaEnv", + "edr_workflows:policy_response:server:ess": "npm run initialize-server:edr-workflows policy_response ess", + "edr_workflows:policy_response:runner:ess": "npm run run-tests:edr-workflows policy_response ess essEnv", + + "edr_workflows:resolver:server:serverless": "npm run initialize-server:edr-workflows resolver serverless", + "edr_workflows:resolver:runner:serverless": "npm run run-tests:edr-workflows resolver serverless serverlessEnv", + "edr_workflows:resolver:qa:serverless": "npm run run-tests:edr-workflows resolver serverless qaPeriodicEnv", + "edr_workflows:resolver:qa:serverless:release": "npm run run-tests:edr-workflows resolver serverless qaEnv", + "edr_workflows:resolver:server:ess": "npm run initialize-server:edr-workflows resolver ess", + "edr_workflows:resolver:runner:ess": "npm run run-tests:edr-workflows resolver ess essEnv", + + "edr_workflows:response_actions:server:serverless": "npm run initialize-server:edr-workflows response_actions serverless", + "edr_workflows:response_actions:runner:serverless": "npm run run-tests:edr-workflows response_actions serverless serverlessEnv", + "edr_workflows:response_actions:qa:serverless": "npm run run-tests:edr-workflows response_actions serverless qaPeriodicEnv", + "edr_workflows:response_actions:qa:serverless:release": "npm run run-tests:edr-workflows response_actions serverless qaEnv", + "edr_workflows:response_actions:server:ess": "npm run initialize-server:edr-workflows response_actions ess", + "edr_workflows:response_actions:runner:ess": "npm run run-tests:edr-workflows response_actions ess essEnv", + "exception_lists_items:server:serverless": "npm run initialize-server:lists:complete exception_lists_items serverless", "exception_lists_items:runner:serverless": "npm run run-tests:lists:complete exception_lists_items serverless serverlessEnv", "exception_lists_items:qa:serverless": "npm run run-tests:lists:complete exception_lists_items serverless qaPeriodicEnv", diff --git a/x-pack/test/security_solution_api_integration/scripts/index.js b/x-pack/test/security_solution_api_integration/scripts/index.js index c3f4637d2b137..86a0e4fc53cc2 100644 --- a/x-pack/test/security_solution_api_integration/scripts/index.js +++ b/x-pack/test/security_solution_api_integration/scripts/index.js @@ -36,7 +36,7 @@ let grepArgs = []; if (type !== 'server') { switch (environment) { case 'serverlessEnv': - grepArgs = ['--grep', '/^(?!.*@skipInServerless).*@serverless.*/']; + grepArgs = ['--grep', '/^(?!.*(^|\\s)@skipInServerless(\\s|$)).*@serverless.*/']; break; case 'essEnv': diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts index 5d73249e576f4..be6464baff393 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning.ts @@ -40,12 +40,12 @@ import { importFile, } from '../../../../../lists_and_exception_lists/utils'; import { - executeSetupModuleRequest, forceStartDatafeeds, getAlerts, getPreviewAlerts, previewRule, previewRuleWithExceptionEntries, + setupMlModulesWithRetry, } from '../../../../utils'; import { createRule, @@ -86,13 +86,12 @@ export default ({ getService }: FtrProviderContext) => { rule_id: 'ml-rule-id', }; - // FLAKY: https://github.com/elastic/kibana/issues/171426 - describe.skip('@ess @serverless @serverlessQA Machine learning type rules', () => { + describe('@ess @serverless @serverlessQA Machine learning type rules', () => { before(async () => { // Order is critical here: auditbeat data must be loaded before attempting to start the ML job, // as the job looks for certain indices on start await esArchiver.load(auditPath); - await executeSetupModuleRequest({ module: siemModule, rspCode: 200, supertest }); + await setupMlModulesWithRetry({ module: siemModule, supertest, retry }); await forceStartDatafeeds({ jobId: mlJobId, rspCode: 200, supertest }); await esArchiver.load('x-pack/test/functional/es_archives/security_solution/anomalies'); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning_alert_suppression.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning_alert_suppression.ts index dc26aa96348a6..d8869681de692 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning_alert_suppression.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/machine_learning_alert_suppression.ts @@ -27,7 +27,6 @@ import { EsArchivePathBuilder } from '../../../../../../es_archive_path_builder' import { FtrProviderContext } from '../../../../../../ftr_provider_context'; import { dataGeneratorFactory, - executeSetupModuleRequest, forceStartDatafeeds, getAlerts, getOpenAlerts, @@ -36,6 +35,7 @@ import { previewRule, previewRuleWithExceptionEntries, setAlertStatus, + setupMlModulesWithRetry, } from '../../../../utils'; import { createRule, @@ -51,6 +51,7 @@ export default ({ getService }: FtrProviderContext) => { const es = getService('es'); const log = getService('log'); const config = getService('config'); + const retry = getService('retry'); const isServerless = config.get('serverless'); const dataPathBuilder = new EsArchivePathBuilder(isServerless); @@ -93,7 +94,7 @@ export default ({ getService }: FtrProviderContext) => { // Order is critical here: auditbeat data must be loaded before attempting to start the ML job, // as the job looks for certain indices on start await esArchiver.load(auditbeatArchivePath); - await executeSetupModuleRequest({ module: mlModuleName, rspCode: 200, supertest }); + await setupMlModulesWithRetry({ module: mlModuleName, retry, supertest }); await forceStartDatafeeds({ jobId: mlJobId, rspCode: 200, supertest }); await esArchiver.load('x-pack/test/functional/es_archives/security_solution/anomalies'); await deleteAllAnomalies(log, es); @@ -112,8 +113,7 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllAnomalies(log, es); }); - // FLAKY: https://github.com/elastic/kibana/issues/187478 - describe.skip('with per-execution suppression duration', () => { + describe('with per-execution suppression duration', () => { beforeEach(() => { ruleProps = { ...baseRuleProps, @@ -245,8 +245,7 @@ export default ({ getService }: FtrProviderContext) => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/187614 - describe.skip('with interval suppression duration', () => { + describe('with interval suppression duration', () => { beforeEach(() => { ruleProps = { ...baseRuleProps, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/machine_learning/machine_learning_setup.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/machine_learning/machine_learning_setup.ts index fa0c6fa4f78b5..bd8214e63e4d1 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/machine_learning/machine_learning_setup.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/machine_learning/machine_learning_setup.ts @@ -6,9 +6,18 @@ */ import type SuperTest from 'supertest'; +import { RetryService } from '@kbn/ftr-common-functional-services'; import { ML_GROUP_ID } from '@kbn/security-solution-plugin/common/constants'; import { getCommonRequestHeader } from '../../../../../functional/services/ml/common_api'; +interface ModuleJob { + id: string; + success: boolean; + error?: { + status: number; + }; +} + export const executeSetupModuleRequest = async ({ module, rspCode, @@ -17,7 +26,7 @@ export const executeSetupModuleRequest = async ({ module: string; rspCode: number; supertest: SuperTest.Agent; -}) => { +}): Promise<{ jobs: ModuleJob[] }> => { const { body } = await supertest .post(`/internal/ml/modules/setup/${module}`) .set(getCommonRequestHeader('1')) @@ -34,6 +43,35 @@ export const executeSetupModuleRequest = async ({ return body; }; +export const setupMlModulesWithRetry = async ({ + module, + retry, + supertest, +}: { + module: string; + retry: RetryService; + supertest: SuperTest.Agent; +}) => + retry.try(async () => { + const response = await executeSetupModuleRequest({ + module, + rspCode: 200, + supertest, + }); + + const allJobsSucceeded = response?.jobs.every((job) => { + return job.success || (job.error?.status && job.error.status < 500); + }); + + if (!allJobsSucceeded) { + throw new Error( + `Expected all jobs to set up successfully, but got ${JSON.stringify(response)}` + ); + } + + return response; + }); + export const forceStartDatafeeds = async ({ jobId, rspCode, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_simple_preview_rule.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_simple_preview_rule.ts index 48e687316dbf5..0cfcbb0cb6039 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_simple_preview_rule.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/get_simple_preview_rule.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { PreviewRulesSchema } from '@kbn/security-solution-plugin/common/api/detection_engine'; +import type { RulePreviewRequestBody } from '@kbn/security-solution-plugin/common/api/detection_engine'; /** * This is a typical simple preview rule for testing that is easy for most basic testing @@ -16,7 +16,7 @@ import type { PreviewRulesSchema } from '@kbn/security-solution-plugin/common/ap export const getSimplePreviewRule = ( ruleId = 'preview-rule-1', invocationCount = 12 -): PreviewRulesSchema => ({ +): RulePreviewRequestBody => ({ name: 'Simple Rule Query', description: 'Simple Rule Query', risk_score: 1, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts index a939454e72c9f..a601edde98168 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts @@ -8,7 +8,7 @@ import type SuperTest from 'supertest'; import type { RuleCreateProps, - PreviewRulesSchema, + RulePreviewRequestBody, RulePreviewLogs, } from '@kbn/security-solution-plugin/common/api/detection_engine'; @@ -36,7 +36,7 @@ export const previewRule = async ({ logs: RulePreviewLogs[]; isAborted: boolean; }> => { - const previewRequest: PreviewRulesSchema = { + const previewRequest: RulePreviewRequestBody = { ...rule, invocationCount, timeframeEnd: timeframeEnd.toISOString(), diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_artifacts/blocklists.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/blocklists.ts similarity index 95% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_artifacts/blocklists.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/blocklists.ts index 99b92012eb12e..10cd447755c25 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_artifacts/blocklists.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/blocklists.ts @@ -13,10 +13,10 @@ import { GLOBAL_ARTIFACT_TAG, } from '@kbn/security-solution-plugin/common/endpoint/service/artifacts'; import { ExceptionsListItemGenerator } from '@kbn/security-solution-plugin/common/endpoint/data_generators/exceptions_list_item_generator'; -import { FtrProviderContext } from '../../configs/ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; import { PolicyTestResourceInfo } from '../../../../../security_solution_endpoint/services/endpoint_policy'; import { ArtifactTestData } from '../../../../../security_solution_endpoint/services/endpoint_artifacts'; -import { ROLE } from '../../services/roles_users'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -24,7 +24,9 @@ export default function ({ getService }: FtrProviderContext) { const endpointPolicyTestResources = getService('endpointPolicyTestResources'); const endpointArtifactTestResources = getService('endpointArtifactTestResources'); - describe('@ess @serverless Endpoint artifacts (via lists plugin): Blocklists', function () { + // @skipInServerlessMKI due to authentication issues - we should migrate from Basic to Bearer token when available + // @skipInServerlessMKI - if you are removing this annotation, make sure to add the test suite to the MKI pipeline in .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml + describe('@ess @serverless @skipInServerlessMKI Endpoint artifacts (via lists plugin): Blocklists', function () { let fleetEndpointPolicy: PolicyTestResourceInfo; before(async () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/configs/ess.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/configs/ess.config.ts new file mode 100644 index 0000000000000..df039b9c8ebe5 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/configs/ess.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/ess/config.base.edr_workflows.trial') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Artifacts Integration Tests - ESS Env - Trial License', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/configs/serverless.config.ts new file mode 100644 index 0000000000000..7b5182211666d --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/configs/serverless.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/serverless/config.base.edr_workflows') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Artifacts Integration Tests - Serverless Env - Complete', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_artifacts/event_filters.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/event_filters.ts similarity index 94% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_artifacts/event_filters.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/event_filters.ts index 59bdc19c36f65..d49af143a7e35 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_artifacts/event_filters.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/event_filters.ts @@ -14,10 +14,10 @@ import { getImportExceptionsListSchemaMock, toNdJsonString, } from '@kbn/lists-plugin/common/schemas/request/import_exceptions_schema.mock'; -import { FtrProviderContext } from '../../configs/ftr_provider_context'; import { PolicyTestResourceInfo } from '../../../../../security_solution_endpoint/services/endpoint_policy'; import { ArtifactTestData } from '../../../../../security_solution_endpoint/services/endpoint_artifacts'; -import { ROLE } from '../../services/roles_users'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -25,7 +25,9 @@ export default function ({ getService }: FtrProviderContext) { const endpointPolicyTestResources = getService('endpointPolicyTestResources'); const endpointArtifactTestResources = getService('endpointArtifactTestResources'); - describe('@ess @serverless Endpoint artifacts (via lists plugin): Event Filters', function () { + // @skipInServerlessMKI due to authentication issues - we should migrate from Basic to Bearer token when available + // @skipInServerlessMKI - if you are removing this annotation, make sure to add the test suite to the MKI pipeline in .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml + describe('@ess @serverless @skipInServerlessMKI Endpoint artifacts (via lists plugin): Event Filters', function () { let fleetEndpointPolicy: PolicyTestResourceInfo; before(async () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_artifacts/host_isolation_exceptions.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/host_isolation_exceptions.ts similarity index 95% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_artifacts/host_isolation_exceptions.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/host_isolation_exceptions.ts index e58ffb299c1e2..df6b9450e4686 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_artifacts/host_isolation_exceptions.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/host_isolation_exceptions.ts @@ -17,10 +17,10 @@ import { toNdJsonString, } from '@kbn/lists-plugin/common/schemas/request/import_exceptions_schema.mock'; import { ExceptionsListItemGenerator } from '@kbn/security-solution-plugin/common/endpoint/data_generators/exceptions_list_item_generator'; -import { FtrProviderContext } from '../../configs/ftr_provider_context'; import { PolicyTestResourceInfo } from '../../../../../security_solution_endpoint/services/endpoint_policy'; import { ArtifactTestData } from '../../../../../security_solution_endpoint/services/endpoint_artifacts'; -import { ROLE } from '../../services/roles_users'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -28,7 +28,9 @@ export default function ({ getService }: FtrProviderContext) { const endpointPolicyTestResources = getService('endpointPolicyTestResources'); const endpointArtifactTestResources = getService('endpointArtifactTestResources'); - describe('@ess @serverless Endpoint Host Isolation Exceptions artifacts (via lists plugin)', function () { + // @skipInServerlessMKI due to authentication issues - we should migrate from Basic to Bearer token when available + // @skipInServerlessMKI - if you are removing this annotation, make sure to add the test suite to the MKI pipeline in .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml + describe('@ess @serverless @skipInServerlessMKI Endpoint Host Isolation Exceptions artifacts (via lists plugin)', function () { let fleetEndpointPolicy: PolicyTestResourceInfo; let hostIsolationExceptionData: ArtifactTestData; diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/index.ts new file mode 100644 index 0000000000000..7320e49902011 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/index.ts @@ -0,0 +1,60 @@ +/* + * 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 { getRegistryUrl as getRegistryUrlFromIngest } from '@kbn/fleet-plugin/server'; +import { isServerlessKibanaFlavor } from '@kbn/security-solution-plugin/scripts/endpoint/common/stack_services'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; + +export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) { + const { loadTestFile, getService } = providerContext; + + describe('Endpoint plugin', async function async() { + const ingestManager = getService('ingestManager'); + const rolesUsersProvider = getService('rolesUsersProvider'); + const kbnClient = getService('kibanaServer'); + const log = getService('log'); + const endpointRegistryHelpers = getService('endpointRegistryHelpers'); + + const roles = Object.values(ROLE); + before(async () => { + try { + if (!endpointRegistryHelpers.isRegistryEnabled()) { + log.warning('These tests are being run with an external package registry'); + } + + const registryUrl = + endpointRegistryHelpers.getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); + log.info(`Package registry URL for tests: ${registryUrl}`); + await ingestManager.setup(); + } catch (err) { + log.warning(`Error setting up ingestManager: ${err}`); + } + + if (!(await isServerlessKibanaFlavor(kbnClient))) { + // create role/user + for (const role of roles) { + await rolesUsersProvider.createRole({ predefinedRole: role }); + await rolesUsersProvider.createUser({ name: role, roles: [role] }); + } + } + }); + + after(async () => { + if (!(await isServerlessKibanaFlavor(kbnClient))) { + // delete role/user + await rolesUsersProvider.deleteUsers(roles); + await rolesUsersProvider.deleteRoles(roles); + } + }); + + // Remember to make sure the suite is enabled in .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml when adding new tests without @skipInServerlessMKI + loadTestFile(require.resolve('./trusted_apps')); + loadTestFile(require.resolve('./event_filters')); + loadTestFile(require.resolve('./host_isolation_exceptions')); + loadTestFile(require.resolve('./blocklists')); + }); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_artifacts/trusted_apps.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/trusted_apps.ts similarity index 95% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_artifacts/trusted_apps.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/trusted_apps.ts index 4dd4cff64806c..e63b37f78bce7 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_artifacts/trusted_apps.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/trusted_apps.ts @@ -13,10 +13,10 @@ import { GLOBAL_ARTIFACT_TAG, } from '@kbn/security-solution-plugin/common/endpoint/service/artifacts'; import { ExceptionsListItemGenerator } from '@kbn/security-solution-plugin/common/endpoint/data_generators/exceptions_list_item_generator'; -import { FtrProviderContext } from '../../configs/ftr_provider_context'; import { PolicyTestResourceInfo } from '../../../../../security_solution_endpoint/services/endpoint_policy'; import { ArtifactTestData } from '../../../../../security_solution_endpoint/services/endpoint_artifacts'; -import { ROLE } from '../../services/roles_users'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -24,7 +24,9 @@ export default function ({ getService }: FtrProviderContext) { const endpointPolicyTestResources = getService('endpointPolicyTestResources'); const endpointArtifactTestResources = getService('endpointArtifactTestResources'); - describe('@ess @serverless Endpoint artifacts (via lists plugin): Trusted Applications', function () { + // @skipInServerlessMKI due to authentication issues - we should migrate from Basic to Bearer token when available + // @skipInServerlessMKI - if you are removing this annotation, make sure to add the test suite to the MKI pipeline in .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml + describe('@ess @serverless @skipInServerlessMKI Endpoint artifacts (via lists plugin): Trusted Applications', function () { let fleetEndpointPolicy: PolicyTestResourceInfo; before(async () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/configs/ess.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/configs/ess.config.ts new file mode 100644 index 0000000000000..71f00a2865015 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/configs/ess.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/ess/config.base.edr_workflows.trial') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Authentication Integration Tests - ESS Env - Trial License', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/configs/serverless.config.ts new file mode 100644 index 0000000000000..17ffaf10a492c --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/configs/serverless.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/serverless/config.base.edr_workflows') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Authentication Integration Tests - Serverless Env - Complete', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_authz.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts similarity index 94% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_authz.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts index a9aa7af829225..3057e4ce5f9f7 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_authz.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/endpoint_authz.ts @@ -24,8 +24,8 @@ import { UNISOLATE_HOST_ROUTE_V2, } from '@kbn/security-solution-plugin/common/endpoint/constants'; import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; -import { FtrProviderContext } from '../configs/ftr_provider_context'; -import { ROLE } from '../services/roles_users'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -38,8 +38,9 @@ export default function ({ getService }: FtrProviderContext) { version?: string; body: Record | (() => Record) | undefined; } - - describe('@ess @serverless When attempting to call an endpoint api', function () { + // @skipInServerlessMKI - this test uses internal index manipulation in before/after hooks + // @skipInServerlessMKI - if you are removing this annotation, make sure to add the test suite to the MKI pipeline in .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml + describe('@ess @serverless @skipInServerlessMKI When attempting to call an endpoint api', function () { let indexedData: IndexedHostsAndAlertsResponse; let actionId = ''; let agentId = ''; diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/index.ts similarity index 72% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/index.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/index.ts index 70c94c9ef3c4c..de537ba971ec6 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/authentication/trial_license_complete_tier/index.ts @@ -6,8 +6,8 @@ */ import { getRegistryUrl as getRegistryUrlFromIngest } from '@kbn/fleet-plugin/server'; import { isServerlessKibanaFlavor } from '@kbn/security-solution-plugin/scripts/endpoint/common/stack_services'; -import { FtrProviderContext } from '../configs/ftr_provider_context'; -import { ROLE } from '../services/roles_users'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) { const { loadTestFile, getService } = providerContext; @@ -51,17 +51,7 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider await rolesUsersProvider.deleteRoles(roles); } }); - - loadTestFile(require.resolve('./resolver')); - loadTestFile(require.resolve('./metadata')); - loadTestFile(require.resolve('./policy')); - loadTestFile(require.resolve('./package')); + // Remember to make sure the suite is enabled in .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml when adding new tests without @skipInServerlessMKI loadTestFile(require.resolve('./endpoint_authz')); - loadTestFile(require.resolve('./endpoint_response_actions/execute')); - loadTestFile(require.resolve('./endpoint_response_actions/agent_type_support')); - loadTestFile(require.resolve('./endpoint_artifacts/trusted_apps')); - loadTestFile(require.resolve('./endpoint_artifacts/event_filters')); - loadTestFile(require.resolve('./endpoint_artifacts/host_isolation_exceptions')); - loadTestFile(require.resolve('./endpoint_artifacts/blocklists')); }); } diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/configs/ess.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/configs/ess.config.ts new file mode 100644 index 0000000000000..3bf9a2bf4d3f0 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/configs/ess.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/ess/config.base.edr_workflows.trial') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Metadata Integration Tests - ESS Env - Trial License', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/configs/serverless.config.ts new file mode 100644 index 0000000000000..04d2eb6593057 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/configs/serverless.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/serverless/config.base.edr_workflows') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Metadata Integration Tests - Serverless Env - Complete', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/index.ts new file mode 100644 index 0000000000000..71ee3b8ebc337 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/index.ts @@ -0,0 +1,56 @@ +/* + * 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 { getRegistryUrl as getRegistryUrlFromIngest } from '@kbn/fleet-plugin/server'; +import { isServerlessKibanaFlavor } from '@kbn/security-solution-plugin/scripts/endpoint/common/stack_services'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; + +export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) { + const { loadTestFile, getService } = providerContext; + + describe('Endpoint plugin', async function () { + const ingestManager = getService('ingestManager'); + const rolesUsersProvider = getService('rolesUsersProvider'); + const kbnClient = getService('kibanaServer'); + const log = getService('log'); + const endpointRegistryHelpers = getService('endpointRegistryHelpers'); + + const roles = Object.values(ROLE); + before(async () => { + if (!endpointRegistryHelpers.isRegistryEnabled()) { + log.warning('These tests are being run with an external package registry'); + } + + const registryUrl = + endpointRegistryHelpers.getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); + log.info(`Package registry URL for tests: ${registryUrl}`); + try { + await ingestManager.setup(); + } catch (err) { + log.warning(`Error setting up ingestManager: ${err}`); + } + + if (!(await isServerlessKibanaFlavor(kbnClient))) { + // create role/user + for (const role of roles) { + await rolesUsersProvider.createRole({ predefinedRole: role }); + await rolesUsersProvider.createUser({ name: role, roles: [role] }); + } + } + }); + + after(async () => { + if (!(await isServerlessKibanaFlavor(kbnClient))) { + // delete role/user + await rolesUsersProvider.deleteUsers(roles); + await rolesUsersProvider.deleteRoles(roles); + } + }); + // Remember to make sure the suite is enabled in .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml when adding new tests without @skipInServerlessMKI + loadTestFile(require.resolve('./metadata')); + }); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/metadata.ts similarity index 96% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/metadata.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/metadata.ts index 5653b7a3c0df4..bd494997b8681 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/metadata/trial_license_complete_tier/metadata.ts @@ -29,9 +29,12 @@ import { EndpointSortableField, MetadataListResponse, } from '@kbn/security-solution-plugin/common/endpoint/types'; -import { generateAgentDocs, generateMetadataDocs } from './metadata.fixtures'; -import { FtrProviderContext } from '../configs/ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { + generateAgentDocs, + generateMetadataDocs, +} from '../../../../config/services/security_solution_edr_workflows_metadata'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -39,7 +42,9 @@ export default function ({ getService }: FtrProviderContext) { const log = getService('log'); const endpointDataStreamHelpers = getService('endpointDataStreamHelpers'); - describe('@ess @serverless test metadata apis', function () { + // @skipInServerlessMKI - this test uses internal index manipulation in before/after hooks + // @skipInServerlessMKI - if you are removing this annotation, make sure to add the test suite to the MKI pipeline in .buildkite/pipelines/security_solution_quality_gate/mki_periodic/mki_periodic_defend_workflows.yml + describe('@ess @serverless @skipInServerlessMKI test metadata apis', function () { describe('list endpoints GET route', () => { const numberOfHostsInFixture = 2; let agent1Timestamp: number; diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/configs/ess.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/configs/ess.config.ts new file mode 100644 index 0000000000000..151c8e40f75b2 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/configs/ess.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/ess/config.base.edr_workflows.trial') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Package Integration Tests - ESS Env - Trial License', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/configs/serverless.config.ts new file mode 100644 index 0000000000000..85285eecd7f5c --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/configs/serverless.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/serverless/config.base.edr_workflows') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Package Integration Tests - Serverless Env - Complete', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/index.ts new file mode 100644 index 0000000000000..a2332c1724d11 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/index.ts @@ -0,0 +1,56 @@ +/* + * 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 { getRegistryUrl as getRegistryUrlFromIngest } from '@kbn/fleet-plugin/server'; +import { isServerlessKibanaFlavor } from '@kbn/security-solution-plugin/scripts/endpoint/common/stack_services'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; + +export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) { + const { loadTestFile, getService } = providerContext; + + describe('Endpoint plugin', async function () { + const ingestManager = getService('ingestManager'); + const rolesUsersProvider = getService('rolesUsersProvider'); + const kbnClient = getService('kibanaServer'); + const log = getService('log'); + const endpointRegistryHelpers = getService('endpointRegistryHelpers'); + + const roles = Object.values(ROLE); + before(async () => { + try { + if (!endpointRegistryHelpers.isRegistryEnabled()) { + log.warning('These tests are being run with an external package registry'); + } + + const registryUrl = + endpointRegistryHelpers.getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); + log.info(`Package registry URL for tests: ${registryUrl}`); + await ingestManager.setup(); + } catch (err) { + log.warning(`Error setting up ingestManager: ${err}`); + } + + if (!(await isServerlessKibanaFlavor(kbnClient))) { + // create role/user + for (const role of roles) { + await rolesUsersProvider.createRole({ predefinedRole: role }); + await rolesUsersProvider.createUser({ name: role, roles: [role] }); + } + } + }); + + after(async () => { + if (!(await isServerlessKibanaFlavor(kbnClient))) { + // delete role/user + await rolesUsersProvider.deleteUsers(roles); + await rolesUsersProvider.deleteRoles(roles); + } + }); + + loadTestFile(require.resolve('./package')); + }); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/package.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/package.ts similarity index 97% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/package.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/package.ts index 745fc8ac85337..11beabd6edd25 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/package.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/package/trial_license_complete_tier/package.ts @@ -15,8 +15,11 @@ import { EndpointDocGenerator, Event, } from '@kbn/security-solution-plugin/common/endpoint/generate_data'; -import { FtrProviderContext } from '../configs/ftr_provider_context'; -import { InsertedEvents, processEventsIndex } from '../services/resolver'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { + InsertedEvents, + processEventsIndex, +} from '../../../../config/services/security_solution_edr_workflows_resolver'; interface EventIngested { event: { diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/configs/ess.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/configs/ess.config.ts new file mode 100644 index 0000000000000..f38883ad58aea --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/configs/ess.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/ess/config.base.edr_workflows.trial') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Policy Response Integration Tests - ESS Env - Trial License', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/configs/serverless.config.ts new file mode 100644 index 0000000000000..3a3c4780f7008 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/configs/serverless.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/serverless/config.base.edr_workflows') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Policy Response Integration Tests - Serverless Env - Complete', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/index.ts new file mode 100644 index 0000000000000..495f784a2ee26 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/index.ts @@ -0,0 +1,56 @@ +/* + * 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 { getRegistryUrl as getRegistryUrlFromIngest } from '@kbn/fleet-plugin/server'; +import { isServerlessKibanaFlavor } from '@kbn/security-solution-plugin/scripts/endpoint/common/stack_services'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; + +export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) { + const { loadTestFile, getService } = providerContext; + + describe('Endpoint plugin', async function () { + const ingestManager = getService('ingestManager'); + const rolesUsersProvider = getService('rolesUsersProvider'); + const kbnClient = getService('kibanaServer'); + const log = getService('log'); + const endpointRegistryHelpers = getService('endpointRegistryHelpers'); + + const roles = Object.values(ROLE); + before(async () => { + if (!endpointRegistryHelpers.isRegistryEnabled()) { + log.warning('These tests are being run with an external package registry'); + } + + const registryUrl = + endpointRegistryHelpers.getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); + log.info(`Package registry URL for tests: ${registryUrl}`); + try { + await ingestManager.setup(); + } catch (err) { + log.warning(`Error setting up ingestManager: ${err}`); + } + + if (!(await isServerlessKibanaFlavor(kbnClient))) { + // create role/user + for (const role of roles) { + await rolesUsersProvider.createRole({ predefinedRole: role }); + await rolesUsersProvider.createUser({ name: role, roles: [role] }); + } + } + }); + + after(async () => { + if (!(await isServerlessKibanaFlavor(kbnClient))) { + // delete role/user + await rolesUsersProvider.deleteUsers(roles); + await rolesUsersProvider.deleteRoles(roles); + } + }); + + loadTestFile(require.resolve('./policy_response')); + }); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/policy.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/policy_response.ts similarity index 95% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/policy.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/policy_response.ts index e41154944f183..61adaf8573ca1 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/policy.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/policy_response/trial_license_complete_tier/policy_response.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../configs/ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/common.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/common.ts similarity index 99% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/common.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/common.ts index 4a9102638edb1..2213850275849 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/common.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/common.ts @@ -28,6 +28,11 @@ import { categoryMapping, } from '@kbn/security-solution-plugin/common/endpoint/generate_data'; +export const HEADERS = Object.freeze({ + 'kbn-xsrf': 'security_solution', + 'x-elastic-internal-origin': 'security_solution', +}); + /** * Schema used for the /tree api post requests that uses the ancestry */ diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/configs/ess.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/configs/ess.config.ts new file mode 100644 index 0000000000000..6e5396e69c099 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/configs/ess.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/ess/config.base.edr_workflows.trial') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Resolver Integration Tests - ESS Env - Trial License', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/configs/serverless.config.ts new file mode 100644 index 0000000000000..376b953ef4f42 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/configs/serverless.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/serverless/config.base.edr_workflows') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Resolver Integration Tests - Serverless Env - Complete', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/entity.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/entity.ts similarity index 96% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/entity.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/entity.ts index 31de9cce299dc..1f79ced2ab20c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/entity.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/entity.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { eventsIndexPattern } from '@kbn/security-solution-plugin/common/endpoint/constants'; import { ResolverEntityIndex } from '@kbn/security-solution-plugin/common/endpoint/types'; -import { FtrProviderContext } from '../../configs/ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -61,7 +61,7 @@ export default function ({ getService }: FtrProviderContext) { }); // illegal_argument_exception: unknown setting [index.lifecycle.name] in before - describe('@brokenInServerless signals index mapping tests', function () { + describe('@skipInServerless @skipInServerlessMKI signals index mapping tests', function () { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/endpoint/resolver/signals'); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/entity_id.ts similarity index 96% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/entity_id.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/entity_id.ts index ab2c96756f463..3e65a0899cd2a 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/entity_id.ts @@ -19,10 +19,12 @@ import { EndpointDocGenerator, Event, } from '@kbn/security-solution-plugin/common/endpoint/generate_data'; -import { FtrProviderContext } from '../../configs/ftr_provider_context'; -import { InsertedEvents, processEventsIndex } from '../../services/resolver'; -import { createAncestryArray, schemaWithAncestry } from './common'; -import { HEADERS } from '../../headers'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { createAncestryArray, schemaWithAncestry, HEADERS } from './common'; +import { + InsertedEvents, + processEventsIndex, +} from '../../../../config/services/security_solution_edr_workflows_resolver'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/events.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/events.ts similarity index 98% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/events.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/events.ts index 5751aad7cb1dc..0d938b5936430 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/events.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/events.ts @@ -18,10 +18,12 @@ import { Tree, RelatedEventCategory, } from '@kbn/security-solution-plugin/common/endpoint/generate_data'; -import { FtrProviderContext } from '../../configs/ftr_provider_context'; -import { Options, GeneratedTrees } from '../../services/resolver'; -import { compareArrays } from './common'; -import { HEADERS } from '../../headers'; +import { + GeneratedTrees, + Options, +} from '../../../../config/services/security_solution_edr_workflows_resolver'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { compareArrays, HEADERS } from './common'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/index.ts new file mode 100644 index 0000000000000..6528ba3ecb293 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/index.ts @@ -0,0 +1,60 @@ +/* + * 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 { getRegistryUrl as getRegistryUrlFromIngest } from '@kbn/fleet-plugin/server'; +import { isServerlessKibanaFlavor } from '@kbn/security-solution-plugin/scripts/endpoint/common/stack_services'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; + +export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) { + const { loadTestFile, getService } = providerContext; + + describe('Endpoint plugin', async function () { + const ingestManager = getService('ingestManager'); + const rolesUsersProvider = getService('rolesUsersProvider'); + const kbnClient = getService('kibanaServer'); + const log = getService('log'); + const endpointRegistryHelpers = getService('endpointRegistryHelpers'); + + const roles = Object.values(ROLE); + before(async () => { + try { + if (!endpointRegistryHelpers.isRegistryEnabled()) { + log.warning('These tests are being run with an external package registry'); + } + + const registryUrl = + endpointRegistryHelpers.getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); + log.info(`Package registry URL for tests: ${registryUrl}`); + + await ingestManager.setup(); + } catch (err) { + log.warning(`Error setting up ingestManager: ${err}`); + } + + if (!(await isServerlessKibanaFlavor(kbnClient))) { + // create role/user + for (const role of roles) { + await rolesUsersProvider.createRole({ predefinedRole: role }); + await rolesUsersProvider.createUser({ name: role, roles: [role] }); + } + } + }); + + after(async () => { + if (!(await isServerlessKibanaFlavor(kbnClient))) { + // delete role/user + await rolesUsersProvider.deleteUsers(roles); + await rolesUsersProvider.deleteRoles(roles); + } + }); + + loadTestFile(require.resolve('./entity_id')); + loadTestFile(require.resolve('./entity')); + loadTestFile(require.resolve('./tree')); + loadTestFile(require.resolve('./events')); + }); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/tree.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/tree.ts similarity index 98% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/tree.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/tree.ts index e161a5d00f4f4..0d28ab42a14ff 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/tree.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/resolver/trial_license_complete_tier/tree.ts @@ -16,10 +16,18 @@ import { Tree, RelatedEventCategory, } from '@kbn/security-solution-plugin/common/endpoint/generate_data'; -import { FtrProviderContext } from '../../configs/ftr_provider_context'; -import { Options, GeneratedTrees } from '../../services/resolver'; -import { schemaWithAncestry, schemaWithName, schemaWithoutAncestry, verifyTree } from './common'; -import { HEADERS } from '../../headers'; +import { + GeneratedTrees, + Options, +} from '../../../../config/services/security_solution_edr_workflows_resolver'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { + schemaWithAncestry, + schemaWithName, + schemaWithoutAncestry, + verifyTree, + HEADERS, +} from './common'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_response_actions/agent_type_support.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/agent_type_support.ts similarity index 93% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_response_actions/agent_type_support.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/agent_type_support.ts index 6631923b1f4bc..75b0d5a68a3a9 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_response_actions/agent_type_support.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/agent_type_support.ts @@ -6,7 +6,7 @@ */ import { ISOLATE_HOST_ROUTE_V2 } from '@kbn/security-solution-plugin/common/endpoint/constants'; -import type { FtrProviderContext } from '../../configs/ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/ess.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/ess.config.ts new file mode 100644 index 0000000000000..61758111e071b --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/ess.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/ess/config.base.edr_workflows.trial') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Response Actions Integration Tests - ESS Env - Trial License', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/serverless.config.ts new file mode 100644 index 0000000000000..4102f1a7dbc76 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/configs/serverless.config.ts @@ -0,0 +1,22 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/serverless/config.base.edr_workflows') + ); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('..')], + junit: { + reportName: 'EDR Workflows - Response Actions Integration Tests - Serverless Env - Complete', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_response_actions/execute.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/execute.ts similarity index 93% rename from x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_response_actions/execute.ts rename to x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/execute.ts index 3a41abf288427..4178fd80b653a 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/endpoint_response_actions/execute.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/execute.ts @@ -8,14 +8,14 @@ import { wrapErrorAndRejectPromise } from '@kbn/security-solution-plugin/common/ import expect from '@kbn/expect'; import { EXECUTE_ROUTE } from '@kbn/security-solution-plugin/common/endpoint/constants'; import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; -import { FtrProviderContext } from '../../configs/ftr_provider_context'; -import { ROLE } from '../../services/roles_users'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const endpointTestResources = getService('endpointTestResources'); - - describe('@ess @serverless Endpoint `execute` response action', function () { + // @skipInServerlessMKI - this test uses internal index manipulation in before/after hooks + describe('@ess @serverless @skipInServerlessMKI Endpoint `execute` response action', function () { let indexedData: IndexedHostsAndAlertsResponse; let agentId = ''; diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/index.ts new file mode 100644 index 0000000000000..c26d7dd125f39 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/response_actions/trial_license_complete_tier/index.ts @@ -0,0 +1,57 @@ +/* + * 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 { getRegistryUrl as getRegistryUrlFromIngest } from '@kbn/fleet-plugin/server'; +import { isServerlessKibanaFlavor } from '@kbn/security-solution-plugin/scripts/endpoint/common/stack_services'; +import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows'; +import { ROLE } from '../../../../config/services/security_solution_edr_workflows_roles_users'; + +export default function endpointAPIIntegrationTests(providerContext: FtrProviderContext) { + const { loadTestFile, getService } = providerContext; + + describe('Endpoint plugin', async function () { + const ingestManager = getService('ingestManager'); + const rolesUsersProvider = getService('rolesUsersProvider'); + const kbnClient = getService('kibanaServer'); + const log = getService('log'); + const endpointRegistryHelpers = getService('endpointRegistryHelpers'); + + const roles = Object.values(ROLE); + before(async () => { + if (!endpointRegistryHelpers.isRegistryEnabled()) { + log.warning('These tests are being run with an external package registry'); + } + + const registryUrl = + endpointRegistryHelpers.getRegistryUrlFromTestEnv() ?? getRegistryUrlFromIngest(); + log.info(`Package registry URL for tests: ${registryUrl}`); + try { + await ingestManager.setup(); + } catch (err) { + log.warning(`Error setting up ingestManager: ${err}`); + } + + if (!(await isServerlessKibanaFlavor(kbnClient))) { + // create role/user + for (const role of roles) { + await rolesUsersProvider.createRole({ predefinedRole: role }); + await rolesUsersProvider.createUser({ name: role, roles: [role] }); + } + } + }); + + after(async () => { + if (!(await isServerlessKibanaFlavor(kbnClient))) { + // delete role/user + await rolesUsersProvider.deleteUsers(roles); + await rolesUsersProvider.deleteRoles(roles); + } + }); + + loadTestFile(require.resolve('./agent_type_support')); + loadTestFile(require.resolve('./execute')); + }); +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/index.ts b/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/index.ts deleted file mode 100644 index 482b89a533099..0000000000000 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/apis/resolver/index.ts +++ /dev/null @@ -1,19 +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 { FtrProviderContext } from '../../configs/ftr_provider_context'; - -export default function (providerContext: FtrProviderContext) { - const { loadTestFile } = providerContext; - - describe('Resolver tests', () => { - loadTestFile(require.resolve('./entity_id')); - loadTestFile(require.resolve('./entity')); - loadTestFile(require.resolve('./tree')); - loadTestFile(require.resolve('./events')); - }); -} diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/headers.ts b/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/headers.ts deleted file mode 100644 index e4ca6acfbbc65..0000000000000 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/headers.ts +++ /dev/null @@ -1,11 +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. - */ - -export const HEADERS = Object.freeze({ - 'kbn-xsrf': 'security_solution', - 'x-elastic-internal-origin': 'security_solution', -}); diff --git a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/services/index.ts b/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/services/index.ts deleted file mode 100644 index fc46bc5587bc6..0000000000000 --- a/x-pack/test/security_solution_api_integration/test_suites/security_solution_endpoint_api_int/services/index.ts +++ /dev/null @@ -1,37 +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 { - KibanaSupertestWithCertProvider, - KibanaSupertestWithCertWithoutAuthProvider, -} from '../../../../security_solution_endpoint/services/supertest_with_cert'; -import { services as xPackAPIServices } from '../../../../api_integration/services'; -import { ResolverGeneratorProvider } from './resolver'; -import { RolesUsersProvider } from './roles_users'; -import { EndpointTestResources } from '../../../../security_solution_endpoint/services/endpoint'; -import { EndpointPolicyTestResourcesProvider } from '../../../../security_solution_endpoint/services/endpoint_policy'; -import { EndpointArtifactsTestResources } from '../../../../security_solution_endpoint/services/endpoint_artifacts'; -import { SecuritySolutionEndpointDataStreamHelpers } from '../../../../common/services/security_solution/endpoint_data_stream_helpers'; -import { SecuritySolutionEndpointRegistryHelpers } from '../../../../common/services/security_solution/endpoint_registry_helpers'; - -export const services = { - ...xPackAPIServices, - resolverGenerator: ResolverGeneratorProvider, - endpointTestResources: EndpointTestResources, - endpointPolicyTestResources: EndpointPolicyTestResourcesProvider, - endpointArtifactTestResources: EndpointArtifactsTestResources, - rolesUsersProvider: RolesUsersProvider, - endpointDataStreamHelpers: SecuritySolutionEndpointDataStreamHelpers, - endpointRegistryHelpers: SecuritySolutionEndpointRegistryHelpers, -}; - -export const svlServices = { - ...services, - - supertest: KibanaSupertestWithCertProvider, - supertestWithoutAuth: KibanaSupertestWithCertWithoutAuthProvider, -}; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/enrichments.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/enrichments.cy.ts index 813560274fcee..8a1be034a444f 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/enrichments.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/entity_analytics/enrichments.cy.ts @@ -33,109 +33,95 @@ import { mockRiskEngineEnabled } from '../../tasks/entity_analytics'; const CURRENT_HOST_RISK_LEVEL = 'Current host risk level'; const ORIGINAL_HOST_RISK_LEVEL = 'Original host risk level'; -describe( - 'Enrichment', - { - tags: ['@ess'], - env: { - ftrConfig: { - kbnServerArgs: [ - `--xpack.securitySolution.enableExperimental=${JSON.stringify([ - 'expandableFlyoutDisabled', - ])}`, - ], - }, - }, - }, - () => { - before(() => { - cy.task('esArchiverUnload', { archiveName: 'risk_scores_new' }); - cy.task('esArchiverUnload', { archiveName: 'risk_scores_new_updated' }); - cy.task('esArchiverLoad', { archiveName: 'risk_users' }); - }); +// this whole suite is failing on main +describe.skip('Enrichment', { tags: ['@ess'] }, () => { + before(() => { + cy.task('esArchiverUnload', { archiveName: 'risk_scores_new' }); + cy.task('esArchiverUnload', { archiveName: 'risk_scores_new_updated' }); + cy.task('esArchiverLoad', { archiveName: 'risk_users' }); + }); - after(() => { - cy.task('esArchiverUnload', { archiveName: 'risk_users' }); - }); + after(() => { + cy.task('esArchiverUnload', { archiveName: 'risk_users' }); + }); - describe('Custom query rule', () => { - // FLAKY: https://github.com/elastic/kibana/issues/176965 - describe.skip('from legacy risk scores', () => { - beforeEach(() => { - cy.task('esArchiverLoad', { archiveName: 'risk_hosts' }); - deleteAlertsAndRules(); - createRule(getNewRule({ rule_id: 'rule1' })); - login(); - visitWithTimeRange(ALERTS_URL); - waitForAlertsToPopulate(); - }); + describe('Custom query rule', () => { + // FLAKY: https://github.com/elastic/kibana/issues/176965 + describe.skip('from legacy risk scores', () => { + beforeEach(() => { + cy.task('esArchiverLoad', { archiveName: 'risk_hosts' }); + deleteAlertsAndRules(); + createRule(getNewRule({ rule_id: 'rule1' })); + login(); + visitWithTimeRange(ALERTS_URL); + waitForAlertsToPopulate(); + }); - afterEach(() => { - cy.task('esArchiverUnload', { archiveName: 'risk_hosts' }); - cy.task('esArchiverUnload', { archiveName: 'risk_hosts_updated' }); - }); + afterEach(() => { + cy.task('esArchiverUnload', { archiveName: 'risk_hosts' }); + cy.task('esArchiverUnload', { archiveName: 'risk_hosts_updated' }); + }); - it('Should has enrichment fields from legacy risk', function () { - cy.get(HOST_RISK_HEADER_COLUMN).contains('Host Risk Level'); - cy.get(USER_RISK_HEADER_COLUMN).contains('User Risk Level'); - scrollAlertTableColumnIntoView(HOST_RISK_COLUMN); - cy.get(HOST_RISK_COLUMN).contains('Low'); - scrollAlertTableColumnIntoView(USER_RISK_COLUMN); - cy.get(USER_RISK_COLUMN).contains('Low'); - scrollAlertTableColumnIntoView(ACTION_COLUMN); - expandFirstAlert(); - cy.get(ENRICHED_DATA_ROW).contains('Low'); - cy.get(ENRICHED_DATA_ROW).contains(CURRENT_HOST_RISK_LEVEL); - cy.get(ENRICHED_DATA_ROW).contains('Critical').should('not.exist'); - cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL).should('not.exist'); + it('Should has enrichment fields from legacy risk', function () { + cy.get(HOST_RISK_HEADER_COLUMN).contains('Host Risk Level'); + cy.get(USER_RISK_HEADER_COLUMN).contains('User Risk Level'); + scrollAlertTableColumnIntoView(HOST_RISK_COLUMN); + cy.get(HOST_RISK_COLUMN).contains('Low'); + scrollAlertTableColumnIntoView(USER_RISK_COLUMN); + cy.get(USER_RISK_COLUMN).contains('Low'); + scrollAlertTableColumnIntoView(ACTION_COLUMN); + expandFirstAlert(); + cy.get(ENRICHED_DATA_ROW).contains('Low'); + cy.get(ENRICHED_DATA_ROW).contains(CURRENT_HOST_RISK_LEVEL); + cy.get(ENRICHED_DATA_ROW).contains('Critical').should('not.exist'); + cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL).should('not.exist'); - closeAlertFlyout(); - cy.task('esArchiverUnload', { archiveName: 'risk_hosts' }); - cy.task('esArchiverLoad', { archiveName: 'risk_hosts_updated' }); - expandFirstAlert(); - cy.get(ENRICHED_DATA_ROW).contains('Critical'); - cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL); - }); + closeAlertFlyout(); + cy.task('esArchiverUnload', { archiveName: 'risk_hosts' }); + cy.task('esArchiverLoad', { archiveName: 'risk_hosts_updated' }); + expandFirstAlert(); + cy.get(ENRICHED_DATA_ROW).contains('Critical'); + cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL); }); + }); - describe('from new risk scores', () => { - beforeEach(() => { - cy.task('esArchiverLoad', { archiveName: 'risk_scores_new' }); - deleteAlertsAndRules(); - createRule(getNewRule({ rule_id: 'rule1' })); - login(); - mockRiskEngineEnabled(); - visitWithTimeRange(ALERTS_URL); - waitForAlertsToPopulate(); - }); + describe('from new risk scores', () => { + beforeEach(() => { + cy.task('esArchiverLoad', { archiveName: 'risk_scores_new' }); + deleteAlertsAndRules(); + createRule(getNewRule({ rule_id: 'rule1' })); + login(); + mockRiskEngineEnabled(); + visitWithTimeRange(ALERTS_URL); + waitForAlertsToPopulate(); + }); - afterEach(() => { - cy.task('esArchiverUnload', { archiveName: 'risk_scores_new' }); - cy.task('esArchiverUnload', { archiveName: 'risk_scores_new_updated' }); - }); + afterEach(() => { + cy.task('esArchiverUnload', { archiveName: 'risk_scores_new' }); + cy.task('esArchiverUnload', { archiveName: 'risk_scores_new_updated' }); + }); - it('Should has enrichment fields from legacy risk', function () { - cy.get(HOST_RISK_HEADER_COLUMN).contains('Host Risk Level'); - cy.get(USER_RISK_HEADER_COLUMN).contains('User Risk Level'); - scrollAlertTableColumnIntoView(HOST_RISK_COLUMN); - cy.get(HOST_RISK_COLUMN).contains('Critical'); - scrollAlertTableColumnIntoView(USER_RISK_COLUMN); - cy.get(USER_RISK_COLUMN).contains('High'); - scrollAlertTableColumnIntoView(ACTION_COLUMN); - expandFirstAlert(); - cy.get(ENRICHED_DATA_ROW).contains('Critical'); - cy.get(ENRICHED_DATA_ROW).contains(CURRENT_HOST_RISK_LEVEL); - cy.get(ENRICHED_DATA_ROW).contains('Low').should('not.exist'); - cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL).should('not.exist'); + it('Should has enrichment fields from legacy risk', function () { + cy.get(HOST_RISK_HEADER_COLUMN).contains('Host Risk Level'); + cy.get(USER_RISK_HEADER_COLUMN).contains('User Risk Level'); + scrollAlertTableColumnIntoView(HOST_RISK_COLUMN); + cy.get(HOST_RISK_COLUMN).contains('Critical'); + scrollAlertTableColumnIntoView(USER_RISK_COLUMN); + cy.get(USER_RISK_COLUMN).contains('High'); + scrollAlertTableColumnIntoView(ACTION_COLUMN); + expandFirstAlert(); + cy.get(ENRICHED_DATA_ROW).contains('Critical'); + cy.get(ENRICHED_DATA_ROW).contains(CURRENT_HOST_RISK_LEVEL); + cy.get(ENRICHED_DATA_ROW).contains('Low').should('not.exist'); + cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL).should('not.exist'); - closeAlertFlyout(); - cy.task('esArchiverUnload', { archiveName: 'risk_scores_new' }); - cy.task('esArchiverLoad', { archiveName: 'risk_scores_new_updated' }); - expandFirstAlert(); - cy.get(ENRICHED_DATA_ROW).contains('Low'); - cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL); - }); + closeAlertFlyout(); + cy.task('esArchiverUnload', { archiveName: 'risk_scores_new' }); + cy.task('esArchiverLoad', { archiveName: 'risk_scores_new_updated' }); + expandFirstAlert(); + cy.get(ENRICHED_DATA_ROW).contains('Low'); + cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL); }); }); - } -); + }); +}); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_details.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_details.cy.ts deleted file mode 100644 index b9655de8eab03..0000000000000 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/alerts_details.cy.ts +++ /dev/null @@ -1,248 +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 { - ALERT_FLYOUT, - CELL_TEXT, - COPY_ALERT_FLYOUT_LINK, - JSON_TEXT, - OVERVIEW_RULE, - SUMMARY_VIEW, - TABLE_CONTAINER, - TABLE_ROWS, -} from '../../../screens/alerts_details'; -import { closeAlertFlyout, expandFirstAlert } from '../../../tasks/alerts'; -import { - changeAlertStatusTo, - filterBy, - openJsonView, - openTable, -} from '../../../tasks/alerts_details'; -import { createRule } from '../../../tasks/api_calls/rules'; -import { deleteAlertsAndRules } from '../../../tasks/api_calls/common'; -import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; -import { login } from '../../../tasks/login'; -import { visit, visitWithTimeRange } from '../../../tasks/navigation'; -import { getNewRule, getUnmappedRule } from '../../../objects/rule'; -import { ALERTS_URL } from '../../../urls/navigation'; -import { tablePageSelector } from '../../../screens/table_pagination'; -import { ALERTS_TABLE_COUNT } from '../../../screens/timeline'; -import { ALERT_SUMMARY_SEVERITY_DONUT_CHART } from '../../../screens/alerts'; -import { - visitRuleDetailsPage, - waitForPageToBeLoaded as waitForRuleDetailsPageToBeLoaded, -} from '../../../tasks/rule_details'; - -// this functionality is now not used anymore (though still accessible if customers turn the `expandableFlyoutDisabled` feature flag on -// the code will be removed entirely for `8.16 (see https://github.com/elastic/security-team/issues/7462) -describe.skip('Alert details flyout', { tags: ['@ess', '@serverless'] }, () => { - describe('Basic functions', () => { - beforeEach(() => { - deleteAlertsAndRules(); - createRule(getNewRule()); - login(); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlert(); - }); - - it('should update the table when status of the alert is updated', () => { - cy.get(OVERVIEW_RULE).should('be.visible'); - cy.get(ALERTS_TABLE_COUNT).should('have.text', '1 alert'); - cy.get(ALERT_SUMMARY_SEVERITY_DONUT_CHART).should('contain.text', '1alert'); - expandFirstAlert(); - changeAlertStatusTo('acknowledged'); - cy.get(ALERTS_TABLE_COUNT).should('have.text', '1 alert'); - cy.get(ALERT_SUMMARY_SEVERITY_DONUT_CHART).should('contain.text', '1alert'); - }); - }); - - describe('With unmapped fields', () => { - before(() => { - cy.task('esArchiverLoad', { archiveName: 'unmapped_fields' }); - }); - - beforeEach(() => { - deleteAlertsAndRules(); - createRule({ ...getUnmappedRule(), investigation_fields: { field_names: ['event.kind'] } }); - login(); - visit(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlert(); - }); - - after(() => { - cy.task('esArchiverUnload', { archiveName: 'unmapped_fields' }); - }); - - it.skip('should display user and system defined highlighted fields', () => { - cy.get(SUMMARY_VIEW) - .should('be.visible') - .and('contain.text', 'event.kind') - .and('contain.text', 'Rule type'); - }); - - it('should display the unmapped field on the JSON view', () => { - const expectedUnmappedValue = 'This is the unmapped field'; - - openJsonView(); - - cy.get(JSON_TEXT).then((x) => { - const parsed = JSON.parse(x.text()); - expect(parsed.fields.unmapped[0]).to.equal(expectedUnmappedValue); - }); - }); - - it('should displays the unmapped field on the table', () => { - const expectedUnmappedField = { - field: 'unmapped', - text: 'This is the unmapped field', - }; - - openTable(); - cy.get(ALERT_FLYOUT).find(tablePageSelector(6)).click({ force: true }); - cy.get(ALERT_FLYOUT) - .find(TABLE_ROWS) - .last() - .within(() => { - cy.get(CELL_TEXT).should('contain', expectedUnmappedField.field); - cy.get(CELL_TEXT).should('contain', expectedUnmappedField.text); - }); - }); - - // This test makes sure that the table does not overflow horizontally - it('table should not scroll horizontally', () => { - openTable(); - - cy.get(ALERT_FLYOUT) - .find(TABLE_CONTAINER) - .within(($tableContainer) => { - expect($tableContainer[0].scrollLeft).to.equal(0); - - // Due to the introduction of pagination on the table, a slight horizontal overflow has been introduced. - // scroll ignores the `overflow-x:hidden` attribute and will still scroll the element if there is a hidden overflow - // Updated the below to < 5 to account for this and keep a test to make sure it doesn't grow - $tableContainer[0].scroll({ left: 1000 }); - - expect($tableContainer[0].scrollLeft).to.be.lessThan(5); - }); - }); - }); - - describe('Url state management', () => { - before(() => { - cy.task('esArchiverLoad', { archiveName: 'query_alert', useCreate: true, docsOnly: true }); - }); - - beforeEach(() => { - deleteAlertsAndRules(); - createRule(getNewRule()); - login(); - visitWithTimeRange(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlert(); - }); - - it('should store the flyout state in the url when it is opened and remove it when closed', () => { - cy.get(OVERVIEW_RULE).should('be.visible'); - cy.url().should('include', 'flyout='); - - closeAlertFlyout(); - - cy.url().should('not.include', 'flyout='); - }); - - it.skip('should open the alert flyout when the page is refreshed', () => { - cy.get(OVERVIEW_RULE).should('be.visible'); - cy.reload(); - cy.get(OVERVIEW_RULE).should('be.visible'); - cy.get(OVERVIEW_RULE).should('contain', 'Endpoint Security'); - }); - - it('should show the copy link button for the flyout', () => { - cy.get(COPY_ALERT_FLYOUT_LINK).should('be.visible'); - }); - - it.skip('should have the `kibana.alert.url` field set', () => { - openTable(); - filterBy('kibana.alert.url'); - cy.get('[data-test-subj="formatted-field-kibana.alert.url"]').should( - 'have.text', - 'http://localhost:5601/app/security/alerts/redirect/eabbdefc23da981f2b74ab58b82622a97bb9878caa11bc914e2adfacc94780f1?index=.alerts-security.alerts-default×tamp=2023-04-27T11:03:57.906Z' - ); - }); - }); - - describe('Localstorage management', () => { - const ARCHIVED_RULE_ID = '7015a3e2-e4ea-11ed-8c11-49608884878f'; - const ARCHIVED_RULE_NAME = 'Endpoint Security'; - - before(() => { - deleteAlertsAndRules(); - // It just imports an alert without a rule but rule details page should work anyway - cy.task('esArchiverLoad', { archiveName: 'query_alert', useCreate: true, docsOnly: true }); - }); - - after(() => { - cy.task('esArchiverUnload', { archiveName: 'query_alert' }); - }); - - beforeEach(() => { - createRule(getNewRule()); - login(); - visitWithTimeRange(ALERTS_URL); - waitForAlertsToPopulate(); - expandFirstAlert(); - }); - - /** - * Localstorage is updated after a delay here x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.ts - * We create this config to re-check localStorage 3 times, every 500ms to avoid any potential flakyness from that delay - */ - - it('should store the flyout state in localstorage', () => { - cy.get(OVERVIEW_RULE).should('be.visible'); - - cy.window().then((win) => { - const securityDataTableStorage = win.localStorage.getItem('securityDataTable'); - cy.wrap(securityDataTableStorage).should('contain', '"panelView":"eventDetail"'); - }); - }); - - it('should remove the flyout details from local storage when closed', () => { - cy.get(OVERVIEW_RULE).should('be.visible'); - closeAlertFlyout(); - - cy.window().then((win) => { - const securityDataTableStorage = win.localStorage.getItem('securityDataTable'); - cy.wrap(securityDataTableStorage).should('not.contain', '"panelView"'); - }); - }); - - it('should remove the flyout state from localstorage when navigating away without closing the flyout', () => { - cy.get(OVERVIEW_RULE).should('be.visible'); - - visitRuleDetailsPage(ARCHIVED_RULE_ID); - waitForRuleDetailsPageToBeLoaded(ARCHIVED_RULE_NAME); - - cy.window().then((win) => { - const securityDataTableStorage = win.localStorage.getItem('securityDataTable'); - cy.wrap(securityDataTableStorage).should('not.contain', '"panelView"'); - }); - }); - - it('should not reopen the flyout when navigating away from the alerts page and returning to it', () => { - cy.get(OVERVIEW_RULE).should('be.visible'); - - visitRuleDetailsPage(ARCHIVED_RULE_ID); - waitForRuleDetailsPageToBeLoaded(ARCHIVED_RULE_NAME); - - visit(ALERTS_URL); - cy.get(OVERVIEW_RULE).should('not.exist'); - }); - }); -}); diff --git a/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts b/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts index 94a8a9cb5ec8e..9cb093b6edae8 100644 --- a/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/integrations/artifact_entries_list.ts @@ -54,7 +54,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Failing: See https://github.com/elastic/kibana/issues/187314 // Failing: See https://github.com/elastic/kibana/issues/187383 - describe('For each artifact list under management', function () { + // Failing: See https://github.com/elastic/kibana/issues/188131 + // Failing: See https://github.com/elastic/kibana/issues/188125 + describe.skip('For each artifact list under management', function () { targetTags(this, ['@ess', '@skipInServerless']); this.timeout(60_000 * 5);