From b0247a9ad0d98b6688c5d9c5ed984e0e41377162 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Fri, 20 May 2022 23:07:08 -0600 Subject: [PATCH] Adds hook for fetching installedIntegrations and cleans up UI components --- .../security_solution/common/constants.ts | 2 + .../integrations_popover/helpers.tsx | 22 +++++ .../components/integrations_popover/index.tsx | 95 +++++++++++++------ .../rules/description_step/helpers.tsx | 31 +++--- .../containers/detection_engine/rules/api.ts | 28 ++++++ .../detection_engine/rules/translations.ts | 7 ++ .../rules/use_installed_integrations.tsx | 55 +++++++++++ .../rules/all/use_columns.tsx | 2 +- .../detection_engine/rules/translations.ts | 6 +- 9 files changed, 204 insertions(+), 44 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/integrations_popover/helpers.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_installed_integrations.tsx diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index f8c159241d00e..104887f7c3af6 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -237,6 +237,8 @@ export const DETECTION_ENGINE_PREPACKAGED_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged` as const; export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges` as const; export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index` as const; +export const DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL = + `${DETECTION_ENGINE_URL}/installed_integrations` as const; export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags` as const; export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged/_status` as const; 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 new file mode 100644 index 0000000000000..b16397933a6f4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/integrations_popover/helpers.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiLink } from '@elastic/eui'; +import { capitalize } from 'lodash'; +import React from 'react'; +import { RelatedIntegration } from '../../../../common/detection_engine/schemas/common'; + +export const getIntegrationLink = (integration: RelatedIntegration, basePath: string) => { + const integrationURL = `${basePath}/app/integrations/detail/${integration.package}-${ + integration.version + }/overview${integration.integration ? `?integration=${integration.integration}` : ''}`; + return ( + + {`${capitalize(integration.package)} ${capitalize(integration.integration)}`} + + ); +}; 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 d731078a9ec54..9c8a6ee6d9ff3 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 @@ -13,16 +13,20 @@ import { EuiPopoverTitle, EuiFlexGroup, EuiText, - EuiLink, } from '@elastic/eui'; import styled from 'styled-components'; -import type { RelatedIntegrationArray } from '../../../../common/detection_engine/schemas/common'; +import { useBasePath } from '../../lib/kibana'; +import { getIntegrationLink } from './helpers'; +import { useInstalledIntegrations } from '../../../detections/containers/detection_engine/rules/use_installed_integrations'; +import type { + RelatedIntegration, + RelatedIntegrationArray, +} from '../../../../common/detection_engine/schemas/common'; import * as i18n from '../../../detections/pages/detection_engine/rules/translations'; export interface IntegrationsPopoverProps { integrations: RelatedIntegrationArray; - installedIntegrations: RelatedIntegrationArray; } const IntegrationsPopoverWrapper = styled(EuiFlexGroup)` @@ -36,18 +40,40 @@ const PopoverWrapper = styled(EuiBadgeGroup)` line-height: ${({ theme }) => theme.eui.euiLineHeight}; `; +const IntegrationListItem = styled('li')` + list-style-type: disc; + margin-left: 25px; +`; /** * Component to render installed and available integrations - * @param integrations - array of items to render - * @param installedIntegrations - array of items to render + * @param integrations - array of integrations to display */ -const IntegrationsPopoverComponent = ({ - integrations, - installedIntegrations, -}: IntegrationsPopoverProps) => { +const IntegrationsPopoverComponent = ({ integrations }: IntegrationsPopoverProps) => { const [isPopoverOpen, setPopoverOpen] = useState(false); + const { data } = useInstalledIntegrations({ packages: [] }); + // const data = undefined; // To test with installed_integrations endpoint not implemented + const basePath = useBasePath(); + + const allInstalledIntegrations: RelatedIntegrationArray = data ?? []; + const availableIntegrations: RelatedIntegrationArray = []; + const installedIntegrations: RelatedIntegrationArray = []; + + integrations.forEach((i: RelatedIntegration) => { + const match = allInstalledIntegrations.find( + (installed) => installed.package === i.package && installed?.integration === i?.integration + ); + if (match != null) { + // TODO: Do version check + installedIntegrations.push(match); + } else { + availableIntegrations.push(i); + } + }); - const integrationsTitle = `${installedIntegrations.length}/${integrations.length} ${i18n.INTEGRATIONS_BADGE}`; + const badgeTitle = + data != null + ? `${installedIntegrations.length}/${integrations.length} ${i18n.INTEGRATIONS_BADGE}` + : `${integrations.length} ${i18n.INTEGRATIONS_BADGE}`; return ( setPopoverOpen(!isPopoverOpen)} - onClickAriaLabel={integrationsTitle} + onClickAriaLabel={badgeTitle} > - {integrationsTitle} + {badgeTitle} } isOpen={isPopoverOpen} @@ -74,25 +100,38 @@ const IntegrationsPopoverComponent = ({ repositionOnScroll > - {i18n.INTEGRATIONS_POPOVER_TITLE(3)} + {i18n.INTEGRATIONS_POPOVER_TITLE(integrations.length)} - {i18n.INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED(1)} - - {'AWS CloudTrail'} - - {i18n.INTEGRATIONS_POPOVER_DESCRIPTION_UNINSTALLED(2)} -
- - {'Endpoint Security'} - -
-
- - {'\nModSecurity Audit'} - -
+ {data != null && ( + <> + + {i18n.INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED(installedIntegrations.length)} + +
    + {installedIntegrations.map((integration, index) => ( + + {getIntegrationLink(integration, basePath)} + + ))} +
+ + )} + {availableIntegrations.length > 0 && ( + <> + + {i18n.INTEGRATIONS_POPOVER_DESCRIPTION_UNINSTALLED(availableIntegrations.length)} + +
    + {availableIntegrations.map((integration, index) => ( + + {getIntegrationLink(integration, basePath)} + + ))} +
+ + )}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index e9a6fe180a3c1..ad48aa7ce8abd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -16,6 +16,7 @@ import { EuiText, EuiIcon, EuiToolTip, + EuiFlexGrid, } from '@elastic/eui'; import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils'; import { capitalize } from 'lodash'; @@ -520,10 +521,10 @@ export const buildRelatedIntegrationsDescription = ( ): ListItems[] => { const badgeInstalledColor = '#E0E5EE'; // 'subdued' not working? const badgeUninstalledColor = 'accent'; - const basePath = 'http://localhost:5601/kbn'; // const { basePath } = useBasePath(); + const basePath = 'http://localhost:5601/kbn'; // const basePath = useBasePath(); const installedText = 'Installed'; const uninstalledText = 'Uninstalled'; - const installedPackages = ['aws']; + const installedPackages = ['aws']; // TODO: Use hook const { data } = useInstalledIntegrations({ packages: [] }); return relatedIntegrations.map((rI, index) => { const isInstalled = installedPackages.includes(rI.package); @@ -538,9 +539,7 @@ export const buildRelatedIntegrationsDescription = ( description: ( <> - {rI.integration - ? `${capitalize(rI.integration)} ${capitalize(rI.integration)}` - : capitalize(rI.package)} + {`${capitalize(rI.package)} ${capitalize(rI.integration)}`} {' '} {badgeText} @@ -552,6 +551,7 @@ export const buildRelatedIntegrationsDescription = ( const FieldTypeText = styled(EuiText)` font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; + display: inline; `; export const buildRequiredFieldsDescription = ( @@ -562,15 +562,22 @@ export const buildRequiredFieldsDescription = ( { title: label, description: ( - + {requiredFields.map((rF, index) => ( - <> - - {` ${rF.name}`} - {index + 1 !== requiredFields.length && <>{', '}} - + + + + + + + + {` ${rF.name}${index + 1 !== requiredFields.length ? ', ' : ''}`} + + + + ))} - + ), }, ]; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 220926ebc1722..177b4daf9fdfd 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -18,10 +18,12 @@ import { DETECTION_ENGINE_RULES_BULK_ACTION, DETECTION_ENGINE_RULES_PREVIEW, detectionEngineRuleExecutionEventsUrl, + DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL, } from '../../../../../common/constants'; import { AggregateRuleExecutionEvent, BulkAction, + RelatedIntegrationArray, RuleExecutionStatus, } from '../../../../../common/detection_engine/schemas/common'; import { @@ -408,3 +410,29 @@ export const getPrePackagedRulesStatus = async ({ signal, } ); + +/** + * Fetch all installed integrations + * + * @param packages array of packages to filter for + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchInstalledIntegrations = async ({ + packages, + signal, +}: { + packages?: string[]; + signal?: AbortSignal; +}): Promise => + KibanaServices.get().http.fetch( + DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL, + { + method: 'GET', + query: { + packages: packages?.sort()?.join(','), + }, + signal, + } + ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts index 5d2bac9e8b501..89d6332c9caaa 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts @@ -123,3 +123,10 @@ export const RULE_EXECUTION_EVENTS_FETCH_FAILURE = i18n.translate( defaultMessage: 'Failed to fetch rule execution events', } ); + +export const INSTALLED_INTEGRATIONS_FETCH_FAILURE = i18n.translate( + 'xpack.securitySolution.containers.detectionEngine.installedIntegrationsFetchFailDescription', + { + defaultMessage: 'Failed to fetch installed integrations', + } +); 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 new file mode 100644 index 0000000000000..82c3bef767133 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_installed_integrations.tsx @@ -0,0 +1,55 @@ +/* + * 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 { useQuery } from 'react-query'; +import { RelatedIntegrationArray } from '../../../../../common/detection_engine/schemas/common'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import * as i18n from './translations'; + +export interface UseInstalledIntegrationsArgs { + packages?: string[]; +} + +export const useInstalledIntegrations = ({ packages }: UseInstalledIntegrationsArgs) => { + const { addError } = useAppToasts(); + + return useQuery( + [ + 'installedIntegrations', + { + packages, + }, + ], + async ({ signal }) => { + // Mock data + const mockInstalledIntegrations = [ + { + package: 'system', + version: '1.6.4', + }, + // { + // package: 'aws', + // integration: 'cloudtrail', + // version: '1.11.0', + // }, + ]; + return mockInstalledIntegrations; + + // Or fetch from new API + // return fetchInstalledIntegrations({ + // packages, + // signal, + // }); + }, + { + keepPreviousData: true, + onError: (e) => { + addError(e, { title: i18n.INSTALLED_INTEGRATIONS_FETCH_FAILURE }); + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx index 2044108da59da..c28776d97d683 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx @@ -167,7 +167,7 @@ const INTEGRATIONS_COLUMN: TableColumn = { return null; } - return ; + return ; }, width: '143px', truncateText: true, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index e259823a36658..daebcc85444af 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -1099,7 +1099,7 @@ export const INTEGRATIONS_POPOVER_TITLE = (integrationsCount: number) => { values: { integrationsCount }, defaultMessage: - 'You have [{integrationsCount}] related {integrationsCount, plural, =1 {# integration} other {# integrations}} to your prebuilt rule', + 'You have [{integrationsCount}] related {integrationsCount, plural, =1 {integration} other {integrations}} to your prebuilt rule', } ); @@ -1109,7 +1109,7 @@ export const INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED = (installedCount: numbe { values: { installedCount }, defaultMessage: - 'You have [{installedCount}] related {installedCount, plural, =1 {# integration} other {# integrations}} installed, click the link below to view the integration:', + 'You have [{installedCount}] related {installedCount, plural, =1 {integration} other {integrations}} installed, click the link below to view the integration:', } ); @@ -1119,6 +1119,6 @@ export const INTEGRATIONS_POPOVER_DESCRIPTION_UNINSTALLED = (uninstalledCount: n { values: { uninstalledCount }, defaultMessage: - 'You have [{uninstalledCount}] related {uninstalledCount, plural, =1 {# integration} other {# integrations}} uninstalled, click the link to add integration:', + 'You have [{uninstalledCount}] related {uninstalledCount, plural, =1 {integration} other {integrations}} uninstalled, click the link to add integration:', } );