[Security Solution] Refactor Rule Details page (#166158)

**Resolves: https://github.com/elastic/kibana/issues/164962**
**Resolves: https://github.com/elastic/kibana/issues/166650**
**Related docs issue:
https://github.com/elastic/security-docs/issues/3970**

## Summary

Refactors Rule Details page by making use of section components from the
rule preview flyout. Namely `RuleAboutSection`, `RuleDefinitionSection`
and `RuleScheduleSection`.

Also fixes a couple UI copy issues:
 - #164962.
 - #166650.


### Checklist
- [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
[issue](https://github.com/elastic/security-docs/issues/3970) created.
- [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)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nikita Indik 2023-10-04 00:44:12 +02:00 committed by GitHub
parent 92f5a883cf
commit 2d7a084da6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 352 additions and 171 deletions

View file

@ -0,0 +1,24 @@
/*
* 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 { Rule } from '../../../rule_management/logic';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
/*
* This is a temporary workaround to suppress TS errors when using
* rule section components on the rule details page.
*
* The rule details page passes a Rule object to the rule section components,
* but section components expect a RuleResponse object. Rule and RuleResponse
* are basically same object type with only a few minor differences.
* This function casts the Rule object to RuleResponse.
*
* In the near future we'll start using codegen to generate proper response
* types and the rule details page will start passing RuleResponse objects,
* so this workaround will no longer be needed.
*/
export const castRuleAsRuleResponse = (rule: Rule) => rule as Partial<RuleResponse>;

View file

@ -64,8 +64,6 @@ import { SpyRoute } from '../../../../common/utils/route/spy_routes';
import { StepAboutRuleToggleDetails } from '../../../../detections/components/rules/step_about_rule_details';
import { AlertsHistogramPanel } from '../../../../detections/components/alerts_kpis/alerts_histogram_panel';
import { useUserData } from '../../../../detections/components/user_info';
import { StepDefineRuleReadOnly } from '../../../../detections/components/rules/step_define_rule';
import { StepScheduleRuleReadOnly } from '../../../../detections/components/rules/step_schedule_rule';
import { StepRuleActionsReadOnly } from '../../../../detections/components/rules/step_rule_actions';
import {
buildAlertsFilter,
@ -120,7 +118,6 @@ import * as ruleI18n from '../../../../detections/pages/detection_engine/rules/t
import { RuleDetailsContextProvider } from './rule_details_context';
// eslint-disable-next-line no-restricted-imports
import { LegacyUrlConflictCallOut } from './legacy_url_conflict_callout';
import { useGetSavedQuery } from '../../../../detections/pages/detection_engine/rules/use_get_saved_query';
import * as i18n from './translations';
import { NeedAdminForUpdateRulesCallOut } from '../../../../detections/components/callouts/need_admin_for_update_callout';
import { MissingPrivilegesCallOut } from '../../../../detections/components/callouts/missing_privileges_callout';
@ -138,12 +135,13 @@ import { useBulkDuplicateExceptionsConfirmation } from '../../../rule_management
import { BulkActionDuplicateExceptionsConfirmation } from '../../../rule_management_ui/components/rules_table/bulk_actions/bulk_duplicate_exceptions_confirmation';
import { useAsyncConfirmation } from '../../../rule_management_ui/components/rules_table/rules_table/use_async_confirmation';
import { RuleSnoozeBadge } from '../../../rule_management/components/rule_snooze_badge';
import { useRuleIndexPattern } from '../../../rule_creation_ui/pages/form';
import { DataSourceType } from '../../../../detections/pages/detection_engine/rules/types';
import { useBoolState } from '../../../../common/hooks/use_bool_state';
import { RuleDefinitionSection } from '../../../rule_management/components/rule_details/rule_definition_section';
import { RuleScheduleSection } from '../../../rule_management/components/rule_details/rule_schedule_section';
// eslint-disable-next-line no-restricted-imports
import { useLegacyUrlRedirect } from './use_redirect_legacy_url';
import { RuleDetailTabs, useRuleDetailsTabs } from './use_rule_details_tabs';
import { castRuleAsRuleResponse } from './cast_rule_as_rule_response';
const RULE_EXCEPTION_LIST_TYPES = [
ExceptionListTypeEnum.DETECTION,
@ -174,7 +172,6 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
clearSelected,
}) => {
const {
data,
application: {
navigateToApp,
capabilities: { actions },
@ -259,38 +256,14 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
onFinish: hideDeleteConfirmation,
});
const {
aboutRuleData,
modifiedAboutRuleDetailsData,
defineRuleData,
scheduleRuleData,
ruleActionsData,
} =
const { aboutRuleData, modifiedAboutRuleDetailsData, ruleActionsData } =
rule != null
? getStepsData({ rule, detailsView: true })
: {
aboutRuleData: null,
modifiedAboutRuleDetailsData: null,
defineRuleData: null,
scheduleRuleData: null,
ruleActionsData: null,
};
const [dataViewTitle, setDataViewTitle] = useState<string>();
useEffect(() => {
const fetchDataViewTitle = async () => {
if (defineRuleData?.dataViewId != null && defineRuleData?.dataViewId !== '') {
const dataView = await data.dataViews.get(defineRuleData?.dataViewId);
setDataViewTitle(dataView.title);
}
};
fetchDataViewTitle();
}, [data.dataViews, defineRuleData?.dataViewId]);
const { indexPattern: ruleIndexPattern } = useRuleIndexPattern({
dataSourceType: defineRuleData?.dataSourceType ?? DataSourceType.IndexPatterns,
index: defineRuleData?.index ?? [],
dataViewId: defineRuleData?.dataViewId,
});
const { showBuildingBlockAlerts, setShowBuildingBlockAlerts, showOnlyThreatIndicatorAlerts } =
useDataTableFilters(TableId.alertsOnRuleDetailsPage);
@ -299,11 +272,6 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
const { globalFullScreen } = useGlobalFullScreen();
const [filterGroup, setFilterGroup] = useState<Status>(FILTER_OPEN);
const { isSavedQueryLoading, savedQueryBar } = useGetSavedQuery({
savedQueryId: rule?.saved_id,
ruleType: rule?.type,
});
// TODO: Refactor license check + hasMlAdminPermissions to common check
const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities);
@ -666,30 +634,25 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem data-test-subj="aboutRule" component="section" grow={1}>
<StepAboutRuleToggleDetails
loading={isLoading}
stepData={aboutRuleData}
stepDataDetails={modifiedAboutRuleDetailsData}
/>
{rule !== null && (
<StepAboutRuleToggleDetails
loading={isLoading}
stepData={aboutRuleData}
stepDataDetails={modifiedAboutRuleDetailsData}
rule={rule}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiFlexGroup direction="column">
<EuiFlexItem component="section" grow={1} data-test-subj="defineRule">
<StepPanel
loading={isLoading || isSavedQueryLoading}
title={ruleI18n.DEFINITION}
>
{defineRuleData != null && !isSavedQueryLoading && !isStartingJobs && (
<StepDefineRuleReadOnly
addPadding={false}
descriptionColumns="singleSplit"
defaultValues={{
dataViewTitle,
...defineRuleData,
queryBar: savedQueryBar ?? defineRuleData.queryBar,
}}
indexPattern={ruleIndexPattern}
<StepPanel loading={isLoading} title={ruleI18n.DEFINITION}>
{rule !== null && !isStartingJobs && (
<RuleDefinitionSection
rule={castRuleAsRuleResponse(rule)}
isInteractive
dataTestSubj="definitionRule"
/>
)}
</StepPanel>
@ -697,12 +660,8 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
<EuiSpacer />
<EuiFlexItem data-test-subj="schedule" component="section" grow={1}>
<StepPanel loading={isLoading} title={ruleI18n.SCHEDULE}>
{scheduleRuleData != null && (
<StepScheduleRuleReadOnly
addPadding={false}
descriptionColumns="singleSplit"
defaultValues={scheduleRuleData}
/>
{rule != null && (
<RuleScheduleSection rule={castRuleAsRuleResponse(rule)} />
)}
</StepPanel>
</EuiFlexItem>

View file

@ -53,11 +53,17 @@ const StyledEuiLink = styled(EuiLink)`
word-break: break-word;
`;
interface NameProps {
name: string;
}
const Name = ({ name }: NameProps) => <EuiText size="s">{name}</EuiText>;
interface DescriptionProps {
description: string;
}
const Description = ({ description }: DescriptionProps) => (
export const Description = ({ description }: DescriptionProps) => (
<EuiText size="s">{description}</EuiText>
);
@ -217,10 +223,29 @@ interface TagsProps {
const Tags = ({ tags }: TagsProps) => <BadgeList badges={tags} />;
const prepareAboutSectionListItems = (rule: RuleResponse): EuiDescriptionListProps['listItems'] => {
// eslint-disable-next-line complexity
const prepareAboutSectionListItems = (
rule: Partial<RuleResponse>,
hideName?: boolean,
hideDescription?: boolean
): EuiDescriptionListProps['listItems'] => {
const aboutSectionListItems: EuiDescriptionListProps['listItems'] = [];
if (rule.author.length > 0) {
if (!hideName && rule.name) {
aboutSectionListItems.push({
title: i18n.NAME_FIELD_LABEL,
description: <Name name={rule.name} />,
});
}
if (!hideDescription && rule.description) {
aboutSectionListItems.push({
title: i18n.DESCRIPTION_FIELD_LABEL,
description: <Description description={rule.description} />,
});
}
if (rule.author && rule.author.length > 0) {
aboutSectionListItems.push({
title: i18n.AUTHOR_FIELD_LABEL,
description: <Author author={rule.author} />,
@ -234,12 +259,14 @@ const prepareAboutSectionListItems = (rule: RuleResponse): EuiDescriptionListPro
});
}
aboutSectionListItems.push({
title: i18n.SEVERITY_FIELD_LABEL,
description: <SeverityBadge value={rule.severity} />,
});
if (rule.severity) {
aboutSectionListItems.push({
title: i18n.SEVERITY_FIELD_LABEL,
description: <SeverityBadge value={rule.severity} />,
});
}
if (rule.severity_mapping.length > 0) {
if (rule.severity_mapping && rule.severity_mapping.length > 0) {
aboutSectionListItems.push(
...rule.severity_mapping
.filter((severityMappingItem) => severityMappingItem.field !== '')
@ -252,12 +279,14 @@ const prepareAboutSectionListItems = (rule: RuleResponse): EuiDescriptionListPro
);
}
aboutSectionListItems.push({
title: i18n.RISK_SCORE_FIELD_LABEL,
description: <RiskScore riskScore={rule.risk_score} />,
});
if (rule.risk_score) {
aboutSectionListItems.push({
title: i18n.RISK_SCORE_FIELD_LABEL,
description: <RiskScore riskScore={rule.risk_score} />,
});
}
if (rule.risk_score_mapping.length > 0) {
if (rule.risk_score_mapping && rule.risk_score_mapping.length > 0) {
aboutSectionListItems.push(
...rule.risk_score_mapping
.filter((riskScoreMappingItem) => riskScoreMappingItem.field !== '')
@ -270,14 +299,14 @@ const prepareAboutSectionListItems = (rule: RuleResponse): EuiDescriptionListPro
);
}
if (rule.references.length > 0) {
if (rule.references && rule.references.length > 0) {
aboutSectionListItems.push({
title: i18n.REFERENCES_FIELD_LABEL,
description: <References references={rule.references} />,
});
}
if (rule.false_positives.length > 0) {
if (rule.false_positives && rule.false_positives.length > 0) {
aboutSectionListItems.push({
title: i18n.FALSE_POSITIVES_FIELD_LABEL,
description: <FalsePositives falsePositives={rule.false_positives} />,
@ -307,7 +336,7 @@ const prepareAboutSectionListItems = (rule: RuleResponse): EuiDescriptionListPro
});
}
if (rule.threat.length > 0) {
if (rule.threat && rule.threat.length > 0) {
aboutSectionListItems.push({
title: i18n.THREAT_FIELD_LABEL,
description: <Threat threat={rule.threat} />,
@ -328,7 +357,7 @@ const prepareAboutSectionListItems = (rule: RuleResponse): EuiDescriptionListPro
});
}
if (rule.tags.length > 0) {
if (rule.tags && rule.tags.length > 0) {
aboutSectionListItems.push({
title: i18n.TAGS_FIELD_LABEL,
description: <Tags tags={rule.tags} />,
@ -339,30 +368,23 @@ const prepareAboutSectionListItems = (rule: RuleResponse): EuiDescriptionListPro
};
export interface RuleAboutSectionProps {
rule: RuleResponse;
rule: Partial<RuleResponse>;
hideName?: boolean;
hideDescription?: boolean;
}
export const RuleAboutSection = ({ rule }: RuleAboutSectionProps) => {
const aboutSectionListItems = prepareAboutSectionListItems(rule);
export const RuleAboutSection = ({ rule, hideName, hideDescription }: RuleAboutSectionProps) => {
const aboutSectionListItems = prepareAboutSectionListItems(rule, hideName, hideDescription);
return (
<div>
{rule.description && (
<EuiDescriptionList
listItems={[
{
title: i18n.DESCRIPTION_FIELD_LABEL,
description: <Description description={rule.description} />,
},
]}
/>
)}
<EuiSpacer size="m" />
<EuiDescriptionList
type="column"
listItems={aboutSectionListItems}
columnWidths={DESCRIPTION_LIST_COLUMN_WIDTHS}
rowGutterSize="m"
data-test-subj="listItemColumnStepRuleDescription"
/>
</div>
);

View file

@ -34,12 +34,17 @@ import type { RequiredFieldArray } from '../../../../../common/api/detection_eng
import { assertUnreachable } from '../../../../../common/utility_types';
import * as descriptionStepI18n from '../../../../detections/components/rules/description_step/translations';
import { RelatedIntegrationsDescription } from '../../../../detections/components/rules/related_integrations/integrations_description';
import { AlertSuppressionTechnicalPreviewBadge } from '../../../../detections/components/rules/description_step/alert_suppression_technical_preview_badge';
import { useGetSavedQuery } from '../../../../detections/pages/detection_engine/rules/use_get_saved_query';
import { useLicense } from '../../../../common/hooks/use_license';
import * as threatMatchI18n from '../../../../common/components/threat_match/translations';
import { AlertSuppressionMissingFieldsStrategy } from '../../../../../common/api/detection_engine/model/rule_schema/specific_attributes/query_attributes';
import * as timelinesI18n from '../../../../timelines/components/timeline/translations';
import { useRuleIndexPattern } from '../../../rule_creation_ui/pages/form';
import { DataSourceType } from '../../../../detections/pages/detection_engine/rules/types';
import type { Duration } from '../../../../detections/pages/detection_engine/rules/types';
import { convertHistoryStartToSize } from '../../../../detections/pages/detection_engine/rules/helpers';
import { MlJobsDescription } from '../../../../detections/components/rules/ml_jobs_description/ml_jobs_description';
import { MlJobLink } from '../../../../detections/components/rules/ml_job_link/ml_job_link';
import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs';
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
@ -170,11 +175,16 @@ const AnomalyThreshold = ({ anomalyThreshold }: AnomalyThresholdProps) => (
interface MachineLearningJobListProps {
jobIds: string[];
isInteractive: boolean;
}
const MachineLearningJobList = ({ jobIds }: MachineLearningJobListProps) => {
const MachineLearningJobList = ({ jobIds, isInteractive }: MachineLearningJobListProps) => {
const { jobs } = useSecurityJobs();
if (isInteractive) {
return <MlJobsDescription jobIds={jobIds} />;
}
const relevantJobs = jobs.filter((job) => jobIds.includes(job.id));
return (
@ -202,7 +212,7 @@ const getRuleTypeDescription = (ruleType: Type) => {
case 'eql':
return descriptionStepI18n.EQL_TYPE_DESCRIPTION;
case 'esql':
return descriptionStepI18n.ESQL_TYPE_DESCRIPTION;
return <TitleWithTechnicalPreviewBadge title={descriptionStepI18n.ESQL_TYPE_DESCRIPTION} />;
case 'threat_match':
return descriptionStepI18n.THREAT_MATCH_TYPE_DESCRIPTION;
case 'new_terms':
@ -301,6 +311,49 @@ const ThreatMapping = ({ threatMapping }: ThreatMappingProps) => {
return <EuiText size="s">{description}</EuiText>;
};
interface TitleWithTechnicalPreviewBadgeProps {
title: string;
}
const TitleWithTechnicalPreviewBadge = ({ title }: TitleWithTechnicalPreviewBadgeProps) => {
const license = useLicense();
return <AlertSuppressionTechnicalPreviewBadge label={title} license={license} />;
};
interface SuppressAlertsByFieldProps {
fields: string[];
}
const SuppressAlertsByField = ({ fields }: SuppressAlertsByFieldProps) => (
<BadgeList badges={fields} />
);
interface SuppressAlertsDurationProps {
duration?: Duration;
}
const SuppressAlertsDuration = ({ duration }: SuppressAlertsDurationProps) => {
const durationDescription = duration
? `${duration.value}${duration.unit}`
: descriptionStepI18n.ALERT_SUPPRESSION_PER_RULE_EXECUTION;
return <EuiText size="s">{durationDescription}</EuiText>;
};
interface MissingFieldsStrategyProps {
missingFieldsStrategy?: AlertSuppressionMissingFieldsStrategy;
}
const MissingFieldsStrategy = ({ missingFieldsStrategy }: MissingFieldsStrategyProps) => {
const missingFieldsDescription =
missingFieldsStrategy === AlertSuppressionMissingFieldsStrategy.Suppress
? descriptionStepI18n.ALERT_SUPPRESSION_SUPPRESS_ON_MISSING_FIELDS
: descriptionStepI18n.ALERT_SUPPRESSION_DO_NOT_SUPPRESS_ON_MISSING_FIELDS;
return <EuiText size="s">{missingFieldsDescription}</EuiText>;
};
interface NewTermsFieldsProps {
newTermsFields: string[];
}
@ -321,7 +374,8 @@ const HistoryWindowSize = ({ historyWindowStart }: HistoryWindowSizeProps) => {
// eslint-disable-next-line complexity
const prepareDefinitionSectionListItems = (
rule: RuleResponse,
rule: Partial<RuleResponse>,
isInteractive: boolean,
savedQuery?: SavedQuery
): EuiDescriptionListProps['listItems'] => {
const definitionSectionListItems: EuiDescriptionListProps['listItems'] = [];
@ -358,13 +412,18 @@ const prepareDefinitionSectionListItems = (
description: <Filters filters={savedQuery.attributes.filters as Filter[]} />,
});
}
if (typeof savedQuery.attributes.query.query === 'string') {
definitionSectionListItems.push({
title: descriptionStepI18n.SAVED_QUERY_LABEL,
description: <Query query={savedQuery.attributes.query.query} />,
});
}
}
if ('filters' in rule && 'data_view_id' in rule && rule.filters?.length) {
if ('filters' in rule && rule.filters?.length) {
definitionSectionListItems.push({
title: savedQuery
? descriptionStepI18n.SAVED_QUERY_FILTERS_LABEL
: descriptionStepI18n.FILTERS_LABEL,
title: descriptionStepI18n.FILTERS_LABEL,
description: (
<Filters
filters={rule.filters as Filter[]}
@ -376,16 +435,27 @@ const prepareDefinitionSectionListItems = (
}
if ('query' in rule && rule.query) {
let title = descriptionStepI18n.QUERY_LABEL;
if (rule.type === 'saved_query') {
title = descriptionStepI18n.SAVED_QUERY_LABEL;
} else if (rule.type === 'eql') {
title = descriptionStepI18n.EQL_QUERY_LABEL;
} else if (rule.type === 'esql') {
title = descriptionStepI18n.ESQL_QUERY_LABEL;
}
definitionSectionListItems.push({
title: savedQuery ? descriptionStepI18n.SAVED_QUERY_LABEL : descriptionStepI18n.QUERY_LABEL,
title,
description: <Query query={rule.query} />,
});
}
definitionSectionListItems.push({
title: i18n.RULE_TYPE_FIELD_LABEL,
description: <RuleType type={rule.type} />,
});
if (rule.type) {
definitionSectionListItems.push({
title: i18n.RULE_TYPE_FIELD_LABEL,
description: <RuleType type={rule.type} />,
});
}
if ('anomaly_threshold' in rule && rule.anomaly_threshold) {
definitionSectionListItems.push({
@ -397,11 +467,16 @@ const prepareDefinitionSectionListItems = (
if ('machine_learning_job_id' in rule) {
definitionSectionListItems.push({
title: i18n.MACHINE_LEARNING_JOB_ID_FIELD_LABEL,
description: <MachineLearningJobList jobIds={rule.machine_learning_job_id as string[]} />,
description: (
<MachineLearningJobList
jobIds={rule.machine_learning_job_id as string[]}
isInteractive={isInteractive}
/>
),
});
}
if (rule.related_integrations.length > 0) {
if (rule.related_integrations && rule.related_integrations.length > 0) {
definitionSectionListItems.push({
title: i18n.RELATED_INTEGRATIONS_FIELD_LABEL,
description: (
@ -410,7 +485,7 @@ const prepareDefinitionSectionListItems = (
});
}
if (rule.required_fields.length > 0) {
if (rule.required_fields && rule.required_fields.length > 0) {
definitionSectionListItems.push({
title: i18n.REQUIRED_FIELDS_FIELD_LABEL,
description: <RequiredFields requiredFields={rule.required_fields} />,
@ -447,9 +522,7 @@ const prepareDefinitionSectionListItems = (
if ('threat_filters' in rule && rule.threat_filters && rule.threat_filters.length > 0) {
definitionSectionListItems.push({
title: savedQuery
? descriptionStepI18n.SAVED_QUERY_FILTERS_LABEL
: descriptionStepI18n.FILTERS_LABEL,
title: i18n.THREAT_FILTERS_FIELD_LABEL,
description: (
<Filters
filters={rule.threat_filters as Filter[]}
@ -462,13 +535,32 @@ const prepareDefinitionSectionListItems = (
if ('threat_query' in rule && rule.threat_query) {
definitionSectionListItems.push({
title: savedQuery
? descriptionStepI18n.SAVED_QUERY_LABEL
: descriptionStepI18n.THREAT_QUERY_LABEL,
title: descriptionStepI18n.THREAT_QUERY_LABEL,
description: <Query query={rule.threat_query} />,
});
}
if ('alert_suppression' in rule && rule.alert_suppression) {
definitionSectionListItems.push({
title: <TitleWithTechnicalPreviewBadge title={i18n.SUPPRESS_ALERTS_BY_FIELD_LABEL} />,
description: <SuppressAlertsByField fields={rule.alert_suppression.group_by} />,
});
definitionSectionListItems.push({
title: <TitleWithTechnicalPreviewBadge title={i18n.SUPPRESS_ALERTS_DURATION_FIELD_LABEL} />,
description: <SuppressAlertsDuration duration={rule.alert_suppression.duration} />,
});
definitionSectionListItems.push({
title: <TitleWithTechnicalPreviewBadge title={i18n.SUPPRESSION_FIELD_MISSING_FIELD_LABEL} />,
description: (
<MissingFieldsStrategy
missingFieldsStrategy={rule.alert_suppression.missing_fields_strategy}
/>
),
});
}
if ('new_terms_fields' in rule && rule.new_terms_fields && rule.new_terms_fields.length > 0) {
definitionSectionListItems.push({
title: i18n.NEW_TERMS_FIELDS_FIELD_LABEL,
@ -476,7 +568,7 @@ const prepareDefinitionSectionListItems = (
});
}
if (rule.type === 'new_terms' || 'history_window_start' in rule) {
if ('history_window_start' in rule) {
definitionSectionListItems.push({
title: i18n.HISTORY_WINDOW_SIZE_FIELD_LABEL,
description: <HistoryWindowSize historyWindowStart={rule.history_window_start} />,
@ -487,24 +579,35 @@ const prepareDefinitionSectionListItems = (
};
export interface RuleDefinitionSectionProps {
rule: RuleResponse;
rule: Partial<RuleResponse>;
isInteractive?: boolean;
dataTestSubj?: string;
}
export const RuleDefinitionSection = ({ rule }: RuleDefinitionSectionProps) => {
export const RuleDefinitionSection = ({
rule,
isInteractive = false,
dataTestSubj,
}: RuleDefinitionSectionProps) => {
const { savedQuery } = useGetSavedQuery({
savedQueryId: rule.type === 'saved_query' ? rule.saved_id : '',
ruleType: rule.type,
});
const definitionSectionListItems = prepareDefinitionSectionListItems(rule, savedQuery);
const definitionSectionListItems = prepareDefinitionSectionListItems(
rule,
isInteractive,
savedQuery
);
return (
<div>
<div data-test-subj={dataTestSubj}>
<EuiDescriptionList
type="column"
listItems={definitionSectionListItems}
columnWidths={DESCRIPTION_LIST_COLUMN_WIDTHS}
rowGutterSize="m"
data-test-subj="listItemColumnStepRuleDescription"
/>
</div>
);

View file

@ -15,7 +15,7 @@ import {
useGeneratedHtmlId,
} from '@elastic/eui';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import { RuleAboutSection } from './rule_about_section';
import { RuleAboutSection, Description } from './rule_about_section';
import { RuleDefinitionSection } from './rule_definition_section';
import { RuleScheduleSection } from './rule_schedule_section';
import { RuleSetupGuideSection } from './rule_setup_guide_section';
@ -103,7 +103,8 @@ export const RuleOverviewTab = ({
isOpen={expandedOverviewSections.about}
toggle={toggleOverviewSection.about}
>
<RuleAboutSection rule={rule} />
{rule.description && <Description description={rule.description} />}
<RuleAboutSection rule={rule} hideDescription hideName />
</ExpandableSection>
<EuiHorizontalRule margin="m" />
<ExpandableSection

View file

@ -28,10 +28,14 @@ const From = ({ from, interval }: FromProps) => (
);
export interface RuleScheduleSectionProps {
rule: RuleResponse;
rule: Partial<RuleResponse>;
}
export const RuleScheduleSection = ({ rule }: RuleScheduleSectionProps) => {
if (!rule.interval || !rule.from) {
return null;
}
const ruleSectionListItems = [];
ruleSectionListItems.push(
@ -46,7 +50,7 @@ export const RuleScheduleSection = ({ rule }: RuleScheduleSectionProps) => {
);
return (
<div>
<div data-test-subj="listItemColumnStepRuleDescription">
<EuiDescriptionList
type="column"
listItems={ruleSectionListItems}

View file

@ -56,6 +56,13 @@ export const SETUP_GUIDE_SECTION_LABEL = i18n.translate(
}
);
export const NAME_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.nameFieldLabel',
{
defaultMessage: 'Name',
}
);
export const DESCRIPTION_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.descriptionFieldLabel',
{
@ -185,7 +192,7 @@ export const INDEX_FIELD_LABEL = i18n.translate(
export const DATA_VIEW_ID_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.dataViewIdFieldLabel',
{
defaultMessage: 'Data view',
defaultMessage: 'Data view ID',
}
);
@ -269,7 +276,28 @@ export const THREAT_MAPPING_FIELD_LABEL = i18n.translate(
export const THREAT_FILTERS_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.threatFiltersFieldLabel',
{
defaultMessage: 'Filters',
defaultMessage: 'Indicator filters',
}
);
export const SUPPRESS_ALERTS_BY_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.suppressAlertsByFieldLabel',
{
defaultMessage: 'Suppress alerts by',
}
);
export const SUPPRESS_ALERTS_DURATION_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.suppressAlertsForFieldLabel',
{
defaultMessage: 'Suppress alerts for',
}
);
export const SUPPRESSION_FIELD_MISSING_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.suppressionFieldMissingFieldLabel',
{
defaultMessage: 'If a suppression field is missing',
}
);

View file

@ -15,12 +15,26 @@ export const FILTERS_LABEL = i18n.translate(
);
export const QUERY_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.QueryLabel',
'xpack.securitySolution.detectionEngine.createRule.queryLabel',
{
defaultMessage: 'Custom query',
}
);
export const EQL_QUERY_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.eqlQueryLabel',
{
defaultMessage: 'EQL query',
}
);
export const ESQL_QUERY_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.esqlQueryLabel',
{
defaultMessage: 'ES|QL query',
}
);
export const THREAT_QUERY_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.threatQueryLabel',
{

View file

@ -11,7 +11,10 @@ import { EuiProgress, EuiButtonGroup } from '@elastic/eui';
import { ThemeProvider } from 'styled-components';
import { StepAboutRuleToggleDetails } from '.';
import { mockAboutStepRule } from '../../../../detection_engine/rule_management_ui/components/rules_table/__mocks__/mock';
import {
mockRule,
mockAboutStepRule,
} from '../../../../detection_engine/rule_management_ui/components/rules_table/__mocks__/mock';
import { HeaderSection } from '../../../../common/components/header_section';
import { StepAboutRule } from '../step_about_rule';
import type { AboutStepRule } from '../../../pages/detection_engine/rules/types';
@ -24,10 +27,10 @@ const mockTheme = getMockTheme({
});
describe('StepAboutRuleToggleDetails', () => {
let mockRule: AboutStepRule;
let stepDataMock: AboutStepRule;
beforeEach(() => {
mockRule = mockAboutStepRule();
stepDataMock = mockAboutStepRule();
});
test('it renders loading component when "loading" is true', () => {
@ -35,11 +38,12 @@ describe('StepAboutRuleToggleDetails', () => {
<StepAboutRuleToggleDetails
loading={true}
stepDataDetails={{
note: mockRule.note,
description: mockRule.description,
note: stepDataMock.note,
description: stepDataMock.description,
setup: '',
}}
stepData={mockRule}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
/>
);
@ -49,7 +53,12 @@ describe('StepAboutRuleToggleDetails', () => {
test('it does not render details if stepDataDetails is null', () => {
const wrapper = shallow(
<StepAboutRuleToggleDetails loading={true} stepDataDetails={null} stepData={mockRule} />
<StepAboutRuleToggleDetails
loading={true}
stepDataDetails={null}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
/>
);
expect(wrapper.find(StepAboutRule).exists()).toBeFalsy();
@ -65,6 +74,7 @@ describe('StepAboutRuleToggleDetails', () => {
setup: '',
}}
stepData={null}
rule={mockRule('mocked-rule-id')}
/>
);
@ -74,7 +84,7 @@ describe('StepAboutRuleToggleDetails', () => {
describe('note value is empty string', () => {
test('it does not render toggle buttons', () => {
const mockAboutStepWithoutNote = {
...mockRule,
...stepDataMock,
note: '',
};
const wrapper = shallow(
@ -82,10 +92,11 @@ describe('StepAboutRuleToggleDetails', () => {
loading={false}
stepDataDetails={{
note: '',
description: mockRule.description,
description: stepDataMock.description,
setup: '',
}}
stepData={mockAboutStepWithoutNote}
rule={mockRule('mocked-rule-id')}
/>
);
@ -103,11 +114,12 @@ describe('StepAboutRuleToggleDetails', () => {
<StepAboutRuleToggleDetails
loading={false}
stepDataDetails={{
note: mockRule.note,
description: mockRule.description,
note: stepDataMock.note,
description: stepDataMock.description,
setup: '',
}}
stepData={mockRule}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
/>
</ThemeProvider>
);
@ -123,11 +135,12 @@ describe('StepAboutRuleToggleDetails', () => {
<StepAboutRuleToggleDetails
loading={false}
stepDataDetails={{
note: mockRule.note,
description: mockRule.description,
note: stepDataMock.note,
description: stepDataMock.description,
setup: '',
}}
stepData={mockRule}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
/>
</ThemeProvider>
);
@ -151,11 +164,12 @@ describe('StepAboutRuleToggleDetails', () => {
<StepAboutRuleToggleDetails
loading={false}
stepDataDetails={{
note: mockRule.note,
description: mockRule.description,
note: stepDataMock.note,
description: stepDataMock.description,
setup: '',
}}
stepData={mockRule}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
/>
</ThemeProvider>
);
@ -180,11 +194,12 @@ describe('StepAboutRuleToggleDetails', () => {
<StepAboutRuleToggleDetails
loading={false}
stepDataDetails={{
note: mockRule.note,
description: mockRule.description,
note: stepDataMock.note,
description: stepDataMock.description,
setup: '',
}}
stepData={mockRule}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
/>
</ThemeProvider>
);
@ -203,11 +218,12 @@ describe('StepAboutRuleToggleDetails', () => {
<StepAboutRuleToggleDetails
loading={false}
stepDataDetails={{
note: mockRule.note,
description: mockRule.description,
setup: mockRule.note, // TODO: Update to mockRule.setup once supported in UI (and mock can be updated)
note: stepDataMock.note,
description: stepDataMock.description,
setup: stepDataMock.note, // TODO: Update to mockRule.setup once supported in UI (and mock can be updated)
}}
stepData={mockRule}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
/>
</ThemeProvider>
);
@ -224,11 +240,12 @@ describe('StepAboutRuleToggleDetails', () => {
<StepAboutRuleToggleDetails
loading={false}
stepDataDetails={{
note: mockRule.note,
description: mockRule.description,
setup: mockRule.note, // TODO: Update to mockRule.setup once supported in UI (and mock can be updated)
note: stepDataMock.note,
description: stepDataMock.description,
setup: stepDataMock.note, // TODO: Update to mockRule.setup once supported in UI (and mock can be updated)
}}
stepData={mockRule}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
/>
</ThemeProvider>
);
@ -254,11 +271,12 @@ describe('StepAboutRuleToggleDetails', () => {
<StepAboutRuleToggleDetails
loading={false}
stepDataDetails={{
note: mockRule.note,
description: mockRule.description,
setup: mockRule.note, // TODO: Update to mockRule.setup once supported in UI (and mock can be updated)
note: stepDataMock.note,
description: stepDataMock.description,
setup: stepDataMock.note, // TODO: Update to mockRule.setup once supported in UI (and mock can be updated)
}}
stepData={mockRule}
stepData={stepDataMock}
rule={mockRule('mocked-rule-id')}
/>
</ThemeProvider>
);

View file

@ -21,14 +21,16 @@ import type { PropsWithChildren } from 'react';
import React, { memo, useCallback, useMemo, useState } from 'react';
import { css } from '@emotion/css';
import type { Rule } from '../../../../detection_engine/rule_management/logic/types';
import { RuleAboutSection } from '../../../../detection_engine/rule_management/components/rule_details/rule_about_section';
import { HeaderSection } from '../../../../common/components/header_section';
import { MarkdownRenderer } from '../../../../common/components/markdown_editor';
import type {
AboutStepRule,
AboutStepRuleDetails,
} from '../../../pages/detection_engine/rules/types';
import { castRuleAsRuleResponse } from '../../../../detection_engine/rule_details_ui/pages/rule_details/cast_rule_as_rule_response';
import * as i18n from './translations';
import { StepAboutRuleReadOnly } from '../step_about_rule';
import { fullHeight } from './styles';
const detailsOption: EuiButtonGroupOptionProps = {
@ -51,12 +53,14 @@ interface StepPanelProps {
stepData: AboutStepRule | null;
stepDataDetails: AboutStepRuleDetails | null;
loading: boolean;
rule: Rule;
}
const StepAboutRuleToggleDetailsComponent: React.FC<StepPanelProps> = ({
stepData,
stepDataDetails,
loading,
rule,
}) => {
const [selectedToggleOption, setToggleOption] = useState('details');
const [aboutPanelHeight, setAboutPanelHeight] = useState(0);
@ -124,10 +128,10 @@ const StepAboutRuleToggleDetailsComponent: React.FC<StepPanelProps> = ({
</VerticalOverflowContent>
</VerticalOverflowContainer>
<EuiSpacer size="m" />
<StepAboutRuleReadOnly
addPadding={false}
descriptionColumns="singleSplit"
defaultValues={stepData}
<RuleAboutSection
rule={castRuleAsRuleResponse(rule)}
hideName
hideDescription
/>
</div>
)}

View file

@ -30606,7 +30606,7 @@
"xpack.securitySolution.detectionEngine.createRule.mlRuleTypeDescription": "Machine Learning",
"xpack.securitySolution.detectionEngine.createRule.newTermsRuleTypeDescription": "Nouveaux termes",
"xpack.securitySolution.detectionEngine.createRule.pageTitle": "Créer une nouvelle règle",
"xpack.securitySolution.detectionEngine.createRule.QueryLabel": "Requête personnalisée",
"xpack.securitySolution.detectionEngine.createRule.queryLabel": "Requête personnalisée",
"xpack.securitySolution.detectionEngine.createRule.queryRuleTypeDescription": "Requête",
"xpack.securitySolution.detectionEngine.createRule.ruleActionsField.ruleActionsFormErrorsTitle": "Veuillez corriger les problèmes répertoriés ci-dessous",
"xpack.securitySolution.detectionEngine.createRule.rulePreviewDescription": "L'aperçu des règles reflète la configuration actuelle de vos paramètres et exceptions de règles. Cliquez sur l'icône d'actualisation pour afficher l'aperçu mis à jour.",

View file

@ -30605,7 +30605,7 @@
"xpack.securitySolution.detectionEngine.createRule.mlRuleTypeDescription": "機械学習",
"xpack.securitySolution.detectionEngine.createRule.newTermsRuleTypeDescription": "新しい用語",
"xpack.securitySolution.detectionEngine.createRule.pageTitle": "新規ルールを作成",
"xpack.securitySolution.detectionEngine.createRule.QueryLabel": "カスタムクエリ",
"xpack.securitySolution.detectionEngine.createRule.queryLabel": "カスタムクエリ",
"xpack.securitySolution.detectionEngine.createRule.queryRuleTypeDescription": "クエリ",
"xpack.securitySolution.detectionEngine.createRule.ruleActionsField.ruleActionsFormErrorsTitle": "次の一覧の問題を解決してください",
"xpack.securitySolution.detectionEngine.createRule.rulePreviewDescription": "ルールプレビューには、ルール設定と例外の現在の構成が反映されます。更新アイコンをクリックすると、更新されたプレビューが表示されます。",

View file

@ -30601,7 +30601,7 @@
"xpack.securitySolution.detectionEngine.createRule.mlRuleTypeDescription": "Machine Learning",
"xpack.securitySolution.detectionEngine.createRule.newTermsRuleTypeDescription": "新字词",
"xpack.securitySolution.detectionEngine.createRule.pageTitle": "创建新规则",
"xpack.securitySolution.detectionEngine.createRule.QueryLabel": "定制查询",
"xpack.securitySolution.detectionEngine.createRule.queryLabel": "定制查询",
"xpack.securitySolution.detectionEngine.createRule.queryRuleTypeDescription": "查询",
"xpack.securitySolution.detectionEngine.createRule.ruleActionsField.ruleActionsFormErrorsTitle": "请修复下面所列的问题",
"xpack.securitySolution.detectionEngine.createRule.rulePreviewDescription": "规则预览反映了您的规则设置和例外的当前配置,单击刷新图标可查看已更新的预览。",

View file

@ -8,7 +8,7 @@
import { getEsqlRule } from '../../../objects/rule';
import {
CUSTOM_QUERY_DETAILS,
ESQL_QUERY_DETAILS,
DEFINITION_DETAILS,
RULE_NAME_HEADER,
RULE_TYPE_DETAILS,
@ -43,7 +43,7 @@ describe('Detection ES|QL rules, details view', { tags: ['@ess'] }, () => {
cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.query);
getDetails(ESQL_QUERY_DETAILS).should('have.text', rule.query);
getDetails(RULE_TYPE_DETAILS).contains('ES|QL');
// ensures ES|QL rule in technical preview

View file

@ -7,7 +7,7 @@
import { getEsqlRule } from '../../../objects/rule';
import { CUSTOM_QUERY_DETAILS, RULE_NAME_OVERRIDE_DETAILS } from '../../../screens/rule_details';
import { ESQL_QUERY_DETAILS, RULE_NAME_OVERRIDE_DETAILS } from '../../../screens/rule_details';
import { ESQL_QUERY_BAR, ESQL_QUERY_BAR_EXPAND_BTN } from '../../../screens/create_new_rule';
@ -60,7 +60,7 @@ describe('Detection ES|QL rules, edit', { tags: ['@ess'] }, () => {
saveEditedRule();
// ensure updated query is displayed on details page
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', expectedValidEsqlQuery);
getDetails(ESQL_QUERY_DETAILS).should('have.text', expectedValidEsqlQuery);
});
it('edits ES|QL rule query and override rule name with new property', () => {

View file

@ -22,7 +22,7 @@ import {
ABOUT_INVESTIGATION_NOTES,
ABOUT_RULE_DESCRIPTION,
ADDITIONAL_LOOK_BACK_DETAILS,
CUSTOM_QUERY_DETAILS,
EQL_QUERY_DETAILS,
DEFINITION_DETAILS,
FALSE_POSITIVES_DETAILS,
removeExternalLinkText,
@ -115,7 +115,7 @@ describe('EQL rules', { tags: ['@ess', '@serverless', '@brokenInServerless'] },
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', getIndexPatterns().join(''));
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.query);
getDetails(EQL_QUERY_DETAILS).should('have.text', rule.query);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Event Correlation');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
});

View file

@ -23,6 +23,10 @@ export const ANOMALY_SCORE_DETAILS = 'Anomaly score';
export const CUSTOM_QUERY_DETAILS = 'Custom query';
export const EQL_QUERY_DETAILS = 'EQL query';
export const ESQL_QUERY_DETAILS = 'ES|QL query';
export const SAVED_QUERY_NAME_DETAILS = 'Saved query name';
export const SAVED_QUERY_DETAILS = /^Saved query$/;