From fb1394812d88d91bde2cdbabfac95487b73b64bf Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Mon, 1 Mar 2021 17:35:21 -0800 Subject: [PATCH 1/6] [Security Solution][Detections] -Fixes rule edit flow bug with max_signals (#92748) ### Summary Fixes a bug where max_signals was being reverted to it's default value when the rule was edited via the UI. --- .../detection_rules/custom_query_rule.spec.ts | 25 +++++++++++++++++++ .../security_solution/cypress/objects/rule.ts | 10 ++++++++ .../cypress/tasks/api_calls/rules.ts | 1 + .../cypress/tasks/rule_details.ts | 5 ---- .../detection_engine/rules/edit/index.tsx | 1 + .../rules/queries/query_with_max_signals.json | 9 +++++++ 6 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_max_signals.json diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts index ecfa96d59170f..201a3c3a5563e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts @@ -108,6 +108,7 @@ import { } from '../../tasks/create_new_rule'; import { saveEditedRule, waitForKibana } from '../../tasks/edit_rule'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; +import { activatesRule } from '../../tasks/rule_details'; import { DETECTIONS_URL } from '../../urls/navigation'; @@ -308,6 +309,21 @@ describe('Custom detection rules deletion and edition', () => { reload(); }); + it('Only modifies rule active status on enable/disable', () => { + activatesRule(); + + cy.intercept('GET', `/api/detection_engine/rules?id=`).as('fetchRuleDetails'); + + goToRuleDetails(); + + cy.wait('@fetchRuleDetails').then(({ response }) => { + cy.wrap(response!.statusCode).should('eql', 200); + + cy.wrap(response!.body.max_signals).should('eql', existingRule.maxSignals); + cy.wrap(response!.body.enabled).should('eql', false); + }); + }); + it('Allows a rule to be edited', () => { editFirstRule(); waitForKibana(); @@ -347,8 +363,17 @@ describe('Custom detection rules deletion and edition', () => { goToAboutStepTab(); cy.get(TAGS_CLEAR_BUTTON).click({ force: true }); fillAboutRule(editedRule); + + cy.intercept('GET', '/api/detection_engine/rules?id').as('getRule'); + saveEditedRule(); + cy.wait('@getRule').then(({ response }) => { + cy.wrap(response!.statusCode).should('eql', 200); + // ensure that editing rule does not modify max_signals + cy.wrap(response!.body.max_signals).should('eql', existingRule.maxSignals); + }); + cy.get(RULE_NAME_HEADER).should('have.text', `${editedRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', editedRule.description); cy.get(ABOUT_DETAILS).within(() => { diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index dadcb98cade8d..88dcd998fc06d 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -54,6 +54,7 @@ export interface CustomRule { runsEvery: Interval; lookBack: Interval; timeline: CompleteTimeline; + maxSignals: number; } export interface ThresholdRule extends CustomRule { @@ -174,6 +175,7 @@ export const newRule: CustomRule = { runsEvery, lookBack, timeline, + maxSignals: 100, }; export const existingRule: CustomRule = { @@ -192,6 +194,9 @@ export const existingRule: CustomRule = { runsEvery, lookBack, timeline, + // Please do not change, or if you do, needs + // to be any number other than default value + maxSignals: 500, }; export const newOverrideRule: OverrideRule = { @@ -213,6 +218,7 @@ export const newOverrideRule: OverrideRule = { runsEvery, lookBack, timeline, + maxSignals: 100, }; export const newThresholdRule: ThresholdRule = { @@ -232,6 +238,7 @@ export const newThresholdRule: ThresholdRule = { runsEvery, lookBack, timeline, + maxSignals: 100, }; export const machineLearningRule: MachineLearningRule = { @@ -265,6 +272,7 @@ export const eqlRule: CustomRule = { runsEvery, lookBack, timeline, + maxSignals: 100, }; export const eqlSequenceRule: CustomRule = { @@ -285,6 +293,7 @@ export const eqlSequenceRule: CustomRule = { runsEvery, lookBack, timeline, + maxSignals: 100, }; export const newThreatIndicatorRule: ThreatIndicatorRule = { @@ -304,6 +313,7 @@ export const newThreatIndicatorRule: ThreatIndicatorRule = { indicatorMapping: 'agent.id', indicatorIndexField: 'agent.threat', timeline, + maxSignals: 100, }; export const severitiesOverride = ['Low', 'Medium', 'High', 'Critical']; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 99f5bd9c20230..4bf5508c19aa9 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -85,6 +85,7 @@ export const createCustomRuleActivated = (rule: CustomRule, ruleId = '1') => language: 'kuery', enabled: true, tags: ['rule1'], + max_signals: 500, }, headers: { 'kbn-xsrf': 'cypress-creds' }, failOnStatusCode: false, diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index 411f326a0ace6..21a2745395419 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -34,11 +34,6 @@ export const activatesRule = () => { }); }; -export const deactivatesRule = () => { - cy.get(RULE_SWITCH).should('be.visible'); - cy.get(RULE_SWITCH).click(); -}; - export const addsException = (exception: Exception) => { cy.get(LOADING_SPINNER).should('exist'); cy.get(LOADING_SPINNER).should('not.exist'); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 74fe97d0c7210..da5cf720d5315 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -251,6 +251,7 @@ const EditRulePageComponent: FC = () => { rule ), ...(ruleId ? { id: ruleId } : {}), + ...(rule != null ? { max_signals: rule.max_signals } : {}), }); } }, [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_max_signals.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_max_signals.json new file mode 100644 index 0000000000000..d03eb8e2366ae --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_max_signals.json @@ -0,0 +1,9 @@ +{ + "name": "Query With Max Signals", + "description": "Simplest query with max signals set to something other than default", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin", + "max_signals": 500 +} From 3e026a3c3c983fddba78eca749e17721e58ae619 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 1 Mar 2021 18:07:57 -0800 Subject: [PATCH 2/6] [DOCS] Fixes links for machine learning alerts (#92744) Co-authored-by: Yuliia Naumenko --- docs/user/alerting/alert-types.asciidoc | 2 +- docs/user/alerting/alerting-getting-started.asciidoc | 2 +- docs/user/alerting/defining-alerts.asciidoc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 5983804c5c862..5afce8fa6cd93 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -28,7 +28,7 @@ For domain-specific alerts, refer to the documentation for that app. * {observability-guide}/create-alerts.html[Observability alerts] * {security-guide}/prebuilt-rules.html[Security alerts] * <> -* <> +* {ml-docs}/ml-configuring-alerts.html[{ml-cap} alerts] [NOTE] ============================================== diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 0a7c17576de3d..8a83a0f8799de 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -5,7 +5,7 @@ -- -Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with {observability-guide}/create-alerts.html[*Observability*], {security-guide}/prebuilt-rules.html[*Security*], <> and <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> (known as stack alerts) for you to use. +Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with {observability-guide}/create-alerts.html[*Observability*], {security-guide}/prebuilt-rules.html[*Security*], <> and {ml-docs}/ml-configuring-alerts.html[*{ml-app}*], can be centrally managed from the <> UI, and provides a set of built-in <> and <> (known as stack alerts) for you to use. image::images/alerting-overview.png[Alerts and actions UI] diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 77a4e5cc41ef2..396896754f2b0 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -2,7 +2,7 @@ [[defining-alerts]] == Defining alerts -{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. +{kib} alerts can be created in a variety of apps including <>, <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. [float] === Alert flyout From 6897a4ac0b0484ba079d83b971f6f2da13084f0a Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 1 Mar 2021 21:39:32 -0500 Subject: [PATCH 3/6] [APM] Fix hidden search bar in error pages while loading (#84476) (#93139) --- .../app/ErrorGroupDetails/index.tsx | 68 +++++++++++-------- .../app/error_group_overview/index.tsx | 2 +- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index 4b8d8ddc6f746..5fcd2914f2225 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -60,6 +60,45 @@ function getShortGroupId(errorGroupId?: string) { return errorGroupId.slice(0, 5); } +function ErrorGroupHeader({ + groupId, + isUnhandled, +}: { + groupId: string; + isUnhandled?: boolean; +}) { + return ( + <> + + + + +

+ {i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', { + defaultMessage: 'Error group {errorGroupId}', + values: { + errorGroupId: getShortGroupId(groupId), + }, + })} +

+
+
+ {isUnhandled && ( + + + {i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', { + defaultMessage: 'Unhandled', + })} + + + )} +
+
+ + + ); +} + type ErrorGroupDetailsProps = RouteComponentProps<{ groupId: string; serviceName: string; @@ -101,7 +140,7 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) { useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 }); if (!errorGroupData || !errorDistributionData) { - return null; + return ; } // If there are 0 occurrences, show only distribution chart w. empty message @@ -114,32 +153,7 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) { return ( <> - - - - -

- {i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', { - defaultMessage: 'Error group {errorGroupId}', - values: { - errorGroupId: getShortGroupId(groupId), - }, - })} -

-
-
- {isUnhandled && ( - - - {i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', { - defaultMessage: 'Unhandled', - })} - - - )} -
-
- + diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index 49e1cc44f9255..96657bcfb4734 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -68,7 +68,7 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 }); if (!errorDistributionData || !errorGroupListData) { - return null; + return ; } return ( From 7a1944a5a069078f59759ff092e5635afaa36ad2 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 1 Mar 2021 19:34:27 -0800 Subject: [PATCH 4/6] [Alerting][Docs] Changed alerting documentation to point to a single source of explaining the configurations. (#92942) * [Alerting][Docs] Changed alerting documentation to poin to a single source of explaining the configurations. * fixed due to comments * fixed due to comments * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * fixed due to comments Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/settings/alert-action-settings.asciidoc | 20 +++++++++++++++++ .../pre-configured-connectors.asciidoc | 2 +- .../alerting-getting-started.asciidoc | 2 +- ...lerting-production-considerations.asciidoc | 8 +++---- docs/user/alerting/defining-alerts.asciidoc | 22 +------------------ x-pack/plugins/triggers_actions_ui/README.md | 2 +- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index d6f5fb1baba8e..6813a77776b5b 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -40,6 +40,8 @@ You can configure the following settings in the `kibana.yml` file. [cols="2*<"] |=== +| `xpack.actions.enabled` + | Feature toggle that enables Actions in {kib}. Defaults to `true`. | `xpack.actions.allowedHosts` {ess-icon} | A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly added to the allowed hosts. An empty list `[]` can be used to block built-in actions from making any external connections. + @@ -51,6 +53,24 @@ You can configure the following settings in the `kibana.yml` file. + Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function. +| `xpack.actions.preconfigured` + | Specifies preconfigured action IDs and configs. Defaults to {}. + +| `xpack.actions.proxyUrl` {ess-icon} + | Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used. + +| `xpack.actions.proxyHeaders` {ess-icon} + | Specifies HTTP headers for the proxy, if using a proxy for actions. Defaults to {}. + +a|`xpack.actions.` +`proxyRejectUnauthorizedCertificates` {ess-icon} + | Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Defaults to `true`. + +| `xpack.actions.rejectUnauthorized` {ess-icon} + | Set to `false` to bypass certificate validation for actions. Defaults to `true`. + + + + As an alternative to setting both `xpack.actions.proxyRejectUnauthorizedCertificates` and `xpack.actions.rejectUnauthorized`, you can point the OS level environment variable `NODE_EXTRA_CA_CERTS` to a file that contains the root CAs needed to trust certificates. + |=== [float] diff --git a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc index 722607ac05f87..a748a06398ef3 100644 --- a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc @@ -95,7 +95,7 @@ This example shows a preconfigured action type with one out-of-the box connector name: 'Server log #xyz' ``` -<1> `enabledActionTypes` excludes the preconfigured action type to prevent creating and deleting connectors. +<1> `enabledActionTypes` prevents the preconfigured action type from creating and deleting connectors. For more details, check <>. <2> `preconfigured` is the setting for defining the list of available connectors for the preconfigured action type. [[managing-pre-configured-action-types]] diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 8a83a0f8799de..6c6e7e6305c81 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -157,7 +157,7 @@ Pre-packaged *alert types* simplify setup, hide the details complex domain-speci If you are using an *on-premises* Elastic Stack deployment: -* In the kibana.yml configuration file, add the <> setting. +* In the kibana.yml configuration file, add the <> setting. * For emails to have a footer with a link back to {kib}, set the <> configuration setting. If you are using an *on-premises* Elastic Stack deployment with <>: diff --git a/docs/user/alerting/alerting-production-considerations.asciidoc b/docs/user/alerting/alerting-production-considerations.asciidoc index 0442b760669cc..58b4a263459b3 100644 --- a/docs/user/alerting/alerting-production-considerations.asciidoc +++ b/docs/user/alerting/alerting-production-considerations.asciidoc @@ -2,9 +2,9 @@ [[alerting-production-considerations]] == Production considerations -{kib} alerting run both alert checks and actions as persistent background tasks managed by the Kibana Task Manager. This has two major benefits: +{kib} alerting runs both alert checks and actions as persistent background tasks managed by the Kibana Task Manager. This has two major benefits: -* *Persistence*: all task state and scheduling is stored in {es}, so if {kib} is restarted, alerts and actions will pick up where they left off. Task definitions for alerts and actions are stored in the index specified by `xpack.task_manager.index` (defaults to `.kibana_task_manager`). It is important to have at least 1 replica of this index for production deployments, since if you lose this index all scheduled alerts and actions are also lost. +* *Persistence*: all task state and scheduling is stored in {es}, so if you restart {kib}, alerts and actions will pick up where they left off. Task definitions for alerts and actions are stored in the index specified by <>. The default is `.kibana_task_manager`. You must have at least one replica of this index for production deployments. If you lose this index, all scheduled alerts and actions are lost. * *Scaling*: multiple {kib} instances can read from and update the same task queue in {es}, allowing the alerting and action load to be distributed across instances. In cases where a {kib} instance no longer has capacity to run alert checks or actions, capacity can be increased by adding additional {kib} instances. [float] @@ -12,7 +12,7 @@ {kib} background tasks are managed by: -* Polling an {es} task index for overdue tasks at 3 second intervals. This interval can be changed using the `xpack.task_manager.poll_interval` setting. +* Polling an {es} task index for overdue tasks at 3 second intervals. You can change this interval using the <> setting. * Tasks are then claiming them by updating them in the {es} index, using optimistic concurrency control to prevent conflicts. Each {kib} instance can run a maximum of 10 concurrent tasks, so a maximum of 10 tasks are claimed each interval. * Tasks are run on the {kib} server. * In the case of alerts which are recurring background checks, upon completion the task is scheduled again according to the <>. @@ -32,4 +32,4 @@ For details on the settings that can influence the performance and throughput of [float] === Deployment considerations -{es} and {kib} instances use the system clock to determine the current time. To ensure schedules are triggered when expected, you should synchronize the clocks of all nodes in the cluster using a time service such as http://www.ntp.org/[Network Time Protocol]. \ No newline at end of file +{es} and {kib} instances use the system clock to determine the current time. To ensure schedules are triggered when expected, you should synchronize the clocks of all nodes in the cluster using a time service such as http://www.ntp.org/[Network Time Protocol]. diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 396896754f2b0..8c8e25cea407a 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -101,29 +101,9 @@ image::images/alert-flyout-add-action.png[You can add multiple actions on an ale [NOTE] ============================================== -Actions are not required on alerts. In some cases you may want to run an alert without actions first to understand its behavior, and configure actions later. +Actions are not required on alerts. You can run an alert without actions to understand its behavior, and then <> later. ============================================== -[float] -[[actions-configuration]] -=== Global actions configuration -Some actions configuration options apply to all actions. -If you are using an *on-prem* Elastic Stack deployment, you can set these in the kibana.yml file. -If you are using a cloud deployment, you can set these via the console. - -Here's a list of the available global configuration options and an explanation of what each one does: - -* `xpack.actions.enabled`: Feature toggle that enables Actions in {kib}. Default: `true` -* `xpack.actions.allowedHosts`: Specifies an array of host names which actions such as email, Slack, PagerDuty, and webhook can connect to. An element of * indicates any host can be connected to. An empty array indicates no hosts can be connected to. Default: [ {asterisk} ] -* `xpack.actions.enabledActionTypes`: Specifies an array of action types that are enabled. An {asterisk} indicates all action types registered are enabled. The action types that {kib} provides are `.email`, `.index`, `.jira`, `.pagerduty`, `.resilient`, `.server-log`, `.servicenow`, `.servicenow-sir`, `.slack`, `.teams`, and `.webhook`. Default: [ {asterisk} ] -* `xpack.actions.preconfigured`: Specifies preconfigured action IDs and configs. Default: {} -* `xpack.actions.proxyUrl`: Specifies the proxy URL to use, if using a proxy for actions. -* `xpack.actions.proxyHeader`: Specifies HTTP headers for proxy, if using a proxy for actions. -* `xpack.actions.proxyRejectUnauthorizedCertificates`: Set to `false` to bypass certificate validation for proxy, if using a proxy for actions. -* `xpack.actions.rejectUnauthorized`: Set to `false` to bypass certificate validation for actions. - -*NOTE:* As an alternative to both `xpack.actions.proxyRejectUnauthorizedCertificates` and `xpack.actions.rejectUnauthorized`, the OS level environment variable `NODE_EXTRA_CA_CERTS` can be set to point to a file that contains the root CA(s) needed for certificates to be trusted. - [float] === Managing alerts diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index 61cad0fc7fd7b..54e627d384936 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -1149,7 +1149,7 @@ triggersActionsUi.actionTypeRegistry.register(getSomeNewActionType()); ## Create and register new action type UI -Before starting the UI implementation, the [server side registration](https://github.com/elastic/kibana/tree/master/x-pack/plugins/actions#kibana-actions-configuration) should be done first. +Before starting the UI implementation, the [server side registration](https://github.com/elastic/kibana/tree/master/x-pack/plugins/actions#action-types) should be done first. Action type UI is expected to be defined as `ActionTypeModel` object. From 90976ee1198ae3c8990da22984ae19da8eac38bc Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Mon, 1 Mar 2021 20:40:54 -0700 Subject: [PATCH 5/6] [Security Solution] [Timeline] Bugfix to include unmapped fields in the timeline event details JSON (#92025) --- .../timeline/events/details/index.ts | 1 + .../__snapshots__/json_view.test.tsx.snap | 2 +- .../components/event_details/columns.tsx | 109 +++--- .../event_details/event_fields_browser.tsx | 1 - .../components/event_details/helpers.tsx | 1 + .../components/event_details/json_view.tsx | 20 +- .../components/event_details/translations.ts | 7 + .../public/common/mock/mock_detail_item.ts | 87 +++-- .../public/common/mock/timeline_results.ts | 2 + .../components/alerts_table/helpers.test.ts | 11 +- .../body/column_headers/header/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 12 +- .../get_column_renderer.test.tsx.snap | 2 +- .../plain_column_renderer.test.tsx.snap | 2 +- .../body/renderers/formatted_field.tsx | 17 +- .../body/renderers/plain_column_renderer.tsx | 4 +- .../public/timelines/store/timeline/model.ts | 3 +- .../search_strategy/helpers/to_array.ts | 45 ++- .../factory/hosts/all/helpers.ts | 8 +- .../factory/hosts/authentications/helpers.ts | 8 +- .../factory/hosts/details/helpers.ts | 8 +- .../hosts/uncommon_processes/helpers.ts | 8 +- .../factory/network/details/helpers.ts | 4 +- .../search_strategy/timeline/eql/helpers.ts | 78 +++-- .../search_strategy/timeline/eql/index.ts | 2 +- .../factory/events/all/helpers.test.ts | 73 ++-- .../timeline/factory/events/all/helpers.ts | 87 +++-- .../timeline/factory/events/all/index.ts | 6 +- .../factory/events/details/helpers.test.ts | 300 +++++++++------- .../factory/events/details/helpers.ts | 105 +++++- .../timeline/factory/events/details/index.ts | 22 +- .../details/query.events_details.dsl.test.ts | 4 + .../details/query.events_details.dsl.ts | 2 + .../timeline/factory/events/mocks.ts | 329 ++++++++++++++++++ .../security_solution/timeline_details.ts | 274 +++++++++++++-- 35 files changed, 1246 insertions(+), 401 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts index 5d6bc33ec49f8..1f9820f8e5c2b 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts @@ -16,6 +16,7 @@ export interface TimelineEventsDetailsItem { values?: Maybe; // eslint-disable-next-line @typescript-eslint/no-explicit-any originalValue?: Maybe; + isObjectArray: boolean; } export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap index 2b681870e92fe..0412b3074e3f1 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap @@ -15,8 +15,8 @@ exports[`JSON View rendering should match snapshot 1`] = ` value="{ \\"_id\\": \\"pEMaMmkBUV60JmNWmWVi\\", \\"_index\\": \\"filebeat-8.0.0-2019.02.19-000001\\", - \\"_type\\": \\"_doc\\", \\"_score\\": 1, + \\"_type\\": \\"_doc\\", \\"@timestamp\\": \\"2019-02-28T16:50:54.621Z\\", \\"agent\\": { \\"ephemeral_id\\": \\"9d391ef2-a734-4787-8891-67031178c641\\", diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index 8fc6633df247f..a62b652492c5f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -85,24 +85,28 @@ export const getColumns = ({ sortable: false, truncateText: false, width: '30px', - render: (field: string) => ( - - c.id === field) !== -1} - data-test-subj={`toggle-field-${field}`} - data-colindex={1} - id={field} - onChange={() => - toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field, - width: DEFAULT_COLUMN_MIN_WIDTH, - }) - } - /> - - ), + render: (field: string, data: EventFieldsData) => { + const label = data.isObjectArray ? i18n.NESTED_COLUMN(field) : i18n.VIEW_COLUMN(field); + return ( + + c.id === field) !== -1} + data-test-subj={`toggle-field-${field}`} + data-colindex={1} + id={field} + onChange={() => + toggleColumn({ + columnHeaderType: defaultColumnHeaderType, + id: field, + width: DEFAULT_COLUMN_MIN_WIDTH, + }) + } + disabled={data.isObjectArray && data.type !== 'geo_point'} + /> + + ); + }, }, { field: 'field', @@ -118,38 +122,42 @@ export const getColumns = ({ - ( -
- - - -
- )} - > - -
+ {data.isObjectArray && data.type !== 'geo_point' ? ( + <>{field} + ) : ( + ( +
+ + + +
+ )} + > + +
+ )}
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index 497768785735b..93d0e6ccfbe3c 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -108,7 +108,6 @@ export const EventFieldsBrowser = React.memo( const columnHeaders = useDeepEqualSelector((state) => { const { columns } = getTimeline(state, timelineId) ?? timelineDefaults; - return getColumnHeaders(columns, browserFields); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx index 7c7b8ba70f9bd..00e2ee276f181 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx @@ -112,6 +112,7 @@ export const getIconFromType = (type: string | null) => { case 'date': return 'clock'; case 'ip': + case 'geo_point': return 'globe'; case 'object': return 'questionInCircle'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx index 449010781d448..c9ca93582cd9a 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx @@ -54,12 +54,14 @@ export const JsonView = React.memo(({ data }) => { JsonView.displayName = 'JsonView'; export const buildJsonView = (data: TimelineEventsDetailsItem[]) => - data.reduce( - (accumulator, item) => - set( - item.field, - Array.isArray(item.originalValue) ? item.originalValue.join() : item.originalValue, - accumulator - ), - {} - ); + data + .sort((a, b) => a.field.localeCompare(b.field)) + .reduce( + (accumulator, item) => + set( + item.field, + Array.isArray(item.originalValue) ? item.originalValue.join() : item.originalValue, + accumulator + ), + {} + ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index c2b7bb4587dbd..3a599b174251a 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -61,3 +61,10 @@ export const VIEW_COLUMN = (field: string) => values: { field }, defaultMessage: 'View {field} column', }); + +export const NESTED_COLUMN = (field: string) => + i18n.translate('xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel', { + values: { field }, + defaultMessage: + 'The {field} field is an object, and is broken down into nested fields which can be added as column', + }); diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts index ca84ef539bec3..198ab084ae0b8 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts @@ -14,105 +14,126 @@ export const mockDetailItemData: TimelineEventsDetailsItem[] = [ field: '_id', originalValue: 'pEMaMmkBUV60JmNWmWVi', values: ['pEMaMmkBUV60JmNWmWVi'], + isObjectArray: false, }, { field: '_index', originalValue: 'filebeat-8.0.0-2019.02.19-000001', values: ['filebeat-8.0.0-2019.02.19-000001'], + isObjectArray: false, }, { field: '_type', originalValue: '_doc', values: ['_doc'], + isObjectArray: false, }, { field: '_score', originalValue: 1, values: ['1'], + isObjectArray: false, }, { field: '@timestamp', originalValue: '2019-02-28T16:50:54.621Z', values: ['2019-02-28T16:50:54.621Z'], + isObjectArray: false, }, { field: 'agent.ephemeral_id', originalValue: '9d391ef2-a734-4787-8891-67031178c641', values: ['9d391ef2-a734-4787-8891-67031178c641'], + isObjectArray: false, }, { field: 'agent.hostname', originalValue: 'siem-kibana', values: ['siem-kibana'], + isObjectArray: false, }, { - field: 'agent.id', - originalValue: '5de03d5f-52f3-482e-91d4-853c7de073c3', - values: ['5de03d5f-52f3-482e-91d4-853c7de073c3'], + field: 'cloud.project.id', + originalValue: 'elastic-beats', + values: ['elastic-beats'], + isObjectArray: false, }, { - field: 'agent.type', - originalValue: 'filebeat', - values: ['filebeat'], + field: 'cloud.provider', + originalValue: 'gce', + values: ['gce'], + isObjectArray: false, }, { - field: 'agent.version', - originalValue: '8.0.0', - values: ['8.0.0'], + field: 'destination.bytes', + originalValue: 584, + values: ['584'], + isObjectArray: false, }, { - field: 'cloud.availability_zone', - originalValue: 'projects/189716325846/zones/us-east1-b', - values: ['projects/189716325846/zones/us-east1-b'], + field: 'destination.ip', + originalValue: '10.47.8.200', + values: ['10.47.8.200'], + isObjectArray: false, }, { - field: 'cloud.instance.id', - originalValue: '5412578377715150143', - values: ['5412578377715150143'], + field: 'agent.id', + originalValue: '5de03d5f-52f3-482e-91d4-853c7de073c3', + values: ['5de03d5f-52f3-482e-91d4-853c7de073c3'], + isObjectArray: false, }, { field: 'cloud.instance.name', originalValue: 'siem-kibana', values: ['siem-kibana'], + isObjectArray: false, }, { field: 'cloud.machine.type', originalValue: 'projects/189716325846/machineTypes/n1-standard-1', values: ['projects/189716325846/machineTypes/n1-standard-1'], + isObjectArray: false, }, { - field: 'cloud.project.id', - originalValue: 'elastic-beats', - values: ['elastic-beats'], - }, - { - field: 'cloud.provider', - originalValue: 'gce', - values: ['gce'], - }, - { - field: 'destination.bytes', - originalValue: 584, - values: ['584'], - }, - { - field: 'destination.ip', - originalValue: '10.47.8.200', - values: ['10.47.8.200'], + field: 'agent.type', + originalValue: 'filebeat', + values: ['filebeat'], + isObjectArray: false, }, { field: 'destination.packets', originalValue: 4, values: ['4'], + isObjectArray: false, }, { field: 'destination.port', originalValue: 902, values: ['902'], + isObjectArray: false, }, { field: 'event.kind', originalValue: 'event', values: ['event'], + isObjectArray: false, + }, + { + field: 'agent.version', + originalValue: '8.0.0', + values: ['8.0.0'], + isObjectArray: false, + }, + { + field: 'cloud.availability_zone', + originalValue: 'projects/189716325846/zones/us-east1-b', + values: ['projects/189716325846/zones/us-east1-b'], + isObjectArray: false, + }, + { + field: 'cloud.instance.id', + originalValue: '5412578377715150143', + values: ['5412578377715150143'], + isObjectArray: false, }, ]; diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 70ed497ce0cac..26b30e0d1f89a 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2287,11 +2287,13 @@ export const mockTimelineDetails: TimelineEventsDetailsItem[] = [ field: 'host.name', values: ['apache'], originalValue: 'apache', + isObjectArray: false, }, { field: 'user.id', values: ['1'], originalValue: 1, + isObjectArray: false, }, ]; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts index 3eb00f8534979..c296b75a0a253 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts @@ -29,6 +29,7 @@ describe('helpers', () => { { field: 'x', values: ['The nickname of the developer we all :heart:'], + isObjectArray: false, originalValue: 'The nickname of the developer we all :heart:', }, ]); @@ -40,6 +41,7 @@ describe('helpers', () => { { field: 'x', values: ['The nickname of the developer we all :heart:'], + isObjectArray: false, originalValue: 'The nickname of the developer we all :heart:', }, ]); @@ -51,6 +53,7 @@ describe('helpers', () => { { field: 'x', values: ['The nickname of the developer we all :heart:', 'We are all made of stars'], + isObjectArray: false, originalValue: 'The nickname of the developer we all :heart:', }, ]); @@ -65,6 +68,7 @@ describe('helpers', () => { { field: 'x.y.z', values: ['zed'], + isObjectArray: false, originalValue: 'zed', }, ]); @@ -76,6 +80,7 @@ describe('helpers', () => { { field: 'x.y.z', values: ['zed'], + isObjectArray: false, originalValue: 'zed', }, ]); @@ -90,6 +95,7 @@ describe('helpers', () => { { field: 'a', values: (5 as unknown) as string[], + isObjectArray: false, originalValue: 'zed', }, ], @@ -104,7 +110,7 @@ describe('helpers', () => { 'when trying to access field:', 'a', 'from data object of:', - [{ field: 'a', originalValue: 'zed', values: 5 }] + [{ field: 'a', isObjectArray: false, originalValue: 'zed', values: 5 }] ); }); @@ -116,6 +122,7 @@ describe('helpers', () => { { field: 'a', values: (['hi', 5] as unknown) as string[], + isObjectArray: false, originalValue: 'zed', }, ], @@ -130,7 +137,7 @@ describe('helpers', () => { 'when trying to access field:', 'a', 'from data object of:', - [{ field: 'a', originalValue: 'zed', values: ['hi', 5] }] + [{ field: 'a', isObjectArray: false, originalValue: 'zed', values: ['hi', 5] }] ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx index bec241e10d613..ece28faedb951 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx @@ -86,6 +86,7 @@ export const HeaderComponent: React.FC = ({ getManageTimelineById, timelineId, ]); + const showSortingCapability = !isEqlOn && !(header.subType && header.subType.nested); return ( <> @@ -94,7 +95,7 @@ export const HeaderComponent: React.FC = ({ isLoading={isLoading} isResizing={false} onClick={onColumnSort} - showSortingCapability={!isEqlOn} + showSortingCapability={showSortingCapability} sort={sort} > @@ -99,7 +99,7 @@ exports[`Columns it renders the expected columns 1`] = ` fieldFormat="" fieldName="event.action" fieldType="" - key="plain-column-renderer-formatted-field-value-test-event.action-1-event.action-Action" + key="plain-column-renderer-formatted-field-value-test-event.action-1-event.action-Action-0" truncate={true} value="Action" /> @@ -129,7 +129,7 @@ exports[`Columns it renders the expected columns 1`] = ` fieldFormat="" fieldName="host.name" fieldType="" - key="plain-column-renderer-formatted-field-value-test-host.name-1-host.name-apache" + key="plain-column-renderer-formatted-field-value-test-host.name-1-host.name-apache-0" truncate={true} value="apache" /> @@ -159,7 +159,7 @@ exports[`Columns it renders the expected columns 1`] = ` fieldFormat="" fieldName="source.ip" fieldType="" - key="plain-column-renderer-formatted-field-value-test-source.ip-1-source.ip-192.168.0.1" + key="plain-column-renderer-formatted-field-value-test-source.ip-1-source.ip-192.168.0.1-0" truncate={true} value="192.168.0.1" /> @@ -189,7 +189,7 @@ exports[`Columns it renders the expected columns 1`] = ` fieldFormat="" fieldName="destination.ip" fieldType="" - key="plain-column-renderer-formatted-field-value-test-destination.ip-1-destination.ip-192.168.0.3" + key="plain-column-renderer-formatted-field-value-test-destination.ip-1-destination.ip-192.168.0.3-0" truncate={true} value="192.168.0.3" /> @@ -219,7 +219,7 @@ exports[`Columns it renders the expected columns 1`] = ` fieldFormat="" fieldName="user.name" fieldType="" - key="plain-column-renderer-formatted-field-value-test-user.name-1-user.name-john.dee" + key="plain-column-renderer-formatted-field-value-test-user.name-1-user.name-john.dee-0" truncate={true} value="john.dee" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap index e2c46a07af8cc..4da4e12e0f7b3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap @@ -8,7 +8,7 @@ exports[`get_column_renderer renders correctly against snapshot 1`] = ` fieldFormat="" fieldName="event.severity" fieldType="" - key="plain-column-renderer-formatted-field-value-test-event.severity-1-message-3" + key="plain-column-renderer-formatted-field-value-test-event.severity-1-message-3-0" value="3" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap index 8ea7708bf5907..13912e6ad3da9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap @@ -8,7 +8,7 @@ exports[`plain_column_renderer rendering renders correctly against snapshot 1`] fieldFormat="" fieldName="event.category" fieldType="keyword" - key="plain-column-renderer-formatted-field-value-test-event.category-1-event.category-Access" + key="plain-column-renderer-formatted-field-value-test-event.category-1-event.category-Access-0" value="Access" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index fa3612f08204d..3032f556251f3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -41,14 +41,27 @@ const columnNamesNotDraggable = [MESSAGE_FIELD_NAME]; const FormattedFieldValueComponent: React.FC<{ contextId: string; eventId: string; + isObjectArray?: boolean; fieldFormat?: string; fieldName: string; fieldType: string; truncate?: boolean; value: string | number | undefined | null; linkValue?: string | null | undefined; -}> = ({ contextId, eventId, fieldFormat, fieldName, fieldType, truncate, value, linkValue }) => { - if (fieldType === IP_FIELD_TYPE) { +}> = ({ + contextId, + eventId, + fieldFormat, + fieldName, + fieldType, + isObjectArray = false, + truncate, + value, + linkValue, +}) => { + if (isObjectArray) { + return <>{value}; + } else if (fieldType === IP_FIELD_TYPE) { return ( values != null - ? values.map((value) => ( + ? values.map((value, i) => ( (value: T | T[] | null): T[] => Array.isArray(value) ? value : value == null ? [] : [value]; - export const toStringArray = (value: T | T[] | null): string[] => { if (Array.isArray(value)) { return value.reduce((acc, v) => { @@ -42,3 +41,47 @@ export const toStringArray = (value: T | T[] | null): string[] => { return [`${value}`]; } }; +export const toObjectArrayOfStrings = ( + value: T | T[] | null +): Array<{ + str: string; + isObjectArray?: boolean; +}> => { + if (Array.isArray(value)) { + return value.reduce< + Array<{ + str: string; + isObjectArray?: boolean; + }> + >((acc, v) => { + if (v != null) { + switch (typeof v) { + case 'number': + case 'boolean': + return [...acc, { str: v.toString() }]; + case 'object': + try { + return [...acc, { str: JSON.stringify(v), isObjectArray: true }]; // need to track when string is not a simple value + } catch { + return [...acc, { str: 'Invalid Object' }]; + } + case 'string': + return [...acc, { str: v }]; + default: + return [...acc, { str: `${v}` }]; + } + } + return acc; + }, []); + } else if (value == null) { + return []; + } else if (!Array.isArray(value) && typeof value === 'object') { + try { + return [{ str: JSON.stringify(value), isObjectArray: true }]; + } catch { + return [{ str: 'Invalid Object' }]; + } + } else { + return [{ str: `${value}` }]; + } +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts index 505f99dd28455..8b2397fd7fab0 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts @@ -11,7 +11,7 @@ import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostsEdges } from '../../../../../../common/search_strategy/security_solution/hosts'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; -import { toStringArray } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; export const HOSTS_FIELDS: readonly string[] = [ '_id', @@ -33,7 +33,11 @@ export const formatHostEdgesData = ( flattenedFields.cursor.value = hostId || ''; const fieldValue = getHostFieldValue(fieldName, bucket); if (fieldValue != null) { - return set(`node.${fieldName}`, toStringArray(fieldValue), flattenedFields); + return set( + `node.${fieldName}`, + toObjectArrayOfStrings(fieldValue).map(({ str }) => str), + flattenedFields + ); } return flattenedFields; }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts index 06d81140f475e..aeaefe690cbde 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts @@ -8,7 +8,7 @@ import { get, getOr, isEmpty } from 'lodash/fp'; import { set } from '@elastic/safer-lodash-set/fp'; import { mergeFieldsWithHit } from '../../../../../utils/build_query'; -import { toStringArray } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; import { AuthenticationsEdges, AuthenticationHit, @@ -55,7 +55,11 @@ export const formatAuthenticationData = ( const fieldPath = `node.${fieldName}`; const fieldValue = get(fieldPath, mergedResult); if (!isEmpty(fieldValue)) { - return set(fieldPath, toStringArray(fieldValue), mergedResult); + return set( + fieldPath, + toObjectArrayOfStrings(fieldValue).map(({ str }) => str), + mergedResult + ); } else { return mergedResult; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index 9c522bd704ef0..2b35517d693d5 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -9,7 +9,7 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostItem } from '../../../../../../common/search_strategy/security_solution/hosts'; -import { toStringArray } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; @@ -42,7 +42,11 @@ export const formatHostItem = (bucket: HostAggEsItem): HostItem => if (fieldName === '_id') { return set('_id', fieldValue, flattenedFields); } - return set(fieldName, toStringArray(fieldValue), flattenedFields); + return set( + fieldName, + toObjectArrayOfStrings(fieldValue).map(({ str }) => str), + flattenedFields + ); } return flattenedFields; }, {}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts index 7b01f4e7dc816..fe202b48540d7 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts @@ -14,7 +14,7 @@ import { HostsUncommonProcessesEdges, HostsUncommonProcessHit, } from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; -import { toStringArray } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; import { HostHits } from '../../../../../../common/search_strategy'; export const uncommonProcessesFields = [ @@ -82,7 +82,11 @@ export const formatUncommonProcessesData = ( fieldPath = `node.hosts.0.name`; fieldValue = get(fieldPath, mergedResult); } - return set(fieldPath, toStringArray(fieldValue), mergedResult); + return set( + fieldPath, + toObjectArrayOfStrings(fieldValue).map(({ str }) => str), + mergedResult + ); }, { node: { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts index 3b387597618e8..8fc7ae0304a35 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts @@ -13,7 +13,7 @@ import { NetworkDetailsHostHit, NetworkHit, } from '../../../../../../common/search_strategy/security_solution/network'; -import { toStringArray } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; export const getNetworkDetailsAgg = (type: string, networkHit: NetworkHit | {}) => { const firstSeen = getOr(null, `firstSeen.value_as_string`, networkHit); @@ -53,7 +53,7 @@ const formatHostEcs = (data: Record | null): HostEcs | null => } return { ...acc, - [key]: toStringArray(value), + [key]: toObjectArrayOfStrings(value).map(({ str }) => str), }; }, {}); }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts index bdd3195b3b756..b007307412e95 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts @@ -7,7 +7,7 @@ import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants'; -import { EqlSearchResponse } from '../../../../common/detection_engine/types'; +import { EqlSearchResponse, EqlSequence } from '../../../../common/detection_engine/types'; import { EventHit, TimelineEdges } from '../../../../common/search_strategy'; import { TimelineEqlRequestOptions, @@ -56,51 +56,53 @@ export const buildEqlDsl = (options: TimelineEqlRequestOptions): Record>, fieldRequested: string[]) => + sequences.reduce>(async (acc, sequence, sequenceIndex) => { + const sequenceParentId = sequence.events[0]?._id ?? null; + const data = await acc; + const allData = await Promise.all( + sequence.events.map(async (event, eventIndex) => { + const item = await formatTimelineData( + fieldRequested, + TIMELINE_EVENTS_FIELDS, + event as EventHit + ); + return Promise.resolve({ + ...item, + node: { + ...item.node, + ecs: { + ...item.node.ecs, + ...(sequenceParentId != null + ? { + eql: { + parentId: sequenceParentId, + sequenceNumber: `${sequenceIndex}-${eventIndex}`, + }, + } + : {}), + }, + }, + }); + }) + ); + return Promise.resolve([...data, ...allData]); + }, Promise.resolve([])); -export const parseEqlResponse = ( +export const parseEqlResponse = async ( options: TimelineEqlRequestOptions, response: EqlSearchStrategyResponse> ): Promise => { const { activePage, querySize } = options.pagination; - // const totalCount = response.rawResponse?.body?.hits?.total?.value ?? 0; let edges: TimelineEdges[] = []; + if (response.rawResponse.body.hits.sequences !== undefined) { - edges = response.rawResponse.body.hits.sequences.reduce( - (data, sequence, sequenceIndex) => { - const sequenceParentId = sequence.events[0]?._id ?? null; - return [ - ...data, - ...sequence.events.map((event, eventIndex) => { - const item = formatTimelineData( - options.fieldRequested, - TIMELINE_EVENTS_FIELDS, - event as EventHit - ); - return { - ...item, - node: { - ...item.node, - ecs: { - ...item.node.ecs, - ...(sequenceParentId != null - ? { - eql: { - parentId: sequenceParentId, - sequenceNumber: `${sequenceIndex}-${eventIndex}`, - }, - } - : {}), - }, - }, - }; - }), - ]; - }, - [] - ); + edges = await parseSequences(response.rawResponse.body.hits.sequences, options.fieldRequested); } else if (response.rawResponse.body.hits.events !== undefined) { - edges = response.rawResponse.body.hits.events.map((event) => - formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, event as EventHit) + edges = await Promise.all( + response.rawResponse.body.hits.events.map(async (event) => + formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, event as EventHit) + ) ); } diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts index cf7877e987ace..249f5582d2a39 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts @@ -38,7 +38,7 @@ export const securitySolutionTimelineEqlSearchStrategyProvider = ( }, }; }), - mergeMap((esSearchRes) => + mergeMap(async (esSearchRes) => parseEqlResponse( request, (esSearchRes as unknown) as EqlSearchStrategyResponse> diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts index 10bb606dc2387..61af6a7664faa 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts @@ -8,54 +8,23 @@ import { EventHit } from '../../../../../../common/search_strategy'; import { TIMELINE_EVENTS_FIELDS } from './constants'; import { formatTimelineData } from './helpers'; +import { eventHit } from '../mocks'; describe('#formatTimelineData', () => { - it('happy path', () => { - const response: EventHit = { - _index: 'auditbeat-7.8.0-2020.11.05-000003', - _id: 'tkCt1nUBaEgqnrVSZ8R_', - _score: 0, - _type: '', - fields: { - 'event.category': ['process'], - 'process.ppid': [3977], - 'user.name': ['jenkins'], - 'process.args': ['go', 'vet', './...'], - message: ['Process go (PID: 4313) by user jenkins STARTED'], - 'process.pid': [4313], - 'process.working_directory': [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', - ], - 'process.entity_id': ['Z59cIkAAIw8ZoK0H'], - 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], - 'process.name': ['go'], - 'event.action': ['process_started'], - 'agent.type': ['auditbeat'], - '@timestamp': ['2020-11-17T14:48:08.922Z'], - 'event.module': ['system'], - 'event.type': ['start'], - 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], - 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'], - 'host.os.family': ['debian'], - 'event.kind': ['event'], - 'host.id': ['e59991e835905c65ed3e455b33e13bd6'], - 'event.dataset': ['process'], - 'process.executable': [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', - ], - }, - _source: {}, - sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'], - aggregations: {}, - }; - - expect( - formatTimelineData( - ['@timestamp', 'host.name', 'destination.ip', 'source.ip'], - TIMELINE_EVENTS_FIELDS, - response - ) - ).toEqual({ + it('happy path', async () => { + const res = await formatTimelineData( + [ + '@timestamp', + 'host.name', + 'destination.ip', + 'source.ip', + 'source.geo.location', + 'threat.indicator.matched.field', + ], + TIMELINE_EVENTS_FIELDS, + eventHit + ); + expect(res).toEqual({ cursor: { tiebreaker: 'beats-ci-immutable-ubuntu-1804-1605624279743236239', value: '1605624488922', @@ -72,6 +41,14 @@ describe('#formatTimelineData', () => { field: 'host.name', value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], }, + { + field: 'source.geo.location', + value: [`{"lon":118.7778,"lat":32.0617}`], + }, + { + field: 'threat.indicator.matched.field', + value: ['matched_field', 'matched_field_2'], + }, ], ecs: { '@timestamp': ['2020-11-17T14:48:08.922Z'], @@ -122,7 +99,7 @@ describe('#formatTimelineData', () => { }); }); - it('rule signal results', () => { + it('rule signal results', async () => { const response: EventHit = { _index: '.siem-signals-patrykkopycinski-default-000007', _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', @@ -290,7 +267,7 @@ describe('#formatTimelineData', () => { }; expect( - formatTimelineData( + await formatTimelineData( ['@timestamp', 'host.name', 'destination.ip', 'source.ip'], TIMELINE_EVENTS_FIELDS, response diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts index 1a83cbf40f1f4..e5bb8cb7e14b7 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts @@ -6,9 +6,13 @@ */ import { get, has, merge, uniq } from 'lodash/fp'; -import { EventHit, TimelineEdges } from '../../../../../../common/search_strategy'; +import { + EventHit, + TimelineEdges, + TimelineNonEcsData, +} from '../../../../../../common/search_strategy'; import { toStringArray } from '../../../../helpers/to_array'; -import { formatGeoLocation, isGeoField } from '../details/helpers'; +import { getDataSafety, getDataFromFieldsHits } from '../details/helpers'; const getTimestamp = (hit: EventHit): string => { if (hit.fields && hit.fields['@timestamp']) { @@ -19,13 +23,14 @@ const getTimestamp = (hit: EventHit): string => { return ''; }; -export const formatTimelineData = ( +export const formatTimelineData = async ( dataFields: readonly string[], ecsFields: readonly string[], hit: EventHit ) => - uniq([...ecsFields, ...dataFields]).reduce( - (flattenedFields, fieldName) => { + uniq([...ecsFields, ...dataFields]).reduce>( + async (acc, fieldName) => { + const flattenedFields: TimelineEdges = await acc; flattenedFields.node._id = hit._id; flattenedFields.node._index = hit._index; flattenedFields.node.ecs._id = hit._id; @@ -35,30 +40,81 @@ export const formatTimelineData = ( flattenedFields.cursor.value = hit.sort[0]; flattenedFields.cursor.tiebreaker = hit.sort[1]; } - return mergeTimelineFieldsWithHit(fieldName, flattenedFields, hit, dataFields, ecsFields); + const waitForIt = await mergeTimelineFieldsWithHit( + fieldName, + flattenedFields, + hit, + dataFields, + ecsFields + ); + return Promise.resolve(waitForIt); }, - { + Promise.resolve({ node: { ecs: { _id: '' }, data: [], _id: '', _index: '' }, cursor: { value: '', tiebreaker: null, }, - } + }) ); const specialFields = ['_id', '_index', '_type', '_score']; -const mergeTimelineFieldsWithHit = ( +const getValuesFromFields = async ( + fieldName: string, + hit: EventHit, + nestedParentFieldName?: string +): Promise => { + if (specialFields.includes(fieldName)) { + return [{ field: fieldName, value: toStringArray(get(fieldName, hit)) }]; + } + + let fieldToEval; + if (has(fieldName, hit._source)) { + fieldToEval = { + [fieldName]: get(fieldName, hit._source), + }; + } else { + if (nestedParentFieldName == null || nestedParentFieldName === fieldName) { + fieldToEval = { + [fieldName]: hit.fields[fieldName], + }; + } else if (nestedParentFieldName != null) { + fieldToEval = { + [nestedParentFieldName]: hit.fields[nestedParentFieldName], + }; + } else { + // fallback, should never hit + fieldToEval = { + [fieldName]: [], + }; + } + } + const formattedData = await getDataSafety(getDataFromFieldsHits, fieldToEval); + return formattedData.reduce( + (acc: TimelineNonEcsData[], { field, values }) => + // nested fields return all field values, pick only the one we asked for + field.includes(fieldName) ? [...acc, { field, value: values }] : acc, + [] + ); +}; + +const mergeTimelineFieldsWithHit = async ( fieldName: string, flattenedFields: T, - hit: { _source: {}; fields: Record }, + hit: EventHit, dataFields: readonly string[], ecsFields: readonly string[] ) => { if (fieldName != null || dataFields.includes(fieldName)) { + const fieldNameAsArray = fieldName.split('.'); + const nestedParentFieldName = Object.keys(hit.fields ?? []).find((f) => { + return f === fieldNameAsArray.slice(0, f.split('.').length).join('.'); + }); if ( has(fieldName, hit._source) || has(fieldName, hit.fields) || + nestedParentFieldName != null || specialFields.includes(fieldName) ) { const objectWithProperty = { @@ -67,16 +123,7 @@ const mergeTimelineFieldsWithHit = ( data: dataFields.includes(fieldName) ? [ ...get('node.data', flattenedFields), - { - field: fieldName, - value: specialFields.includes(fieldName) - ? toStringArray(get(fieldName, hit)) - : isGeoField(fieldName) - ? formatGeoLocation(hit.fields[fieldName]) - : has(fieldName, hit._source) - ? toStringArray(get(fieldName, hit._source)) - : toStringArray(hit.fields[fieldName]), - }, + ...(await getValuesFromFields(fieldName, hit, nestedParentFieldName)), ] : get('node.data', flattenedFields), ecs: ecsFields.includes(fieldName) diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts index 93985baed770e..05058e3ee7a2d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts @@ -40,8 +40,10 @@ export const timelineEventsAll: SecuritySolutionTimelineFactory - formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit as EventHit) + const edges: TimelineEdges[] = await Promise.all( + hits.map((hit) => + formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit as EventHit) + ) ); const inspect = { dsl: [inspectStringifyObject(buildTimelineEventsAllQuery(queryOptions))], diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts index ca9a4b708161d..dc3efc6909c63 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts @@ -5,150 +5,192 @@ * 2.0. */ -import { EventHit } from '../../../../../../common/search_strategy'; -import { getDataFromFieldsHits } from './helpers'; +import { EventHit, EventSource } from '../../../../../../common/search_strategy'; +import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './helpers'; +import { eventDetailsFormattedFields, eventHit } from '../mocks'; -describe('#getDataFromFieldsHits', () => { - it('happy path', () => { - const response: EventHit = { - _index: 'auditbeat-7.8.0-2020.11.05-000003', - _id: 'tkCt1nUBaEgqnrVSZ8R_', - _score: 0, - _type: '', - fields: { - 'event.category': ['process'], - 'process.ppid': [3977], - 'user.name': ['jenkins'], - 'process.args': ['go', 'vet', './...'], - message: ['Process go (PID: 4313) by user jenkins STARTED'], - 'process.pid': [4313], - 'process.working_directory': [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', +describe('Events Details Helpers', () => { + const fields: EventHit['fields'] = eventHit.fields; + const resultFields = eventDetailsFormattedFields; + describe('#getDataFromFieldsHits', () => { + it('happy path', () => { + const result = getDataFromFieldsHits(fields); + expect(result).toEqual(resultFields); + }); + it('lets get weird', () => { + const whackFields = { + 'crazy.pants': [ + { + 'matched.field': ['matched_field'], + first_seen: ['2021-02-22T17:29:25.195Z'], + provider: ['yourself'], + type: ['custom'], + 'matched.atomic': ['matched_atomic'], + lazer: [ + { + 'great.field': ['grrrrr'], + lazer: [ + { + lazer: [ + { + cool: true, + lazer: [ + { + lazer: [ + { + lazer: [ + { + lazer: [ + { + whoa: false, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + lazer: [ + { + cool: false, + }, + ], + }, + ], + }, + { + 'great.field': ['grrrrr_2'], + }, + ], + }, ], - 'process.entity_id': ['Z59cIkAAIw8ZoK0H'], - 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], - 'process.name': ['go'], - 'event.action': ['process_started'], - 'agent.type': ['auditbeat'], - '@timestamp': ['2020-11-17T14:48:08.922Z'], - 'event.module': ['system'], - 'event.type': ['start'], - 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], - 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'], - 'host.os.family': ['debian'], - 'event.kind': ['event'], - 'host.id': ['e59991e835905c65ed3e455b33e13bd6'], - 'event.dataset': ['process'], - 'process.executable': [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', - ], - }, - _source: {}, - sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'], - aggregations: {}, + }; + const whackResultFields = [ + { + category: 'crazy', + field: 'crazy.pants.matched.field', + values: ['matched_field'], + originalValue: ['matched_field'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.first_seen', + values: ['2021-02-22T17:29:25.195Z'], + originalValue: ['2021-02-22T17:29:25.195Z'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.provider', + values: ['yourself'], + originalValue: ['yourself'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.type', + values: ['custom'], + originalValue: ['custom'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.matched.atomic', + values: ['matched_atomic'], + originalValue: ['matched_atomic'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.lazer.great.field', + values: ['grrrrr', 'grrrrr_2'], + originalValue: ['grrrrr', 'grrrrr_2'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.lazer.lazer.lazer.cool', + values: ['true', 'false'], + originalValue: ['true', 'false'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.lazer.lazer.lazer.lazer.lazer.lazer.lazer.whoa', + values: ['false'], + originalValue: ['false'], + isObjectArray: false, + }, + ]; + const result = getDataFromFieldsHits(whackFields); + expect(result).toEqual(whackResultFields); + }); + }); + it('#getDataFromSourceHits', () => { + const _source: EventSource = { + '@timestamp': '2021-02-24T00:41:06.527Z', + 'signal.status': 'open', + 'signal.rule.name': 'Rawr', + 'threat.indicator': [ + { + provider: 'yourself', + type: 'custom', + first_seen: ['2021-02-22T17:29:25.195Z'], + matched: { atomic: 'atom', field: 'field', type: 'type' }, + }, + { + provider: 'other_you', + type: 'custom', + first_seen: '2021-02-22T17:29:25.195Z', + matched: { atomic: 'atom', field: 'field', type: 'type' }, + }, + ], }; - - expect(getDataFromFieldsHits(response.fields)).toEqual([ - { - category: 'event', - field: 'event.category', - originalValue: ['process'], - values: ['process'], - }, - { category: 'process', field: 'process.ppid', originalValue: ['3977'], values: ['3977'] }, - { category: 'user', field: 'user.name', originalValue: ['jenkins'], values: ['jenkins'] }, - { - category: 'process', - field: 'process.args', - originalValue: ['go', 'vet', './...'], - values: ['go', 'vet', './...'], - }, - { - category: 'base', - field: 'message', - originalValue: ['Process go (PID: 4313) by user jenkins STARTED'], - values: ['Process go (PID: 4313) by user jenkins STARTED'], - }, - { category: 'process', field: 'process.pid', originalValue: ['4313'], values: ['4313'] }, - { - category: 'process', - field: 'process.working_directory', - originalValue: [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', - ], - values: [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', - ], - }, - { - category: 'process', - field: 'process.entity_id', - originalValue: ['Z59cIkAAIw8ZoK0H'], - values: ['Z59cIkAAIw8ZoK0H'], - }, - { - category: 'host', - field: 'host.ip', - originalValue: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], - values: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], - }, - { category: 'process', field: 'process.name', originalValue: ['go'], values: ['go'] }, - { - category: 'event', - field: 'event.action', - originalValue: ['process_started'], - values: ['process_started'], - }, - { - category: 'agent', - field: 'agent.type', - originalValue: ['auditbeat'], - values: ['auditbeat'], - }, + expect(getDataFromSourceHits(_source)).toEqual([ { category: 'base', field: '@timestamp', - originalValue: ['2020-11-17T14:48:08.922Z'], - values: ['2020-11-17T14:48:08.922Z'], + values: ['2021-02-24T00:41:06.527Z'], + originalValue: ['2021-02-24T00:41:06.527Z'], + isObjectArray: false, }, - { category: 'event', field: 'event.module', originalValue: ['system'], values: ['system'] }, - { category: 'event', field: 'event.type', originalValue: ['start'], values: ['start'] }, { - category: 'host', - field: 'host.name', - originalValue: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], - values: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + category: 'signal', + field: 'signal.status', + values: ['open'], + originalValue: ['open'], + isObjectArray: false, }, { - category: 'process', - field: 'process.hash.sha1', - originalValue: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], - values: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + category: 'signal', + field: 'signal.rule.name', + values: ['Rawr'], + originalValue: ['Rawr'], + isObjectArray: false, }, - { category: 'host', field: 'host.os.family', originalValue: ['debian'], values: ['debian'] }, - { category: 'event', field: 'event.kind', originalValue: ['event'], values: ['event'] }, { - category: 'host', - field: 'host.id', - originalValue: ['e59991e835905c65ed3e455b33e13bd6'], - values: ['e59991e835905c65ed3e455b33e13bd6'], - }, - { - category: 'event', - field: 'event.dataset', - originalValue: ['process'], - values: ['process'], - }, - { - category: 'process', - field: 'process.executable', - originalValue: [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', - ], + category: 'threat', + field: 'threat.indicator', values: [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + '{"provider":"yourself","type":"custom","first_seen":["2021-02-22T17:29:25.195Z"],"matched":{"atomic":"atom","field":"field","type":"type"}}', + '{"provider":"other_you","type":"custom","first_seen":"2021-02-22T17:29:25.195Z","matched":{"atomic":"atom","field":"field","type":"type"}}', + ], + originalValue: [ + '{"provider":"yourself","type":"custom","first_seen":["2021-02-22T17:29:25.195Z"],"matched":{"atomic":"atom","field":"field","type":"type"}}', + '{"provider":"other_you","type":"custom","first_seen":"2021-02-22T17:29:25.195Z","matched":{"atomic":"atom","field":"field","type":"type"}}', ], + isObjectArray: true, }, ]); }); + it('#getDataSafety', async () => { + const result = await getDataSafety(getDataFromFieldsHits, fields); + expect(result).toEqual(resultFields); + }); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts index 779454e9474ee..2fc729729e435 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts @@ -7,8 +7,12 @@ import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp'; -import { EventSource, TimelineEventsDetailsItem } from '../../../../../../common/search_strategy'; -import { toStringArray } from '../../../../helpers/to_array'; +import { + EventHit, + EventSource, + TimelineEventsDetailsItem, +} from '../../../../../../common/search_strategy'; +import { toObjectArrayOfStrings, toStringArray } from '../../../../helpers/to_array'; export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; @@ -24,7 +28,10 @@ export const formatGeoLocation = (item: unknown[]) => { const itemGeo = item.length > 0 ? (item[0] as { coordinates: number[] }) : null; if (itemGeo != null && !isEmpty(itemGeo.coordinates)) { try { - return toStringArray({ long: itemGeo.coordinates[0], lat: itemGeo.coordinates[1] }); + return toStringArray({ + lon: itemGeo.coordinates[0], + lat: itemGeo.coordinates[1], + }); } catch { return toStringArray(item); } @@ -46,13 +53,18 @@ export const getDataFromSourceHits = ( const field = path ? `${path}.${source}` : source; const fieldCategory = getFieldCategory(field); + const objArrStr = toObjectArrayOfStrings(item); + const strArr = objArrStr.map(({ str }) => str); + const isObjectArray = objArrStr.some((o) => o.isObjectArray); + return [ ...accumulator, { category: fieldCategory, field, - values: toStringArray(item), - originalValue: toStringArray(item), + values: strArr, + originalValue: strArr, + isObjectArray, } as TimelineEventsDetailsItem, ]; } else if (isObject(item)) { @@ -65,18 +77,81 @@ export const getDataFromSourceHits = ( }, []); export const getDataFromFieldsHits = ( - fields: Record + fields: EventHit['fields'], + prependField?: string, + prependFieldCategory?: string ): TimelineEventsDetailsItem[] => Object.keys(fields).reduce((accumulator, field) => { const item: unknown[] = fields[field]; - const fieldCategory = getFieldCategory(field); - return [ + + const fieldCategory = + prependFieldCategory != null ? prependFieldCategory : getFieldCategory(field); + if (isGeoField(field)) { + return [ + ...accumulator, + { + category: fieldCategory, + field, + values: formatGeoLocation(item), + originalValue: formatGeoLocation(item), + isObjectArray: true, // important for UI + }, + ]; + } + const objArrStr = toObjectArrayOfStrings(item); + const strArr = objArrStr.map(({ str }) => str); + const isObjectArray = objArrStr.some((o) => o.isObjectArray); + const dotField = prependField ? `${prependField}.${field}` : field; + + // return simple field value (non-object, non-array) + if (!isObjectArray) { + return [ + ...accumulator, + { + category: fieldCategory, + field: dotField, + values: strArr, + originalValue: strArr, + isObjectArray, + }, + ]; + } + + // format nested fields + const nestedFields = Array.isArray(item) + ? item + .reduce((acc, i) => [...acc, getDataFromFieldsHits(i, dotField, fieldCategory)], []) + .flat() + : getDataFromFieldsHits(item, prependField, fieldCategory); + + // combine duplicate fields + const flat: Record = [ ...accumulator, - { - category: fieldCategory, - field, - values: isGeoField(field) ? formatGeoLocation(item) : toStringArray(item), - originalValue: toStringArray(item), - } as TimelineEventsDetailsItem, - ]; + ...nestedFields, + ].reduce( + (acc, f) => ({ + ...acc, + // acc/flat is hashmap to determine if we already have the field or not without an array iteration + // its converted back to array in return with Object.values + ...(acc[f.field] != null + ? { + [f.field]: { + ...f, + originalValue: acc[f.field].originalValue.includes(f.originalValue[0]) + ? acc[f.field].originalValue + : [...acc[f.field].originalValue, ...f.originalValue], + values: acc[f.field].values.includes(f.values[0]) + ? acc[f.field].values + : [...acc[f.field].values, ...f.values], + }, + } + : { [f.field]: f }), + }), + {} + ); + + return Object.values(flat); }, []); + +export const getDataSafety = (fn: (args: A) => T, args: A): Promise => + new Promise((resolve) => setTimeout(() => resolve(fn(args)))); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts index f5deb258fc1f4..7794de7f0f411 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { cloneDeep, merge } from 'lodash/fp'; +import { cloneDeep, merge, unionBy } from 'lodash/fp'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { @@ -13,11 +13,13 @@ import { TimelineEventsQueries, TimelineEventsDetailsStrategyResponse, TimelineEventsDetailsRequestOptions, + TimelineEventsDetailsItem, + EventSource, } from '../../../../../../common/search_strategy'; import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionTimelineFactory } from '../../types'; import { buildTimelineDetailsQuery } from './query.events_details.dsl'; -import { getDataFromSourceHits } from './helpers'; +import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './helpers'; export const timelineEventsDetails: SecuritySolutionTimelineFactory = { buildDsl: (options: TimelineEventsDetailsRequestOptions) => { @@ -29,11 +31,10 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory ): Promise => { const { indexName, eventId, docValueFields = [] } = options; - const { _source, ...hitsData } = cloneDeep(response.rawResponse.hits.hits[0] ?? {}); + const { _source, fields, ...hitsData } = cloneDeep(response.rawResponse.hits.hits[0] ?? {}); const inspect = { dsl: [inspectStringifyObject(buildTimelineDetailsQuery(indexName, eventId, docValueFields))], }; - if (response.isRunning) { return { ...response, @@ -41,12 +42,19 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory( + getDataFromSourceHits, + _source + ); + const fieldsData = await getDataSafety( + getDataFromFieldsHits, + merge(fields, hitsData) + ); - const sourceData = getDataFromSourceHits(merge(_source, hitsData)); - + const data = unionBy('field', fieldsData, sourceData); return { ...response, - data: sourceData, + data, inspect, }; }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts index 47716e21bca31..4545a3a3e136b 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts @@ -24,6 +24,7 @@ describe('buildTimelineDetailsQuery', () => { Object { "allowNoIndices": true, "body": Object { + "_source": true, "docvalue_fields": Array [ Object { "field": "@timestamp", @@ -38,6 +39,9 @@ describe('buildTimelineDetailsQuery', () => { "field": "agent.name", }, ], + "fields": Array [ + "*", + ], "query": Object { "terms": Object { "_id": Array [ diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts index e8890072c1aff..c624eb14ae969 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts @@ -22,6 +22,8 @@ export const buildTimelineDetailsQuery = ( _id: [id], }, }, + fields: ['*'], + _source: true, }, size: 1, }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts new file mode 100644 index 0000000000000..13b7fe7051246 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts @@ -0,0 +1,329 @@ +/* + * 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 eventHit = { + _index: 'auditbeat-7.8.0-2020.11.05-000003', + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _score: 0, + _type: '', + fields: { + 'event.category': ['process'], + 'process.ppid': [3977], + 'user.name': ['jenkins'], + 'process.args': ['go', 'vet', './...'], + message: ['Process go (PID: 4313) by user jenkins STARTED'], + 'process.pid': [4313], + 'process.working_directory': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + 'process.entity_id': ['Z59cIkAAIw8ZoK0H'], + 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + 'process.name': ['go'], + 'event.action': ['process_started'], + 'agent.type': ['auditbeat'], + '@timestamp': ['2020-11-17T14:48:08.922Z'], + 'event.module': ['system'], + 'event.type': ['start'], + 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + 'host.os.family': ['debian'], + 'event.kind': ['event'], + 'host.id': ['e59991e835905c65ed3e455b33e13bd6'], + 'event.dataset': ['process'], + 'process.executable': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + 'source.geo.location': [{ coordinates: [118.7778, 32.0617], type: 'Point' }], + 'threat.indicator': [ + { + 'matched.field': ['matched_field'], + first_seen: ['2021-02-22T17:29:25.195Z'], + provider: ['yourself'], + type: ['custom'], + 'matched.atomic': ['matched_atomic'], + lazer: [ + { + 'great.field': ['grrrrr'], + }, + { + 'great.field': ['grrrrr_2'], + }, + ], + }, + { + 'matched.field': ['matched_field_2'], + first_seen: ['2021-02-22T17:29:25.195Z'], + provider: ['other_you'], + type: ['custom'], + 'matched.atomic': ['matched_atomic_2'], + lazer: [ + { + 'great.field': [ + { + wowoe: [ + { + fooooo: ['grrrrr'], + }, + ], + astring: 'cool', + aNumber: 1, + anObject: { + neat: true, + }, + }, + ], + }, + ], + }, + ], + }, + _source: {}, + sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'], + aggregations: {}, +}; + +export const eventDetailsFormattedFields = [ + { + category: 'event', + field: 'event.category', + isObjectArray: false, + originalValue: ['process'], + values: ['process'], + }, + { + category: 'process', + field: 'process.ppid', + isObjectArray: false, + originalValue: ['3977'], + values: ['3977'], + }, + { + category: 'user', + field: 'user.name', + isObjectArray: false, + originalValue: ['jenkins'], + values: ['jenkins'], + }, + { + category: 'process', + field: 'process.args', + isObjectArray: false, + originalValue: ['go', 'vet', './...'], + values: ['go', 'vet', './...'], + }, + { + category: 'base', + field: 'message', + isObjectArray: false, + originalValue: ['Process go (PID: 4313) by user jenkins STARTED'], + values: ['Process go (PID: 4313) by user jenkins STARTED'], + }, + { + category: 'process', + field: 'process.pid', + isObjectArray: false, + originalValue: ['4313'], + values: ['4313'], + }, + { + category: 'process', + field: 'process.working_directory', + isObjectArray: false, + originalValue: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + values: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + }, + { + category: 'process', + field: 'process.entity_id', + isObjectArray: false, + originalValue: ['Z59cIkAAIw8ZoK0H'], + values: ['Z59cIkAAIw8ZoK0H'], + }, + { + category: 'host', + field: 'host.ip', + isObjectArray: false, + originalValue: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + values: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + }, + { + category: 'process', + field: 'process.name', + isObjectArray: false, + originalValue: ['go'], + values: ['go'], + }, + { + category: 'event', + field: 'event.action', + isObjectArray: false, + originalValue: ['process_started'], + values: ['process_started'], + }, + { + category: 'agent', + field: 'agent.type', + isObjectArray: false, + originalValue: ['auditbeat'], + values: ['auditbeat'], + }, + { + category: 'base', + field: '@timestamp', + isObjectArray: false, + originalValue: ['2020-11-17T14:48:08.922Z'], + values: ['2020-11-17T14:48:08.922Z'], + }, + { + category: 'event', + field: 'event.module', + isObjectArray: false, + originalValue: ['system'], + values: ['system'], + }, + { + category: 'event', + field: 'event.type', + isObjectArray: false, + originalValue: ['start'], + values: ['start'], + }, + { + category: 'host', + field: 'host.name', + isObjectArray: false, + originalValue: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + values: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + }, + { + category: 'process', + field: 'process.hash.sha1', + isObjectArray: false, + originalValue: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + values: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + }, + { + category: 'host', + field: 'host.os.family', + isObjectArray: false, + originalValue: ['debian'], + values: ['debian'], + }, + { + category: 'event', + field: 'event.kind', + isObjectArray: false, + originalValue: ['event'], + values: ['event'], + }, + { + category: 'host', + field: 'host.id', + isObjectArray: false, + originalValue: ['e59991e835905c65ed3e455b33e13bd6'], + values: ['e59991e835905c65ed3e455b33e13bd6'], + }, + { + category: 'event', + field: 'event.dataset', + isObjectArray: false, + originalValue: ['process'], + values: ['process'], + }, + { + category: 'process', + field: 'process.executable', + isObjectArray: false, + originalValue: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + values: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + }, + { + category: 'source', + field: 'source.geo.location', + isObjectArray: true, + originalValue: [`{"lon":118.7778,"lat":32.0617}`], + values: [`{"lon":118.7778,"lat":32.0617}`], + }, + { + category: 'threat', + field: 'threat.indicator.matched.field', + values: ['matched_field', 'matched_field_2'], + originalValue: ['matched_field', 'matched_field_2'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.first_seen', + values: ['2021-02-22T17:29:25.195Z'], + originalValue: ['2021-02-22T17:29:25.195Z'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.provider', + values: ['yourself', 'other_you'], + originalValue: ['yourself', 'other_you'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.type', + values: ['custom'], + originalValue: ['custom'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.matched.atomic', + values: ['matched_atomic', 'matched_atomic_2'], + originalValue: ['matched_atomic', 'matched_atomic_2'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.lazer.great.field', + values: ['grrrrr', 'grrrrr_2'], + originalValue: ['grrrrr', 'grrrrr_2'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.lazer.great.field.wowoe.fooooo', + values: ['grrrrr'], + originalValue: ['grrrrr'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.lazer.great.field.astring', + values: ['cool'], + originalValue: ['cool'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.lazer.great.field.aNumber', + values: ['1'], + originalValue: ['1'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.lazer.great.field.neat', + values: ['true'], + originalValue: ['true'], + isObjectArray: false, + }, +]; diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index 296e497eb4952..d653528fd47e2 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -20,96 +20,133 @@ const EXPECTED_DATA = [ field: '@timestamp', values: ['2019-02-10T02:39:44.107Z'], originalValue: ['2019-02-10T02:39:44.107Z'], + isObjectArray: false, }, { category: '@version', field: '@version', values: ['1'], originalValue: ['1'], + isObjectArray: false, + }, + { + category: '_id', + field: '_id', + values: ['QRhG1WgBqd-n62SwZYDT'], + originalValue: ['QRhG1WgBqd-n62SwZYDT'], + isObjectArray: false, + }, + { + category: '_index', + field: '_index', + values: ['filebeat-7.0.0-iot-2019.06'], + originalValue: ['filebeat-7.0.0-iot-2019.06'], + isObjectArray: false, + }, + { + category: '_score', + field: '_score', + values: ['1'], + originalValue: ['1'], + isObjectArray: false, }, { category: 'agent', field: 'agent.ephemeral_id', values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], originalValue: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], + isObjectArray: false, }, { category: 'agent', field: 'agent.hostname', values: ['raspberrypi'], originalValue: ['raspberrypi'], + isObjectArray: false, }, { category: 'agent', field: 'agent.id', values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], originalValue: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], + isObjectArray: false, }, { category: 'agent', field: 'agent.type', values: ['filebeat'], originalValue: ['filebeat'], + isObjectArray: false, }, { category: 'agent', field: 'agent.version', values: ['7.0.0'], originalValue: ['7.0.0'], + isObjectArray: false, }, { category: 'destination', field: 'destination.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + isObjectArray: false, }, { category: 'destination', field: 'destination.ip', values: ['10.100.7.196'], originalValue: ['10.100.7.196'], + isObjectArray: false, }, { category: 'destination', field: 'destination.port', values: ['40684'], originalValue: ['40684'], + isObjectArray: false, }, { category: 'ecs', field: 'ecs.version', values: ['1.0.0-beta2'], originalValue: ['1.0.0-beta2'], + isObjectArray: false, }, { category: 'event', field: 'event.dataset', values: ['suricata.eve'], originalValue: ['suricata.eve'], + isObjectArray: false, }, { category: 'event', field: 'event.end', values: ['2019-02-10T02:39:44.107Z'], originalValue: ['2019-02-10T02:39:44.107Z'], + isObjectArray: false, }, { category: 'event', field: 'event.kind', values: ['event'], originalValue: ['event'], + isObjectArray: false, }, { category: 'event', field: 'event.module', values: ['suricata'], originalValue: ['suricata'], + isObjectArray: false, }, { category: 'event', field: 'event.type', values: ['fileinfo'], originalValue: ['fileinfo'], + isObjectArray: false, }, { category: 'file', @@ -120,270 +157,484 @@ const EXPECTED_DATA = [ originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], + isObjectArray: false, }, { category: 'file', field: 'file.size', values: ['48277'], originalValue: ['48277'], + isObjectArray: false, }, { category: 'fileset', field: 'fileset.name', values: ['eve'], originalValue: ['eve'], + isObjectArray: false, }, { category: 'flow', field: 'flow.locality', values: ['public'], originalValue: ['public'], + isObjectArray: false, }, { category: 'host', field: 'host.architecture', values: ['armv7l'], originalValue: ['armv7l'], + isObjectArray: false, + }, + { + category: 'host', + field: 'host.containerized', + values: ['false'], + originalValue: ['false'], + isObjectArray: false, }, { category: 'host', field: 'host.hostname', values: ['raspberrypi'], originalValue: ['raspberrypi'], + isObjectArray: false, }, { category: 'host', field: 'host.id', values: ['b19a781f683541a7a25ee345133aa399'], originalValue: ['b19a781f683541a7a25ee345133aa399'], + isObjectArray: false, }, { category: 'host', field: 'host.name', values: ['raspberrypi'], originalValue: ['raspberrypi'], + isObjectArray: false, }, { category: 'host', field: 'host.os.codename', values: ['stretch'], originalValue: ['stretch'], + isObjectArray: false, }, { category: 'host', field: 'host.os.family', values: [''], originalValue: [''], + isObjectArray: false, }, { category: 'host', field: 'host.os.kernel', values: ['4.14.50-v7+'], originalValue: ['4.14.50-v7+'], + isObjectArray: false, }, { category: 'host', field: 'host.os.name', values: ['Raspbian GNU/Linux'], originalValue: ['Raspbian GNU/Linux'], + isObjectArray: false, }, { category: 'host', field: 'host.os.platform', values: ['raspbian'], originalValue: ['raspbian'], + isObjectArray: false, }, { category: 'host', field: 'host.os.version', values: ['9 (stretch)'], originalValue: ['9 (stretch)'], + isObjectArray: false, }, { category: 'http', field: 'http.request.method', values: ['get'], originalValue: ['get'], + isObjectArray: false, }, { category: 'http', field: 'http.response.body.bytes', values: ['48277'], originalValue: ['48277'], + isObjectArray: false, }, { category: 'http', field: 'http.response.status_code', values: ['206'], originalValue: ['206'], + isObjectArray: false, }, { category: 'input', field: 'input.type', values: ['log'], originalValue: ['log'], + isObjectArray: false, }, { category: 'base', field: 'labels.pipeline', values: ['filebeat-7.0.0-suricata-eve-pipeline'], originalValue: ['filebeat-7.0.0-suricata-eve-pipeline'], + isObjectArray: false, }, { category: 'log', field: 'log.file.path', values: ['/var/log/suricata/eve.json'], originalValue: ['/var/log/suricata/eve.json'], + isObjectArray: false, }, { category: 'log', field: 'log.offset', values: ['1856288115'], originalValue: ['1856288115'], + isObjectArray: false, }, { category: 'network', field: 'network.name', values: ['iot'], originalValue: ['iot'], + isObjectArray: false, }, { category: 'network', field: 'network.protocol', values: ['http'], originalValue: ['http'], + isObjectArray: false, }, { category: 'network', field: 'network.transport', values: ['tcp'], originalValue: ['tcp'], + isObjectArray: false, }, { category: 'service', field: 'service.type', values: ['suricata'], originalValue: ['suricata'], + isObjectArray: false, }, { category: 'source', field: 'source.as.num', values: ['16509'], originalValue: ['16509'], + isObjectArray: false, }, { category: 'source', field: 'source.as.org', values: ['Amazon.com, Inc.'], originalValue: ['Amazon.com, Inc.'], + isObjectArray: false, }, { category: 'source', field: 'source.domain', values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], originalValue: ['server-54-239-219-210.jfk51.r.cloudfront.net'], + isObjectArray: false, }, { category: 'source', field: 'source.geo.city_name', values: ['Seattle'], originalValue: ['Seattle'], + isObjectArray: false, }, { category: 'source', field: 'source.geo.continent_name', values: ['North America'], originalValue: ['North America'], + isObjectArray: false, }, { category: 'source', field: 'source.geo.country_iso_code', values: ['US'], originalValue: ['US'], + isObjectArray: false, + }, + { + category: 'source', + field: 'source.geo.location', + values: ['{"lon":-122.3341,"lat":47.6103}'], + originalValue: ['{"lon":-122.3341,"lat":47.6103}'], + isObjectArray: true, }, { category: 'source', field: 'source.geo.location.lat', values: ['47.6103'], originalValue: ['47.6103'], + isObjectArray: false, }, { category: 'source', field: 'source.geo.location.lon', values: ['-122.3341'], originalValue: ['-122.3341'], + isObjectArray: false, }, { category: 'source', field: 'source.geo.region_iso_code', values: ['US-WA'], originalValue: ['US-WA'], + isObjectArray: false, }, { category: 'source', field: 'source.geo.region_name', values: ['Washington'], originalValue: ['Washington'], + isObjectArray: false, }, { category: 'source', field: 'source.ip', values: ['54.239.219.210'], originalValue: ['54.239.219.210'], + isObjectArray: false, }, { category: 'source', field: 'source.port', values: ['80'], originalValue: ['80'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.app_proto', + values: ['http'], + originalValue: ['http'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.dest_ip', + values: ['10.100.7.196'], + originalValue: ['10.100.7.196'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.dest_port', + values: ['40684'], + originalValue: ['40684'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.filename', + values: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + originalValue: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.size', + values: ['48277'], + originalValue: ['48277'], + isObjectArray: false, }, { category: 'suricata', field: 'suricata.eve.fileinfo.state', values: ['CLOSED'], originalValue: ['CLOSED'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.stored', + values: ['false'], + originalValue: ['false'], + isObjectArray: false, }, { category: 'suricata', field: 'suricata.eve.fileinfo.tx_id', values: ['301'], originalValue: ['301'], + isObjectArray: false, }, { category: 'suricata', field: 'suricata.eve.flow_id', values: ['196625917175466'], originalValue: ['196625917175466'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.http.hostname', + values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + isObjectArray: false, }, { category: 'suricata', field: 'suricata.eve.http.http_content_type', values: ['video/mp4'], originalValue: ['video/mp4'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.http.http_method', + values: ['get'], + originalValue: ['get'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.http.length', + values: ['48277'], + originalValue: ['48277'], + isObjectArray: false, }, { category: 'suricata', field: 'suricata.eve.http.protocol', values: ['HTTP/1.1'], originalValue: ['HTTP/1.1'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.http.status', + values: ['206'], + originalValue: ['206'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.http.url', + values: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + originalValue: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + isObjectArray: false, }, { category: 'suricata', field: 'suricata.eve.in_iface', values: ['eth0'], originalValue: ['eth0'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.proto', + values: ['tcp'], + originalValue: ['tcp'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.src_ip', + values: ['54.239.219.210'], + originalValue: ['54.239.219.210'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.src_port', + values: ['80'], + originalValue: ['80'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.timestamp', + values: ['2019-02-10T02:39:44.107Z'], + originalValue: ['2019-02-10T02:39:44.107Z'], + isObjectArray: false, }, { category: 'base', field: 'tags', values: ['suricata'], originalValue: ['suricata'], + isObjectArray: false, + }, + { + category: 'traefik', + field: 'traefik.access.geoip.city_name', + values: ['Seattle'], + originalValue: ['Seattle'], + isObjectArray: false, + }, + { + category: 'traefik', + field: 'traefik.access.geoip.continent_name', + values: ['North America'], + originalValue: ['North America'], + isObjectArray: false, + }, + { + category: 'traefik', + field: 'traefik.access.geoip.country_iso_code', + values: ['US'], + originalValue: ['US'], + isObjectArray: false, + }, + { + category: 'traefik', + field: 'traefik.access.geoip.location', + values: ['{"lon":-122.3341,"lat":47.6103}'], + originalValue: ['{"lon":-122.3341,"lat":47.6103}'], + isObjectArray: true, + }, + { + category: 'traefik', + field: 'traefik.access.geoip.region_iso_code', + values: ['US-WA'], + originalValue: ['US-WA'], + isObjectArray: false, + }, + { + category: 'traefik', + field: 'traefik.access.geoip.region_name', + values: ['Washington'], + originalValue: ['Washington'], + isObjectArray: false, }, { category: 'url', field: 'url.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + isObjectArray: false, }, { category: 'url', @@ -394,6 +645,7 @@ const EXPECTED_DATA = [ originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], + isObjectArray: false, }, { category: 'url', @@ -404,27 +656,9 @@ const EXPECTED_DATA = [ originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - }, - { - category: '_index', - field: '_index', - values: ['filebeat-7.0.0-iot-2019.06'], - originalValue: ['filebeat-7.0.0-iot-2019.06'], - }, - { - category: '_id', - field: '_id', - values: ['QRhG1WgBqd-n62SwZYDT'], - originalValue: ['QRhG1WgBqd-n62SwZYDT'], - }, - { - category: '_score', - field: '_score', - values: ['1'], - originalValue: ['1'], + isObjectArray: false, }, ]; - const EXPECTED_KPI_COUNTS = { destinationIpCount: 154, hostCount: 1, @@ -456,7 +690,7 @@ export default function ({ getService }: FtrProviderContext) { wait_for_completion_timeout: '10s', }) .expect(200); - expect(sortBy(detailsData, 'name')).to.eql(sortBy(EXPECTED_DATA, 'name')); + expect(sortBy(detailsData, 'field')).to.eql(sortBy(EXPECTED_DATA, 'field')); }); it('Make sure that we get kpi data', async () => { From 2903844dd1ad7d82b811ec8802457b2bea07899c Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 1 Mar 2021 22:46:22 -0500 Subject: [PATCH 6/6] [SECURITY SOLUTIONS] Bug case connector (#93104) * bring back case connector to design * disable connector sir in collection * missing to only create collection type * fix fields connector when you need to hide service-now sir --- .../cases/components/case_view/index.tsx | 3 + .../connectors_dropdown.test.tsx | 153 ++++++++++++++++-- .../configure_cases/connectors_dropdown.tsx | 49 +++--- .../components/connector_selector/form.tsx | 3 + .../connectors/case/alert_fields.tsx | 21 +-- .../connectors/case/existing_case.tsx | 114 +++++-------- .../connectors/case/translations.ts | 16 +- .../cases/components/create/connector.tsx | 24 ++- .../public/cases/components/create/form.tsx | 134 +++++++-------- .../cases/components/create/form_context.tsx | 31 ++-- .../cases/components/edit_connector/index.tsx | 5 +- .../create_case_modal.tsx | 13 +- .../use_create_case_modal/index.tsx | 5 +- .../public/cases/containers/api.ts | 3 + .../public/cases/containers/types.ts | 1 + .../public/cases/containers/use_get_cases.tsx | 8 +- .../rules/rule_actions_field/index.test.tsx | 68 +++++++- .../rules/rule_actions_field/index.tsx | 61 ++++++- .../rules/step_rule_actions/index.tsx | 8 +- .../use_manage_case_action.tsx | 63 ++++++++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 22 files changed, 567 insertions(+), 218 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 108e020d014c4..83a0c4e7acd3d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -447,6 +447,9 @@ export const CaseComponent = React.memo( caseFields={caseData.connector.fields} connectors={connectors} disabled={!userCanCrud} + hideConnectorServiceNowSir={ + subCaseId != null || caseData.type === CaseType.collection + } isLoading={isLoadingConnectors || (isLoading && updateKey === 'connector')} onSubmit={onSubmitConnector} selectedConnector={caseData.connector.id} diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.test.tsx index e8c074faed32e..1f1876756773d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.test.tsx @@ -34,22 +34,122 @@ describe('ConnectorsDropdown', () => { test('it formats the connectors correctly', () => { const selectProps = wrapper.find(EuiSuperSelect).props(); - expect(selectProps.options).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - value: 'none', - 'data-test-subj': 'dropdown-connector-no-connector', - }), - expect.objectContaining({ - value: 'servicenow-1', - 'data-test-subj': 'dropdown-connector-servicenow-1', - }), - expect.objectContaining({ - value: 'resilient-2', - 'data-test-subj': 'dropdown-connector-resilient-2', - }), - ]) - ); + expect(selectProps.options).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "dropdown-connector-no-connector", + "inputDisplay": + + + + + + No connector selected + + + , + "value": "none", + }, + Object { + "data-test-subj": "dropdown-connector-servicenow-1", + "inputDisplay": + + + + + + My Connector + + + , + "value": "servicenow-1", + }, + Object { + "data-test-subj": "dropdown-connector-resilient-2", + "inputDisplay": + + + + + + My Connector 2 + + + , + "value": "resilient-2", + }, + Object { + "data-test-subj": "dropdown-connector-jira-1", + "inputDisplay": + + + + + + Jira + + + , + "value": "jira-1", + }, + Object { + "data-test-subj": "dropdown-connector-servicenow-sir", + "inputDisplay": + + + + + + My Connector SIR + + + , + "value": "servicenow-sir", + }, + ] + `); }); test('it disables the dropdown', () => { @@ -79,4 +179,25 @@ describe('ConnectorsDropdown', () => { expect(newWrapper.find('button span:not([data-euiicon-type])').text()).toEqual('My Connector'); }); + + test('if the props hideConnectorServiceNowSir is true, the connector should not be part of the list of options ', () => { + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + const selectProps = newWrapper.find(EuiSuperSelect).props(); + const options = selectProps.options as Array<{ 'data-test-subj': string }>; + expect( + options.some((o) => o['data-test-subj'] === 'dropdown-connector-servicenow-1') + ).toBeTruthy(); + expect( + options.some((o) => o['data-test-subj'] === 'dropdown-connector-servicenow-sir') + ).toBeFalsy(); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx index ab4b9fcfe7093..b8eacb9dfdd91 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx @@ -9,6 +9,7 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; import styled from 'styled-components'; +import { ConnectorTypes } from '../../../../../case/common/api'; import { ActionConnector } from '../../containers/configure/types'; import { connectorsConfiguration } from '../connectors'; import * as i18n from './translations'; @@ -20,6 +21,7 @@ export interface Props { onChange: (id: string) => void; selectedConnector: string; appendAddConnectorButton?: boolean; + hideConnectorServiceNowSir?: boolean; } const ICON_SIZE = 'm'; @@ -61,29 +63,36 @@ const ConnectorsDropdownComponent: React.FC = ({ onChange, selectedConnector, appendAddConnectorButton = false, + hideConnectorServiceNowSir = false, }) => { const connectorsAsOptions = useMemo(() => { const connectorsFormatted = connectors.reduce( - (acc, connector) => [ - ...acc, - { - value: connector.id, - inputDisplay: ( - - - - - - {connector.name} - - - ), - 'data-test-subj': `dropdown-connector-${connector.id}`, - }, - ], + (acc, connector) => { + if (hideConnectorServiceNowSir && connector.actionTypeId === ConnectorTypes.serviceNowSIR) { + return acc; + } + + return [ + ...acc, + { + value: connector.id, + inputDisplay: ( + + + + + + {connector.name} + + + ), + 'data-test-subj': `dropdown-connector-${connector.id}`, + }, + ]; + }, [noConnectorOption] ); diff --git a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx index d5f5530acde9b..586a7c19cc532 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx @@ -22,6 +22,7 @@ interface ConnectorSelectorProps { isEdit: boolean; isLoading: boolean; handleChange?: (newValue: string) => void; + hideConnectorServiceNowSir?: boolean; } export const ConnectorSelector = ({ connectors, @@ -32,6 +33,7 @@ export const ConnectorSelector = ({ isEdit = true, isLoading = false, handleChange, + hideConnectorServiceNowSir = false, }: ConnectorSelectorProps) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); const onChange = useCallback( @@ -58,6 +60,7 @@ export const ConnectorSelector = ({ ` - margin-top: ${theme.eui?.euiSize ?? '16px'}; + padding: ${theme.eui?.euiSizeS ?? '8px'} ${theme.eui?.euiSizeL ?? '24px'} ${ + theme.eui?.euiSizeL ?? '24px' + } ${theme.eui?.euiSizeL ?? '24px'}; `} `; const defaultAlertComment = { type: CommentType.generatedAlert, - alerts: `[{{#context.alerts}}{"_id": "{{_id}}", "_index": "{{_index}}", "ruleId": "{{rule.id}}", "ruleName": "{{rule.name}}"}__SEPARATOR__{{/context.alerts}}]`, + alerts: `[{{#context.alerts}}{"_id": "{{_id}}", "_index": "{{_index}}", "ruleId": "{{signal.rule.id}}", "ruleName": "{{signal.rule.name}}"}__SEPARATOR__{{/context.alerts}}]`, }; const CaseParamsFields: React.FunctionComponent> = ({ @@ -90,12 +92,13 @@ const CaseParamsFields: React.FunctionComponent - - - - - + + + + +

{i18n.CASE_CONNECTOR_CALL_OUT_MSG}

+
+
); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx index 5f564d7b62464..c1013718d5756 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx @@ -5,22 +5,15 @@ * 2.0. */ -import { - EuiButton, - EuiButtonIcon, - EuiCallOut, - EuiTextColor, - EuiLoadingSpinner, -} from '@elastic/eui'; -import { isEmpty } from 'lodash'; -import React, { memo, useEffect, useCallback, useState } from 'react'; +import React, { memo, useMemo, useCallback } from 'react'; import { CaseType } from '../../../../../../case/common/api'; -import { Case } from '../../../containers/types'; -import { useDeleteCases } from '../../../containers/use_delete_cases'; -import { useGetCase } from '../../../containers/use_get_case'; -import { ConfirmDeleteCaseModal } from '../../confirm_delete_case'; +import { + useGetCases, + DEFAULT_QUERY_PARAMS, + DEFAULT_FILTER_OPTIONS, +} from '../../../containers/use_get_cases'; import { useCreateCaseModal } from '../../use_create_case_modal'; -import * as i18n from './translations'; +import { CasesDropdown, ADD_CASE_BUTTON_ID } from './cases_dropdown'; interface ExistingCaseProps { selectedCase: string | null; @@ -28,76 +21,53 @@ interface ExistingCaseProps { } const ExistingCaseComponent: React.FC = ({ onCaseChanged, selectedCase }) => { - const { data, isLoading, isError } = useGetCase(selectedCase ?? ''); - const [createdCase, setCreatedCase] = useState(null); + const { data: cases, loading: isLoadingCases, refetchCases } = useGetCases(DEFAULT_QUERY_PARAMS, { + ...DEFAULT_FILTER_OPTIONS, + onlyCollectionType: true, + }); const onCaseCreated = useCallback( - (newCase: Case) => { + (newCase) => { + refetchCases(); onCaseChanged(newCase.id); - setCreatedCase(newCase); }, - [onCaseChanged] + [onCaseChanged, refetchCases] ); - const { modal, openModal } = useCreateCaseModal({ caseType: CaseType.collection, onCaseCreated }); + const { modal, openModal } = useCreateCaseModal({ + onCaseCreated, + caseType: CaseType.collection, + // FUTURE DEVELOPER + // We are making the assumption that this component is only used in rules creation + // that's why we want to hide ServiceNow SIR + hideConnectorServiceNowSir: true, + }); - // Delete case - const { - dispatchResetIsDeleted, - handleOnDeleteConfirm, - handleToggleModal, - isLoading: isDeleting, - isDeleted, - isDisplayConfirmDeleteModal, - } = useDeleteCases(); + const onChange = useCallback( + (id: string) => { + if (id === ADD_CASE_BUTTON_ID) { + openModal(); + return; + } - useEffect(() => { - if (isDeleted) { - setCreatedCase(null); - onCaseChanged(''); - dispatchResetIsDeleted(); - } - // onCaseChanged and/or dispatchResetIsDeleted causes re-renders - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isDeleted]); + onCaseChanged(id); + }, + [onCaseChanged, openModal] + ); - useEffect(() => { - if (!isLoading && !isError && data != null) { - setCreatedCase(data); - onCaseChanged(data.id); - } - // onCaseChanged causes re-renders - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data, isLoading, isError]); + const isCasesLoading = useMemo( + () => isLoadingCases.includes('cases') || isLoadingCases.includes('caseUpdate'), + [isLoadingCases] + ); return ( <> - {createdCase == null && isEmpty(selectedCase) && ( - - {i18n.CREATE_CASE} - - )} - {createdCase == null && isLoading && } - {createdCase != null && !isLoading && ( - <> - - - {createdCase.title}{' '} - {!isDeleting && ( - - )} - {isDeleting && } - - - - - )} + {modal} ); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts index 731e94a17d923..6ce5316d0eb88 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts @@ -40,7 +40,7 @@ export const CASE_CONNECTOR_COMMENT_REQUIRED = i18n.translate( export const CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL = i18n.translate( 'xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel', { - defaultMessage: 'Case', + defaultMessage: 'Case allowing sub-cases', } ); @@ -72,10 +72,18 @@ export const CASE_CONNECTOR_CASE_REQUIRED = i18n.translate( } ); -export const CASE_CONNECTOR_CALL_OUT_INFO = i18n.translate( - 'xpack.securitySolution.case.components.connectors.case.callOutInfo', +export const CASE_CONNECTOR_CALL_OUT_TITLE = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.callOutTitle', { - defaultMessage: 'All alerts after rule creation will be appended to the selected case.', + defaultMessage: 'Generated alerts will be attached to sub-cases', + } +); + +export const CASE_CONNECTOR_CALL_OUT_MSG = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.callOutMsg', + { + defaultMessage: + 'A case can contain multiple sub-cases to allow grouping of generated alerts. Sub-cases will give more granular control over the status of these generated alerts and prevents having too many alerts attached to one case.', } ); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx index 5e7972aec9d4b..bfe0d8dd78e28 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx @@ -8,6 +8,7 @@ import React, { memo, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { ConnectorTypes } from '../../../../../case/common/api'; import { UseField, useFormData, FieldHook, useFormContext } from '../../../shared_imports'; import { useConnectors } from '../../containers/configure/use_connectors'; import { ConnectorSelector } from '../connector_selector/form'; @@ -18,19 +19,32 @@ import { FormProps } from './schema'; interface Props { isLoading: boolean; + hideConnectorServiceNowSir?: boolean; } interface ConnectorsFieldProps { connectors: ActionConnector[]; field: FieldHook; isEdit: boolean; + hideConnectorServiceNowSir?: boolean; } -const ConnectorFields = ({ connectors, isEdit, field }: ConnectorsFieldProps) => { +const ConnectorFields = ({ + connectors, + isEdit, + field, + hideConnectorServiceNowSir = false, +}: ConnectorsFieldProps) => { const [{ connectorId }] = useFormData({ watch: ['connectorId'] }); const { setValue } = field; - const connector = getConnectorById(connectorId, connectors) ?? null; - + let connector = getConnectorById(connectorId, connectors) ?? null; + if ( + connector && + hideConnectorServiceNowSir && + connector.actionTypeId === ConnectorTypes.serviceNowSIR + ) { + connector = null; + } return ( ); }; -const ConnectorComponent: React.FC = ({ isLoading }) => { +const ConnectorComponent: React.FC = ({ hideConnectorServiceNowSir = false, isLoading }) => { const { getFields } = useFormContext(); const { loading: isLoadingConnectors, connectors } = useConnectors(); const handleConnectorChange = useCallback( @@ -61,6 +75,7 @@ const ConnectorComponent: React.FC = ({ isLoading }) => { componentProps={{ connectors, handleChange: handleConnectorChange, + hideConnectorServiceNowSir, dataTestSubj: 'caseConnectors', disabled: isLoading || isLoadingConnectors, idAria: 'caseConnectors', @@ -74,6 +89,7 @@ const ConnectorComponent: React.FC = ({ isLoading }) => { component={ConnectorFields} componentProps={{ connectors, + hideConnectorServiceNowSir, isEdit: true, }} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form.tsx index f5b113ae8e26f..09518c6f6adc1 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form.tsx @@ -36,78 +36,84 @@ const MySpinner = styled(EuiLoadingSpinner)` `; interface Props { + hideConnectorServiceNowSir?: boolean; withSteps?: boolean; } -export const CreateCaseForm: React.FC = React.memo(({ withSteps = true }) => { - const { isSubmitting } = useFormContext(); +export const CreateCaseForm: React.FC = React.memo( + ({ hideConnectorServiceNowSir = false, withSteps = true }) => { + const { isSubmitting } = useFormContext(); - const firstStep = useMemo( - () => ({ - title: i18n.STEP_ONE_TITLE, - children: ( - <> - + const firstStep = useMemo( + () => ({ + title: i18n.STEP_ONE_TITLE, + children: ( + <> + <Title isLoading={isSubmitting} /> + <Container> + <Tags isLoading={isSubmitting} /> + </Container> + <Container big> + <Description isLoading={isSubmitting} /> + </Container> + </> + ), + }), + [isSubmitting] + ); + + const secondStep = useMemo( + () => ({ + title: i18n.STEP_TWO_TITLE, + children: ( <Container> - <Tags isLoading={isSubmitting} /> - </Container> - <Container big> - <Description isLoading={isSubmitting} /> + <SyncAlertsToggle isLoading={isSubmitting} /> </Container> - </> - ), - }), - [isSubmitting] - ); - - const secondStep = useMemo( - () => ({ - title: i18n.STEP_TWO_TITLE, - children: ( - <Container> - <SyncAlertsToggle isLoading={isSubmitting} /> - </Container> - ), - }), - [isSubmitting] - ); + ), + }), + [isSubmitting] + ); - const thirdStep = useMemo( - () => ({ - title: i18n.STEP_THREE_TITLE, - children: ( - <Container> - <Connector isLoading={isSubmitting} /> - </Container> - ), - }), - [isSubmitting] - ); + const thirdStep = useMemo( + () => ({ + title: i18n.STEP_THREE_TITLE, + children: ( + <Container> + <Connector + hideConnectorServiceNowSir={hideConnectorServiceNowSir} + isLoading={isSubmitting} + /> + </Container> + ), + }), + [hideConnectorServiceNowSir, isSubmitting] + ); - const allSteps = useMemo(() => [firstStep, secondStep, thirdStep], [ - firstStep, - secondStep, - thirdStep, - ]); + const allSteps = useMemo(() => [firstStep, secondStep, thirdStep], [ + firstStep, + secondStep, + thirdStep, + ]); - return ( - <> - {isSubmitting && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} - {withSteps ? ( - <EuiSteps - headingElement="h2" - steps={allSteps} - data-test-subj={'case-creation-form-steps'} - /> - ) : ( - <> - {firstStep.children} - {secondStep.children} - {thirdStep.children} - </> - )} - </> - ); -}); + return ( + <> + {isSubmitting && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} + {withSteps ? ( + <EuiSteps + headingElement="h2" + steps={allSteps} + data-test-subj={'case-creation-form-steps'} + /> + ) : ( + <> + {firstStep.children} + {secondStep.children} + {thirdStep.children} + </> + )} + </> + ); + } +); CreateCaseForm.displayName = 'CreateCaseForm'; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index 26203d7268fd3..f56dcafdc95e4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -19,7 +19,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service' import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { Case } from '../../containers/types'; -import { CaseType } from '../../../../../case/common/api'; +import { CaseType, ConnectorTypes } from '../../../../../case/common/api'; const initialCaseValue: FormProps = { description: '', @@ -31,29 +31,40 @@ const initialCaseValue: FormProps = { }; interface Props { + afterCaseCreated?: (theCase: Case) => Promise<void>; caseType?: CaseType; + hideConnectorServiceNowSir?: boolean; onSuccess?: (theCase: Case) => Promise<void>; - afterCaseCreated?: (theCase: Case) => Promise<void>; } export const FormContext: React.FC<Props> = ({ + afterCaseCreated, caseType = CaseType.individual, children, + hideConnectorServiceNowSir, onSuccess, - afterCaseCreated, }) => { const { connectors } = useConnectors(); const { connector: configurationConnector } = useCaseConfigure(); const { postCase } = usePostCase(); const { pushCaseToExternalService } = usePostPushToService(); - const connectorId = useMemo( - () => - connectors.some((connector) => connector.id === configurationConnector.id) - ? configurationConnector.id - : 'none', - [configurationConnector.id, connectors] - ); + const connectorId = useMemo(() => { + if ( + hideConnectorServiceNowSir && + configurationConnector.type === ConnectorTypes.serviceNowSIR + ) { + return 'none'; + } + return connectors.some((connector) => connector.id === configurationConnector.id) + ? configurationConnector.id + : 'none'; + }, [ + configurationConnector.id, + configurationConnector.type, + connectors, + hideConnectorServiceNowSir, + ]); const submitCase = useCallback( async ( diff --git a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx index 34dcacaf42a98..d0f478dc17f81 100644 --- a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx @@ -34,7 +34,6 @@ import * as i18n from './translations'; interface EditConnectorProps { caseFields: ConnectorTypeFields['fields']; connectors: ActionConnector[]; - disabled?: boolean; isLoading: boolean; onSubmit: ( connectorId: string, @@ -44,6 +43,8 @@ interface EditConnectorProps { ) => void; selectedConnector: string; userActions: CaseUserActions[]; + disabled?: boolean; + hideConnectorServiceNowSir?: boolean; } const MyFlexGroup = styled(EuiFlexGroup)` @@ -105,6 +106,7 @@ export const EditConnector = React.memo( caseFields, connectors, disabled = false, + hideConnectorServiceNowSir = false, isLoading, onSubmit, selectedConnector, @@ -234,6 +236,7 @@ export const EditConnector = React.memo( dataTestSubj: 'caseConnectors', defaultValue: selectedConnector, disabled, + hideConnectorServiceNowSir, idAria: 'caseConnectors', isEdit: editConnector, isLoading, diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx index 3e11ee526839c..b1edaa56cd348 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx @@ -21,6 +21,7 @@ export interface CreateCaseModalProps { onCloseCaseModal: () => void; onSuccess: (theCase: Case) => Promise<void>; caseType?: CaseType; + hideConnectorServiceNowSir?: boolean; } const Container = styled.div` @@ -35,6 +36,7 @@ const CreateModalComponent: React.FC<CreateCaseModalProps> = ({ onCloseCaseModal, onSuccess, caseType = CaseType.individual, + hideConnectorServiceNowSir = false, }) => { return isModalOpen ? ( <EuiModal onClose={onCloseCaseModal} data-test-subj="all-cases-modal"> @@ -42,8 +44,15 @@ const CreateModalComponent: React.FC<CreateCaseModalProps> = ({ <EuiModalHeaderTitle>{i18n.CREATE_TITLE}</EuiModalHeaderTitle> </EuiModalHeader> <EuiModalBody> - <FormContext caseType={caseType} onSuccess={onSuccess}> - <CreateCaseForm withSteps={false} /> + <FormContext + hideConnectorServiceNowSir={hideConnectorServiceNowSir} + caseType={caseType} + onSuccess={onSuccess} + > + <CreateCaseForm + withSteps={false} + hideConnectorServiceNowSir={hideConnectorServiceNowSir} + /> <Container> <SubmitCaseButton /> </Container> diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx index 1cef63ae9cfbf..50887f08dee6e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx @@ -13,6 +13,7 @@ import { CreateCaseModal } from './create_case_modal'; export interface UseCreateCaseModalProps { onCaseCreated: (theCase: Case) => void; caseType?: CaseType; + hideConnectorServiceNowSir?: boolean; } export interface UseCreateCaseModalReturnedValues { modal: JSX.Element; @@ -24,6 +25,7 @@ export interface UseCreateCaseModalReturnedValues { export const useCreateCaseModal = ({ caseType = CaseType.individual, onCaseCreated, + hideConnectorServiceNowSir = false, }: UseCreateCaseModalProps) => { const [isModalOpen, setIsModalOpen] = useState<boolean>(false); const closeModal = useCallback(() => setIsModalOpen(false), []); @@ -41,6 +43,7 @@ export const useCreateCaseModal = ({ modal: ( <CreateCaseModal caseType={caseType} + hideConnectorServiceNowSir={hideConnectorServiceNowSir} isModalOpen={isModalOpen} onCloseCaseModal={closeModal} onSuccess={onSuccess} @@ -50,7 +53,7 @@ export const useCreateCaseModal = ({ closeModal, openModal, }), - [caseType, closeModal, isModalOpen, onSuccess, openModal] + [caseType, closeModal, hideConnectorServiceNowSir, isModalOpen, onSuccess, openModal] ); return state; diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index c87e210b42bc0..01ef040aa19cd 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -15,6 +15,7 @@ import { CasesResponse, CasesStatusResponse, CaseStatuses, + CaseType, CaseUserActionsResponse, CommentRequest, CommentType, @@ -165,6 +166,7 @@ export const getSubCaseUserActions = async ( export const getCases = async ({ filterOptions = { + onlyCollectionType: false, search: '', reporters: [], status: CaseStatuses.open, @@ -183,6 +185,7 @@ export const getCases = async ({ tags: filterOptions.tags.map((t) => `"${t.replace(/"/g, '\\"')}"`), status: filterOptions.status, ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), + ...(filterOptions.onlyCollectionType ? { type: CaseType.collection } : {}), ...queryParams, }; const response = await KibanaServices.get().http.fetch<CasesFindResponse>(`${CASES_URL}/_find`, { diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index d2931a790bd79..399d8d43ce065 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -99,6 +99,7 @@ export interface FilterOptions { status: CaseStatuses; tags: string[]; reporters: User[]; + onlyCollectionType?: boolean; } export interface CasesStatus { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx index c83cc02dedb97..f2e8e280bf158 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx @@ -97,6 +97,7 @@ export const DEFAULT_FILTER_OPTIONS: FilterOptions = { reporters: [], status: CaseStatuses.open, tags: [], + onlyCollectionType: false, }; export const DEFAULT_QUERY_PARAMS: QueryParams = { @@ -129,10 +130,13 @@ export interface UseGetCases extends UseGetCasesState { setSelectedCases: (mySelectedCases: Case[]) => void; } -export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => { +export const useGetCases = ( + initialQueryParams?: QueryParams, + initialFilterOptions?: FilterOptions +): UseGetCases => { const [state, dispatch] = useReducer(dataFetchReducer, { data: initialData, - filterOptions: DEFAULT_FILTER_OPTIONS, + filterOptions: initialFilterOptions ?? DEFAULT_FILTER_OPTIONS, isError: false, loading: [], queryParams: initialQueryParams ?? DEFAULT_QUERY_PARAMS, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx index 7563c8d8f99f0..5dbe1f1cef5be 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx @@ -8,10 +8,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { RuleActionsField } from './index'; +import { getSupportedActions, RuleActionsField } from './index'; import { useForm, Form } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; import { useFormFieldMock } from '../../../../common/mock'; +import { ActionType } from '../../../../../../actions/common'; jest.mock('../../../../common/lib/kibana'); describe('RuleActionsField', () => { @@ -45,7 +46,11 @@ describe('RuleActionsField', () => { return ( <Form form={form}> - <RuleActionsField field={field} messageVariables={messageVariables} /> + <RuleActionsField + field={field} + messageVariables={messageVariables} + hasErrorOnCreationCaseAction={false} + /> </Form> ); }; @@ -53,4 +58,63 @@ describe('RuleActionsField', () => { expect(wrapper.dive().find('ActionForm')).toHaveLength(0); }); + + describe('#getSupportedActions', () => { + const actions: ActionType[] = [ + { + id: '.jira', + name: 'My Jira', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + minimumLicenseRequired: 'gold', + }, + { + id: '.case', + name: 'Cases', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + ]; + + it('if we have an error on case action creation, we do not support case connector', () => { + expect(getSupportedActions(actions, true)).toMatchInlineSnapshot(` + Array [ + Object { + "enabled": true, + "enabledInConfig": false, + "enabledInLicense": true, + "id": ".jira", + "minimumLicenseRequired": "gold", + "name": "My Jira", + }, + ] + `); + }); + + it('if we do NOT have an error on case action creation, we are supporting case connector', () => { + expect(getSupportedActions(actions, false)).toMatchInlineSnapshot(` + Array [ + Object { + "enabled": true, + "enabledInConfig": false, + "enabledInLicense": true, + "id": ".jira", + "minimumLicenseRequired": "gold", + "name": "My Jira", + }, + Object { + "enabled": true, + "enabledInConfig": false, + "enabledInLicense": true, + "id": ".case", + "minimumLicenseRequired": "basic", + "name": "Cases", + }, + ] + `); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index cee85df5db436..9fd9e910ee0f8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -26,6 +26,7 @@ import { FORM_ERRORS_TITLE } from './translations'; interface Props { field: FieldHook; + hasErrorOnCreationCaseAction: boolean; messageVariables: ActionVariables; } @@ -39,7 +40,44 @@ const FieldErrorsContainer = styled.div` } `; -export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) => { +const ContainerActions = styled.div.attrs( + ({ className = '', $caseIndexes = [] }: { className?: string; $caseIndexes: string[] }) => ({ + className, + }) +)<{ $caseIndexes: string[] }>` + ${({ $caseIndexes }) => + $caseIndexes.map( + (index) => ` + div[id="${index}"].euiAccordion__childWrapper .euiAccordion__padding--l { + padding: 0px; + .euiFlexGroup { + display: none; + } + .euiSpacer.euiSpacer--xl { + height: 0px; + } + } + ` + )} +`; + +export const getSupportedActions = ( + actionTypes: ActionType[], + hasErrorOnCreationCaseAction: boolean +): ActionType[] => { + return actionTypes.filter((actionType) => { + if (actionType.id === '.case' && hasErrorOnCreationCaseAction) { + return false; + } + return NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id); + }); +}; + +export const RuleActionsField: React.FC<Props> = ({ + field, + hasErrorOnCreationCaseAction, + messageVariables, +}) => { const [fieldErrors, setFieldErrors] = useState<string | null>(null); const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>(); const form = useFormContext(); @@ -54,6 +92,17 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) = [field.value] ); + const caseActionIndexes = useMemo( + () => + actions.reduce<string[]>((acc, action, actionIndex) => { + if (action.actionTypeId === '.case') { + return [...acc, `${actionIndex}`]; + } + return acc; + }, []), + [actions] + ); + const setActionIdByIndex = useCallback( (id: string, index: number) => { const updatedActions = [...(actions as Array<Partial<AlertAction>>)]; @@ -83,13 +132,11 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) = useEffect(() => { (async function () { const actionTypes = await loadActionTypes({ http }); - const supportedTypes = actionTypes.filter((actionType) => - NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id) - ); + const supportedTypes = getSupportedActions(actionTypes, hasErrorOnCreationCaseAction); setSupportedActionTypes(supportedTypes); })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [hasErrorOnCreationCaseAction]); useEffect(() => { if (isSubmitting || !field.errors.length) { @@ -104,7 +151,7 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) = if (!supportedActionTypes) return <></>; return ( - <> + <ContainerActions $caseIndexes={caseActionIndexes}> {fieldErrors ? ( <> <FieldErrorsContainer> @@ -126,6 +173,6 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) = actionTypes={supportedActionTypes} defaultActionMessage={DEFAULT_ACTION_MESSAGE} /> - </> + </ContainerActions> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index 30898cdeca4a3..a31371c31cbbb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -36,6 +36,7 @@ import { useKibana } from '../../../../common/lib/kibana'; import { getSchema } from './schema'; import * as I18n from './translations'; import { APP_ID } from '../../../../../common/constants'; +import { useManageCaseAction } from './use_manage_case_action'; interface StepRuleActionsProps extends RuleStepProps { defaultValues?: ActionsStepRule | null; @@ -70,6 +71,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({ setForm, actionMessageParams, }) => { + const [isLoadingCaseAction, hasErrorOnCreationCaseAction] = useManageCaseAction(); const { services: { application, @@ -138,13 +140,14 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({ () => ({ idAria: 'detectionEngineStepRuleActionsThrottle', isDisabled: isLoading, + isLoading: isLoadingCaseAction, dataTestSubj: 'detectionEngineStepRuleActionsThrottle', hasNoInitialSelection: false, euiFieldProps: { options: throttleOptions, }, }), - [isLoading, throttleOptions] + [isLoading, isLoadingCaseAction, throttleOptions] ); const displayActionsOptions = useMemo( @@ -157,13 +160,14 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({ component={RuleActionsField} componentProps={{ messageVariables: actionMessageParams, + hasErrorOnCreationCaseAction, }} /> </> ) : ( <UseField path="actions" component={GhostFormField} /> ), - [throttle, actionMessageParams] + [throttle, actionMessageParams, hasErrorOnCreationCaseAction] ); // only display the actions dropdown if the user has "read" privileges for actions const displayActionsDropDown = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx new file mode 100644 index 0000000000000..55b2aefe21310 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx @@ -0,0 +1,63 @@ +/* + * 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, useRef, useState } from 'react'; +import { ACTION_URL } from '../../../../../../case/common/constants'; +import { KibanaServices } from '../../../../common/lib/kibana'; + +interface CaseAction { + actionTypeId: string; + id: string; + isPreconfigured: boolean; + name: string; + referencedByCount: number; +} + +const CASE_ACTION_NAME = 'Cases'; + +export const useManageCaseAction = () => { + const hasInit = useRef(true); + const [loading, setLoading] = useState(true); + const [hasError, setHasError] = useState(false); + + useEffect(() => { + const abortCtrl = new AbortController(); + const fetchActions = async () => { + try { + const actions = await KibanaServices.get().http.fetch<CaseAction[]>(ACTION_URL, { + method: 'GET', + signal: abortCtrl.signal, + }); + if (!actions.some((a) => a.actionTypeId === '.case' && a.name === CASE_ACTION_NAME)) { + await KibanaServices.get().http.post<CaseAction[]>(`${ACTION_URL}/action`, { + method: 'POST', + body: JSON.stringify({ + actionTypeId: '.case', + config: {}, + name: CASE_ACTION_NAME, + secrets: {}, + }), + signal: abortCtrl.signal, + }); + } + setLoading(false); + } catch { + setLoading(false); + setHasError(true); + } + }; + if (hasInit.current) { + hasInit.current = false; + fetchActions(); + } + + return () => { + abortCtrl.abort(); + }; + }, []); + return [loading, hasError]; +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 92fead153328a..d6eb4ceb258f9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17199,7 +17199,6 @@ "xpack.securitySolution.case.common.noConnector": "コネクターを選択していません", "xpack.securitySolution.case.components.connectors.case.actionTypeTitle": "ケース", "xpack.securitySolution.case.components.connectors.case.addNewCaseOption": "新規ケースの追加", - "xpack.securitySolution.case.components.connectors.case.callOutInfo": "ルールを作成した後のすべてのアラートは、選択したケースの最後に追加されます。", "xpack.securitySolution.case.components.connectors.case.caseRequired": "ケースの選択が必要です。", "xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder": "ケースを選択", "xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel": "ケース", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b00c863cad9aa..d9b2350e9e9c0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17242,7 +17242,6 @@ "xpack.securitySolution.case.common.noConnector": "未选择任何连接器", "xpack.securitySolution.case.components.connectors.case.actionTypeTitle": "案例", "xpack.securitySolution.case.components.connectors.case.addNewCaseOption": "添加新案例", - "xpack.securitySolution.case.components.connectors.case.callOutInfo": "规则创建后的所有告警将追加到选定案例。", "xpack.securitySolution.case.components.connectors.case.caseRequired": "必须选择策略。", "xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder": "选择案例", "xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel": "案例",