Skip to content

Commit

Permalink
[SecuritySolution][Detections] Adds UI for new Rule Fields: Related I…
Browse files Browse the repository at this point in the history
…ntegrations, 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 #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
* elastic/security-team#2083 (internal)
* elastic/security-team#558 (internal)
* elastic/security-team#2856 (internal)
* elastic/security-team#1801 (internal)
* elastic/security-team#3624 (internal)

##### Related Links
* elastic/security-docs#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 #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: elastic/security-docs#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))
  • Loading branch information
spong authored May 23, 2022
1 parent f110118 commit 76e77c2
Show file tree
Hide file tree
Showing 26 changed files with 803 additions and 41 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/kbn-react-field/src/field_icon/field_icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface FieldIconProps extends Omit<EuiTokenProps, 'iconType'> {
| 'geo_shape'
| 'ip'
| 'ip_range'
| 'match_only_text'
| 'murmur3'
| 'number'
| 'number_range'
Expand All @@ -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' },
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
@@ -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;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ import {
EuiBasicTable,
EuiButton,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiText,
} from '@elastic/eui';

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';
Expand Down Expand Up @@ -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',
},

{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`}
Expand Down
Loading

0 comments on commit 76e77c2

Please sign in to comment.