[Security Solution] Prebuilt rule installation / upgrade flyout (#163304)

**Addresses:** https://github.com/elastic/kibana/issues/162334

## Summary

This PR adds a flyout for viewing a prebuilt rule before installing or
updating it. The flyout can be opened by clicking on a rule title within
"Add Elastic Rules" page and within "Rule Updates" tab of the Rule
Managament table.

I plan to add tests and do minor visual tweaks after the FF.

<img width="1269" alt="Screenshot 2023-08-14 at 03 59 30"
src="c8200ff8-fbe2-445a-a03e-3545ea77f750">

An additional goal of these changes was to create lightweight reusable
components for rule details sections ("About", "Definition", "Schedule")
and for rule properties, so that these can later be reused in other
flyouts within the Security Solution, on MITRE ATT&CK™ overview page and
potentially on the Rule Details page.
These reusable section components are basically copy-pasted components
from the Rule Details page that were refactored to remove the dependence
from the form schema,

### 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)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [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
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](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))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
Nikita Indik 2023-08-15 16:05:01 +02:00 committed by GitHub
parent 8706702aea
commit 07312bf087
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1968 additions and 23 deletions

View file

@ -219,7 +219,7 @@ export const KqlQueryLanguage = t.keyof({ kuery: null, lucene: null });
export type EqlQueryLanguage = t.TypeOf<typeof EqlQueryLanguage>;
export const EqlQueryLanguage = t.literal('eql');
const eqlSchema = buildRuleSchemas({
export const eqlSchema = buildRuleSchemas({
required: {
type: t.literal('eql'),
language: EqlQueryLanguage,
@ -254,7 +254,7 @@ export const EqlPatchParams = eqlSchema.patch;
// -------------------------------------------------------------------------------------------------
// Indicator Match rule schema
const threatMatchSchema = buildRuleSchemas({
export const threatMatchSchema = buildRuleSchemas({
required: {
type: t.literal('threat_match'),
query: RuleQuery,
@ -305,7 +305,7 @@ export const ThreatMatchPatchParams = threatMatchSchema.patch;
// -------------------------------------------------------------------------------------------------
// Custom Query rule schema
const querySchema = buildRuleSchemas({
export const querySchema = buildRuleSchemas({
required: {
type: t.literal('query'),
},
@ -341,7 +341,7 @@ export const QueryPatchParams = querySchema.patch;
// -------------------------------------------------------------------------------------------------
// Saved Query rule schema
const savedQuerySchema = buildRuleSchemas({
export const savedQuerySchema = buildRuleSchemas({
required: {
type: t.literal('saved_query'),
saved_id,
@ -385,7 +385,7 @@ export const SavedQueryPatchParams = savedQuerySchema.patch;
// -------------------------------------------------------------------------------------------------
// Threshold rule schema
const thresholdSchema = buildRuleSchemas({
export const thresholdSchema = buildRuleSchemas({
required: {
type: t.literal('threshold'),
query: RuleQuery,
@ -420,7 +420,7 @@ export const ThresholdPatchParams = thresholdSchema.patch;
// -------------------------------------------------------------------------------------------------
// Machine Learning rule schema
const machineLearningSchema = buildRuleSchemas({
export const machineLearningSchema = buildRuleSchemas({
required: {
type: t.literal('machine_learning'),
anomaly_threshold,
@ -460,7 +460,7 @@ export const MachineLearningPatchParams = machineLearningSchema.patch;
// -------------------------------------------------------------------------------------------------
// New Terms rule schema
const newTermsSchema = buildRuleSchemas({
export const newTermsSchema = buildRuleSchemas({
required: {
type: t.literal('new_terms'),
query: RuleQuery,

View file

@ -28,6 +28,7 @@ export interface RuleUpgradeInfoForReview {
id: RuleObjectId;
rule_id: RuleSignatureId;
rule: DiffableRule;
target_rule: DiffableRule;
diff: PartialRuleDiff;
revision: number;
}

View file

@ -0,0 +1,351 @@
/*
* 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 type * as t from 'io-ts';
import { parseDuration } from '@kbn/alerting-plugin/common/parse_duration';
import type {
DiffableRule,
DiffableCustomQueryFields,
DiffableSavedQueryFields,
DiffableEqlFields,
DiffableThreatMatchFields,
DiffableThresholdFields,
DiffableMachineLearningFields,
DiffableNewTermsFields,
} from '../api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule';
import type {
RuleSchedule,
SavedKqlQuery,
RuleDataSource as DiffableRuleDataSource,
RuleKqlQuery as DiffableRuleKqlQuery,
} from '../api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_field_types';
import type {
RuleResponse,
querySchema,
savedQuerySchema,
eqlSchema,
threatMatchSchema,
thresholdSchema,
machineLearningSchema,
newTermsSchema,
SharedResponseProps,
KqlQueryLanguage,
} from '../api/detection_engine/model/rule_schema/rule_schemas';
import type { RuleFilterArray } from '../api/detection_engine/model/rule_schema/common_attributes';
import { assertUnreachable } from '../utility_types';
type RuleResponseCustomQueryFields = t.TypeOf<typeof querySchema.create>;
type RuleResponseSavedQueryFields = t.TypeOf<typeof savedQuerySchema.create>;
type RuleResponseEqlFields = t.TypeOf<typeof eqlSchema.create>;
type RuleResponseThreatMatchFields = t.TypeOf<typeof threatMatchSchema.create>;
type RuleResponseThresholdFields = t.TypeOf<typeof thresholdSchema.create>;
type RuleResponseMachineLearningFields = t.TypeOf<typeof machineLearningSchema.create>;
type RuleResponseNewTermsFields = t.TypeOf<typeof newTermsSchema.create>;
interface RuleResponseScheduleFields {
from: string;
to: string;
interval: string;
}
const extractRuleSchedule = (ruleSchedule: RuleSchedule): RuleResponseScheduleFields => {
const { interval, lookback } = ruleSchedule;
const lookbackSeconds = Math.floor(parseDuration(lookback) / 1000);
const intervalSeconds = Math.floor(parseDuration(interval) / 1000);
const totalSeconds = lookbackSeconds + intervalSeconds;
let totalDuration: string;
if (totalSeconds % 3600 === 0) {
totalDuration = `${totalSeconds / 3600}h`;
} else if (totalSeconds % 60 === 0) {
totalDuration = `${totalSeconds / 60}m`;
} else {
totalDuration = `${totalSeconds}s`;
}
const from = `now-${totalDuration}`;
return {
from,
to: 'now',
interval,
};
};
type RuleResponseDataSource = { index: string[] } | { data_view_id: string };
const extractDataSource = (
diffableRuleDataSource: DiffableRuleDataSource
): RuleResponseDataSource => {
if (diffableRuleDataSource.type === 'index_patterns') {
return { index: diffableRuleDataSource.index_patterns };
} else if (diffableRuleDataSource.type === 'data_view') {
return { data_view_id: diffableRuleDataSource.data_view_id };
}
return assertUnreachable(diffableRuleDataSource);
};
type RuleResponseKqlQuery =
| { query: string; language: KqlQueryLanguage; filters: RuleFilterArray }
| { saved_id: string };
const extractKqlQuery = (diffableRuleKqlQuery: DiffableRuleKqlQuery): RuleResponseKqlQuery => {
if (diffableRuleKqlQuery.type === 'inline_query') {
return {
query: diffableRuleKqlQuery.query,
language: diffableRuleKqlQuery.language,
filters: diffableRuleKqlQuery.filters,
};
}
if (diffableRuleKqlQuery.type === 'saved_query') {
return { saved_id: diffableRuleKqlQuery.saved_query_id };
}
return assertUnreachable(diffableRuleKqlQuery);
};
const extractCommonFields = (diffableRule: DiffableRule): Partial<SharedResponseProps> => {
const { from, to, interval } = extractRuleSchedule(diffableRule.rule_schedule);
const commonFields: Partial<SharedResponseProps> = {
rule_id: diffableRule.rule_id,
version: diffableRule.version,
meta: diffableRule.meta,
name: diffableRule.name,
tags: diffableRule.tags,
description: diffableRule.description,
severity: diffableRule.severity,
severity_mapping: diffableRule.severity_mapping,
risk_score: diffableRule.risk_score,
risk_score_mapping: diffableRule.risk_score_mapping,
references: diffableRule.references,
false_positives: diffableRule.false_positives,
threat: diffableRule.threat,
note: diffableRule.note,
related_integrations: diffableRule.related_integrations,
required_fields: diffableRule.required_fields,
author: diffableRule.author,
license: diffableRule.license,
from,
to,
interval,
actions: diffableRule.actions,
throttle: diffableRule.throttle,
exceptions_list: diffableRule.exceptions_list,
max_signals: diffableRule.max_signals,
setup: diffableRule.setup,
};
if (diffableRule.building_block?.type) {
commonFields.building_block_type = diffableRule.building_block.type;
}
if (diffableRule.rule_name_override?.field_name) {
commonFields.rule_name_override = diffableRule.rule_name_override.field_name;
}
if (diffableRule.timeline_template?.timeline_id) {
commonFields.timeline_id = diffableRule.timeline_template.timeline_id;
}
if (diffableRule.timeline_template?.timeline_title) {
commonFields.timeline_title = diffableRule.timeline_template.timeline_title;
}
if (diffableRule.timestamp_override?.field_name) {
commonFields.timestamp_override = diffableRule.timestamp_override.field_name;
}
if (diffableRule.timestamp_override?.fallback_disabled) {
commonFields.timestamp_override_fallback_disabled =
diffableRule.timestamp_override.fallback_disabled;
}
return commonFields;
};
const extractCustomQueryFields = (
diffableRule: DiffableCustomQueryFields
): RuleResponseCustomQueryFields => {
const customQueryFields: RuleResponseCustomQueryFields = {
type: diffableRule.type,
...(diffableRule.data_source ? extractDataSource(diffableRule.data_source) : {}),
...(diffableRule.kql_query ? extractKqlQuery(diffableRule.kql_query) : {}),
};
if (diffableRule.alert_suppression) {
customQueryFields.alert_suppression = diffableRule.alert_suppression;
}
return customQueryFields;
};
const extractSavedQueryFields = (
diffableRule: DiffableSavedQueryFields
): RuleResponseSavedQueryFields => {
/* Typecasting to SavedKqlQuery because a "save_query" DiffableRule can only have "kql_query" of type SavedKqlQuery */
const diffableRuleKqlQuery = diffableRule.kql_query as SavedKqlQuery;
const savedQueryFields: RuleResponseSavedQueryFields = {
type: diffableRule.type,
saved_id: diffableRuleKqlQuery.saved_query_id,
...(diffableRule.data_source ? extractDataSource(diffableRule.data_source) : {}),
};
if (diffableRule.alert_suppression) {
savedQueryFields.alert_suppression = diffableRule.alert_suppression;
}
return savedQueryFields;
};
const extractEqlFields = (diffableRule: DiffableEqlFields): RuleResponseEqlFields => {
const eqlFields: RuleResponseEqlFields = {
type: diffableRule.type,
query: diffableRule.eql_query.query,
language: diffableRule.eql_query.language,
filters: diffableRule.eql_query.filters,
event_category_override: diffableRule.event_category_override,
timestamp_field: diffableRule.timestamp_field,
tiebreaker_field: diffableRule.tiebreaker_field,
...(diffableRule.data_source ? extractDataSource(diffableRule.data_source) : {}),
};
return eqlFields;
};
const extractThreatMatchFields = (
diffableRule: DiffableThreatMatchFields
): RuleResponseThreatMatchFields => {
const threatMatchFields: RuleResponseThreatMatchFields = {
type: diffableRule.type,
query:
'' /* Indicator match rules have a "query" equal to an empty string if saved query is used */,
threat_query: diffableRule.threat_query.query ?? '',
threat_language: diffableRule.threat_query.language ?? '',
threat_filters: diffableRule.threat_query.filters ?? [],
threat_index: diffableRule.threat_index,
threat_mapping: diffableRule.threat_mapping,
threat_indicator_path: diffableRule.threat_indicator_path,
...(diffableRule.data_source ? extractDataSource(diffableRule.data_source) : {}),
...(diffableRule.kql_query ? extractKqlQuery(diffableRule.kql_query) : {}),
};
if (diffableRule.concurrent_searches) {
threatMatchFields.concurrent_searches = diffableRule.concurrent_searches;
}
if (diffableRule.items_per_search) {
threatMatchFields.items_per_search = diffableRule.items_per_search;
}
return threatMatchFields;
};
const extractThresholdFields = (
diffableRule: DiffableThresholdFields
): RuleResponseThresholdFields => {
const thresholdFields: RuleResponseThresholdFields = {
type: diffableRule.type,
query: '' /* Threshold rules have a "query" equal to an empty string if saved query is used */,
threshold: diffableRule.threshold,
...(diffableRule.data_source ? extractDataSource(diffableRule.data_source) : {}),
...(diffableRule.kql_query ? extractKqlQuery(diffableRule.kql_query) : {}),
};
return thresholdFields;
};
const extractMachineLearningFields = (
diffableRule: DiffableMachineLearningFields
): RuleResponseMachineLearningFields => {
const machineLearningFields: RuleResponseMachineLearningFields = {
type: diffableRule.type,
machine_learning_job_id: diffableRule.machine_learning_job_id,
anomaly_threshold: diffableRule.anomaly_threshold,
};
return machineLearningFields;
};
const extractNewTermsFields = (
diffableRule: DiffableNewTermsFields
): RuleResponseNewTermsFields => {
const newTermsFields: RuleResponseNewTermsFields = {
type: diffableRule.type,
query: diffableRule.kql_query.query,
language: diffableRule.kql_query.language,
filters: diffableRule.kql_query.filters,
new_terms_fields: diffableRule.new_terms_fields,
history_window_start: diffableRule.history_window_start,
...(diffableRule.data_source ? extractDataSource(diffableRule.data_source) : {}),
};
return newTermsFields;
};
/**
* Converts a rule of type DiffableRule to a rule of type RuleResponse.
* Note that DiffableRule doesn't include all the fields that RuleResponse has, so they will be missing from the returned object. These are meta fields like "enabled", "created_at", "created_by", "updated_at", "updated_by", "id", "immutable", "output_index", "revision"
*/
export const diffableRuleToRuleResponse = (diffableRule: DiffableRule): Partial<RuleResponse> => {
const commonFields = extractCommonFields(diffableRule);
if (diffableRule.type === 'query') {
return {
...commonFields,
...extractCustomQueryFields(diffableRule),
};
}
if (diffableRule.type === 'saved_query') {
return {
...commonFields,
...extractSavedQueryFields(diffableRule),
};
}
if (diffableRule.type === 'eql') {
return {
...commonFields,
...extractEqlFields(diffableRule),
};
}
if (diffableRule.type === 'threat_match') {
return {
...commonFields,
...extractThreatMatchFields(diffableRule),
};
}
if (diffableRule.type === 'threshold') {
return {
...commonFields,
...extractThresholdFields(diffableRule),
};
}
if (diffableRule.type === 'machine_learning') {
return {
...commonFields,
...extractMachineLearningFields(diffableRule),
};
}
if (diffableRule.type === 'new_terms') {
return {
...commonFields,
...extractNewTermsFields(diffableRule),
};
}
return assertUnreachable(diffableRule);
};

View file

@ -0,0 +1,30 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui';
const StyledEuiBadge = styled(EuiBadge)`
.euiBadge__text {
white-space: pre-wrap !important;
}
` as unknown as typeof EuiBadge;
interface BadgeListProps {
badges: string[];
}
export const BadgeList = ({ badges }: BadgeListProps) => (
<EuiFlexGroup responsive={false} gutterSize="xs" wrap>
{badges.map((badge: string) => (
<EuiFlexItem grow={false} key={`badge-${badge}`}>
<StyledEuiBadge color="hollow">{badge}</StyledEuiBadge>
</EuiFlexItem>
))}
</EuiFlexGroup>
);

View file

@ -0,0 +1,353 @@
/*
* 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 { isEmpty } from 'lodash/fp';
import {
EuiDescriptionList,
EuiFlexGroup,
EuiFlexItem,
EuiToolTip,
EuiIcon,
EuiText,
EuiLink,
EuiSpacer,
} from '@elastic/eui';
import type { EuiDescriptionListProps } from '@elastic/eui';
import type {
SeverityMappingItem as SeverityMappingItemType,
RiskScoreMappingItem as RiskScoreMappingItemType,
Threats,
} from '@kbn/securitysolution-io-ts-alerting-types';
import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import { SeverityBadge } from '../../../../detections/components/rules/severity_badge';
import { defaultToEmptyTag } from '../../../../common/components/empty_value';
import { filterEmptyThreats } from '../../../rule_creation_ui/pages/rule_creation/helpers';
import { ThreatEuiFlexGroup } from '../../../../detections/components/rules/description_step/threat_description';
import { BadgeList } from './badge_list';
import * as i18n from './translations';
const OverrideColumn = styled(EuiFlexItem)`
width: 125px;
max-width: 125px;
overflow: hidden;
text-overflow: ellipsis;
`;
const OverrideValueColumn = styled(EuiFlexItem)`
width: 30px;
max-width: 30px;
overflow: hidden;
text-overflow: ellipsis;
`;
const StyledEuiLink = styled(EuiLink)`
word-break: break-word;
`;
interface DescriptionProps {
description: string;
}
const Description = ({ description }: DescriptionProps) => (
<EuiText size="s">{description}</EuiText>
);
interface AuthorProps {
author: string[];
}
const Author = ({ author }: AuthorProps) => <BadgeList badges={author} />;
const BuildingBlock = () => <EuiText size="s">{i18n.BUILDING_BLOCK_FIELD_DESCRIPTION}</EuiText>;
interface SeverityMappingItemProps {
severityMappingItem: SeverityMappingItemType;
}
const SeverityMappingItem = ({ severityMappingItem }: SeverityMappingItemProps) => (
<EuiFlexGroup alignItems="center" gutterSize="s">
<OverrideColumn>
<EuiToolTip
content={severityMappingItem.field}
data-test-subj={`severityOverrideField-${severityMappingItem.value}`}
>
<>{`${severityMappingItem.field}:`}</>
</EuiToolTip>
</OverrideColumn>
<OverrideValueColumn>
<EuiToolTip
content={severityMappingItem.value}
data-test-subj={`severityOverrideValue-${severityMappingItem.value}`}
>
{defaultToEmptyTag(severityMappingItem.value)}
</EuiToolTip>
</OverrideValueColumn>
<EuiFlexItem grow={false}>
<EuiIcon type={'sortRight'} />
</EuiFlexItem>
<EuiFlexItem>
<SeverityBadge
data-test-subj={`severityOverrideSeverity-${severityMappingItem.value}`}
value={severityMappingItem.severity}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
interface RiskScoreProps {
riskScore: number;
}
const RiskScore = ({ riskScore }: RiskScoreProps) => <EuiText size="s">{riskScore}</EuiText>;
interface RiskScoreMappingItemProps {
riskScoreMappingItem: RiskScoreMappingItemType;
}
const RiskScoreMappingItem = ({ riskScoreMappingItem }: RiskScoreMappingItemProps) => (
<EuiFlexGroup alignItems="center" gutterSize="s">
<OverrideColumn>
<EuiToolTip
content={riskScoreMappingItem.field}
data-test-subj={`riskScoreOverrideField-${riskScoreMappingItem.value}`}
>
<>{riskScoreMappingItem.field}</>
</EuiToolTip>
</OverrideColumn>
<EuiFlexItem grow={false}>
<EuiIcon type={'sortRight'} />
</EuiFlexItem>
<EuiFlexItem>{ALERT_RISK_SCORE}</EuiFlexItem>
</EuiFlexGroup>
);
interface ReferencesProps {
references: string[];
}
const References = ({ references }: ReferencesProps) => (
<EuiText size="s">
<ul>
{references
.filter((reference) => !isEmpty(reference))
.map((reference, index) => (
<li data-test-subj="urlsDescriptionReferenceLinkItem" key={`${index}-${reference}`}>
<StyledEuiLink href={reference} external target="_blank">
{reference}
</StyledEuiLink>
</li>
))}
</ul>
</EuiText>
);
const FalsePositives = ({ falsePositives }: { falsePositives: string[] }) => (
<EuiText size="s">
<ul>
{falsePositives.map((falsePositivesItem) => (
<li
data-test-subj="unorderedListArrayDescriptionItem"
key={`falsePositives-${falsePositivesItem}`}
>
{falsePositivesItem}
</li>
))}
</ul>
</EuiText>
);
interface LicenseProps {
license: string;
}
const License = ({ license }: LicenseProps) => <EuiText size="s">{license}</EuiText>;
interface RuleNameOverrideProps {
ruleNameOverride: string;
}
const RuleNameOverride = ({ ruleNameOverride }: RuleNameOverrideProps) => (
<EuiText size="s">{ruleNameOverride}</EuiText>
);
interface ThreatProps {
threat: Threats;
}
const Threat = ({ threat }: ThreatProps) => (
<ThreatEuiFlexGroup threat={filterEmptyThreats(threat)} label="" />
);
interface ThreatIndicatorPathProps {
threatIndicatorPath: string;
}
const ThreatIndicatorPath = ({ threatIndicatorPath }: ThreatIndicatorPathProps) => (
<EuiText size="s">{threatIndicatorPath}</EuiText>
);
interface TimestampOverrideProps {
timestampOverride: string;
}
const TimestampOverride = ({ timestampOverride }: TimestampOverrideProps) => (
<EuiText size="s">{timestampOverride}</EuiText>
);
interface TagsProps {
tags: string[];
}
const Tags = ({ tags }: TagsProps) => <BadgeList badges={tags} />;
// eslint-disable-next-line complexity
const prepareAboutSectionListItems = (
rule: Partial<RuleResponse>
): EuiDescriptionListProps['listItems'] => {
const aboutSectionListItems: EuiDescriptionListProps['listItems'] = [];
if (rule.author) {
aboutSectionListItems.push({
title: i18n.AUTHOR_FIELD_LABEL,
description: <Author author={rule.author} />,
});
}
if (rule.building_block_type) {
aboutSectionListItems.push({
title: i18n.BUILDING_BLOCK_FIELD_LABEL,
description: <BuildingBlock />,
});
}
if (rule.severity) {
aboutSectionListItems.push({
title: i18n.SEVERITY_FIELD_LABEL,
description: <SeverityBadge value={rule.severity} />,
});
}
if (rule.severity_mapping && rule.severity_mapping.length > 0) {
aboutSectionListItems.push(
...rule.severity_mapping
.filter((severityMappingItem) => severityMappingItem.field !== '')
.map((severityMappingItem, index) => {
return {
title: index === 0 ? i18n.SEVERITY_MAPPING_FIELD_LABEL : '',
description: <SeverityMappingItem severityMappingItem={severityMappingItem} />,
};
})
);
}
if (rule.risk_score) {
aboutSectionListItems.push({
title: i18n.RISK_SCORE_FIELD_LABEL,
description: <RiskScore riskScore={rule.risk_score} />,
});
}
if (rule.risk_score_mapping && rule.risk_score_mapping.length > 0) {
aboutSectionListItems.push(
...rule.risk_score_mapping
.filter((riskScoreMappingItem) => riskScoreMappingItem.field !== '')
.map((riskScoreMappingItem, index) => {
return {
title: index === 0 ? i18n.RISK_SCORE_MAPPING_FIELD_LABEL : '',
description: <RiskScoreMappingItem riskScoreMappingItem={riskScoreMappingItem} />,
};
})
);
}
if (rule.references && rule.references.length > 0) {
aboutSectionListItems.push({
title: i18n.REFERENCES_FIELD_LABEL,
description: <References references={rule.references} />,
});
}
if (rule.false_positives && rule.false_positives.length > 0) {
aboutSectionListItems.push({
title: i18n.FALSE_POSITIVES_FIELD_LABEL,
description: <FalsePositives falsePositives={rule.false_positives} />,
});
}
if (rule.license) {
aboutSectionListItems.push({
title: i18n.LICENSE_FIELD_LABEL,
description: <License license={rule.license} />,
});
}
if (rule.rule_name_override) {
aboutSectionListItems.push({
title: i18n.RULE_NAME_OVERRIDE_FIELD_LABEL,
description: <RuleNameOverride ruleNameOverride={rule.rule_name_override} />,
});
}
if (rule.threat && rule.threat.length > 0) {
aboutSectionListItems.push({
title: i18n.THREAT_FIELD_LABEL,
description: <Threat threat={rule.threat} />,
});
}
if ('threat_indicator_path' in rule && rule.threat_indicator_path) {
aboutSectionListItems.push({
title: i18n.THREAT_INDICATOR_PATH_LABEL,
description: <ThreatIndicatorPath threatIndicatorPath={rule.threat_indicator_path} />,
});
}
if (rule.timestamp_override) {
aboutSectionListItems.push({
title: i18n.TIMESTAMP_OVERRIDE_FIELD_LABEL,
description: <TimestampOverride timestampOverride={rule.timestamp_override} />,
});
}
if (rule.tags && rule.tags.length > 0) {
aboutSectionListItems.push({
title: i18n.TAGS_FIELD_LABEL,
description: <Tags tags={rule.tags} />,
});
}
return aboutSectionListItems;
};
export interface RuleAboutSectionProps {
rule: Partial<RuleResponse>;
}
export const RuleAboutSection = ({ rule }: RuleAboutSectionProps) => {
const aboutSectionListItems = prepareAboutSectionListItems(rule);
return (
<div>
{rule.description && (
<EuiDescriptionList
listItems={[
{
title: i18n.DESCRIPTION_FIELD_LABEL,
description: <Description description={rule.description} />,
},
]}
/>
)}
<EuiSpacer size="m" />
<EuiDescriptionList type="column" listItems={aboutSectionListItems} />
</div>
);
};

View file

@ -0,0 +1,257 @@
/*
* 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 { isEmpty } from 'lodash/fp';
import styled from 'styled-components';
import { EuiDescriptionList, EuiText, EuiFlexGrid, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import type { EuiDescriptionListProps } from '@elastic/eui';
import type {
Type,
ThreatMapping as ThreatMappingType,
} from '@kbn/securitysolution-io-ts-alerting-types';
import { FieldIcon } from '@kbn/react-field';
import { castEsToKbnFieldTypeName } from '@kbn/field-types';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import type { Threshold as ThresholdType } from '../../../../../common/api/detection_engine/model/rule_schema/specific_attributes/threshold_attributes';
import type { RequiredFieldArray } from '../../../../../common/api/detection_engine/model/rule_schema/common_attributes';
import { assertUnreachable } from '../../../../../common/utility_types';
import * as descriptionStepI18n from '../../../../detections/components/rules/description_step/translations';
import { MlJobsDescription } from '../../../../detections/components/rules/ml_jobs_description/ml_jobs_description';
import { RelatedIntegrationsDescription } from '../../../../detections/components/rules/related_integrations/integrations_description';
import * as threatMatchI18n from '../../../../common/components/threat_match/translations';
import { BadgeList } from './badge_list';
import * as i18n from './translations';
interface IndexProps {
index: string[];
}
const Index = ({ index }: IndexProps) => <BadgeList badges={index} />;
interface DataViewProps {
dataViewId: string;
}
const DataView = ({ dataViewId }: DataViewProps) => <EuiText size="s">{dataViewId}</EuiText>;
interface ThresholdProps {
threshold: ThresholdType;
}
const Threshold = ({ threshold }: ThresholdProps) => (
<>
{isEmpty(threshold.field[0])
? `${descriptionStepI18n.THRESHOLD_RESULTS_ALL} >= ${threshold.value}`
: `${descriptionStepI18n.THRESHOLD_RESULTS_AGGREGATED_BY} ${
Array.isArray(threshold.field) ? threshold.field.join(',') : threshold.field
} >= ${threshold.value}`}
</>
);
const getRuleTypeDescription = (ruleType: Type) => {
switch (ruleType) {
case 'machine_learning':
return descriptionStepI18n.ML_TYPE_DESCRIPTION;
case 'query':
case 'saved_query':
return descriptionStepI18n.QUERY_TYPE_DESCRIPTION;
case 'threshold':
return descriptionStepI18n.THRESHOLD_TYPE_DESCRIPTION;
case 'eql':
return descriptionStepI18n.EQL_TYPE_DESCRIPTION;
case 'threat_match':
return descriptionStepI18n.THREAT_MATCH_TYPE_DESCRIPTION;
case 'new_terms':
return descriptionStepI18n.NEW_TERMS_TYPE_DESCRIPTION;
default:
return assertUnreachable(ruleType);
}
};
interface RuleTypeProps {
type: Type;
}
const RuleType = ({ type }: RuleTypeProps) => (
<EuiText size="s">{getRuleTypeDescription(type)}</EuiText>
);
const StyledFieldTypeText = styled(EuiText)`
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
font-family: ${({ theme }) => theme.eui.euiCodeFontFamily};
display: inline;
`;
interface RequiredFieldsProps {
requiredFields: RequiredFieldArray;
}
const RequiredFields = ({ requiredFields }: RequiredFieldsProps) => (
<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={castEsToKbnFieldTypeName(rF.type)}
label={rF.type}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<StyledFieldTypeText grow={false} size={'s'}>
{` ${rF.name}${index + 1 !== requiredFields.length ? ', ' : ''}`}
</StyledFieldTypeText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
))}
</EuiFlexGrid>
);
interface TimelineTitleProps {
timelineTitle: string;
}
const TimelineTitle = ({ timelineTitle }: TimelineTitleProps) => (
<EuiText size="s">{timelineTitle}</EuiText>
);
interface ThreatIndexProps {
threatIndex: string[];
}
const ThreatIndex = ({ threatIndex }: ThreatIndexProps) => <BadgeList badges={threatIndex} />;
interface ThreatMappingProps {
threatMapping: ThreatMappingType;
}
const ThreatMapping = ({ threatMapping }: ThreatMappingProps) => {
const description = threatMapping.reduce<string>(
(accumThreatMaps, threatMap, threatMapIndex, { length: threatMappingLength }) => {
const matches = threatMap.entries.reduce<string>(
(accumItems, item, itemsIndex, { length: threatMapLength }) => {
if (threatMapLength === 1) {
return `${item.field} ${threatMatchI18n.MATCHES} ${item.value}`;
} else if (itemsIndex === 0) {
return `(${item.field} ${threatMatchI18n.MATCHES} ${item.value})`;
} else {
return `${accumItems} ${threatMatchI18n.AND} (${item.field} ${threatMatchI18n.MATCHES} ${item.value})`;
}
},
''
);
if (threatMappingLength === 1) {
return `${matches}`;
} else if (threatMapIndex === 0) {
return `(${matches})`;
} else {
return `${accumThreatMaps} ${threatMatchI18n.OR} (${matches})`;
}
},
''
);
return <EuiText size="s">{description}</EuiText>;
};
const prepareDefinitionSectionListItems = (
rule: Partial<RuleResponse>
): EuiDescriptionListProps['listItems'] => {
const definitionSectionListItems: EuiDescriptionListProps['listItems'] = [];
if ('index' in rule && rule.index && rule.index.length > 0) {
definitionSectionListItems.push({
title: i18n.INDEX_FIELD_LABEL,
description: <Index index={rule.index} />,
});
}
if ('data_view_id' in rule && rule.data_view_id) {
definitionSectionListItems.push({
title: i18n.DATA_VIEW_FIELD_LABEL,
description: <DataView dataViewId={rule.data_view_id} />,
});
}
if (rule.type) {
definitionSectionListItems.push({
title: i18n.RULE_TYPE_FIELD_LABEL,
description: <RuleType type={rule.type} />,
});
}
if ('machine_learning_job_id' in rule) {
definitionSectionListItems.push({
title: i18n.MACHINE_LEARNING_JOB_ID_FIELD_LABEL,
description: <MlJobsDescription jobIds={rule.machine_learning_job_id as string[]} />,
});
}
if (rule.related_integrations && rule.related_integrations.length > 0) {
definitionSectionListItems.push({
title: i18n.RELATED_INTEGRATIONS_FIELD_LABEL,
description: (
<RelatedIntegrationsDescription relatedIntegrations={rule.related_integrations} />
),
});
}
if (rule.required_fields && rule.required_fields.length > 0) {
definitionSectionListItems.push({
title: i18n.REQUIRED_FIELDS_FIELD_LABEL,
description: <RequiredFields requiredFields={rule.required_fields} />,
});
}
if (rule.timeline_title) {
definitionSectionListItems.push({
title: i18n.TIMELINE_TITLE_FIELD_LABEL,
description: <TimelineTitle timelineTitle={rule.timeline_title} />,
});
}
if ('threshold' in rule && rule.threshold) {
definitionSectionListItems.push({
title: i18n.THRESHOLD_FIELD_LABEL,
description: <Threshold threshold={rule.threshold} />,
});
}
if ('threat_index' in rule && rule.threat_index) {
definitionSectionListItems.push({
title: i18n.THREAT_INDEX_FIELD_LABEL,
description: <ThreatIndex threatIndex={rule.threat_index} />,
});
}
if ('threat_mapping' in rule && rule.threat_mapping) {
definitionSectionListItems.push({
title: i18n.THREAT_MAPPING_FIELD_LABEL,
description: <ThreatMapping threatMapping={rule.threat_mapping} />,
});
}
return definitionSectionListItems;
};
export interface RuleDefinitionSectionProps {
rule: Partial<RuleResponse>;
}
export const RuleDefinitionSection = ({ rule }: RuleDefinitionSectionProps) => {
const definitionSectionListItems = prepareDefinitionSectionListItems(rule);
return (
<div>
<EuiDescriptionList type="column" listItems={definitionSectionListItems} />
</div>
);
};

View file

@ -0,0 +1,189 @@
/*
* 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, { useMemo, useState, useEffect } from 'react';
import styled from 'styled-components';
import {
EuiButton,
EuiButtonEmpty,
EuiTitle,
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiTabbedContent,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import type { EuiTabbedContentTab } from '@elastic/eui';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import { RuleOverviewTab, useOverviewTabSections } from './rule_overview_tab';
import { RuleInvestigationGuideTab } from './rule_investigation_guide_tab';
import * as i18n from './translations';
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
.euiFlyoutBody__overflow {
display: flex;
flex: 1;
overflow: hidden;
.euiFlyoutBody__overflowContent {
flex: 1;
overflow: hidden;
padding: ${({ theme }) => `0 ${theme.eui.euiSizeL} ${theme.eui.euiSizeM}`};
}
}
`;
const StyledFlexGroup = styled(EuiFlexGroup)`
height: 100%;
`;
const StyledEuiFlexItem = styled(EuiFlexItem)`
&.euiFlexItem {
flex: 1 0 0;
overflow: hidden;
}
`;
const StyledEuiTabbedContent = styled(EuiTabbedContent)`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
> [role='tabpanel'] {
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
overflow-y: auto;
::-webkit-scrollbar {
-webkit-appearance: none;
width: 7px;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
-webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
}
}
`;
interface RuleDetailsFlyoutProps {
rule: Partial<RuleResponse>;
actionButtonLabel: string;
isActionButtonDisabled: boolean;
onActionButtonClick: (ruleId: string) => void;
closeFlyout: () => void;
}
export const RuleDetailsFlyout = ({
rule,
actionButtonLabel,
isActionButtonDisabled,
onActionButtonClick,
closeFlyout,
}: RuleDetailsFlyoutProps) => {
const { expandedOverviewSections, toggleOverviewSection } = useOverviewTabSections();
const overviewTab: EuiTabbedContentTab = useMemo(
() => ({
id: 'overview',
name: i18n.OVERVIEW_TAB_LABEL,
content: (
<RuleOverviewTab
rule={rule}
expandedOverviewSections={expandedOverviewSections}
toggleOverviewSection={toggleOverviewSection}
/>
),
}),
[rule, expandedOverviewSections, toggleOverviewSection]
);
const investigationGuideTab: EuiTabbedContentTab = useMemo(
() => ({
id: 'investigationGuide',
name: i18n.INVESTIGATION_GUIDE_TAB_LABEL,
content: <RuleInvestigationGuideTab note={rule.note ?? ''} />,
}),
[rule.note]
);
const tabs = useMemo(() => {
if (rule.note) {
return [overviewTab, investigationGuideTab];
} else {
return [overviewTab];
}
}, [overviewTab, investigationGuideTab, rule.note]);
const [selectedTabId, setSelectedTabId] = useState<string>(tabs[0].id);
const selectedTab = tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0];
useEffect(() => {
if (!tabs.find((tab) => tab.id === selectedTabId)) {
// Switch to first tab if currently selected tab is not available for this rule
setSelectedTabId(tabs[0].id);
}
}, [tabs, selectedTabId]);
const onTabClick = (tab: EuiTabbedContentTab) => {
setSelectedTabId(tab.id);
};
return (
<EuiFlyout
size="m"
onClose={closeFlyout}
ownFocus={false}
key="prebuilt-rules-flyout"
paddingSize="l"
>
<EuiFlyoutHeader>
<EuiTitle size="m" data-test-subj="rulesBulkEditFormTitle">
<h2>{rule.name}</h2>
</EuiTitle>
<EuiSpacer size="l" />
</EuiFlyoutHeader>
<StyledEuiFlyoutBody>
<StyledFlexGroup direction="column" gutterSize="none">
<StyledEuiFlexItem grow={true}>
<StyledEuiTabbedContent tabs={tabs} selectedTab={selectedTab} onTabClick={onTabClick} />
</StyledEuiFlexItem>
</StyledFlexGroup>
</StyledEuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={closeFlyout} flush="left">
{i18n.DISMISS_BUTTON_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
disabled={isActionButtonDisabled}
onClick={() => {
onActionButtonClick(rule.rule_id ?? '');
closeFlyout();
}}
fill
>
{actionButtonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};

View file

@ -0,0 +1,30 @@
/*
* 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 { css } from '@emotion/react';
import { EuiSpacer } from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
import { MarkdownRenderer } from '../../../../common/components/markdown_editor';
import type { InvestigationGuide } from '../../../../../common/api/detection_engine/model/rule_schema/common_attributes';
interface RuleInvestigationGuideTabProps {
note: InvestigationGuide;
}
export const RuleInvestigationGuideTab = ({ note }: RuleInvestigationGuideTabProps) => {
return (
<div
css={css`
padding: 0 ${euiThemeVars.euiSizeM};
`}
>
<EuiSpacer size="m" />
<MarkdownRenderer>{note}</MarkdownRenderer>
</div>
);
};

View file

@ -0,0 +1,144 @@
/*
* 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, useMemo, useCallback } from 'react';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import {
EuiTitle,
EuiAccordion,
EuiSpacer,
EuiFlexGroup,
EuiHorizontalRule,
useGeneratedHtmlId,
} from '@elastic/eui';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import { RuleAboutSection } from './rule_about_section';
import { RuleDefinitionSection } from './rule_definition_section';
import { RuleScheduleSection } from './rule_schedule_section';
import { RuleSetupGuideSection } from './rule_setup_guide_section';
import * as i18n from './translations';
const defaultOverviewOpenSections = {
about: true,
definition: true,
schedule: true,
setup: true,
} as const;
type OverviewTabSectionName = keyof typeof defaultOverviewOpenSections;
export const useOverviewTabSections = () => {
const [expandedOverviewSections, setOpenOverviewSections] = useState(defaultOverviewOpenSections);
const toggleSection = useCallback((sectionName: OverviewTabSectionName) => {
setOpenOverviewSections((prevOpenSections) => ({
...prevOpenSections,
[sectionName]: !prevOpenSections[sectionName],
}));
}, []);
const toggleOverviewSection = useMemo(
() => ({
about: () => toggleSection('about'),
definition: () => toggleSection('definition'),
schedule: () => toggleSection('schedule'),
setup: () => toggleSection('setup'),
}),
[toggleSection]
);
return { expandedOverviewSections, toggleOverviewSection };
};
interface ExpandableSectionProps {
title: string;
isOpen: boolean;
toggle: () => void;
children: React.ReactNode;
}
const ExpandableSection = ({ title, isOpen, toggle, children }: ExpandableSectionProps) => {
const accordionId = useGeneratedHtmlId({ prefix: 'accordion' });
return (
<EuiAccordion
forceState={isOpen ? 'open' : 'closed'}
onToggle={toggle}
paddingSize="none"
id={accordionId}
buttonContent={
<EuiTitle size="s">
<h3>{title}</h3>
</EuiTitle>
}
initialIsOpen={true}
>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="none" direction="column">
{children}
</EuiFlexGroup>
</EuiAccordion>
);
};
interface RuleOverviewTabProps {
rule: Partial<RuleResponse>;
expandedOverviewSections: Record<keyof typeof defaultOverviewOpenSections, boolean>;
toggleOverviewSection: Record<keyof typeof defaultOverviewOpenSections, () => void>;
}
export const RuleOverviewTab = ({
rule,
expandedOverviewSections,
toggleOverviewSection,
}: RuleOverviewTabProps) => (
<div
css={css`
padding: 0 ${euiThemeVars.euiSizeM};
`}
>
<EuiSpacer size="m" />
<ExpandableSection
title={i18n.ABOUT_SECTION_LABEL}
isOpen={expandedOverviewSections.about}
toggle={toggleOverviewSection.about}
>
<RuleAboutSection rule={rule} />
</ExpandableSection>
<EuiHorizontalRule margin="m" />
<ExpandableSection
title={i18n.DEFINITION_SECTION_LABEL}
isOpen={expandedOverviewSections.definition}
toggle={toggleOverviewSection.definition}
>
<RuleDefinitionSection rule={rule} />
</ExpandableSection>
<EuiHorizontalRule margin="m" />
<ExpandableSection
title={i18n.SCHEDULE_SECTION_LABEL}
isOpen={expandedOverviewSections.schedule}
toggle={toggleOverviewSection.schedule}
>
<RuleScheduleSection rule={rule} />
</ExpandableSection>
<EuiHorizontalRule margin="m" />
{rule.setup && (
<>
<ExpandableSection
title={i18n.SETUP_GUIDE_SECTION_LABEL}
isOpen={expandedOverviewSections.setup}
toggle={toggleOverviewSection.setup}
>
<RuleSetupGuideSection setup={rule.setup} />
</ExpandableSection>
<EuiHorizontalRule margin="m" />
</>
)}
</div>
);

View file

@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiDescriptionList, EuiText } from '@elastic/eui';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import { getHumanizedDuration } from '../../../../detections/pages/detection_engine/rules/helpers';
import * as i18n from './translations';
interface IntervalProps {
interval: string;
}
const Interval = ({ interval }: IntervalProps) => <EuiText size="s">{interval}</EuiText>;
interface FromProps {
from: string;
interval: string;
}
const From = ({ from, interval }: FromProps) => (
<EuiText size="s">{getHumanizedDuration(from, interval)}</EuiText>
);
export interface RuleScheduleSectionProps {
rule: Partial<RuleResponse>;
}
export const RuleScheduleSection = ({ rule }: RuleScheduleSectionProps) => {
const ruleSectionListItems = [];
if (rule.interval) {
ruleSectionListItems.push({
title: i18n.INTERVAL_FIELD_LABEL,
description: <Interval interval={rule.interval} />,
});
}
if (rule.interval && rule.from) {
ruleSectionListItems.push({
title: i18n.FROM_FIELD_LABEL,
description: <From from={rule.from} interval={rule.interval} />,
});
}
return (
<div>
<EuiDescriptionList type="column" listItems={ruleSectionListItems} />
</div>
);
};

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { MarkdownRenderer } from '../../../../common/components/markdown_editor';
interface RuleSetupGuideSectionProps {
setup: string;
}
export const RuleSetupGuideSection = ({ setup }: RuleSetupGuideSectionProps) => {
return (
<div>
<MarkdownRenderer>{setup}</MarkdownRenderer>
</div>
);
};

View file

@ -0,0 +1,260 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const OVERVIEW_TAB_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.overviewTabLabel',
{
defaultMessage: 'Overview',
}
);
export const INVESTIGATION_GUIDE_TAB_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.investigationGuideTabLabel',
{
defaultMessage: 'Investigation guide',
}
);
export const DISMISS_BUTTON_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.dismissButtonLabel',
{
defaultMessage: 'Dismiss',
}
);
export const ABOUT_SECTION_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.aboutSectionLabel',
{
defaultMessage: 'About',
}
);
export const DEFINITION_SECTION_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.definitionSectionLabel',
{
defaultMessage: 'Definition',
}
);
export const SCHEDULE_SECTION_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.scheduleSectionLabel',
{
defaultMessage: 'Schedule',
}
);
export const SETUP_GUIDE_SECTION_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.setupGuideSectionLabel',
{
defaultMessage: 'Setup guide',
}
);
export const DESCRIPTION_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.descriptionFieldLabel',
{
defaultMessage: 'Description',
}
);
export const AUTHOR_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.authorFieldLabel',
{
defaultMessage: 'Author',
}
);
export const BUILDING_BLOCK_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.buildingBlockFieldLabel',
{
defaultMessage: 'Building block',
}
);
export const BUILDING_BLOCK_FIELD_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.buildingBlockFieldDescription',
{
defaultMessage: 'All generated alerts will be marked as "building block" alerts',
}
);
export const SEVERITY_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.severityFieldLabel',
{
defaultMessage: 'Severity',
}
);
export const SEVERITY_MAPPING_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.severityMappingFieldLabel',
{
defaultMessage: 'Severity override',
}
);
export const RISK_SCORE_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.riskScoreFieldLabel',
{
defaultMessage: 'Risk score',
}
);
export const RISK_SCORE_MAPPING_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.riskScoreMappingFieldLabel',
{
defaultMessage: 'Risk score override',
}
);
export const REFERENCES_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.referencesFieldLabel',
{
defaultMessage: 'Reference URLs',
}
);
export const FALSE_POSITIVES_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.falsePositivesFieldLabel',
{
defaultMessage: 'False positive examples',
}
);
export const LICENSE_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.licenseFieldLabel',
{
defaultMessage: 'License',
}
);
export const RULE_NAME_OVERRIDE_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.ruleNameOverrideFieldLabel',
{
defaultMessage: 'Rule name override',
}
);
export const THREAT_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.threatFieldLabel',
{
defaultMessage: 'MITRE ATT&CK\\u2122',
}
);
export const THREAT_INDICATOR_PATH_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.threatIndicatorPathFieldLabel',
{
defaultMessage: 'Indicator prefix override',
}
);
export const TIMESTAMP_OVERRIDE_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.timestampOverrideFieldLabel',
{
defaultMessage: 'Timestamp override',
}
);
export const TAGS_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.tagsFieldLabel',
{
defaultMessage: 'Tags',
}
);
export const INDEX_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.indexFieldLabel',
{
defaultMessage: 'Index patterns',
}
);
export const DATA_VIEW_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.dataViewFieldLabel',
{
defaultMessage: 'Data View',
}
);
export const RULE_TYPE_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.ruleTypeFieldLabel',
{
defaultMessage: 'Rule type',
}
);
export const THRESHOLD_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.thresholdFieldLabel',
{
defaultMessage: 'Threshold',
}
);
export const MACHINE_LEARNING_JOB_ID_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.machineLearningJobIdFieldLabel',
{
defaultMessage: 'Machine Learning job',
}
);
export const RELATED_INTEGRATIONS_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.relatedIntegrationsFieldLabel',
{
defaultMessage: 'Related integrations',
}
);
export const REQUIRED_FIELDS_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.requiredFieldsFieldLabel',
{
defaultMessage: 'Required fields',
}
);
export const TIMELINE_TITLE_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.timelineTitleFieldLabel',
{
defaultMessage: 'Timeline template',
}
);
export const THREAT_INDEX_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.threatIndexFieldLabel',
{
defaultMessage: 'Indicator index patterns',
}
);
export const THREAT_MAPPING_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.threatMappingFieldLabel',
{
defaultMessage: 'Indicator mapping',
}
);
export const THREAT_FILTERS_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.threatFiltersFieldLabel',
{
defaultMessage: 'Filters',
}
);
export const INTERVAL_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.intervalFieldLabel',
{
defaultMessage: 'Runs every',
}
);
export const FROM_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.fromFieldLabel',
{
defaultMessage: 'Additional look-back time',
}
);

View file

@ -0,0 +1,50 @@
/*
* 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, { useCallback } from 'react';
import { invariant } from '../../../../../common/utils/invariant';
import type {
RuleInstallationInfoForReview,
RuleSignatureId,
} from '../../../../../common/api/detection_engine';
import type { DiffableRule } from '../../../../../common/api/detection_engine/prebuilt_rules/model/diff/diffable_rule/diffable_rule';
export interface RuleDetailsFlyoutState {
flyoutRule: RuleInstallationInfoForReview | null;
}
export interface RuleDetailsFlyoutActions {
openFlyoutForRuleId: (ruleId: RuleSignatureId) => void;
closeFlyout: () => void;
}
export const useRuleDetailsFlyout = (
rules: DiffableRule[]
): RuleDetailsFlyoutState & RuleDetailsFlyoutActions => {
const [flyoutRule, setFlyoutRule] = React.useState<RuleInstallationInfoForReview | null>(null);
const openFlyoutForRuleId = useCallback(
(ruleId: RuleSignatureId) => {
const ruleToShowInFlyout = rules.find((rule) => rule.rule_id === ruleId);
invariant(ruleToShowInFlyout, `Rule with id ${ruleId} not found`);
if (ruleToShowInFlyout) {
setFlyoutRule(ruleToShowInFlyout);
}
},
[rules, setFlyoutRule]
);
const closeFlyout = useCallback(() => {
setFlyoutRule(null);
}, []);
return {
openFlyoutForRuleId,
closeFlyout,
flyoutRule,
};
};

View file

@ -0,0 +1,37 @@
/*
* 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 { useAddPrebuiltRulesTableContext } from './add_prebuilt_rules_table_context';
import { RuleDetailsFlyout } from '../../../../rule_management/components/rule_details/rule_details_flyout';
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import { diffableRuleToRuleResponse } from '../../../../../../common/detection_engine/diffable_rule_to_rule_response';
import * as i18n from './translations';
export const AddPrebuiltRulesFlyout = () => {
const {
state: { flyoutRule, isFlyoutInstallButtonDisabled },
actions: { installOneRule, closeFlyout },
} = useAddPrebuiltRulesTableContext();
if (flyoutRule == null) {
return null;
}
const ruleResponse: Partial<RuleResponse> = diffableRuleToRuleResponse(flyoutRule);
return (
<RuleDetailsFlyout
rule={ruleResponse}
actionButtonLabel={i18n.INSTALL_BUTTON_LABEL}
isActionButtonDisabled={isFlyoutInstallButtonDisabled}
onActionButtonClick={installOneRule}
closeFlyout={closeFlyout}
/>
);
};

View file

@ -22,6 +22,7 @@ import { AddPrebuiltRulesTableNoItemsMessage } from './add_prebuilt_rules_no_ite
import { useAddPrebuiltRulesTableContext } from './add_prebuilt_rules_table_context';
import { AddPrebuiltRulesTableFilters } from './add_prebuilt_rules_table_filters';
import { useAddPrebuiltRulesTableColumns } from './use_add_prebuilt_rules_table_columns';
import { AddPrebuiltRulesFlyout } from './add_prebuilt_rules_flyout';
/**
* Table Component for displaying new rules that are available to be installed
@ -78,6 +79,7 @@ export const AddPrebuiltRulesTable = React.memo(() => {
<AddPrebuiltRulesTableFilters />
</EuiFlexItem>
</EuiFlexGroup>
<EuiInMemoryTable
items={filteredRules}
sorting
@ -95,6 +97,8 @@ export const AddPrebuiltRulesTable = React.memo(() => {
data-test-subj="add-prebuilt-rules-table"
columns={rulesColumns}
/>
<AddPrebuiltRulesFlyout />
</>
)
}

View file

@ -21,6 +21,7 @@ import {
import { usePrebuiltRulesInstallReview } from '../../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_install_review';
import type { AddPrebuiltRulesTableFilterOptions } from './use_filter_prebuilt_rules_to_install';
import { useFilterPrebuiltRulesToInstall } from './use_filter_prebuilt_rules_to_install';
import { useRuleDetailsFlyout } from '../../../../rule_management/components/rule_details/use_rule_details_flyout';
export interface AddPrebuiltRulesTableState {
/**
@ -68,6 +69,16 @@ export interface AddPrebuiltRulesTableState {
* Rule rows selected in EUI InMemory Table
*/
selectedRules: RuleInstallationInfoForReview[];
/**
* Rule that is currently displayed in the flyout or null if flyout is closed
*/
flyoutRule: RuleInstallationInfoForReview | null;
/**
* Is true when the install button in the flyout is disabled
* (e.g. when the rule is already being installed or when the table is being refetched)
*
**/
isFlyoutInstallButtonDisabled: boolean;
}
export interface AddPrebuiltRulesTableActions {
@ -77,6 +88,8 @@ export interface AddPrebuiltRulesTableActions {
installSelectedRules: () => void;
setFilterOptions: Dispatch<SetStateAction<AddPrebuiltRulesTableFilterOptions>>;
selectRules: (rules: RuleInstallationInfoForReview[]) => void;
openFlyoutForRuleId: (ruleId: RuleSignatureId) => void;
closeFlyout: () => void;
}
export interface AddPrebuiltRulesContextType {
@ -129,6 +142,15 @@ export const AddPrebuiltRulesTableContextProvider = ({
const { mutateAsync: installAllRulesRequest } = usePerformInstallAllRules();
const { mutateAsync: installSpecificRulesRequest } = usePerformInstallSpecificRules();
const filteredRules = useFilterPrebuiltRulesToInstall({ filterOptions, rules });
const { openFlyoutForRuleId, closeFlyout, flyoutRule } = useRuleDetailsFlyout(filteredRules);
const isFlyoutInstallButtonDisabled = Boolean(
(flyoutRule?.rule_id && loadingRules.includes(flyoutRule.rule_id)) ||
isRefetching ||
isUpgradingSecurityPackages
);
const installOneRule = useCallback(
async (ruleId: RuleSignatureId) => {
const rule = rules.find((r) => r.rule_id === ruleId);
@ -177,12 +199,19 @@ export const AddPrebuiltRulesTableContextProvider = ({
installSelectedRules,
reFetchRules: refetch,
selectRules: setSelectedRules,
openFlyoutForRuleId,
closeFlyout,
}),
[installAllRules, installOneRule, installSelectedRules, refetch]
[
installAllRules,
installOneRule,
installSelectedRules,
refetch,
openFlyoutForRuleId,
closeFlyout,
]
);
const filteredRules = useFilterPrebuiltRulesToInstall({ filterOptions, rules });
const providerValue = useMemo<AddPrebuiltRulesContextType>(() => {
return {
state: {
@ -197,6 +226,8 @@ export const AddPrebuiltRulesTableContextProvider = ({
isUpgradingSecurityPackages,
selectedRules,
lastUpdated: dataUpdatedAt,
flyoutRule,
isFlyoutInstallButtonDisabled,
},
actions,
};
@ -212,6 +243,8 @@ export const AddPrebuiltRulesTableContextProvider = ({
isUpgradingSecurityPackages,
selectedRules,
dataUpdatedAt,
flyoutRule,
isFlyoutInstallButtonDisabled,
actions,
]);

View file

@ -30,3 +30,10 @@ export const SEARCH_PLACEHOLDER = i18n.translate(
defaultMessage: 'Search by rule name',
}
);
export const INSTALL_BUTTON_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.installButtonLabel',
{
defaultMessage: 'Install',
}
);

View file

@ -6,7 +6,7 @@
*/
import type { EuiBasicTableColumn } from '@elastic/eui';
import { EuiButtonEmpty, EuiBadge, EuiText, EuiLoadingSpinner } from '@elastic/eui';
import { EuiButtonEmpty, EuiBadge, EuiText, EuiLoadingSpinner, EuiLink } from '@elastic/eui';
import React, { useMemo } from 'react';
import { SHOW_RELATED_INTEGRATIONS_SETTING } from '../../../../../../common/constants';
import { PopoverItems } from '../../../../../common/components/popover_items';
@ -25,13 +25,32 @@ import { getNormalizedSeverity } from '../helpers';
export type TableColumn = EuiBasicTableColumn<RuleInstallationInfoForReview>;
interface RuleNameProps {
name: string;
ruleId: string;
}
const RuleName = ({ name, ruleId }: RuleNameProps) => {
const {
actions: { openFlyoutForRuleId },
} = useAddPrebuiltRulesTableContext();
return (
<EuiLink
onClick={() => {
openFlyoutForRuleId(ruleId);
}}
>
{name}
</EuiLink>
);
};
export const RULE_NAME_COLUMN: TableColumn = {
field: 'name',
name: i18n.COLUMN_RULE,
render: (value: RuleInstallationInfoForReview['name']) => (
<EuiText id={value} size="s">
{value}
</EuiText>
render: (value: RuleInstallationInfoForReview['name'], rule: RuleInstallationInfoForReview) => (
<RuleName name={value} ruleId={rule.rule_id} />
),
sortable: true,
truncateText: true,

View file

@ -30,3 +30,10 @@ export const SEARCH_PLACEHOLDER = i18n.translate(
defaultMessage: 'Search by rule name',
}
);
export const UPDATE_BUTTON_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.updateButtonLabel',
{
defaultMessage: 'Update',
}
);

View file

@ -0,0 +1,37 @@
/*
* 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 { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context';
import { RuleDetailsFlyout } from '../../../../rule_management/components/rule_details/rule_details_flyout';
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import { diffableRuleToRuleResponse } from '../../../../../../common/detection_engine/diffable_rule_to_rule_response';
import * as i18n from './translations';
export const UpgradePrebuiltRulesFlyout = () => {
const {
state: { flyoutRule, isFlyoutInstallButtonDisabled },
actions: { upgradeOneRule, closeFlyout },
} = useUpgradePrebuiltRulesTableContext();
if (flyoutRule == null) {
return null;
}
const ruleResponse: Partial<RuleResponse> = diffableRuleToRuleResponse(flyoutRule);
return (
<RuleDetailsFlyout
rule={ruleResponse}
actionButtonLabel={i18n.UPDATE_BUTTON_LABEL}
isActionButtonDisabled={isFlyoutInstallButtonDisabled}
onActionButtonClick={upgradeOneRule}
closeFlyout={closeFlyout}
/>
);
};

View file

@ -23,6 +23,7 @@ import { UpgradePrebuiltRulesTableButtons } from './upgrade_prebuilt_rules_table
import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context';
import { UpgradePrebuiltRulesTableFilters } from './upgrade_prebuilt_rules_table_filters';
import { useUpgradePrebuiltRulesTableColumns } from './use_upgrade_prebuilt_rules_table_columns';
import { UpgradePrebuiltRulesFlyout } from './upgrade_prebuilt_rules_flyout';
const NO_ITEMS_MESSAGE = (
<EuiEmptyPrompt
@ -118,6 +119,8 @@ export const UpgradePrebuiltRulesTable = React.memo(() => {
data-test-subj="rules-upgrades-table"
columns={rulesColumns}
/>
<UpgradePrebuiltRulesFlyout />
</>
)
}

View file

@ -22,6 +22,7 @@ import { usePrebuiltRulesUpgradeReview } from '../../../../rule_management/logic
import type { UpgradePrebuiltRulesTableFilterOptions } from './use_filter_prebuilt_rules_to_upgrade';
import { useFilterPrebuiltRulesToUpgrade } from './use_filter_prebuilt_rules_to_upgrade';
import { useAsyncConfirmation } from '../rules_table/use_async_confirmation';
import { useRuleDetailsFlyout } from '../../../../rule_management/components/rule_details/use_rule_details_flyout';
import { MlJobUpgradeModal } from '../../../../../detections/components/modals/ml_job_upgrade_modal';
@ -72,6 +73,16 @@ export interface UpgradePrebuiltRulesTableState {
* Rule rows selected in EUI InMemory Table
*/
selectedRules: RuleUpgradeInfoForReview[];
/**
* Rule that is currently displayed in the flyout or null if flyout is closed
*/
flyoutRule: RuleUpgradeInfoForReview['rule'] | null;
/**
* Is true when the upgrade button in the flyout is disabled
* (e.g. when the rule is already being upgrade or when the table is being refetched)
*
**/
isFlyoutInstallButtonDisabled: boolean;
}
export interface UpgradePrebuiltRulesTableActions {
@ -81,6 +92,8 @@ export interface UpgradePrebuiltRulesTableActions {
upgradeAllRules: () => void;
setFilterOptions: Dispatch<SetStateAction<UpgradePrebuiltRulesTableFilterOptions>>;
selectRules: (rules: RuleUpgradeInfoForReview[]) => void;
openFlyoutForRuleId: (ruleId: RuleSignatureId) => void;
closeFlyout: () => void;
}
export interface UpgradePrebuiltRulesContextType {
@ -126,6 +139,17 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
const { mutateAsync: upgradeAllRulesRequest } = usePerformUpgradeAllRules();
const { mutateAsync: upgradeSpecificRulesRequest } = usePerformUpgradeSpecificRules();
const filteredRules = useFilterPrebuiltRulesToUpgrade({ filterOptions, rules });
const { openFlyoutForRuleId, closeFlyout, flyoutRule } = useRuleDetailsFlyout(
filteredRules.map((upgradeInfo) => upgradeInfo.target_rule)
);
const isFlyoutInstallButtonDisabled = Boolean(
(flyoutRule?.rule_id && loadingRules.includes(flyoutRule.rule_id)) ||
isRefetching ||
isUpgradingSecurityPackages
);
// Wrapper to add confirmation modal for users who may be running older ML Jobs that would
// be overridden by updating their rules. For details, see: https://github.com/elastic/kibana/issues/128121
const [isUpgradeModalVisible, showUpgradeModal, hideUpgradeModal] = useBoolState(false);
@ -203,12 +227,19 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
upgradeAllRules,
setFilterOptions,
selectRules: setSelectedRules,
openFlyoutForRuleId,
closeFlyout,
}),
[refetch, upgradeOneRule, upgradeSelectedRules, upgradeAllRules]
[
refetch,
upgradeOneRule,
upgradeSelectedRules,
upgradeAllRules,
openFlyoutForRuleId,
closeFlyout,
]
);
const filteredRules = useFilterPrebuiltRulesToUpgrade({ filterOptions, rules });
const providerValue = useMemo<UpgradePrebuiltRulesContextType>(() => {
return {
state: {
@ -223,6 +254,8 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
selectedRules,
loadingRules,
lastUpdated: dataUpdatedAt,
flyoutRule,
isFlyoutInstallButtonDisabled,
},
actions,
};
@ -239,6 +272,8 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
selectedRules,
loadingRules,
dataUpdatedAt,
flyoutRule,
isFlyoutInstallButtonDisabled,
actions,
]);

View file

@ -6,7 +6,7 @@
*/
import type { EuiBasicTableColumn } from '@elastic/eui';
import { EuiBadge, EuiButtonEmpty, EuiLoadingSpinner, EuiText } from '@elastic/eui';
import { EuiBadge, EuiButtonEmpty, EuiLink, EuiLoadingSpinner, EuiText } from '@elastic/eui';
import React, { useMemo } from 'react';
import { SHOW_RELATED_INTEGRATIONS_SETTING } from '../../../../../../common/constants';
import type { RuleUpgradeInfoForReview } from '../../../../../../common/api/detection_engine/prebuilt_rules';
@ -25,13 +25,32 @@ import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_ta
export type TableColumn = EuiBasicTableColumn<RuleUpgradeInfoForReview>;
interface RuleNameProps {
name: string;
ruleId: string;
}
const RuleName = ({ name, ruleId }: RuleNameProps) => {
const {
actions: { openFlyoutForRuleId },
} = useUpgradePrebuiltRulesTableContext();
return (
<EuiLink
onClick={() => {
openFlyoutForRuleId(ruleId);
}}
>
{name}
</EuiLink>
);
};
const RULE_NAME_COLUMN: TableColumn = {
field: 'rule.name',
name: i18n.COLUMN_RULE,
render: (value: RuleUpgradeInfoForReview['rule']['name']) => (
<EuiText id={value} size="s">
{value}
</EuiText>
render: (value: RuleUpgradeInfoForReview['rule']['name'], rule: RuleUpgradeInfoForReview) => (
<RuleName name={value} ruleId={rule.rule.rule_id} />
),
sortable: true,
truncateText: true,

View file

@ -87,6 +87,7 @@ const calculateRuleInfos = (results: CalculateRuleDiffResult[]): RuleUpgradeInfo
const { ruleDiff, ruleVersions } = result;
const installedCurrentVersion = ruleVersions.input.current;
const diffableCurrentVersion = ruleVersions.output.current;
const diffableTargetVersion = ruleVersions.output.target;
invariant(installedCurrentVersion != null, 'installedCurrentVersion not found');
return {
@ -94,6 +95,7 @@ const calculateRuleInfos = (results: CalculateRuleDiffResult[]): RuleUpgradeInfo
rule_id: installedCurrentVersion.rule_id,
revision: installedCurrentVersion.revision,
rule: diffableCurrentVersion,
target_rule: diffableTargetVersion,
diff: {
fields: pickBy<ThreeWayDiff<unknown>>(
ruleDiff.fields,