From 3cacbe7c49ad40b26632d7da3c2e6ab9f9f2674b Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Wed, 25 May 2022 06:48:21 -0600 Subject: [PATCH] [Security Solution][Detections] Integrates installed integrations into interface (#132847) ## Summary Wires up the new Installed Integrations API added in https://github.com/elastic/kibana/pull/132667 to the new Related Integrations UI added in https://github.com/elastic/kibana/pull/131475. #### Additional changes include (though not all necessary for this specific PR): - [X] Updates integrations badge icon to `package` on Rules Table - [ ] Add Kibana Advanced Setting for disabling integrations badge on Rules Table - [ ] Add loaders where necessary since there can now be API delay - [ ] Separate description step components to specific files Please see https://github.com/elastic/kibana/pull/131475 for screenshots and additional details. #### Steps to test In this initial implementation these new fields are only visible with Prebuilt Rules, and so there is limited API support and currently no UI for editing them. If a Prebuilt Rule is duplicated, these fields are emptied (set to `''` or `[]`). When a Rule is exported these fields are included (as empty values), and it is possible to edit the `ndjson` and re-import and then see these fields for the Custom Rule (but still not editable in the UI). This is expected behavior, and is actually a nice and easy way to test. Here is a sample export you can paste into a `test.ndjson` file and import to test this feature. You can modify the `package`/`version` fields to test corner cases like if a package is installed but it's the wrong version. ``` {"id":"6cc39c80-da3a-11ec-9fce-65c1a0bee904","updated_at":"2022-05-23T01:48:23.422Z","updated_by":"elastic","created_at":"2022-05-23T01:48:20.940Z","created_by":"elastic","name":"Testing #131475, don't mind me...","tags":["Elastic","Endpoint Security"],"interval":"5m","enabled":false,"description":"Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.","risk_score":47,"severity":"medium","license":"Elastic License v2","output_index":".siem-signals-default","meta":{"from":"5m"},"rule_name_override":"message","timestamp_override":"event.ingested","author":["Elastic"],"false_positives":[],"from":"now-600s","rule_id":"2c66bf23-6ae9-4eb2-859e-446bea181ae9","max_signals":10000,"risk_score_mapping":[{"field":"event.risk_score","operator":"equals","value":""}],"severity_mapping":[{"field":"event.severity","operator":"equals","severity":"low","value":"21"},{"field":"event.severity","operator":"equals","severity":"medium","value":"47"},{"field":"event.severity","operator":"equals","severity":"high","value":"73"},{"field":"event.severity","operator":"equals","severity":"critical","value":"99"}],"threat":[],"to":"now","references":[],"version":7,"exceptions_list":[{"id":"endpoint_list","list_id":"endpoint_list","namespace_type":"agnostic","type":"endpoint"}],"immutable":false,"related_integrations":[{"package":"system","version":"1.6.4"},{"package":"aws","integration":"cloudtrail","version":"1.11.0"}],"required_fields":[{"ecs":true,"name":"event.code","type":"keyword"},{"ecs":true,"name":"message","type":"match_only_text"},{"ecs":false,"name":"winlog.event_data.AttributeLDAPDisplayName","type":"keyword"},{"ecs":false,"name":"winlog.event_data.AttributeValue","type":"keyword"},{"ecs":false,"name":"winlog.event_data.ShareName","type":"keyword"},{"ecs":false,"name":"winlog.event_data.RelativeTargetName","type":"keyword"},{"ecs":false,"name":"winlog.event_data.AccessList","type":"keyword"}],"setup":"## Config\\n\\nThe 'Audit Detailed File Share' audit policy must be configured (Success Failure).\\nSteps to implement the logging policy with with Advanced Audit Configuration:\\n\\n```\\nComputer Configuration > \\nPolicies > \\nWindows Settings > \\nSecurity Settings > \\nAdvanced Audit Policies Configuration > \\nAudit Policies > \\nObject Access > \\nAudit Detailed File Share (Success,Failure)\\n```\\n\\nThe 'Audit Directory Service Changes' audit policy must be configured (Success Failure).\\nSteps to implement the logging policy with with Advanced Audit Configuration:\\n\\n```\\nComputer Configuration > \\nPolicies > \\nWindows Settings > \\nSecurity Settings > \\nAdvanced Audit Policies Configuration > \\nAudit Policies > \\nDS Access > \\nAudit Directory Service Changes (Success,Failure)\\n```\\n","type":"query","language":"kuery","index":["logs-endpoint.alerts-*"],"query":"event.kind:alert and event.module:(endpoint and not endgame)\\n","filters":[],"throttle":"no_actions","actions":[]} {"exported_count":1,"exported_rules_count":1,"missing_rules":[],"missing_rules_count":0,"exported_exception_list_count":0,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0} ``` --- .../integrations_popover/helpers.tsx | 58 +++++++--- .../components/integrations_popover/index.tsx | 8 +- .../required_integrations_description.tsx | 5 +- .../containers/detection_engine/rules/api.ts | 6 +- .../rules/use_installed_integrations.tsx | 32 ++---- .../__snapshots__/index.test.tsx.snap | 108 +++++++++--------- 6 files changed, 116 insertions(+), 101 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/integrations_popover/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/integrations_popover/helpers.tsx index 65854b0cc1fb50..b7855f2492a487 100644 --- a/x-pack/plugins/security_solution/public/common/components/integrations_popover/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/integrations_popover/helpers.tsx @@ -10,29 +10,58 @@ import { capitalize } from 'lodash'; import React from 'react'; import semver from 'semver'; import { + InstalledIntegration, + InstalledIntegrationArray, RelatedIntegration, RelatedIntegrationArray, } from '../../../../common/detection_engine/schemas/common'; /** * Returns an `EuiLink` that will link to a given package/integration/version page within fleet - * @param integration - * @param basePath + * TODO: Add `title` to RelatedIntegration so we can accurately display the integration pretty name + * + * @param integration either RelatedIntegration or InstalledIntegration + * @param basePath kbn basepath for composing the fleet URL */ -export const getIntegrationLink = (integration: RelatedIntegration, basePath: string) => { - const integrationURL = `${basePath}/app/integrations/detail/${integration.package}-${ - integration.version - }/overview${integration.integration ? `?integration=${integration.integration}` : ''}`; +export const getIntegrationLink = ( + integration: RelatedIntegration | InstalledIntegration, + basePath: string +) => { + let packageName: string; + let integrationName: string | undefined; + let integrationTitle: string; + let version: string | null; + + // InstalledIntegration + if ('package_name' in integration) { + packageName = integration.package_name; + integrationName = integration.integration_name; + integrationTitle = integration.integration_title ?? integration.package_name; + version = integration.package_version; + } else { + // RelatedIntegration + packageName = integration.package; + integrationName = integration.integration; + integrationTitle = `${capitalize(integration.package)} ${capitalize(integration.integration)}`; + version = semver.valid(semver.coerce(integration.version)); + } + + const integrationURL = + version != null + ? `${basePath}/app/integrations/detail/${packageName}-${version}/overview${ + integrationName ? `?integration=${integrationName}` : '' + }` + : `${basePath}/app/integrations/detail/${packageName}`; return ( - {`${capitalize(integration.package)} ${capitalize(integration.integration)}`} + {integrationTitle} ); }; -export interface InstalledIntegration extends RelatedIntegration { +export interface InstalledIntegrationAugmented extends InstalledIntegration { targetVersion: string; - versionSatisfied?: boolean; + versionSatisfied: boolean; } /** @@ -44,21 +73,22 @@ export interface InstalledIntegration extends RelatedIntegration { */ export const getInstalledRelatedIntegrations = ( integrations: RelatedIntegrationArray, - installedIntegrations: RelatedIntegrationArray + installedIntegrations: InstalledIntegrationArray ): { availableIntegrations: RelatedIntegrationArray; - installedRelatedIntegrations: InstalledIntegration[]; + installedRelatedIntegrations: InstalledIntegrationAugmented[]; } => { const availableIntegrations: RelatedIntegrationArray = []; - const installedRelatedIntegrations: InstalledIntegration[] = []; + const installedRelatedIntegrations: InstalledIntegrationAugmented[] = []; integrations.forEach((i: RelatedIntegration) => { const match = installedIntegrations.find( - (installed) => installed.package === i.package && installed?.integration === i?.integration + (installed) => + installed.package_name === i.package && installed?.integration_name === i?.integration ); if (match != null) { // Version check e.g. fleet match `1.2.3` satisfies rule dependency `~1.2.1` - const versionSatisfied = semver.satisfies(match.version, i.version); + const versionSatisfied = semver.satisfies(match.package_version, i.version); installedRelatedIntegrations.push({ ...match, targetVersion: i.version, versionSatisfied }); } else { availableIntegrations.push(i); diff --git a/x-pack/plugins/security_solution/public/common/components/integrations_popover/index.tsx b/x-pack/plugins/security_solution/public/common/components/integrations_popover/index.tsx index 8574a96ed9516a..45d4b72690643a 100644 --- a/x-pack/plugins/security_solution/public/common/components/integrations_popover/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/integrations_popover/index.tsx @@ -15,6 +15,7 @@ import { EuiIconTip, } from '@elastic/eui'; import styled from 'styled-components'; +import { InstalledIntegrationArray } from '../../../../common/detection_engine/schemas/common'; import { useBasePath } from '../../lib/kibana'; import { getInstalledRelatedIntegrations, getIntegrationLink } from './helpers'; import { useInstalledIntegrations } from '../../../detections/containers/detection_engine/rules/use_installed_integrations'; @@ -50,12 +51,13 @@ const IntegrationsPopoverComponent = ({ integrations }: IntegrationsPopoverProps const { data } = useInstalledIntegrations({ packages: [] }); const basePath = useBasePath(); - const allInstalledIntegrations: RelatedIntegrationArray = data ?? []; + const allInstalledIntegrations: InstalledIntegrationArray = data?.installed_integrations ?? []; const { availableIntegrations, installedRelatedIntegrations } = getInstalledRelatedIntegrations( integrations, allInstalledIntegrations ); + // TODO: Add loader to installed integrations value const badgeTitle = data != null ? `${installedRelatedIntegrations.length}/${integrations.length} ${i18n.INTEGRATIONS_BADGE}` @@ -72,7 +74,7 @@ const IntegrationsPopoverComponent = ({ integrations }: IntegrationsPopoverProps data-test-subj={'IntegrationsDisplayPopover'} button={ setPopoverOpen(!isPopoverOpen)} @@ -105,7 +107,7 @@ const IntegrationsPopoverComponent = ({ integrations }: IntegrationsPopoverProps => - KibanaServices.get().http.fetch( +}): Promise => + KibanaServices.get().http.fetch( DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL, { method: 'GET', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_installed_integrations.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_installed_integrations.tsx index caf46ddc8f210f..dc961efdb9670b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_installed_integrations.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_installed_integrations.tsx @@ -6,7 +6,8 @@ */ import { useQuery } from 'react-query'; -import { RelatedIntegrationArray } from '../../../../../common/detection_engine/schemas/common'; +import { GetInstalledIntegrationsResponse } from '../../../../../common/detection_engine/schemas/response/get_installed_integrations_response_schema'; +import { fetchInstalledIntegrations } from './api'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import * as i18n from './translations'; @@ -17,9 +18,7 @@ export interface UseInstalledIntegrationsArgs { export const useInstalledIntegrations = ({ packages }: UseInstalledIntegrationsArgs) => { const { addError } = useAppToasts(); - // TODO: Once API is merged update return type: - // See: https://github.com/elastic/kibana/pull/132667/files#diff-f9d9583d37123ed28fd08fc153eb06026e7ee0c3241364656fb707dcbc0a4872R58-R65 - return useQuery( + return useQuery( [ 'installedIntegrations', { @@ -27,27 +26,10 @@ export const useInstalledIntegrations = ({ packages }: UseInstalledIntegrationsA }, ], async ({ signal }) => { - return undefined; - - // Mock data -- uncomment to test full UI - // const mockInstalledIntegrations = [ - // { - // package: 'system', - // version: '1.7.4', - // }, - // // { - // // package: 'aws', - // // integration: 'cloudtrail', - // // version: '1.11.0', - // // }, - // ]; - // return mockInstalledIntegrations; - - // Or fetch from new API - // return fetchInstalledIntegrations({ - // packages, - // signal, - // }); + return fetchInstalledIntegrations({ + packages, + signal, + }); }, { keepPreviousData: true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index b557f84b431b51..9e857ccf7d995f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -731,6 +731,33 @@ exports[`Details Panel Component DetailsPanel:HostDetails: rendering it should r color: #535966; } +.c2 dt { + font-size: 12px !important; +} + +.c2 dd { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} + +.c2 dd > div { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} + +.c1 { + position: relative; +} + +.c1 .euiButtonIcon { + position: absolute; + right: 12px; + top: 6px; + z-index: 2; +} + .c0 { width: 100%; display: -webkit-box; @@ -759,33 +786,6 @@ exports[`Details Panel Component DetailsPanel:HostDetails: rendering it should r opacity: 1; } -.c2 dt { - font-size: 12px !important; -} - -.c2 dd { - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; -} - -.c2 dd > div { - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; -} - -.c1 { - position: relative; -} - -.c1 .euiButtonIcon { - position: absolute; - right: 12px; - top: 6px; - z-index: 2; -} - .c4 { padding: 16px; background: rgba(250,251,253,0.9); @@ -1872,6 +1872,33 @@ exports[`Details Panel Component DetailsPanel:NetworkDetails: rendering it shoul color: #535966; } +.c2 dt { + font-size: 12px !important; +} + +.c2 dd { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} + +.c2 dd > div { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} + +.c1 { + position: relative; +} + +.c1 .euiButtonIcon { + position: absolute; + right: 12px; + top: 6px; + z-index: 2; +} + .c0 { width: 100%; display: -webkit-box; @@ -1900,33 +1927,6 @@ exports[`Details Panel Component DetailsPanel:NetworkDetails: rendering it shoul opacity: 1; } -.c2 dt { - font-size: 12px !important; -} - -.c2 dd { - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; -} - -.c2 dd > div { - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; -} - -.c1 { - position: relative; -} - -.c1 .euiButtonIcon { - position: absolute; - right: 12px; - top: 6px; - z-index: 2; -} - .c4 { padding: 16px; background: rgba(250,251,253,0.9);