[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:
Garrett Spong 2022-05-23 14:10:36 -06:00 committed by GitHub
parent f110118739
commit 76e77c265b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 803 additions and 41 deletions

View file

@ -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"

View file

@ -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' },

View file

@ -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

View file

@ -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,
};
};

View file

@ -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;

View file

@ -11,7 +11,7 @@ exports[`JobsTableComponent renders correctly against snapshot 1`] = `
Object {
"name": "Groups",
"render": [Function],
"width": "140px",
"width": "80px",
},
Object {
"align": "center",

View file

@ -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',
},
{

View file

@ -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`}

View file

@ -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>
),
},
];
};

View file

@ -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);

View file

@ -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} />,
},
];

View file

@ -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',
}
);

View file

@ -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'
);
});
});
});

View file

@ -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>
)}

View file

@ -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',
{

View file

@ -80,6 +80,8 @@ export const stepDefineDefaultValue: DefineStepRule = {
filters: [],
saved_id: undefined,
},
requiredFields: [],
relatedIntegrations: [],
threatMapping: [],
threshold: {
field: [],

View file

@ -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',

View file

@ -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,
}
);

View file

@ -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',
}
);

View file

@ -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 });
},
}
);
};

View file

@ -195,6 +195,8 @@ export const mockDefineStepRule = (): DefineStepRule => ({
index: ['filebeat-'],
queryBar: mockQueryBar,
threatQueryBar: mockQueryBar,
requiredFields: [],
relatedIntegrations: [],
threatMapping: [],
timeline: {
id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2',

View file

@ -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',

View file

@ -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);
});

View file

@ -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);

View file

@ -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}`',
}
);

View file

@ -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;