mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
8706702aea
commit
07312bf087
24 changed files with 1968 additions and 23 deletions
|
@ -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,
|
||||
|
|
|
@ -28,6 +28,7 @@ export interface RuleUpgradeInfoForReview {
|
|||
id: RuleObjectId;
|
||||
rule_id: RuleSignatureId;
|
||||
rule: DiffableRule;
|
||||
target_rule: DiffableRule;
|
||||
diff: PartialRuleDiff;
|
||||
revision: number;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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>
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue