[Security Solution][Detections] Integrates installed integrations into interface (#132847)

## Summary

Wires up the new Installed Integrations API added in https://github.com/elastic/kibana/pull/132667 to the new Related Integrations UI added in https://github.com/elastic/kibana/pull/131475.

#### Additional changes include (though not all necessary for this specific PR):
- [X] Updates integrations badge icon to `package` on Rules Table
- [ ] Add Kibana Advanced Setting for disabling integrations badge on Rules Table
- [ ] Add loaders where necessary since there can now be API delay
- [ ] Separate description step components to specific files

Please see https://github.com/elastic/kibana/pull/131475 for screenshots and additional details.


#### 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 a `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}
```
This commit is contained in:
Garrett Spong 2022-05-25 06:48:21 -06:00 committed by GitHub
parent b5ab073189
commit 3cacbe7c49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 116 additions and 101 deletions

View file

@ -10,29 +10,58 @@ import { capitalize } from 'lodash';
import React from 'react';
import semver from 'semver';
import {
InstalledIntegration,
InstalledIntegrationArray,
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
* TODO: Add `title` to RelatedIntegration so we can accurately display the integration pretty name
*
* @param integration either RelatedIntegration or InstalledIntegration
* @param basePath kbn basepath for composing the fleet URL
*/
export const getIntegrationLink = (integration: RelatedIntegration, basePath: string) => {
const integrationURL = `${basePath}/app/integrations/detail/${integration.package}-${
integration.version
}/overview${integration.integration ? `?integration=${integration.integration}` : ''}`;
export const getIntegrationLink = (
integration: RelatedIntegration | InstalledIntegration,
basePath: string
) => {
let packageName: string;
let integrationName: string | undefined;
let integrationTitle: string;
let version: string | null;
// InstalledIntegration
if ('package_name' in integration) {
packageName = integration.package_name;
integrationName = integration.integration_name;
integrationTitle = integration.integration_title ?? integration.package_name;
version = integration.package_version;
} else {
// RelatedIntegration
packageName = integration.package;
integrationName = integration.integration;
integrationTitle = `${capitalize(integration.package)} ${capitalize(integration.integration)}`;
version = semver.valid(semver.coerce(integration.version));
}
const integrationURL =
version != null
? `${basePath}/app/integrations/detail/${packageName}-${version}/overview${
integrationName ? `?integration=${integrationName}` : ''
}`
: `${basePath}/app/integrations/detail/${packageName}`;
return (
<EuiLink href={integrationURL} target="_blank">
{`${capitalize(integration.package)} ${capitalize(integration.integration)}`}
{integrationTitle}
</EuiLink>
);
};
export interface InstalledIntegration extends RelatedIntegration {
export interface InstalledIntegrationAugmented extends InstalledIntegration {
targetVersion: string;
versionSatisfied?: boolean;
versionSatisfied: boolean;
}
/**
@ -44,21 +73,22 @@ export interface InstalledIntegration extends RelatedIntegration {
*/
export const getInstalledRelatedIntegrations = (
integrations: RelatedIntegrationArray,
installedIntegrations: RelatedIntegrationArray
installedIntegrations: InstalledIntegrationArray
): {
availableIntegrations: RelatedIntegrationArray;
installedRelatedIntegrations: InstalledIntegration[];
installedRelatedIntegrations: InstalledIntegrationAugmented[];
} => {
const availableIntegrations: RelatedIntegrationArray = [];
const installedRelatedIntegrations: InstalledIntegration[] = [];
const installedRelatedIntegrations: InstalledIntegrationAugmented[] = [];
integrations.forEach((i: RelatedIntegration) => {
const match = installedIntegrations.find(
(installed) => installed.package === i.package && installed?.integration === i?.integration
(installed) =>
installed.package_name === i.package && installed?.integration_name === 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);
const versionSatisfied = semver.satisfies(match.package_version, i.version);
installedRelatedIntegrations.push({ ...match, targetVersion: i.version, versionSatisfied });
} else {
availableIntegrations.push(i);

View file

@ -15,6 +15,7 @@ import {
EuiIconTip,
} from '@elastic/eui';
import styled from 'styled-components';
import { InstalledIntegrationArray } from '../../../../common/detection_engine/schemas/common';
import { useBasePath } from '../../lib/kibana';
import { getInstalledRelatedIntegrations, getIntegrationLink } from './helpers';
import { useInstalledIntegrations } from '../../../detections/containers/detection_engine/rules/use_installed_integrations';
@ -50,12 +51,13 @@ const IntegrationsPopoverComponent = ({ integrations }: IntegrationsPopoverProps
const { data } = useInstalledIntegrations({ packages: [] });
const basePath = useBasePath();
const allInstalledIntegrations: RelatedIntegrationArray = data ?? [];
const allInstalledIntegrations: InstalledIntegrationArray = data?.installed_integrations ?? [];
const { availableIntegrations, installedRelatedIntegrations } = getInstalledRelatedIntegrations(
integrations,
allInstalledIntegrations
);
// TODO: Add loader to installed integrations value
const badgeTitle =
data != null
? `${installedRelatedIntegrations.length}/${integrations.length} ${i18n.INTEGRATIONS_BADGE}`
@ -72,7 +74,7 @@ const IntegrationsPopoverComponent = ({ integrations }: IntegrationsPopoverProps
data-test-subj={'IntegrationsDisplayPopover'}
button={
<EuiBadge
iconType={'tag'}
iconType={'package'}
color="hollow"
data-test-subj={'IntegrationsDisplayPopoverButton'}
onClick={() => setPopoverOpen(!isPopoverOpen)}
@ -105,7 +107,7 @@ const IntegrationsPopoverComponent = ({ integrations }: IntegrationsPopoverProps
<EuiIconTip
type="alert"
content={i18n.INTEGRATIONS_INSTALLED_VERSION_TOOLTIP(
integration.version,
integration.package_version,
integration.targetVersion
)}
position="right"

View file

@ -16,6 +16,7 @@ import {
} from '../../../../common/components/integrations_popover/helpers';
import {
InstalledIntegrationArray,
RelatedIntegration,
RelatedIntegrationArray,
} from '../../../../../common/detection_engine/schemas/common';
@ -35,7 +36,7 @@ const IntegrationDescriptionComponent: React.FC<{ integration: RelatedIntegratio
const badgeUninstalledColor = 'accent';
const { data } = useInstalledIntegrations({ packages: [] });
const allInstalledIntegrations: RelatedIntegrationArray = data ?? [];
const allInstalledIntegrations: InstalledIntegrationArray = data?.installed_integrations ?? [];
const { availableIntegrations, installedRelatedIntegrations } = getInstalledRelatedIntegrations(
[integration],
allInstalledIntegrations
@ -59,7 +60,7 @@ const IntegrationDescriptionComponent: React.FC<{ integration: RelatedIntegratio
<EuiIconTip
type="alert"
content={INTEGRATIONS_INSTALLED_VERSION_TOOLTIP(
installedRelatedIntegrations[0]?.version,
installedRelatedIntegrations[0]?.package_version,
installedRelatedIntegrations[0]?.targetVersion
)}
position="right"

View file

@ -23,7 +23,6 @@ import {
import {
AggregateRuleExecutionEvent,
BulkAction,
RelatedIntegrationArray,
RuleExecutionStatus,
} from '../../../../../common/detection_engine/schemas/common';
import {
@ -34,6 +33,7 @@ import {
RulesSchema,
GetAggregateRuleExecutionEventsResponse,
} from '../../../../../common/detection_engine/schemas/response';
import { GetInstalledIntegrationsResponse } from '../../../../../common/detection_engine/schemas/response/get_installed_integrations_response_schema';
import {
UpdateRulesProps,
@ -425,8 +425,8 @@ export const fetchInstalledIntegrations = async ({
}: {
packages?: string[];
signal?: AbortSignal;
}): Promise<RelatedIntegrationArray> =>
KibanaServices.get().http.fetch<RelatedIntegrationArray>(
}): Promise<GetInstalledIntegrationsResponse> =>
KibanaServices.get().http.fetch<GetInstalledIntegrationsResponse>(
DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL,
{
method: 'GET',

View file

@ -6,7 +6,8 @@
*/
import { useQuery } from 'react-query';
import { RelatedIntegrationArray } from '../../../../../common/detection_engine/schemas/common';
import { GetInstalledIntegrationsResponse } from '../../../../../common/detection_engine/schemas/response/get_installed_integrations_response_schema';
import { fetchInstalledIntegrations } from './api';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import * as i18n from './translations';
@ -17,9 +18,7 @@ export interface UseInstalledIntegrationsArgs {
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>(
return useQuery<GetInstalledIntegrationsResponse>(
[
'installedIntegrations',
{
@ -27,27 +26,10 @@ export const useInstalledIntegrations = ({ packages }: UseInstalledIntegrationsA
},
],
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,
// });
return fetchInstalledIntegrations({
packages,
signal,
});
},
{
keepPreviousData: true,

View file

@ -731,6 +731,33 @@ exports[`Details Panel Component DetailsPanel:HostDetails: rendering it should r
color: #535966;
}
.c2 dt {
font-size: 12px !important;
}
.c2 dd {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c2 dd > div {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c1 {
position: relative;
}
.c1 .euiButtonIcon {
position: absolute;
right: 12px;
top: 6px;
z-index: 2;
}
.c0 {
width: 100%;
display: -webkit-box;
@ -759,33 +786,6 @@ exports[`Details Panel Component DetailsPanel:HostDetails: rendering it should r
opacity: 1;
}
.c2 dt {
font-size: 12px !important;
}
.c2 dd {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c2 dd > div {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c1 {
position: relative;
}
.c1 .euiButtonIcon {
position: absolute;
right: 12px;
top: 6px;
z-index: 2;
}
.c4 {
padding: 16px;
background: rgba(250,251,253,0.9);
@ -1872,6 +1872,33 @@ exports[`Details Panel Component DetailsPanel:NetworkDetails: rendering it shoul
color: #535966;
}
.c2 dt {
font-size: 12px !important;
}
.c2 dd {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c2 dd > div {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c1 {
position: relative;
}
.c1 .euiButtonIcon {
position: absolute;
right: 12px;
top: 6px;
z-index: 2;
}
.c0 {
width: 100%;
display: -webkit-box;
@ -1900,33 +1927,6 @@ exports[`Details Panel Component DetailsPanel:NetworkDetails: rendering it shoul
opacity: 1;
}
.c2 dt {
font-size: 12px !important;
}
.c2 dd {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c2 dd > div {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c1 {
position: relative;
}
.c1 .euiButtonIcon {
position: absolute;
right: 12px;
top: 6px;
z-index: 2;
}
.c4 {
padding: 16px;
background: rgba(250,251,253,0.9);