From d2058fd1003bf39419372b37977196796e342557 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Tue, 3 May 2022 18:52:20 -0600 Subject: [PATCH 1/9] First pass at UI for new rule fields --- .../schemas/common/schemas.ts | 27 +++++++ .../rules/description_step/helpers.tsx | 74 ++++++++++++++++++- .../rules/description_step/index.tsx | 23 ++++-- .../rules/step_about_rule_details/index.tsx | 44 +++++++---- .../step_about_rule_details/translations.ts | 7 ++ .../rules/step_define_rule/index.tsx | 2 + .../rules/step_define_rule/schema.tsx | 28 +++++++ .../pages/detection_engine/rules/helpers.tsx | 3 + .../pages/detection_engine/rules/types.ts | 5 ++ .../routes/rules/read_rules_route.ts | 51 +++++++++++++ 10 files changed, 244 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index a47107cd068eb..b12755505137c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -185,12 +185,39 @@ export type Name = t.TypeOf; export const nameOrUndefined = t.union([name, t.undefined]); export type NameOrUndefined = t.TypeOf; +export const related_integration = t.exact( + t.type({ + package: t.string, + version: t.string, + integration: t.union([t.string, t.undefined]), + }) +); +export type RelatedIntegration = t.TypeOf; + +export const related_integrations = t.array(related_integration); +export type RelatedIntegrations = t.TypeOf; + +export const required_field = t.exact( + t.type({ + name: t.string, + type: t.string, + ecs: t.boolean, + }) +); +export type RequiredField = t.TypeOf; + +export const required_fields = t.array(required_field); +export type RequiredFields = t.TypeOf; + export const rule_name_override = t.string; export type RuleNameOverride = t.TypeOf; export const ruleNameOverrideOrUndefined = t.union([rule_name_override, t.undefined]); export type RuleNameOverrideOrUndefined = t.TypeOf; +export const setup = t.string; +export type Setup = t.TypeOf; + export const status = t.keyof({ open: null, closed: null, 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 df407e4e88ecf..8682631854d9e 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,8 +16,10 @@ import { EuiText, EuiIcon, EuiToolTip, + EuiToken, } from '@elastic/eui'; import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils'; +import { capitalize } from 'lodash'; import { isEmpty } from 'lodash/fp'; import React from 'react'; @@ -30,7 +32,11 @@ import { MATCHES, AND, OR } from '../../../../common/components/threat_match/tra import { assertUnreachable } from '../../../../../common/utility_types'; import * as i18nSeverity from '../severity_mapping/translations'; import * as i18nRiskScore from '../risk_score_mapping/translations'; -import { Threshold } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { + RelatedIntegrations, + RequiredFields, + Threshold, +} from '../../../../../common/detection_engine/schemas/common/schemas'; import { subtechniquesOptions, tacticsOptions, @@ -507,3 +513,69 @@ export const buildThreatMappingDescription = ( }, ]; }; + +export const buildRelatedIntegrationsDescription = ( + label: string, + relatedIntegrations: RelatedIntegrations +): ListItems[] => { + const badgeInstalledColor = '#E0E5EE'; // 'subdued' not working? + const badgeUninstalledColor = 'accent'; + const basePath = 'http://localhost:5601/kbn'; // const { basePath } = useBasePath(); + const installedText = 'Installed'; + const uninstalledText = 'Uninstalled'; + const installedPackages = ['aws']; + + return relatedIntegrations.map((rI, index) => { + const isInstalled = installedPackages.includes(rI.package); + const badgeColor = isInstalled ? badgeInstalledColor : badgeUninstalledColor; + const badgeText = isInstalled ? installedText : uninstalledText; + const integrationURL = `${basePath}/app/integrations/detail/${rI.package}-${ + rI.version + }/overview${rI.integration ? `?integration=${rI.integration}` : ''}`; + + return { + title: index === 0 ? label : '', + description: ( + <> + + {rI.integration + ? `${capitalize(rI.integration)} ${capitalize(rI.integration)}` + : capitalize(rI.package)} + {' '} + {badgeText} + + ), + }; + }); +}; + +const FieldTypeText = styled(EuiText)` + font-family: 'Roboto Mono', 'serif'; // const { euiTheme } = useEuiTheme(); // Use 'Inter' from designs +`; + +export const buildRequiredFieldsDescription = ( + label: string, + requiredFields: RequiredFields +): ListItems[] => { + const typeToTokenMapping: Record = { + keyword: 'keyword', + match_only_text: 'text', + }; + + return [ + { + title: label, + description: ( + + {requiredFields.map((rF, index) => ( + <> + + {` ${rF.name}`} + {index + 1 !== requiredFields.length && <>{', '}} + + ))} + + ), + }, + ]; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index c72b96242df07..c5792760f26c2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -13,6 +13,10 @@ import styled from 'styled-components'; import { ThreatMapping, Threats, Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { DataViewBase, Filter, FilterStateStore } from '@kbn/es-query'; import { FilterManager } from '@kbn/data-plugin/public'; +import { + RelatedIntegrations, + RequiredFields, +} from '../../../../../common/detection_engine/schemas/common'; import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { useKibana } from '../../../../common/lib/kibana'; import { AboutStepRiskScore, AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; @@ -22,7 +26,7 @@ import { ListItems } from './types'; import { buildQueryBarDescription, buildSeverityDescription, - buildStringArrayDescription, + // buildStringArrayDescription, buildThreatDescription, buildUnorderedListArrayDescription, buildUrlsDescription, @@ -31,6 +35,8 @@ import { buildRuleTypeDescription, buildThresholdDescription, buildThreatMappingDescription, + buildRelatedIntegrationsDescription, + buildRequiredFieldsDescription, } from './helpers'; import { buildMlJobsDescription } from './ml_job_description'; import { buildActionsDescription } from './actions_description'; @@ -151,7 +157,7 @@ export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => { }); }; -/* eslint complexity: ["error", 21]*/ +/* eslint complexity: ["error", 25]*/ export const getDescriptionItem = ( field: string, label: string, @@ -183,15 +189,22 @@ export const getDescriptionItem = ( } else if (field === 'falsePositives') { const values: string[] = get(field, data); return buildUnorderedListArrayDescription(label, field, values); - } else if (Array.isArray(get(field, data)) && field !== 'threatMapping') { - const values: string[] = get(field, data); - return buildStringArrayDescription(label, field, values); + // TODO: lol 😭 + // } else if (Array.isArray(get(field, data)) && field !== 'threatMapping') { + // const values: string[] = get(field, data); + // return buildStringArrayDescription(label, field, values); } else if (field === 'riskScore') { const values: AboutStepRiskScore = get(field, data); return buildRiskScoreDescription(values); } else if (field === 'severity') { const values: AboutStepSeverity = get(field, data); return buildSeverityDescription(values); + } else if (field === 'requiredFields') { + const requiredFields = get(field, data) as RequiredFields; + return buildRequiredFieldsDescription(label, requiredFields); + } else if (field === 'relatedIntegrations') { + const relatedIntegrations = get(field, data) as RelatedIntegrations; + return buildRelatedIntegrationsDescription(label, relatedIntegrations); } else if (field === 'timeline') { const timeline = get(field, data) as FieldValueTimeline; return [ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx index f400887f43927..8feeedb9ea609 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx @@ -18,7 +18,6 @@ import { } from '@elastic/eui'; import React, { memo, useCallback, useState } from 'react'; import styled from 'styled-components'; -import { isEmpty } from 'lodash/fp'; import { HeaderSection } from '../../../../common/components/header_section'; import { MarkdownRenderer } from '../../../../common/components/markdown_editor'; @@ -59,6 +58,11 @@ const toggleOptions: EuiButtonGroupOptionProps[] = [ label: i18n.ABOUT_PANEL_NOTES_TAB, 'data-test-subj': 'stepAboutDetailsToggle-notes', }, + { + id: 'setup', + label: i18n.ABOUT_PANEL_SETUP_TAB, + 'data-test-subj': 'stepAboutDetailsToggle-setup', + }, ]; interface StepPanelProps { @@ -94,21 +98,19 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ - {!isEmpty(stepDataDetails.note) && stepDataDetails.note.trim() !== '' && ( - { - setToggleOption(val); - }} - data-test-subj="stepAboutDetailsToggle" - legend={i18n.ABOUT_CONTROL_LEGEND} - /> - )} + { + setToggleOption(val); + }} + data-test-subj="stepAboutDetailsToggle" + legend={i18n.ABOUT_CONTROL_LEGEND} + /> - {selectedToggleOption === 'details' ? ( + {selectedToggleOption === 'details' && ( {(resizeRef) => ( @@ -132,7 +134,8 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ )} - ) : ( + )} + {selectedToggleOption === 'note' && ( = ({ )} + {selectedToggleOption === 'setup' && ( + + + {stepDataDetails.setup} + + + )} )} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts index e479cad3151a5..40a30dffb6021 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/translations.ts @@ -28,6 +28,13 @@ export const ABOUT_PANEL_NOTES_TAB = i18n.translate( } ); +export const ABOUT_PANEL_SETUP_TAB = i18n.translate( + 'xpack.securitySolution.detectionEngine.details.stepAboutRule.setupGuideLabel', + { + defaultMessage: 'Setup guide', + } +); + export const ABOUT_CONTROL_LEGEND = i18n.translate( 'xpack.securitySolution.detectionEngine.details.stepAboutRule.controlLegend', { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 2113af02d0d06..21074226d92c6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -80,6 +80,8 @@ export const stepDefineDefaultValue: DefineStepRule = { filters: [], saved_id: undefined, }, + requiredFields: [], + relatedIntegrations: [], threatMapping: [], threshold: { field: [], diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx index a2018280bebc6..f066b5dd97fce 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx @@ -175,6 +175,34 @@ export const schema: FormSchema = { }, ], }, + relatedIntegrations: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRelatedIntegrationsLabel', + { + defaultMessage: 'Related integrations', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRelatedIntegrationsHelpText', + { + defaultMessage: 'Integration related to this Rule.', + } + ), + }, + requiredFields: { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRequiredFieldsLabel', + { + defaultMessage: 'Required fields', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRequiredFieldsHelpText', + { + defaultMessage: 'Fields required for this Rule to function.', + } + ), + }, timeline: { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index a8037ea9216a3..7e446416cfaa9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -95,6 +95,8 @@ export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ filters: (rule.filters ?? []) as Filter[], saved_id: rule.saved_id, }, + relatedIntegrations: rule.related_integrations ?? [], + requiredFields: rule.required_fields ?? [], timeline: { id: rule.timeline_id ?? null, title: rule.timeline_title ?? null, @@ -227,6 +229,7 @@ export const determineDetailsValue = ( export const getModifiedAboutDetailsData = (rule: Rule): AboutStepRuleDetails => ({ note: rule.note ?? '', description: rule.description, + setup: rule.setup ?? '', }); export const useQuery = () => new URLSearchParams(useLocation().search); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index e94365da0a3a7..bfea6efb38fce 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -25,6 +25,8 @@ import { Author, BuildingBlockType, License, + RelatedIntegrations, + RequiredFields, RuleNameOverride, SortOrder, TimestampOverride, @@ -109,6 +111,7 @@ export interface AboutStepRule { export interface AboutStepRuleDetails { note: string; description: string; + setup: string; } export interface AboutStepSeverity { @@ -128,6 +131,8 @@ export interface DefineStepRule { index: string[]; machineLearningJobId: string[]; queryBar: FieldValueQueryBar; + relatedIntegrations: RelatedIntegrations; + requiredFields: RequiredFields; ruleType: Type; timeline: FieldValueTimeline; threshold: FieldValueThreshold; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts index 37af66b50c6bb..b2638b3486f50 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -67,6 +67,57 @@ export const readRulesRoute = (router: SecuritySolutionPluginRouter, logger: Log if (transformed == null) { return siemResponse.error({ statusCode: 500, body: 'Internal error transforming' }); } else { + transformed.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"; + transformed.required_fields = [ + { + name: 'event.code', + type: 'keyword', + ecs: true, + }, + { + name: 'message', + type: 'match_only_text', + ecs: true, + }, + { + 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', + ecs: false, + }, + ]; + transformed.related_integrations = [ + { + package: 'system', + version: '1.6.4', + integration: undefined, + }, + { + package: 'aws', + integration: 'cloudtrail', + version: '1.11.0', + }, + ]; return response.ok({ body: transformed ?? {} }); } } else { From 788acc7efd6a5d900aad89760092c61e512564e3 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Thu, 19 May 2022 21:08:26 -0600 Subject: [PATCH 2/9] Removes mocks, adds integrations to rules tables, uses FieldIcon and correct font for fields --- .../src/field_icon/field_icon.tsx | 2 + .../schemas/common/schemas.ts | 27 ----- .../components/integrations_popover/index.tsx | 106 ++++++++++++++++++ .../ml_popover/jobs_table/jobs_table.tsx | 31 +++-- .../common/components/popover_items/index.tsx | 8 +- .../rules/description_step/helpers.tsx | 24 ++-- .../rules/description_step/index.tsx | 10 +- .../rules/all/use_columns.tsx | 18 +++ .../detection_engine/rules/translations.ts | 37 ++++++ .../pages/detection_engine/rules/types.ts | 15 +-- .../routes/rules/read_rules_route.ts | 51 --------- .../elastic_endpoint_security.json | 49 ++++++++ 12 files changed, 259 insertions(+), 119 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/integrations_popover/index.tsx diff --git a/packages/kbn-react-field/src/field_icon/field_icon.tsx b/packages/kbn-react-field/src/field_icon/field_icon.tsx index 621b8d0199d04..82b9a3919c071 100644 --- a/packages/kbn-react-field/src/field_icon/field_icon.tsx +++ b/packages/kbn-react-field/src/field_icon/field_icon.tsx @@ -20,6 +20,7 @@ export interface FieldIconProps extends Omit { | 'geo_shape' | 'ip' | 'ip_range' + | 'match_only_text' | 'murmur3' | 'number' | 'number_range' @@ -45,6 +46,7 @@ export const typeToEuiIconMap: Partial> = { geo_shape: { iconType: 'tokenGeo' }, ip: { iconType: 'tokenIP' }, ip_range: { iconType: 'tokenIP' }, + match_only_text: { iconType: 'tokenString' }, // is a plugin's data type https://www.elastic.co/guide/en/elasticsearch/plugins/current/mapper-murmur3-usage.html murmur3: { iconType: 'tokenSearchType' }, number: { iconType: 'tokenNumber' }, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index b12755505137c..a47107cd068eb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -185,39 +185,12 @@ export type Name = t.TypeOf; export const nameOrUndefined = t.union([name, t.undefined]); export type NameOrUndefined = t.TypeOf; -export const related_integration = t.exact( - t.type({ - package: t.string, - version: t.string, - integration: t.union([t.string, t.undefined]), - }) -); -export type RelatedIntegration = t.TypeOf; - -export const related_integrations = t.array(related_integration); -export type RelatedIntegrations = t.TypeOf; - -export const required_field = t.exact( - t.type({ - name: t.string, - type: t.string, - ecs: t.boolean, - }) -); -export type RequiredField = t.TypeOf; - -export const required_fields = t.array(required_field); -export type RequiredFields = t.TypeOf; - export const rule_name_override = t.string; export type RuleNameOverride = t.TypeOf; export const ruleNameOverrideOrUndefined = t.union([rule_name_override, t.undefined]); export type RuleNameOverrideOrUndefined = t.TypeOf; -export const setup = t.string; -export type Setup = t.TypeOf; - export const status = t.keyof({ open: null, closed: null, 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 new file mode 100644 index 0000000000000..d731078a9ec54 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/integrations_popover/index.tsx @@ -0,0 +1,106 @@ +/* + * 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 React, { useState } from 'react'; +import { + EuiPopover, + EuiBadgeGroup, + EuiBadge, + EuiPopoverTitle, + EuiFlexGroup, + EuiText, + EuiLink, +} from '@elastic/eui'; +import styled from 'styled-components'; +import type { 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)` + width: 100%; +`; + +const PopoverWrapper = styled(EuiBadgeGroup)` + max-height: 400px; + max-width: 368px; + overflow: auto; + line-height: ${({ theme }) => theme.eui.euiLineHeight}; +`; + +/** + * Component to render installed and available integrations + * @param integrations - array of items to render + * @param installedIntegrations - array of items to render + */ +const IntegrationsPopoverComponent = ({ + integrations, + installedIntegrations, +}: IntegrationsPopoverProps) => { + const [isPopoverOpen, setPopoverOpen] = useState(false); + + const integrationsTitle = `${installedIntegrations.length}/${integrations.length} ${i18n.INTEGRATIONS_BADGE}`; + + return ( + + setPopoverOpen(!isPopoverOpen)} + onClickAriaLabel={integrationsTitle} + > + {integrationsTitle} + + } + isOpen={isPopoverOpen} + closePopover={() => setPopoverOpen(!isPopoverOpen)} + repositionOnScroll + > + + {i18n.INTEGRATIONS_POPOVER_TITLE(3)} + + + + {i18n.INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED(1)} + + {'AWS CloudTrail'} + + {i18n.INTEGRATIONS_POPOVER_DESCRIPTION_UNINSTALLED(2)} +
+ + {'Endpoint Security'} + +
+
+ + {'\nModSecurity Audit'} + +
+
+
+
+ ); +}; + +const MemoizedIntegrationsPopover = React.memo(IntegrationsPopoverComponent); +MemoizedIntegrationsPopover.displayName = 'IntegrationsPopover'; + +export const IntegrationsPopover = + MemoizedIntegrationsPopover as typeof IntegrationsPopoverComponent; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx index 842b199e4b7aa..ab86093243d5c 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx @@ -13,8 +13,6 @@ import { EuiBasicTable, EuiButton, EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, EuiIcon, EuiLink, EuiText, @@ -22,6 +20,7 @@ import { import styled from 'styled-components'; import { useMlHref, ML_PAGES } from '@kbn/ml-plugin/public'; +import { PopoverItems } from '../../popover_items'; import { useBasePath, useKibana } from '../../../lib/kibana'; import * as i18n from './translations'; import { JobSwitch } from './job_switch'; @@ -82,16 +81,24 @@ const getJobsTableColumns = ( }, { name: i18n.COLUMN_GROUPS, - render: ({ groups }: SecurityJob) => ( - - {groups.map((group) => ( - - {group} - - ))} - - ), - width: '140px', + render: ({ groups }: SecurityJob) => { + const renderItem = (group: string, i: number) => ( + + {group} + + ); + + return ( + + ); + }, + width: '80px', }, { diff --git a/x-pack/plugins/security_solution/public/common/components/popover_items/index.tsx b/x-pack/plugins/security_solution/public/common/components/popover_items/index.tsx index d0c806e7cae98..bd4bce2ded380 100644 --- a/x-pack/plugins/security_solution/public/common/components/popover_items/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/popover_items/index.tsx @@ -76,9 +76,11 @@ const PopoverItemsComponent = ({ return ( - - - + {numberOfItemsToDisplay !== 0 && ( + + + + )} { const badgeInstalledColor = '#E0E5EE'; // 'subdued' not working? const badgeUninstalledColor = 'accent'; @@ -550,18 +550,14 @@ export const buildRelatedIntegrationsDescription = ( }; const FieldTypeText = styled(EuiText)` - font-family: 'Roboto Mono', 'serif'; // const { euiTheme } = useEuiTheme(); // Use 'Inter' from designs + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; `; export const buildRequiredFieldsDescription = ( label: string, - requiredFields: RequiredFields + requiredFields: RequiredFieldArray ): ListItems[] => { - const typeToTokenMapping: Record = { - keyword: 'keyword', - match_only_text: 'text', - }; - return [ { title: label, @@ -569,7 +565,7 @@ export const buildRequiredFieldsDescription = ( {requiredFields.map((rF, index) => ( <> - + {` ${rF.name}`} {index + 1 !== requiredFields.length && <>{', '}} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index c5792760f26c2..6c77d8b3c408e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -13,9 +13,9 @@ import styled from 'styled-components'; import { ThreatMapping, Threats, Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { DataViewBase, Filter, FilterStateStore } from '@kbn/es-query'; import { FilterManager } from '@kbn/data-plugin/public'; -import { - RelatedIntegrations, - RequiredFields, +import type { + RelatedIntegrationArray, + RequiredFieldArray, } from '../../../../../common/detection_engine/schemas/common'; import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { useKibana } from '../../../../common/lib/kibana'; @@ -200,10 +200,10 @@ export const getDescriptionItem = ( const values: AboutStepSeverity = get(field, data); return buildSeverityDescription(values); } else if (field === 'requiredFields') { - const requiredFields = get(field, data) as RequiredFields; + const requiredFields: RequiredFieldArray = get(field, data); return buildRequiredFieldsDescription(label, requiredFields); } else if (field === 'relatedIntegrations') { - const relatedIntegrations = get(field, data) as RelatedIntegrations; + const relatedIntegrations: RelatedIntegrationArray = get(field, data); return buildRelatedIntegrationsDescription(label, relatedIntegrations); } else if (field === 'timeline') { const timeline = get(field, data) as FieldValueTimeline; 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 a6891af1cc7da..2044108da59da 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 @@ -15,6 +15,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useMemo } from 'react'; +import { IntegrationsPopover } from '../../../../../common/components/integrations_popover'; import { APP_UI_ID, DEFAULT_RELATIVE_DATE_THRESHOLD, @@ -157,6 +158,21 @@ const TAGS_COLUMN: TableColumn = { truncateText: true, }; +const INTEGRATIONS_COLUMN: TableColumn = { + field: 'related_integrations', + name: null, + align: 'center', + render: (integrations: Rule['related_integrations']) => { + if (integrations?.length === 0) { + return null; + } + + return ; + }, + width: '143px', + truncateText: true, +}; + const useActionsColumn = (): EuiTableActionsColumnType => { const { navigateToApp } = useKibana().services.application; const hasActionsPrivileges = useHasActionsPrivileges(); @@ -187,6 +203,7 @@ export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[] return useMemo( () => [ ruleNameColumn, + INTEGRATIONS_COLUMN, TAGS_COLUMN, { field: 'risk_score', @@ -292,6 +309,7 @@ export const useMonitoringColumns = ({ hasPermissions }: ColumnsProps): TableCol ...ruleNameColumn, width: '28%', }, + INTEGRATIONS_COLUMN, TAGS_COLUMN, { field: 'execution_summary.last_execution.metrics.total_indexing_duration_ms', 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 7071ef95c8c7e..646f906ce4b69 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 @@ -1085,3 +1085,40 @@ export const RULES_BULK_EDIT_FAILURE_DESCRIPTION = (rulesCount: number) => defaultMessage: '{rulesCount, plural, =1 {# rule} other {# rules}} failed to update.', } ); + +export const INTEGRATIONS_BADGE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.integrations.badgeTitle', + { + defaultMessage: 'integrations', + } +); + +export const INTEGRATIONS_POPOVER_TITLE = (integrationsCount: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.integrations.popoverTitle', + { + values: { integrationsCount }, + defaultMessage: + 'You have [{integrationsCount}] related {integrationsCount, plural, =1 {# integration} other {# integrations}} to your prebuilt rule', + } + ); + +export const INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED = (installedCount: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.integrations.popoverTitle', + { + values: { installedCount }, + defaultMessage: + 'You have [{installedCount}] related {installedCount, plural, =1 {# integration} other {# integrations}} installed, click the link below to view the integration:', + } + ); + +export const INTEGRATIONS_POPOVER_DESCRIPTION_UNINSTALLED = (uninstalledCount: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.integrations.popoverTitle', + { + values: { uninstalledCount }, + defaultMessage: + 'You have [{uninstalledCount}] related {uninstalledCount, plural, =1 {# integration} other {# integrations}} uninstalled, click the link to add integration:', + } + ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index bfea6efb38fce..5c962ed9e7960 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -21,16 +21,17 @@ import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { FieldValueQueryBar } from '../../../components/rules/query_bar'; import { FieldValueTimeline } from '../../../components/rules/pick_timeline'; import { FieldValueThreshold } from '../../../components/rules/threshold_input'; -import { +import type { Author, BuildingBlockType, License, - RelatedIntegrations, - RequiredFields, + RelatedIntegrationArray, + RequiredFieldArray, RuleNameOverride, SortOrder, + SetupGuide, TimestampOverride, -} from '../../../../../common/detection_engine/schemas/common/schemas'; +} from '../../../../../common/detection_engine/schemas/common'; export interface EuiBasicTableSortTypes { field: string; @@ -111,7 +112,7 @@ export interface AboutStepRule { export interface AboutStepRuleDetails { note: string; description: string; - setup: string; + setup: SetupGuide; } export interface AboutStepSeverity { @@ -131,8 +132,8 @@ export interface DefineStepRule { index: string[]; machineLearningJobId: string[]; queryBar: FieldValueQueryBar; - relatedIntegrations: RelatedIntegrations; - requiredFields: RequiredFields; + relatedIntegrations: RelatedIntegrationArray; + requiredFields: RequiredFieldArray; ruleType: Type; timeline: FieldValueTimeline; threshold: FieldValueThreshold; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts index b2638b3486f50..37af66b50c6bb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -67,57 +67,6 @@ export const readRulesRoute = (router: SecuritySolutionPluginRouter, logger: Log if (transformed == null) { return siemResponse.error({ statusCode: 500, body: 'Internal error transforming' }); } else { - transformed.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"; - transformed.required_fields = [ - { - name: 'event.code', - type: 'keyword', - ecs: true, - }, - { - name: 'message', - type: 'match_only_text', - ecs: true, - }, - { - 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', - ecs: false, - }, - ]; - transformed.related_integrations = [ - { - package: 'system', - version: '1.6.4', - integration: undefined, - }, - { - package: 'aws', - integration: 'cloudtrail', - version: '1.11.0', - }, - ]; return response.ok({ body: transformed ?? {} }); } } else { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json index 63bf6fea698ae..c59efa94ea225 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json @@ -21,6 +21,55 @@ "max_signals": 10000, "name": "Endpoint Security", "query": "event.kind:alert and event.module:(endpoint and not endgame)\n", + "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", + "required_fields": [ + { + "name": "event.code", + "type": "keyword", + "ecs": true + }, + { + "name": "message", + "type": "match_only_text", + "ecs": true + }, + { + "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", + "ecs": false + } + ], + "related_integrations": [ + { + "package": "system", + "version": "1.6.4" + }, + { + "package": "aws", + "integration": "cloudtrail", + "version": "1.11.0" + } + ], "risk_score": 47, "risk_score_mapping": [ { From cd706e2fd164c734091affa49469563d1780c9e4 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Thu, 19 May 2022 21:35:38 -0600 Subject: [PATCH 3/9] Fixing types --- .../components/rules/step_about_rule_details/index.test.tsx | 6 ++++++ .../pages/detection_engine/rules/all/__mocks__/mock.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx index d2003c411f05c..35b39ce8759cb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx @@ -37,6 +37,7 @@ describe('StepAboutRuleToggleDetails', () => { stepDataDetails={{ note: mockRule.note, description: mockRule.description, + setup: '', }} stepData={mockRule} /> @@ -61,6 +62,7 @@ describe('StepAboutRuleToggleDetails', () => { stepDataDetails={{ note: '', description: '', + setup: '', }} stepData={null} /> @@ -81,6 +83,7 @@ describe('StepAboutRuleToggleDetails', () => { stepDataDetails={{ note: '', description: mockRule.description, + setup: '', }} stepData={mockAboutStepWithoutNote} /> @@ -101,6 +104,7 @@ describe('StepAboutRuleToggleDetails', () => { stepDataDetails={{ note: mockRule.note, description: mockRule.description, + setup: '', }} stepData={mockRule} /> @@ -120,6 +124,7 @@ describe('StepAboutRuleToggleDetails', () => { stepDataDetails={{ note: mockRule.note, description: mockRule.description, + setup: '', }} stepData={mockRule} /> @@ -147,6 +152,7 @@ describe('StepAboutRuleToggleDetails', () => { stepDataDetails={{ note: mockRule.note, description: mockRule.description, + setup: '', }} stepData={mockRule} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts index d9f16242a544a..ddc651b696dde 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -195,6 +195,8 @@ export const mockDefineStepRule = (): DefineStepRule => ({ index: ['filebeat-'], queryBar: mockQueryBar, threatQueryBar: mockQueryBar, + requiredFields: [], + relatedIntegrations: [], threatMapping: [], timeline: { id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', From 0b4857780d8c49c835388298effc9e575198b6d8 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Thu, 19 May 2022 22:03:35 -0600 Subject: [PATCH 4/9] Fixing i18n keys --- .../detections/pages/detection_engine/rules/translations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 646f906ce4b69..e259823a36658 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 @@ -1105,7 +1105,7 @@ export const INTEGRATIONS_POPOVER_TITLE = (integrationsCount: number) => export const INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED = (installedCount: number) => i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.integrations.popoverTitle', + 'xpack.securitySolution.detectionEngine.rules.allRules.integrations.popoverDescriptionInstalledTitle', { values: { installedCount }, defaultMessage: @@ -1115,7 +1115,7 @@ export const INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED = (installedCount: numbe export const INTEGRATIONS_POPOVER_DESCRIPTION_UNINSTALLED = (uninstalledCount: number) => i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.integrations.popoverTitle', + 'xpack.securitySolution.detectionEngine.rules.allRules.integrations.popoverDescriptionUninstalledTitle', { values: { uninstalledCount }, defaultMessage: From b0247a9ad0d98b6688c5d9c5ed984e0e41377162 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Fri, 20 May 2022 23:07:08 -0600 Subject: [PATCH 5/9] 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:', } ); From 8646667c7299ff19449e80932dfe8315bb0010e8 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Sat, 21 May 2022 17:46:16 -0600 Subject: [PATCH 6/9] Increases integrity of test outputs --- .../__snapshots__/field_icon.test.tsx.snap | 10 +++ .../__snapshots__/jobs_table.test.tsx.snap | 2 +- .../rules/description_step/helpers.tsx | 8 +++ .../rules/description_step/index.tsx | 9 ++- .../step_about_rule_details/index.test.tsx | 1 + .../rules/step_about_rule_details/index.tsx | 67 +++++++++++-------- .../detection_engine/rules/helpers.test.tsx | 14 +++- .../elastic_endpoint_security.json | 49 -------------- 8 files changed, 76 insertions(+), 84 deletions(-) diff --git a/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap b/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap index 7328e2c61b961..acb1f152e738b 100644 --- a/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap +++ b/packages/kbn-react-field/src/field_icon/__snapshots__/field_icon.test.tsx.snap @@ -135,6 +135,16 @@ exports[`FieldIcon renders known field types keyword is rendered 1`] = ` /> `; +exports[`FieldIcon renders known field types match_only_text is rendered 1`] = ` + +`; + exports[`FieldIcon renders known field types murmur3 is rendered 1`] = ` { const isInstalled = installedPackages.includes(rI.package); const badgeColor = isInstalled ? badgeInstalledColor : badgeUninstalledColor; @@ -558,6 +562,10 @@ export const buildRequiredFieldsDescription = ( label: string, requiredFields: RequiredFieldArray ): ListItems[] => { + if (requiredFields == null) { + return []; + } + return [ { title: label, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 6c77d8b3c408e..6803791ee3f77 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -26,7 +26,7 @@ import { ListItems } from './types'; import { buildQueryBarDescription, buildSeverityDescription, - // buildStringArrayDescription, + buildStringArrayDescription, buildThreatDescription, buildUnorderedListArrayDescription, buildUrlsDescription, @@ -189,10 +189,6 @@ export const getDescriptionItem = ( } else if (field === 'falsePositives') { const values: string[] = get(field, data); return buildUnorderedListArrayDescription(label, field, values); - // TODO: lol 😭 - // } else if (Array.isArray(get(field, data)) && field !== 'threatMapping') { - // const values: string[] = get(field, data); - // return buildStringArrayDescription(label, field, values); } else if (field === 'riskScore') { const values: AboutStepRiskScore = get(field, data); return buildRiskScoreDescription(values); @@ -237,6 +233,9 @@ export const getDescriptionItem = ( } else if (field === 'threatMapping') { const threatMap: ThreatMapping = get(field, data); return buildThreatMappingDescription(label, threatMap); + } else if (Array.isArray(get(field, data)) && field !== 'threatMapping') { + const values: string[] = get(field, data); + return buildStringArrayDescription(label, field, values); } const description: string = get(field, data); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx index 35b39ce8759cb..c0bb25625f538 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx @@ -91,6 +91,7 @@ describe('StepAboutRuleToggleDetails', () => { expect(wrapper.find('[data-test-subj="stepAboutDetailsToggle"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="stepAboutDetailsNoteContent"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="stepAboutDetailsSetupContent"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="stepAboutDetailsContent"]').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx index 8feeedb9ea609..439e981059a6b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx @@ -16,7 +16,8 @@ import { EuiFlexGroup, EuiResizeObserver, } from '@elastic/eui'; -import React, { memo, useCallback, useState } from 'react'; +import { isEmpty } from 'lodash'; +import React, { memo, useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { HeaderSection } from '../../../../common/components/header_section'; @@ -47,23 +48,21 @@ const AboutContent = styled.div` height: 100%; `; -const toggleOptions: EuiButtonGroupOptionProps[] = [ - { - id: 'details', - label: i18n.ABOUT_PANEL_DETAILS_TAB, - 'data-test-subj': 'stepAboutDetailsToggle-details', - }, - { - id: 'notes', - label: i18n.ABOUT_PANEL_NOTES_TAB, - 'data-test-subj': 'stepAboutDetailsToggle-notes', - }, - { - id: 'setup', - label: i18n.ABOUT_PANEL_SETUP_TAB, - 'data-test-subj': 'stepAboutDetailsToggle-setup', - }, -]; +const detailsOption: EuiButtonGroupOptionProps = { + id: 'details', + label: i18n.ABOUT_PANEL_DETAILS_TAB, + 'data-test-subj': 'stepAboutDetailsToggle-details', +}; +const notesOption: EuiButtonGroupOptionProps = { + id: 'notes', + label: i18n.ABOUT_PANEL_NOTES_TAB, + 'data-test-subj': 'stepAboutDetailsToggle-notes', +}; +const setupOption: EuiButtonGroupOptionProps = { + id: 'setup', + label: i18n.ABOUT_PANEL_SETUP_TAB, + 'data-test-subj': 'stepAboutDetailsToggle-setup', +}; interface StepPanelProps { stepData: AboutStepRule | null; @@ -86,6 +85,16 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ [setAboutPanelHeight] ); + const toggleOptions: EuiButtonGroupOptionProps[] = useMemo(() => { + const notesExist = !isEmpty(stepDataDetails?.note) && stepDataDetails?.note.trim() !== ''; + const setupExists = !isEmpty(stepDataDetails?.setup) && stepDataDetails?.setup.trim() !== ''; + return [ + ...(notesExist || setupExists ? [detailsOption] : []), + ...(notesExist ? [notesOption] : []), + ...(setupExists ? [setupOption] : []), + ]; + }, [stepDataDetails]); + return ( {loading && ( @@ -98,15 +107,17 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ - { - setToggleOption(val); - }} - data-test-subj="stepAboutDetailsToggle" - legend={i18n.ABOUT_CONTROL_LEGEND} - /> + {toggleOptions.length > 0 && ( + { + setToggleOption(val); + }} + data-test-subj="stepAboutDetailsToggle" + legend={i18n.ABOUT_CONTROL_LEGEND} + /> + )} @@ -135,7 +146,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC = ({ )} )} - {selectedToggleOption === 'note' && ( + {selectedToggleOption === 'notes' && ( { ], saved_id: 'test123', }, + relatedIntegrations: [], + requiredFields: [], threshold: { field: ['host.name'], value: '50', @@ -131,6 +133,7 @@ describe('rule helpers', () => { const aboutRuleDataDetailsData = { note: '# this is some markdown documentation', description: '24/7', + setup: '', }; expect(defineRuleData).toEqual(defineRuleStepData); @@ -214,6 +217,8 @@ describe('rule helpers', () => { filters: [], saved_id: "Garrett's IP", }, + relatedIntegrations: [], + requiredFields: [], threshold: { field: [], value: '100', @@ -256,6 +261,8 @@ describe('rule helpers', () => { filters: [], saved_id: undefined, }, + relatedIntegrations: [], + requiredFields: [], threshold: { field: [], value: '100', @@ -388,6 +395,7 @@ describe('rule helpers', () => { const aboutRuleDataDetailsData = { note: '# this is some markdown documentation', description: '24/7', + setup: '', }; expect(result).toEqual(aboutRuleDataDetailsData); @@ -397,7 +405,11 @@ describe('rule helpers', () => { const { note, ...mockRuleWithoutNote } = { ...mockRuleWithEverything('test-id') }; const result: AboutStepRuleDetails = getModifiedAboutDetailsData(mockRuleWithoutNote); - const aboutRuleDetailsData = { note: '', description: mockRuleWithoutNote.description }; + const aboutRuleDetailsData = { + note: '', + description: mockRuleWithoutNote.description, + setup: '', + }; expect(result).toEqual(aboutRuleDetailsData); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json index c59efa94ea225..63bf6fea698ae 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json @@ -21,55 +21,6 @@ "max_signals": 10000, "name": "Endpoint Security", "query": "event.kind:alert and event.module:(endpoint and not endgame)\n", - "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", - "required_fields": [ - { - "name": "event.code", - "type": "keyword", - "ecs": true - }, - { - "name": "message", - "type": "match_only_text", - "ecs": true - }, - { - "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", - "ecs": false - } - ], - "related_integrations": [ - { - "package": "system", - "version": "1.6.4" - }, - { - "package": "aws", - "integration": "cloudtrail", - "version": "1.11.0" - } - ], "risk_score": 47, "risk_score_mapping": [ { From 7afa4f7abf5fe2a169b30530520528b53d5757a9 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Sun, 22 May 2022 22:58:57 -0600 Subject: [PATCH 7/9] Adds tests, makes version and install check generic, matches new integraiton API, and removes mocked installed data --- .../security_solution/common/constants.ts | 4 +- .../integrations_popover/helpers.tsx | 52 ++++++++- .../components/integrations_popover/index.tsx | 54 +++++---- .../rules/description_step/helpers.tsx | 39 ------- .../rules/description_step/index.tsx | 2 +- .../required_integrations_description.tsx | 95 ++++++++++++++++ .../rules/description_step/translations.tsx | 14 +++ .../step_about_rule_details/index.test.tsx | 103 ++++++++++++++++++ .../rules/use_installed_integrations.tsx | 32 +++--- .../detection_engine/rules/translations.ts | 13 +++ 10 files changed, 323 insertions(+), 85 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/description_step/required_integrations_description.tsx diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 104887f7c3af6..35da825087b15 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -237,8 +237,6 @@ 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; @@ -260,6 +258,8 @@ export const DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL = `${INTERNAL_DETECTION_ENGINE_URL}/rules/{ruleId}/execution/events` as const; export const detectionEngineRuleExecutionEventsUrl = (ruleId: string) => `${INTERNAL_DETECTION_ENGINE_URL}/rules/${ruleId}/execution/events` as const; +export const DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL = + `${INTERNAL_DETECTION_ENGINE_URL}/fleet/integrations/installed` as const; /** * Telemetry detection endpoint for any previews requested of what data we are 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 b16397933a6f4..09eb4be8aefd9 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 @@ -8,8 +8,17 @@ import { EuiLink } from '@elastic/eui'; import { capitalize } from 'lodash'; import React from 'react'; -import { RelatedIntegration } from '../../../../common/detection_engine/schemas/common'; +import semver from 'semver'; +import { + RelatedIntegration, + RelatedIntegrationArray, +} from '../../../../common/detection_engine/schemas/common'; +/** + * Returns and `EuiLink` that will link to a given package/integration/version page within fleet + * @param integration + * @param basePath + */ export const getIntegrationLink = (integration: RelatedIntegration, basePath: string) => { const integrationURL = `${basePath}/app/integrations/detail/${integration.package}-${ integration.version @@ -20,3 +29,44 @@ export const getIntegrationLink = (integration: RelatedIntegration, basePath: st ); }; + +export interface InstalledIntegration extends RelatedIntegration { + targetVersion: string; + versionSatisfied?: boolean; +} + +/** + * Given an array of integrations and an array of installed integrations this will return which + * integrations are `available`/`uninstalled` and which are `installed`, and also augmented with + * `targetVersion` and `versionSatisfied` + * @param integrations + * @param installedIntegrations + */ +export const getInstalledRelatedIntegrations = ( + integrations: RelatedIntegrationArray, + installedIntegrations: RelatedIntegrationArray +): { + availableIntegrations: RelatedIntegrationArray; + installedRelatedIntegrations: InstalledIntegration[]; +} => { + const availableIntegrations: RelatedIntegrationArray = []; + const installedRelatedIntegrations: InstalledIntegration[] = []; + + integrations.forEach((i: RelatedIntegration) => { + const match = installedIntegrations.find( + (installed) => installed.package === i.package && installed?.integration === 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); + installedRelatedIntegrations.push({ ...match, targetVersion: i.version, versionSatisfied }); + } else { + availableIntegrations.push(i); + } + }); + + return { + availableIntegrations, + installedRelatedIntegrations, + }; +}; 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 9c8a6ee6d9ff3..8574a96ed9516 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 @@ -8,20 +8,17 @@ import React, { useState } from 'react'; import { EuiPopover, - EuiBadgeGroup, EuiBadge, EuiPopoverTitle, EuiFlexGroup, EuiText, + EuiIconTip, } from '@elastic/eui'; import styled from 'styled-components'; import { useBasePath } from '../../lib/kibana'; -import { getIntegrationLink } from './helpers'; +import { getInstalledRelatedIntegrations, 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 type { RelatedIntegrationArray } from '../../../../common/detection_engine/schemas/common'; import * as i18n from '../../../detections/pages/detection_engine/rules/translations'; @@ -33,7 +30,7 @@ const IntegrationsPopoverWrapper = styled(EuiFlexGroup)` width: 100%; `; -const PopoverWrapper = styled(EuiBadgeGroup)` +const PopoverContentWrapper = styled('div')` max-height: 400px; max-width: 368px; overflow: auto; @@ -51,28 +48,17 @@ const IntegrationListItem = styled('li')` 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 { availableIntegrations, installedRelatedIntegrations } = getInstalledRelatedIntegrations( + integrations, + allInstalledIntegrations + ); const badgeTitle = data != null - ? `${installedIntegrations.length}/${integrations.length} ${i18n.INTEGRATIONS_BADGE}` + ? `${installedRelatedIntegrations.length}/${integrations.length} ${i18n.INTEGRATIONS_BADGE}` : `${integrations.length} ${i18n.INTEGRATIONS_BADGE}`; return ( @@ -99,20 +85,32 @@ const IntegrationsPopoverComponent = ({ integrations }: IntegrationsPopoverProps closePopover={() => setPopoverOpen(!isPopoverOpen)} repositionOnScroll > - + {i18n.INTEGRATIONS_POPOVER_TITLE(integrations.length)} - + {data != null && ( <> - {i18n.INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED(installedIntegrations.length)} + {i18n.INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED( + installedRelatedIntegrations.length + )}
    - {installedIntegrations.map((integration, index) => ( + {installedRelatedIntegrations.map((integration, index) => ( {getIntegrationLink(integration, basePath)} + {!integration?.versionSatisfied && ( + + )} ))}
@@ -132,7 +130,7 @@ const IntegrationsPopoverComponent = ({ integrations }: IntegrationsPopoverProps )} -
+ ); 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 5ce4a1c6339fc..adb855e372961 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 @@ -19,7 +19,6 @@ import { EuiFlexGrid, } from '@elastic/eui'; import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils'; -import { capitalize } from 'lodash'; import { isEmpty } from 'lodash/fp'; import React from 'react'; @@ -34,7 +33,6 @@ import { assertUnreachable } from '../../../../../common/utility_types'; import * as i18nSeverity from '../severity_mapping/translations'; import * as i18nRiskScore from '../risk_score_mapping/translations'; import type { - RelatedIntegrationArray, RequiredFieldArray, Threshold, } from '../../../../../common/detection_engine/schemas/common'; @@ -515,43 +513,6 @@ export const buildThreatMappingDescription = ( ]; }; -export const buildRelatedIntegrationsDescription = ( - label: string, - relatedIntegrations: RelatedIntegrationArray -): ListItems[] => { - const badgeInstalledColor = '#E0E5EE'; // 'subdued' not working? - const badgeUninstalledColor = 'accent'; - const basePath = 'http://localhost:5601/kbn'; // const basePath = useBasePath(); - const installedText = 'Installed'; - const uninstalledText = 'Uninstalled'; - const installedPackages = ['aws']; // TODO: Use hook const { data } = useInstalledIntegrations({ packages: [] }); - - if (relatedIntegrations == null) { - return []; - } - - return relatedIntegrations.map((rI, index) => { - const isInstalled = installedPackages.includes(rI.package); - const badgeColor = isInstalled ? badgeInstalledColor : badgeUninstalledColor; - const badgeText = isInstalled ? installedText : uninstalledText; - const integrationURL = `${basePath}/app/integrations/detail/${rI.package}-${ - rI.version - }/overview${rI.integration ? `?integration=${rI.integration}` : ''}`; - - return { - title: index === 0 ? label : '', - description: ( - <> - - {`${capitalize(rI.package)} ${capitalize(rI.integration)}`} - {' '} - {badgeText} - - ), - }; - }); -}; - const FieldTypeText = styled(EuiText)` font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; font-family: ${({ theme }) => theme.eui.euiCodeFontFamily}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 6803791ee3f77..af3f739fec044 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -13,6 +13,7 @@ import styled from 'styled-components'; import { ThreatMapping, Threats, Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { DataViewBase, Filter, FilterStateStore } from '@kbn/es-query'; import { FilterManager } from '@kbn/data-plugin/public'; +import { buildRelatedIntegrationsDescription } from './required_integrations_description'; import type { RelatedIntegrationArray, RequiredFieldArray, @@ -35,7 +36,6 @@ import { buildRuleTypeDescription, buildThresholdDescription, buildThreatMappingDescription, - buildRelatedIntegrationsDescription, buildRequiredFieldsDescription, } from './helpers'; import { buildMlJobsDescription } from './ml_job_description'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/required_integrations_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/required_integrations_description.tsx new file mode 100644 index 0000000000000..933de9737ed6f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/required_integrations_description.tsx @@ -0,0 +1,95 @@ +/* + * 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 React from 'react'; +import styled from 'styled-components'; +import { EuiBadge, EuiIconTip } from '@elastic/eui'; +import { INTEGRATIONS_INSTALLED_VERSION_TOOLTIP } from '../../../pages/detection_engine/rules/translations'; +import { useInstalledIntegrations } from '../../../containers/detection_engine/rules/use_installed_integrations'; +import { + getInstalledRelatedIntegrations, + getIntegrationLink, +} from '../../../../common/components/integrations_popover/helpers'; + +import { + RelatedIntegration, + RelatedIntegrationArray, +} from '../../../../../common/detection_engine/schemas/common'; +import { useBasePath } from '../../../../common/lib/kibana'; +import { ListItems } from './types'; +import * as i18n from './translations'; + +const Wrapper = styled.div` + overflow: hidden; +`; + +const IntegrationDescriptionComponent: React.FC<{ integration: RelatedIntegration }> = ({ + integration, +}) => { + const basePath = useBasePath(); + const badgeInstalledColor = '#E0E5EE'; + const badgeUninstalledColor = 'accent'; + const { data } = useInstalledIntegrations({ packages: [] }); + + const allInstalledIntegrations: RelatedIntegrationArray = data ?? []; + const { availableIntegrations, installedRelatedIntegrations } = getInstalledRelatedIntegrations( + [integration], + allInstalledIntegrations + ); + + if (availableIntegrations.length > 0) { + return ( + + {getIntegrationLink(integration, basePath)}{' '} + {data != null && ( + {i18n.RELATED_INTEGRATIONS_UNINSTALLED} + )} + + ); + } else if (installedRelatedIntegrations.length > 0) { + return ( + + {getIntegrationLink(integration, basePath)}{' '} + {i18n.RELATED_INTEGRATIONS_INSTALLED} + {!installedRelatedIntegrations[0]?.versionSatisfied && ( + + )} + + ); + } else { + return <>; + } +}; + +export const IntegrationDescription = React.memo(IntegrationDescriptionComponent); + +const RelatedIntegrationsDescription: React.FC<{ integrations: RelatedIntegrationArray }> = ({ + integrations, +}) => ( + <> + {integrations.map((integration, index) => ( + + ))} + +); + +export const buildRelatedIntegrationsDescription = ( + label: string, + relatedIntegrations: RelatedIntegrationArray +): ListItems[] => [ + { + title: label, + description: , + }, +]; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx index 1b0d906528b95..949d5b32e88de 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx @@ -97,3 +97,17 @@ export const THRESHOLD_RESULTS_AGGREGATED_BY = i18n.translate( defaultMessage: 'Results aggregated by', } ); + +export const RELATED_INTEGRATIONS_INSTALLED = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrationsInstalledDescription', + { + defaultMessage: 'Installed', + } +); + +export const RELATED_INTEGRATIONS_UNINSTALLED = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrationsUninstalledDescription', + { + defaultMessage: 'Uninstalled', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx index c0bb25625f538..8164c5099db4a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.test.tsx @@ -172,4 +172,107 @@ describe('StepAboutRuleToggleDetails', () => { ); }); }); + + describe('setup value is empty string', () => { + test('it does render toggle buttons if note is not empty', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(EuiButtonGroup).exists()).toBeTruthy(); + expect(wrapper.find('#details').at(0).prop('isSelected')).toBeTruthy(); + expect(wrapper.find('#notes').at(0).prop('isSelected')).toBeFalsy(); + expect(wrapper.find('[data-test-subj="stepAboutDetailsSetupContent"]').exists()).toBeFalsy(); + }); + }); + + describe('setup value does exist', () => { + test('it renders toggle buttons, defaulted to "details"', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(EuiButtonGroup).exists()).toBeTruthy(); + expect(wrapper.find('#details').at(0).prop('isSelected')).toBeTruthy(); + expect(wrapper.find('#notes').at(0).prop('isSelected')).toBeFalsy(); + expect(wrapper.find('#setup').at(0).prop('isSelected')).toBeFalsy(); + }); + + test('it allows users to toggle between "details" and "setup"', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[idSelected="details"]').exists()).toBeTruthy(); + expect(wrapper.find('[idSelected="notes"]').exists()).toBeFalsy(); + expect(wrapper.find('[idSelected="setup"]').exists()).toBeFalsy(); + + wrapper + .find('[title="Setup guide"]') + .at(0) + .find('input') + .simulate('change', { target: { value: 'setup' } }); + + expect(wrapper.find('[idSelected="details"]').exists()).toBeFalsy(); + expect(wrapper.find('[idSelected="notes"]').exists()).toBeFalsy(); + expect(wrapper.find('[idSelected="setup"]').exists()).toBeTruthy(); + }); + + test('it displays notes markdown when user toggles to "setup"', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[title="Setup guide"]') + .at(0) + .find('input') + .simulate('change', { target: { value: 'setup' } }); + + expect(wrapper.find('EuiButtonGroup[idSelected="setup"]').exists()).toBeTruthy(); + expect(wrapper.find('div.euiMarkdownFormat').text()).toEqual( + 'this is some markdown documentation' + ); + }); + }); }); 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 82c3bef767133..caf46ddc8f210 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 @@ -17,7 +17,9 @@ export interface UseInstalledIntegrationsArgs { export const useInstalledIntegrations = ({ packages }: UseInstalledIntegrationsArgs) => { const { addError } = useAppToasts(); - return useQuery( + // TODO: Once API is merged update return type: + // See: https://github.com/elastic/kibana/pull/132667/files#diff-f9d9583d37123ed28fd08fc153eb06026e7ee0c3241364656fb707dcbc0a4872R58-R65 + return useQuery( [ 'installedIntegrations', { @@ -25,19 +27,21 @@ export const useInstalledIntegrations = ({ packages }: UseInstalledIntegrationsA }, ], async ({ signal }) => { - // Mock data - const mockInstalledIntegrations = [ - { - package: 'system', - version: '1.6.4', - }, - // { - // package: 'aws', - // integration: 'cloudtrail', - // version: '1.11.0', - // }, - ]; - return mockInstalledIntegrations; + 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({ 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 daebcc85444af..e5e005f8d6b9b 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 @@ -1122,3 +1122,16 @@ export const INTEGRATIONS_POPOVER_DESCRIPTION_UNINSTALLED = (uninstalledCount: n 'You have [{uninstalledCount}] related {uninstalledCount, plural, =1 {integration} other {integrations}} uninstalled, click the link to add integration:', } ); + +export const INTEGRATIONS_INSTALLED_VERSION_TOOLTIP = ( + installedVersion: string, + targetVersion: string +) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.integrations.popoverDescriptionInstalledVersionTooltip', + { + values: { installedVersion, targetVersion }, + defaultMessage: + 'Version mismatch -- please resolve! Installed version `{installedVersion}` when target version `{targetVersion}`', + } + ); From 17ede9c5b41d5ac82075b3dd8018cdd353f299a8 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Mon, 23 May 2022 00:25:03 -0600 Subject: [PATCH 8/9] Supports empty case for readonly view on rule create --- .../description_step/required_integrations_description.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/required_integrations_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/required_integrations_description.tsx index 933de9737ed6f..3e07ccd17dfa8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/required_integrations_description.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/required_integrations_description.tsx @@ -78,7 +78,7 @@ const RelatedIntegrationsDescription: React.FC<{ integrations: RelatedIntegratio integrations, }) => ( <> - {integrations.map((integration, index) => ( + {integrations?.map((integration, index) => ( ))} From ede4c6013947283b5c9dd4c05b8f5018b4f41b31 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Mon, 23 May 2022 00:47:25 -0600 Subject: [PATCH 9/9] Fixes typo --- .../public/common/components/integrations_popover/helpers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 09eb4be8aefd9..65854b0cc1fb5 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 @@ -15,7 +15,7 @@ import { } from '../../../../common/detection_engine/schemas/common'; /** - * Returns and `EuiLink` that will link to a given package/integration/version page within fleet + * Returns an `EuiLink` that will link to a given package/integration/version page within fleet * @param integration * @param basePath */