mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[SecuritySolution][Detections] Adds UI for new Rule Fields: Related Integrations, Required Fields, and Setup (#131475)
## Summary Adds UI for new Rule Fields `Related Integrations`, `Required Fields`, and `Setup` to both the Rules Table and Rule Details pages. On the Rules Table a new column is added that shows the number of related integrations, and upon clicking will show you details about those integrations and links off to the integration page in fleet. On the Rule Details page `Setup` is added as a tab pill within the About section (if provided), and `Related Integrations` and `Required Fields` are displayed in the Definition section. Once package/integration install data is added in https://github.com/elastic/kibana/pull/132667, the UI will show the installed status of an integration, and whether or not the installed version satisfies the related integration dependency. NOTE: Until then, please follow the test instructions below for how to add a custom rule and return mock data to test the `installed/uninstalled` UI. ##### Related Issues * https://github.com/elastic/security-team/issues/2083 (internal) * https://github.com/elastic/security-team/issues/558 (internal) * https://github.com/elastic/security-team/issues/2856 (internal) * https://github.com/elastic/security-team/issues/1801 (internal) * https://github.com/elastic/security-team/issues/3624 (internal) ##### Related Links * https://github.com/elastic/security-docs/issues/2015 * [Figma mocks](https://www.figma.com/file/zZs8TOrYsp13T6Z2HoMMFN/%5B8.2%5D-Associate-prebuilt-rules-with-Related-Integrations?node-id=0%3A1) ##### Steps to test In this initial implementation these new fields are only visible with Prebuilt Rules, and so there is limited API support and currently no UI for editing them. If a Prebuilt Rule is duplicated, these fields are emptied (set to `''` or `[]`). When a Rule is exported these fields are included (as empty values), and it is possible to edit the `ndjson` and re-import and then see these fields for the Custom Rule (but still not editable in the UI). This is expected behavior, and is actually a nice and easy way to test. Here is a sample export you can paste into an `test.ndjson` file and import to test this feature. You can modify the `package`/`version` fields to test corner cases like if a package is installed but it's the wrong version. ``` {"id":"6cc39c80-da3a-11ec-9fce-65c1a0bee904","updated_at":"2022-05-23T01:48:23.422Z","updated_by":"elastic","created_at":"2022-05-23T01:48:20.940Z","created_by":"elastic","name":"Testing #131475, don't mind me...","tags":["Elastic","Endpoint Security"],"interval":"5m","enabled":false,"description":"Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.","risk_score":47,"severity":"medium","license":"Elastic License v2","output_index":".siem-signals-default","meta":{"from":"5m"},"rule_name_override":"message","timestamp_override":"event.ingested","author":["Elastic"],"false_positives":[],"from":"now-600s","rule_id":"2c66bf23-6ae9-4eb2-859e-446bea181ae9","max_signals":10000,"risk_score_mapping":[{"field":"event.risk_score","operator":"equals","value":""}],"severity_mapping":[{"field":"event.severity","operator":"equals","severity":"low","value":"21"},{"field":"event.severity","operator":"equals","severity":"medium","value":"47"},{"field":"event.severity","operator":"equals","severity":"high","value":"73"},{"field":"event.severity","operator":"equals","severity":"critical","value":"99"}],"threat":[],"to":"now","references":[],"version":7,"exceptions_list":[{"id":"endpoint_list","list_id":"endpoint_list","namespace_type":"agnostic","type":"endpoint"}],"immutable":false,"related_integrations":[{"package":"system","version":"1.6.4"},{"package":"aws","integration":"cloudtrail","version":"1.11.0"}],"required_fields":[{"ecs":true,"name":"event.code","type":"keyword"},{"ecs":true,"name":"message","type":"match_only_text"},{"ecs":false,"name":"winlog.event_data.AttributeLDAPDisplayName","type":"keyword"},{"ecs":false,"name":"winlog.event_data.AttributeValue","type":"keyword"},{"ecs":false,"name":"winlog.event_data.ShareName","type":"keyword"},{"ecs":false,"name":"winlog.event_data.RelativeTargetName","type":"keyword"},{"ecs":false,"name":"winlog.event_data.AccessList","type":"keyword"}],"setup":"## Config\\n\\nThe 'Audit Detailed File Share' audit policy must be configured (Success Failure).\\nSteps to implement the logging policy with with Advanced Audit Configuration:\\n\\n```\\nComputer Configuration > \\nPolicies > \\nWindows Settings > \\nSecurity Settings > \\nAdvanced Audit Policies Configuration > \\nAudit Policies > \\nObject Access > \\nAudit Detailed File Share (Success,Failure)\\n```\\n\\nThe 'Audit Directory Service Changes' audit policy must be configured (Success Failure).\\nSteps to implement the logging policy with with Advanced Audit Configuration:\\n\\n```\\nComputer Configuration > \\nPolicies > \\nWindows Settings > \\nSecurity Settings > \\nAdvanced Audit Policies Configuration > \\nAudit Policies > \\nDS Access > \\nAudit Directory Service Changes (Success,Failure)\\n```\\n","type":"query","language":"kuery","index":["logs-endpoint.alerts-*"],"query":"event.kind:alert and event.module:(endpoint and not endgame)\\n","filters":[],"throttle":"no_actions","actions":[]} {"exported_count":1,"exported_rules_count":1,"missing_rules":[],"missing_rules_count":0,"exported_exception_list_count":0,"exported_exception_list_item_count":0,"missing_exception_list_item_count":0,"missing_exception_list_items":[],"missing_exception_lists":[],"missing_exception_lists_count":0} ``` ##### Existing plumbing for showing integration install state This PR includes a `useInstalledIntegrations` hook wired up to the `DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL` route to be added in https://github.com/elastic/kibana/pull/132667. I plumbed the initial logic as if that API returned an array of integrations in the same format stored by the rule (i.e. the `RelatedIntegrationArray` type), so this will need to be adapted when integrating this feature. There's also a `packages[]` that can be provided to `useInstalledIntegrations` to constrain the search against all installed integrations, but this may not be used in the initial API. To test the Installed Integrations UI, just uncomment the mock data return in `use_installed_integrations.tsx`. ##### Additional Notes/Todo: * Probably want to revisit the copy on the Rules Table integrations popover -- it reads a little off if we don't have have integration install data. Will work with docs folks on this. * No additional overflow logic was added for `Related Integrations`/`Required Fields`. We don't really have an overflow pattern for these description list items, so instead of just adding support for these two fields (as like another description list item that's a popover), would like to solve this for generically for all items. * TODO: Increase test coverage * ##### Rule Details <p align="center"> <img width="650" src="https://user-images.githubusercontent.com/2946766/169636465-fa9ac477-2175-40ea-8064-bc194e7c3cbc.png" /> </p> <p align="center"> <img width="650" src="https://user-images.githubusercontent.com/2946766/169745657-a35cf8b8-fe9b-4580-b637-1c98e4e6f90a.png" /> </p> <p align="center"> <img width="650" src="https://user-images.githubusercontent.com/2946766/169745451-a02612b3-dd5b-46dc-a168-8823f9b1753d.png" /> </p> ##### Rule Details without `Installed Integrations` API changes <p align="center"> <img width="650" src="https://user-images.githubusercontent.com/2946766/169745986-6bef1d55-c305-4931-9845-96a0da76d030.png" /> </p> ##### Rules Table <p align="center"> <img width="650" src="https://user-images.githubusercontent.com/2946766/169636693-0d10bf67-9981-4815-a069-1439a7c5e1ce.png" /> </p> <p align="center"> <img width="650" src="https://user-images.githubusercontent.com/2946766/169636541-ac279369-c753-4184-b25b-c4352586f347.png" /> </p> ##### Rules Table without `Installed Integrations` API changes <p align="center"> <img width="650" src="https://user-images.githubusercontent.com/2946766/169636630-983277aa-6211-487c-a50f-8cf000446436.png" /> </p> <p align="center"> <img width="650" src="https://user-images.githubusercontent.com/2946766/169636649-fb03f44f-e28e-4a3f-8944-d3e300ed94c8.png" /> </p> ##### Version mismatch In cases where the related package/integration is installed, but the version is not satisfied, a warning icon/tooltip will display next to the integration link letting the user know the installed vs targeted version. I just wanted to make sure this case was handled so copy/UI isn't final -- any feedback welcome here! 🙂 <p align="center"> <img width="650" src="https://user-images.githubusercontent.com/2946766/169730187-6269ce6c-6833-4455-83b8-e98c8dcfa387.png" /> </p> <p align="center"> <img width="650" src="https://user-images.githubusercontent.com/2946766/169745221-c0ac5e90-a89b-420a-8ade-deedfc1d1194.png" /> </p> ### Checklist Delete any items that are not applicable to this PR. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [X] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - Collaborating with docs folks on this dedicated docs issue: https://github.com/elastic/security-docs/issues/2015 - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - Some added, but need to follow-up with additional tests around versioning logic once we finalize installed integrations API - [X] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) (https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [X] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
This commit is contained in:
parent
f110118739
commit
76e77c265b
26 changed files with 803 additions and 41 deletions
|
@ -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`] = `
|
||||
<EuiToken
|
||||
aria-label="match_only_text"
|
||||
className="kbnFieldIcon"
|
||||
iconType="tokenString"
|
||||
size="s"
|
||||
title="match_only_text"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`FieldIcon renders known field types murmur3 is rendered 1`] = `
|
||||
<EuiToken
|
||||
aria-label="murmur3"
|
||||
|
|
|
@ -20,6 +20,7 @@ export interface FieldIconProps extends Omit<EuiTokenProps, 'iconType'> {
|
|||
| 'geo_shape'
|
||||
| 'ip'
|
||||
| 'ip_range'
|
||||
| 'match_only_text'
|
||||
| 'murmur3'
|
||||
| 'number'
|
||||
| 'number_range'
|
||||
|
@ -45,6 +46,7 @@ export const typeToEuiIconMap: Partial<Record<string, EuiTokenProps>> = {
|
|||
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' },
|
||||
|
|
|
@ -257,6 +257,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
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 semver from 'semver';
|
||||
import {
|
||||
RelatedIntegration,
|
||||
RelatedIntegrationArray,
|
||||
} from '../../../../common/detection_engine/schemas/common';
|
||||
|
||||
/**
|
||||
* Returns an `EuiLink` that will link to a given package/integration/version page within fleet
|
||||
* @param integration
|
||||
* @param basePath
|
||||
*/
|
||||
export const getIntegrationLink = (integration: RelatedIntegration, basePath: string) => {
|
||||
const integrationURL = `${basePath}/app/integrations/detail/${integration.package}-${
|
||||
integration.version
|
||||
}/overview${integration.integration ? `?integration=${integration.integration}` : ''}`;
|
||||
return (
|
||||
<EuiLink href={integrationURL} target="_blank">
|
||||
{`${capitalize(integration.package)} ${capitalize(integration.integration)}`}
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiBadge,
|
||||
EuiPopoverTitle,
|
||||
EuiFlexGroup,
|
||||
EuiText,
|
||||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { useBasePath } from '../../lib/kibana';
|
||||
import { getInstalledRelatedIntegrations, getIntegrationLink } from './helpers';
|
||||
import { useInstalledIntegrations } from '../../../detections/containers/detection_engine/rules/use_installed_integrations';
|
||||
import type { RelatedIntegrationArray } from '../../../../common/detection_engine/schemas/common';
|
||||
|
||||
import * as i18n from '../../../detections/pages/detection_engine/rules/translations';
|
||||
|
||||
export interface IntegrationsPopoverProps {
|
||||
integrations: RelatedIntegrationArray;
|
||||
}
|
||||
|
||||
const IntegrationsPopoverWrapper = styled(EuiFlexGroup)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const PopoverContentWrapper = styled('div')`
|
||||
max-height: 400px;
|
||||
max-width: 368px;
|
||||
overflow: auto;
|
||||
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 integrations to display
|
||||
*/
|
||||
const IntegrationsPopoverComponent = ({ integrations }: IntegrationsPopoverProps) => {
|
||||
const [isPopoverOpen, setPopoverOpen] = useState(false);
|
||||
const { data } = useInstalledIntegrations({ packages: [] });
|
||||
const basePath = useBasePath();
|
||||
|
||||
const allInstalledIntegrations: RelatedIntegrationArray = data ?? [];
|
||||
const { availableIntegrations, installedRelatedIntegrations } = getInstalledRelatedIntegrations(
|
||||
integrations,
|
||||
allInstalledIntegrations
|
||||
);
|
||||
|
||||
const badgeTitle =
|
||||
data != null
|
||||
? `${installedRelatedIntegrations.length}/${integrations.length} ${i18n.INTEGRATIONS_BADGE}`
|
||||
: `${integrations.length} ${i18n.INTEGRATIONS_BADGE}`;
|
||||
|
||||
return (
|
||||
<IntegrationsPopoverWrapper
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
data-test-subj={'IntegrationsWrapper'}
|
||||
>
|
||||
<EuiPopover
|
||||
ownFocus
|
||||
data-test-subj={'IntegrationsDisplayPopover'}
|
||||
button={
|
||||
<EuiBadge
|
||||
iconType={'tag'}
|
||||
color="hollow"
|
||||
data-test-subj={'IntegrationsDisplayPopoverButton'}
|
||||
onClick={() => setPopoverOpen(!isPopoverOpen)}
|
||||
onClickAriaLabel={badgeTitle}
|
||||
>
|
||||
{badgeTitle}
|
||||
</EuiBadge>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setPopoverOpen(!isPopoverOpen)}
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiPopoverTitle data-test-subj={'IntegrationsPopoverTitle'}>
|
||||
{i18n.INTEGRATIONS_POPOVER_TITLE(integrations.length)}
|
||||
</EuiPopoverTitle>
|
||||
|
||||
<PopoverContentWrapper data-test-subj={'IntegrationsPopoverContentWrapper'}>
|
||||
{data != null && (
|
||||
<>
|
||||
<EuiText size={'s'}>
|
||||
{i18n.INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED(
|
||||
installedRelatedIntegrations.length
|
||||
)}
|
||||
</EuiText>
|
||||
<ul>
|
||||
{installedRelatedIntegrations.map((integration, index) => (
|
||||
<IntegrationListItem key={index}>
|
||||
{getIntegrationLink(integration, basePath)}
|
||||
{!integration?.versionSatisfied && (
|
||||
<EuiIconTip
|
||||
type="alert"
|
||||
content={i18n.INTEGRATIONS_INSTALLED_VERSION_TOOLTIP(
|
||||
integration.version,
|
||||
integration.targetVersion
|
||||
)}
|
||||
position="right"
|
||||
/>
|
||||
)}
|
||||
</IntegrationListItem>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
{availableIntegrations.length > 0 && (
|
||||
<>
|
||||
<EuiText size={'s'}>
|
||||
{i18n.INTEGRATIONS_POPOVER_DESCRIPTION_UNINSTALLED(availableIntegrations.length)}
|
||||
</EuiText>
|
||||
<ul>
|
||||
{availableIntegrations.map((integration, index) => (
|
||||
<IntegrationListItem key={index}>
|
||||
{getIntegrationLink(integration, basePath)}
|
||||
</IntegrationListItem>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</PopoverContentWrapper>
|
||||
</EuiPopover>
|
||||
</IntegrationsPopoverWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoizedIntegrationsPopover = React.memo(IntegrationsPopoverComponent);
|
||||
MemoizedIntegrationsPopover.displayName = 'IntegrationsPopover';
|
||||
|
||||
export const IntegrationsPopover =
|
||||
MemoizedIntegrationsPopover as typeof IntegrationsPopoverComponent;
|
|
@ -11,7 +11,7 @@ exports[`JobsTableComponent renders correctly against snapshot 1`] = `
|
|||
Object {
|
||||
"name": "Groups",
|
||||
"render": [Function],
|
||||
"width": "140px",
|
||||
"width": "80px",
|
||||
},
|
||||
Object {
|
||||
"align": "center",
|
||||
|
|
|
@ -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) => (
|
||||
<EuiFlexGroup wrap responsive={true} gutterSize="xs">
|
||||
{groups.map((group) => (
|
||||
<EuiFlexItem grow={false} key={group}>
|
||||
<EuiBadge color={'hollow'}>{group}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
width: '140px',
|
||||
render: ({ groups }: SecurityJob) => {
|
||||
const renderItem = (group: string, i: number) => (
|
||||
<EuiBadge color="hollow" key={`${group}-${i}`} data-test-subj="group">
|
||||
{group}
|
||||
</EuiBadge>
|
||||
);
|
||||
|
||||
return (
|
||||
<PopoverItems
|
||||
items={groups}
|
||||
numberOfItemsToDisplay={0}
|
||||
popoverButtonTitle={`${groups.length} Groups`}
|
||||
renderItem={renderItem}
|
||||
dataTestPrefix="groups"
|
||||
/>
|
||||
);
|
||||
},
|
||||
width: '80px',
|
||||
},
|
||||
|
||||
{
|
||||
|
|
|
@ -76,9 +76,11 @@ const PopoverItemsComponent = <T extends unknown>({
|
|||
|
||||
return (
|
||||
<PopoverItemsWrapper alignItems="center" gutterSize="s" data-test-subj={dataTestPrefix}>
|
||||
<EuiFlexItem grow={1} className="eui-textTruncate">
|
||||
<OverflowList items={items.slice(0, numberOfItemsToDisplay)} />
|
||||
</EuiFlexItem>
|
||||
{numberOfItemsToDisplay !== 0 && (
|
||||
<EuiFlexItem grow={1} className="eui-textTruncate">
|
||||
<OverflowList items={items.slice(0, numberOfItemsToDisplay)} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiPopover
|
||||
ownFocus
|
||||
data-test-subj={`${dataTestPrefix}DisplayPopover`}
|
||||
|
|
|
@ -16,12 +16,14 @@ import {
|
|||
EuiText,
|
||||
EuiIcon,
|
||||
EuiToolTip,
|
||||
EuiFlexGrid,
|
||||
} from '@elastic/eui';
|
||||
import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils';
|
||||
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FieldIcon } from '@kbn/react-field';
|
||||
|
||||
import { ThreatMapping, Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { getDisplayValueFromFilter } from '@kbn/data-plugin/public';
|
||||
|
@ -30,7 +32,10 @@ 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 type {
|
||||
RequiredFieldArray,
|
||||
Threshold,
|
||||
} from '../../../../../common/detection_engine/schemas/common';
|
||||
import {
|
||||
subtechniquesOptions,
|
||||
tacticsOptions,
|
||||
|
@ -507,3 +512,42 @@ export const buildThreatMappingDescription = (
|
|||
},
|
||||
];
|
||||
};
|
||||
|
||||
const FieldTypeText = styled(EuiText)`
|
||||
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
|
||||
font-family: ${({ theme }) => theme.eui.euiCodeFontFamily};
|
||||
display: inline;
|
||||
`;
|
||||
|
||||
export const buildRequiredFieldsDescription = (
|
||||
label: string,
|
||||
requiredFields: RequiredFieldArray
|
||||
): ListItems[] => {
|
||||
if (requiredFields == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
title: label,
|
||||
description: (
|
||||
<EuiFlexGrid gutterSize={'s'}>
|
||||
{requiredFields.map((rF, index) => (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize={'xs'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldIcon data-test-subj="field-type-icon" type={rF.type} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldTypeText grow={false} size={'s'}>
|
||||
{` ${rF.name}${index + 1 !== requiredFields.length ? ', ' : ''}`}
|
||||
</FieldTypeText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGrid>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
@ -13,6 +13,11 @@ 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,
|
||||
} 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';
|
||||
|
@ -31,6 +36,7 @@ import {
|
|||
buildRuleTypeDescription,
|
||||
buildThresholdDescription,
|
||||
buildThreatMappingDescription,
|
||||
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,18 @@ 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);
|
||||
} 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: RequiredFieldArray = get(field, data);
|
||||
return buildRequiredFieldsDescription(label, requiredFields);
|
||||
} else if (field === 'relatedIntegrations') {
|
||||
const relatedIntegrations: RelatedIntegrationArray = get(field, data);
|
||||
return buildRelatedIntegrationsDescription(label, relatedIntegrations);
|
||||
} else if (field === 'timeline') {
|
||||
const timeline = get(field, data) as FieldValueTimeline;
|
||||
return [
|
||||
|
@ -224,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);
|
||||
|
|
|
@ -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 (
|
||||
<Wrapper>
|
||||
{getIntegrationLink(integration, basePath)}{' '}
|
||||
{data != null && (
|
||||
<EuiBadge color={badgeUninstalledColor}>{i18n.RELATED_INTEGRATIONS_UNINSTALLED}</EuiBadge>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
} else if (installedRelatedIntegrations.length > 0) {
|
||||
return (
|
||||
<Wrapper>
|
||||
{getIntegrationLink(integration, basePath)}{' '}
|
||||
<EuiBadge color={badgeInstalledColor}>{i18n.RELATED_INTEGRATIONS_INSTALLED}</EuiBadge>
|
||||
{!installedRelatedIntegrations[0]?.versionSatisfied && (
|
||||
<EuiIconTip
|
||||
type="alert"
|
||||
content={INTEGRATIONS_INSTALLED_VERSION_TOOLTIP(
|
||||
installedRelatedIntegrations[0]?.version,
|
||||
installedRelatedIntegrations[0]?.targetVersion
|
||||
)}
|
||||
position="right"
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
export const IntegrationDescription = React.memo(IntegrationDescriptionComponent);
|
||||
|
||||
const RelatedIntegrationsDescription: React.FC<{ integrations: RelatedIntegrationArray }> = ({
|
||||
integrations,
|
||||
}) => (
|
||||
<>
|
||||
{integrations?.map((integration, index) => (
|
||||
<IntegrationDescription key={`${integration.package}-${index}`} integration={integration} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
export const buildRelatedIntegrationsDescription = (
|
||||
label: string,
|
||||
relatedIntegrations: RelatedIntegrationArray
|
||||
): ListItems[] => [
|
||||
{
|
||||
title: label,
|
||||
description: <RelatedIntegrationsDescription integrations={relatedIntegrations} />,
|
||||
},
|
||||
];
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
@ -88,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();
|
||||
});
|
||||
});
|
||||
|
@ -101,6 +105,7 @@ describe('StepAboutRuleToggleDetails', () => {
|
|||
stepDataDetails={{
|
||||
note: mockRule.note,
|
||||
description: mockRule.description,
|
||||
setup: '',
|
||||
}}
|
||||
stepData={mockRule}
|
||||
/>
|
||||
|
@ -120,6 +125,7 @@ describe('StepAboutRuleToggleDetails', () => {
|
|||
stepDataDetails={{
|
||||
note: mockRule.note,
|
||||
description: mockRule.description,
|
||||
setup: '',
|
||||
}}
|
||||
stepData={mockRule}
|
||||
/>
|
||||
|
@ -147,6 +153,7 @@ describe('StepAboutRuleToggleDetails', () => {
|
|||
stepDataDetails={{
|
||||
note: mockRule.note,
|
||||
description: mockRule.description,
|
||||
setup: '',
|
||||
}}
|
||||
stepData={mockRule}
|
||||
/>
|
||||
|
@ -165,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(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<StepAboutRuleToggleDetails
|
||||
loading={false}
|
||||
stepDataDetails={{
|
||||
note: mockRule.note,
|
||||
description: mockRule.description,
|
||||
setup: '',
|
||||
}}
|
||||
stepData={mockRule}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<StepAboutRuleToggleDetails
|
||||
loading={false}
|
||||
stepDataDetails={{
|
||||
note: mockRule.note,
|
||||
description: mockRule.description,
|
||||
setup: mockRule.note, // TODO: Update to mockRule.setup once supported in UI (and mock can be updated)
|
||||
}}
|
||||
stepData={mockRule}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<StepAboutRuleToggleDetails
|
||||
loading={false}
|
||||
stepDataDetails={{
|
||||
note: mockRule.note,
|
||||
description: mockRule.description,
|
||||
setup: mockRule.note, // TODO: Update to mockRule.setup once supported in UI (and mock can be updated)
|
||||
}}
|
||||
stepData={mockRule}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
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(
|
||||
<ThemeProvider theme={mockTheme}>
|
||||
<StepAboutRuleToggleDetails
|
||||
loading={false}
|
||||
stepDataDetails={{
|
||||
note: mockRule.note,
|
||||
description: mockRule.description,
|
||||
setup: mockRule.note, // TODO: Update to mockRule.setup once supported in UI (and mock can be updated)
|
||||
}}
|
||||
stepData={mockRule}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,9 +16,9 @@ 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 { isEmpty } from 'lodash/fp';
|
||||
|
||||
import { HeaderSection } from '../../../../common/components/header_section';
|
||||
import { MarkdownRenderer } from '../../../../common/components/markdown_editor';
|
||||
|
@ -48,18 +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',
|
||||
},
|
||||
];
|
||||
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;
|
||||
|
@ -82,6 +85,16 @@ const StepAboutRuleToggleDetailsComponent: React.FC<StepPanelProps> = ({
|
|||
[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 (
|
||||
<MyPanel hasBorder>
|
||||
{loading && (
|
||||
|
@ -94,7 +107,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC<StepPanelProps> = ({
|
|||
<FlexGroupFullHeight gutterSize="xs" direction="column">
|
||||
<EuiFlexItem grow={false} key="header">
|
||||
<HeaderSection title={i18n.ABOUT_TEXT}>
|
||||
{!isEmpty(stepDataDetails.note) && stepDataDetails.note.trim() !== '' && (
|
||||
{toggleOptions.length > 0 && (
|
||||
<EuiButtonGroup
|
||||
options={toggleOptions}
|
||||
idSelected={selectedToggleOption}
|
||||
|
@ -108,7 +121,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC<StepPanelProps> = ({
|
|||
</HeaderSection>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem key="details">
|
||||
{selectedToggleOption === 'details' ? (
|
||||
{selectedToggleOption === 'details' && (
|
||||
<EuiResizeObserver data-test-subj="stepAboutDetailsContent" onResize={onResize}>
|
||||
{(resizeRef) => (
|
||||
<AboutContent ref={resizeRef}>
|
||||
|
@ -132,7 +145,8 @@ const StepAboutRuleToggleDetailsComponent: React.FC<StepPanelProps> = ({
|
|||
</AboutContent>
|
||||
)}
|
||||
</EuiResizeObserver>
|
||||
) : (
|
||||
)}
|
||||
{selectedToggleOption === 'notes' && (
|
||||
<VerticalOverflowContainer
|
||||
data-test-subj="stepAboutDetailsNoteContent"
|
||||
maxHeight={aboutPanelHeight}
|
||||
|
@ -145,6 +159,19 @@ const StepAboutRuleToggleDetailsComponent: React.FC<StepPanelProps> = ({
|
|||
</VerticalOverflowContent>
|
||||
</VerticalOverflowContainer>
|
||||
)}
|
||||
{selectedToggleOption === 'setup' && (
|
||||
<VerticalOverflowContainer
|
||||
data-test-subj="stepAboutDetailsSetupContent"
|
||||
maxHeight={aboutPanelHeight}
|
||||
>
|
||||
<VerticalOverflowContent
|
||||
maxHeight={aboutPanelHeight}
|
||||
className="eui-yScrollWithShadows"
|
||||
>
|
||||
<MarkdownRenderer>{stepDataDetails.setup}</MarkdownRenderer>
|
||||
</VerticalOverflowContent>
|
||||
</VerticalOverflowContainer>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</FlexGroupFullHeight>
|
||||
)}
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -80,6 +80,8 @@ export const stepDefineDefaultValue: DefineStepRule = {
|
|||
filters: [],
|
||||
saved_id: undefined,
|
||||
},
|
||||
requiredFields: [],
|
||||
relatedIntegrations: [],
|
||||
threatMapping: [],
|
||||
threshold: {
|
||||
field: [],
|
||||
|
|
|
@ -175,6 +175,34 @@ export const schema: FormSchema<DefineStepRule> = {
|
|||
},
|
||||
],
|
||||
},
|
||||
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',
|
||||
|
|
|
@ -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<RelatedIntegrationArray> =>
|
||||
KibanaServices.get().http.fetch<RelatedIntegrationArray>(
|
||||
DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL,
|
||||
{
|
||||
method: 'GET',
|
||||
query: {
|
||||
packages: packages?.sort()?.join(','),
|
||||
},
|
||||
signal,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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();
|
||||
|
||||
// TODO: Once API is merged update return type:
|
||||
// See: https://github.com/elastic/kibana/pull/132667/files#diff-f9d9583d37123ed28fd08fc153eb06026e7ee0c3241364656fb707dcbc0a4872R58-R65
|
||||
return useQuery<RelatedIntegrationArray | undefined>(
|
||||
[
|
||||
'installedIntegrations',
|
||||
{
|
||||
packages,
|
||||
},
|
||||
],
|
||||
async ({ signal }) => {
|
||||
return undefined;
|
||||
|
||||
// Mock data -- uncomment to test full UI
|
||||
// const mockInstalledIntegrations = [
|
||||
// {
|
||||
// package: 'system',
|
||||
// version: '1.7.4',
|
||||
// },
|
||||
// // {
|
||||
// // package: 'aws',
|
||||
// // integration: 'cloudtrail',
|
||||
// // version: '1.11.0',
|
||||
// // },
|
||||
// ];
|
||||
// return mockInstalledIntegrations;
|
||||
|
||||
// Or fetch from new API
|
||||
// return fetchInstalledIntegrations({
|
||||
// packages,
|
||||
// signal,
|
||||
// });
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
onError: (e) => {
|
||||
addError(e, { title: i18n.INSTALLED_INTEGRATIONS_FETCH_FAILURE });
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -195,6 +195,8 @@ export const mockDefineStepRule = (): DefineStepRule => ({
|
|||
index: ['filebeat-'],
|
||||
queryBar: mockQueryBar,
|
||||
threatQueryBar: mockQueryBar,
|
||||
requiredFields: [],
|
||||
relatedIntegrations: [],
|
||||
threatMapping: [],
|
||||
timeline: {
|
||||
id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',
|
||||
|
|
|
@ -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 <IntegrationsPopover integrations={integrations} />;
|
||||
},
|
||||
width: '143px',
|
||||
truncateText: true,
|
||||
};
|
||||
|
||||
const useActionsColumn = (): EuiTableActionsColumnType<Rule> => {
|
||||
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',
|
||||
|
|
|
@ -81,6 +81,8 @@ describe('rule helpers', () => {
|
|||
],
|
||||
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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1085,3 +1085,53 @@ 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.popoverDescriptionInstalledTitle',
|
||||
{
|
||||
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.popoverDescriptionUninstalledTitle',
|
||||
{
|
||||
values: { uninstalledCount },
|
||||
defaultMessage:
|
||||
'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}`',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -21,14 +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,
|
||||
RelatedIntegrationArray,
|
||||
RequiredFieldArray,
|
||||
RuleNameOverride,
|
||||
SortOrder,
|
||||
SetupGuide,
|
||||
TimestampOverride,
|
||||
} from '../../../../../common/detection_engine/schemas/common/schemas';
|
||||
} from '../../../../../common/detection_engine/schemas/common';
|
||||
|
||||
export interface EuiBasicTableSortTypes {
|
||||
field: string;
|
||||
|
@ -109,6 +112,7 @@ export interface AboutStepRule {
|
|||
export interface AboutStepRuleDetails {
|
||||
note: string;
|
||||
description: string;
|
||||
setup: SetupGuide;
|
||||
}
|
||||
|
||||
export interface AboutStepSeverity {
|
||||
|
@ -128,6 +132,8 @@ export interface DefineStepRule {
|
|||
index: string[];
|
||||
machineLearningJobId: string[];
|
||||
queryBar: FieldValueQueryBar;
|
||||
relatedIntegrations: RelatedIntegrationArray;
|
||||
requiredFields: RequiredFieldArray;
|
||||
ruleType: Type;
|
||||
timeline: FieldValueTimeline;
|
||||
threshold: FieldValueThreshold;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue