[8.10] [Security Solution] Prebuilt rules installation / upgrade flyout improvements (#164179) (#164897)

# Backport

This will backport the following commits from `main` to `8.10`:
- [[Security Solution] Prebuilt rules installation / upgrade flyout
improvements (#164179)](https://github.com/elastic/kibana/pull/164179)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Nikita
Indik","email":"nikita.indik@elastic.co"},"sourceCommit":{"committedDate":"2023-08-25T19:47:13Z","message":"[Security
Solution] Prebuilt rules installation / upgrade flyout improvements
(#164179)\n\n**Addresses:
https://github.com/elastic/kibana/issues/162334**\r\n**Base PR:
https://github.com/elastic/kibana/pull/163304**\r\n\r\n<img
width=\"1177\" alt=\"Screenshot 2023-08-24 at 04 09
07\"\r\nsrc=\"73ac6726-69d4-4c46-bb16-da704a02aba5\">\r\n\r\n##
Summary\r\n\r\nThis is a follow-up refactoring and bugfix PR to improve
the prebuilt\r\nrules flyout. Base PR: #163304\r\n\r\n#### Changes\r\n-
[x] Tweak UI so that it matches the design more
closely.\r\n[Design](https://www.figma.com/file/gLHm8LpTtSkAUQHrkG3RHU/%5B8.7%5D-%5BRules%5D-Rule-Immutability%2FCustomization?type=design&node-id=3563-612771&mode=design&t=yqZ6LI0vAjbir9xc-0)\r\n(external).\r\n-
[x] Rewrite preview installation and upgrade API endpoints to
respond\r\nwith `RuleResponse` instead of `DiffableRule`\r\n- [x] Revert
some changes introduced by
this\r\n[PR](https://github.com/elastic/kibana/pull/163304)\r\n- [x]
Revert exports
in\r\n`x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.ts`\r\n-
[x]
Delete\r\n`x-pack/plugins/security_solution/common/detection_engine/diffable_rule_to_rule_response.ts`\r\n-
[x] Make the data contexts unaware of any UI elements that
are\r\nconsuming them\r\n- [x] Move rendering of specialized flyout
components into to the\r\ncontext provider so that the table is unaware
of the flyout.\r\n- [x] Make \"flyoutRule\" and \"closeFlyout\" internal
to the context.\r\nComponents outside don't need to know anything about
how a rule is\r\ndisplayed. We can encapsulate this knowledge inside the
context and\r\nexpose only a generic method, like
openRulePreview(ruleId)\r\n - [x] Remove unnecessary checks after using
\"invariant\"\r\n- [x] Make sure query, timeline template and all the
other fields are\r\nshown in the flyout. Compare each rule in a flyout
with the Rule Details\r\nto ensure that all fields are in place.\r\n-
[x] Remove the enable / disable switch machine learning job UI
switch\r\nelement\r\n- [x] Add custom highlighted fields to the
flyout\r\n([comment](https://github.com/elastic/kibana/pull/163235#discussion_r1293821203))\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials.
[Docs\r\nticket](https://github.com/elastic/security-docs/issues/3798)\r\n-
[x] Any UI touched in this PR does not create any new axe
failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[x] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[x] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)","sha":"c115f5d3d6f580b195e823c9e948f7b1daf8fddc","branchLabelMapping":{"^v8.11.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Detections
and Resp","Team: SecuritySolution","Team:Detection Rule
Management","Feature:Prebuilt Detection
Rules","v8.10.0","v8.11.0"],"number":164179,"url":"https://github.com/elastic/kibana/pull/164179","mergeCommit":{"message":"[Security
Solution] Prebuilt rules installation / upgrade flyout improvements
(#164179)\n\n**Addresses:
https://github.com/elastic/kibana/issues/162334**\r\n**Base PR:
https://github.com/elastic/kibana/pull/163304**\r\n\r\n<img
width=\"1177\" alt=\"Screenshot 2023-08-24 at 04 09
07\"\r\nsrc=\"73ac6726-69d4-4c46-bb16-da704a02aba5\">\r\n\r\n##
Summary\r\n\r\nThis is a follow-up refactoring and bugfix PR to improve
the prebuilt\r\nrules flyout. Base PR: #163304\r\n\r\n#### Changes\r\n-
[x] Tweak UI so that it matches the design more
closely.\r\n[Design](https://www.figma.com/file/gLHm8LpTtSkAUQHrkG3RHU/%5B8.7%5D-%5BRules%5D-Rule-Immutability%2FCustomization?type=design&node-id=3563-612771&mode=design&t=yqZ6LI0vAjbir9xc-0)\r\n(external).\r\n-
[x] Rewrite preview installation and upgrade API endpoints to
respond\r\nwith `RuleResponse` instead of `DiffableRule`\r\n- [x] Revert
some changes introduced by
this\r\n[PR](https://github.com/elastic/kibana/pull/163304)\r\n- [x]
Revert exports
in\r\n`x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.ts`\r\n-
[x]
Delete\r\n`x-pack/plugins/security_solution/common/detection_engine/diffable_rule_to_rule_response.ts`\r\n-
[x] Make the data contexts unaware of any UI elements that
are\r\nconsuming them\r\n- [x] Move rendering of specialized flyout
components into to the\r\ncontext provider so that the table is unaware
of the flyout.\r\n- [x] Make \"flyoutRule\" and \"closeFlyout\" internal
to the context.\r\nComponents outside don't need to know anything about
how a rule is\r\ndisplayed. We can encapsulate this knowledge inside the
context and\r\nexpose only a generic method, like
openRulePreview(ruleId)\r\n - [x] Remove unnecessary checks after using
\"invariant\"\r\n- [x] Make sure query, timeline template and all the
other fields are\r\nshown in the flyout. Compare each rule in a flyout
with the Rule Details\r\nto ensure that all fields are in place.\r\n-
[x] Remove the enable / disable switch machine learning job UI
switch\r\nelement\r\n- [x] Add custom highlighted fields to the
flyout\r\n([comment](https://github.com/elastic/kibana/pull/163235#discussion_r1293821203))\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials.
[Docs\r\nticket](https://github.com/elastic/security-docs/issues/3798)\r\n-
[x] Any UI touched in this PR does not create any new axe
failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[x] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[x] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)","sha":"c115f5d3d6f580b195e823c9e948f7b1daf8fddc"}},"sourceBranch":"main","suggestedTargetBranches":["8.10"],"targetPullRequestStates":[{"branch":"8.10","label":"v8.10.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.11.0","labelRegex":"^v8.11.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/164179","number":164179,"mergeCommit":{"message":"[Security
Solution] Prebuilt rules installation / upgrade flyout improvements
(#164179)\n\n**Addresses:
https://github.com/elastic/kibana/issues/162334**\r\n**Base PR:
https://github.com/elastic/kibana/pull/163304**\r\n\r\n<img
width=\"1177\" alt=\"Screenshot 2023-08-24 at 04 09
07\"\r\nsrc=\"73ac6726-69d4-4c46-bb16-da704a02aba5\">\r\n\r\n##
Summary\r\n\r\nThis is a follow-up refactoring and bugfix PR to improve
the prebuilt\r\nrules flyout. Base PR: #163304\r\n\r\n#### Changes\r\n-
[x] Tweak UI so that it matches the design more
closely.\r\n[Design](https://www.figma.com/file/gLHm8LpTtSkAUQHrkG3RHU/%5B8.7%5D-%5BRules%5D-Rule-Immutability%2FCustomization?type=design&node-id=3563-612771&mode=design&t=yqZ6LI0vAjbir9xc-0)\r\n(external).\r\n-
[x] Rewrite preview installation and upgrade API endpoints to
respond\r\nwith `RuleResponse` instead of `DiffableRule`\r\n- [x] Revert
some changes introduced by
this\r\n[PR](https://github.com/elastic/kibana/pull/163304)\r\n- [x]
Revert exports
in\r\n`x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.ts`\r\n-
[x]
Delete\r\n`x-pack/plugins/security_solution/common/detection_engine/diffable_rule_to_rule_response.ts`\r\n-
[x] Make the data contexts unaware of any UI elements that
are\r\nconsuming them\r\n- [x] Move rendering of specialized flyout
components into to the\r\ncontext provider so that the table is unaware
of the flyout.\r\n- [x] Make \"flyoutRule\" and \"closeFlyout\" internal
to the context.\r\nComponents outside don't need to know anything about
how a rule is\r\ndisplayed. We can encapsulate this knowledge inside the
context and\r\nexpose only a generic method, like
openRulePreview(ruleId)\r\n - [x] Remove unnecessary checks after using
\"invariant\"\r\n- [x] Make sure query, timeline template and all the
other fields are\r\nshown in the flyout. Compare each rule in a flyout
with the Rule Details\r\nto ensure that all fields are in place.\r\n-
[x] Remove the enable / disable switch machine learning job UI
switch\r\nelement\r\n- [x] Add custom highlighted fields to the
flyout\r\n([comment](https://github.com/elastic/kibana/pull/163235#discussion_r1293821203))\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials.
[Docs\r\nticket](https://github.com/elastic/security-docs/issues/3798)\r\n-
[x] Any UI touched in this PR does not create any new axe
failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[x] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[x] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)","sha":"c115f5d3d6f580b195e823c9e948f7b1daf8fddc"}}]}]
BACKPORT-->

Co-authored-by: Nikita Indik <nikita.indik@elastic.co>
Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com>
This commit is contained in:
Georgii Gorbachev 2023-08-26 12:35:03 +02:00 committed by GitHub
parent 14535262b7
commit 41b32c7a18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 536 additions and 685 deletions

View file

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

View file

@ -5,15 +5,15 @@
* 2.0.
*/
import type { RuleSignatureId, RuleTagArray, RuleVersion } from '../../model';
import type { DiffableRule } from '../model';
import type { RuleTagArray } from '../../model';
import type { RuleResponse } from '../../model/rule_schema/rule_schemas';
export interface ReviewRuleInstallationResponseBody {
/** Aggregated info about all rules available for installation */
stats: RuleInstallationStatsForReview;
/** Info about individual rules: one object per each rule available for installation */
rules: RuleInstallationInfoForReview[];
rules: RuleResponse[];
}
export interface RuleInstallationStatsForReview {
@ -23,8 +23,3 @@ export interface RuleInstallationStatsForReview {
/** A union of all tags of all rules available for installation */
tags: RuleTagArray;
}
export type RuleInstallationInfoForReview = DiffableRule & {
rule_id: RuleSignatureId;
version: RuleVersion;
};

View file

@ -6,7 +6,8 @@
*/
import type { RuleObjectId, RuleSignatureId, RuleTagArray } from '../../model';
import type { DiffableRule, PartialRuleDiff } from '../model';
import type { PartialRuleDiff } from '../model';
import type { RuleResponse } from '../../model/rule_schema/rule_schemas';
export interface ReviewRuleUpgradeResponseBody {
/** Aggregated info about all rules available for upgrade */
@ -27,8 +28,8 @@ export interface RuleUpgradeStatsForReview {
export interface RuleUpgradeInfoForReview {
id: RuleObjectId;
rule_id: RuleSignatureId;
rule: DiffableRule;
target_rule: DiffableRule;
current_rule: RuleResponse;
target_rule: RuleResponse;
diff: PartialRuleDiff;
revision: number;
}

View file

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

View file

@ -7,7 +7,7 @@
import React, { memo, useMemo } from 'react';
import { cloneDeep } from 'lodash/fp';
import type { EuiLinkAnchorProps } from '@elastic/eui';
import type { EuiLinkAnchorProps, EuiTextProps } from '@elastic/eui';
import { EuiMarkdownFormat, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui';
import unified from 'unified';
import { FormattedMessage } from '@kbn/i18n-react';
@ -21,9 +21,10 @@ import * as i18n from './translations';
interface Props {
children: string;
disableLinks?: boolean;
textSize?: EuiTextProps['size'];
}
const MarkdownRendererComponent: React.FC<Props> = ({ children, disableLinks }) => {
const MarkdownRendererComponent: React.FC<Props> = ({ children, disableLinks, textSize = 'm' }) => {
const MarkdownLinkProcessingComponent: React.FC<EuiLinkAnchorProps> = useMemo(
// eslint-disable-next-line react/display-name
() => (props) => <MarkdownLink {...props} disableLinks={disableLinks} />,
@ -97,6 +98,7 @@ const MarkdownRendererComponent: React.FC<Props> = ({ children, disableLinks })
<EuiMarkdownFormat
parsingPluginList={parsingPlugins}
processingPluginList={processingPluginList}
textSize={textSize}
>
{children}
</EuiMarkdownFormat>

View file

@ -164,6 +164,14 @@ const FalsePositives = ({ falsePositives }: { falsePositives: string[] }) => (
</EuiText>
);
interface InvestigationFieldsProps {
investigationFields: string[];
}
const InvestigationFields = ({ investigationFields }: InvestigationFieldsProps) => (
<BadgeList badges={investigationFields} />
);
interface LicenseProps {
license: string;
}
@ -208,13 +216,10 @@ interface TagsProps {
const Tags = ({ tags }: TagsProps) => <BadgeList badges={tags} />;
// eslint-disable-next-line complexity
const prepareAboutSectionListItems = (
rule: Partial<RuleResponse>
): EuiDescriptionListProps['listItems'] => {
const prepareAboutSectionListItems = (rule: RuleResponse): EuiDescriptionListProps['listItems'] => {
const aboutSectionListItems: EuiDescriptionListProps['listItems'] = [];
if (rule.author) {
if (rule.author.length > 0) {
aboutSectionListItems.push({
title: i18n.AUTHOR_FIELD_LABEL,
description: <Author author={rule.author} />,
@ -228,14 +233,12 @@ const prepareAboutSectionListItems = (
});
}
if (rule.severity) {
aboutSectionListItems.push({
title: i18n.SEVERITY_FIELD_LABEL,
description: <SeverityBadge value={rule.severity} />,
});
}
aboutSectionListItems.push({
title: i18n.SEVERITY_FIELD_LABEL,
description: <SeverityBadge value={rule.severity} />,
});
if (rule.severity_mapping && rule.severity_mapping.length > 0) {
if (rule.severity_mapping.length > 0) {
aboutSectionListItems.push(
...rule.severity_mapping
.filter((severityMappingItem) => severityMappingItem.field !== '')
@ -248,14 +251,12 @@ const prepareAboutSectionListItems = (
);
}
if (rule.risk_score) {
aboutSectionListItems.push({
title: i18n.RISK_SCORE_FIELD_LABEL,
description: <RiskScore riskScore={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) {
if (rule.risk_score_mapping.length > 0) {
aboutSectionListItems.push(
...rule.risk_score_mapping
.filter((riskScoreMappingItem) => riskScoreMappingItem.field !== '')
@ -268,20 +269,27 @@ const prepareAboutSectionListItems = (
);
}
if (rule.references && rule.references.length > 0) {
if (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) {
if (rule.false_positives.length > 0) {
aboutSectionListItems.push({
title: i18n.FALSE_POSITIVES_FIELD_LABEL,
description: <FalsePositives falsePositives={rule.false_positives} />,
});
}
if (rule.investigation_fields && rule.investigation_fields.length > 0) {
aboutSectionListItems.push({
title: i18n.INVESTIGATION_FIELDS_FIELD_LABEL,
description: <InvestigationFields investigationFields={rule.investigation_fields} />,
});
}
if (rule.license) {
aboutSectionListItems.push({
title: i18n.LICENSE_FIELD_LABEL,
@ -296,7 +304,7 @@ const prepareAboutSectionListItems = (
});
}
if (rule.threat && rule.threat.length > 0) {
if (rule.threat.length > 0) {
aboutSectionListItems.push({
title: i18n.THREAT_FIELD_LABEL,
description: <Threat threat={rule.threat} />,
@ -317,7 +325,7 @@ const prepareAboutSectionListItems = (
});
}
if (rule.tags && rule.tags.length > 0) {
if (rule.tags.length > 0) {
aboutSectionListItems.push({
title: i18n.TAGS_FIELD_LABEL,
description: <Tags tags={rule.tags} />,
@ -328,7 +336,7 @@ const prepareAboutSectionListItems = (
};
export interface RuleAboutSectionProps {
rule: Partial<RuleResponse>;
rule: RuleResponse;
}
export const RuleAboutSection = ({ rule }: RuleAboutSectionProps) => {

View file

@ -8,25 +8,99 @@
import React from 'react';
import { isEmpty } from 'lodash/fp';
import styled from 'styled-components';
import { EuiDescriptionList, EuiText, EuiFlexGrid, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import {
EuiDescriptionList,
EuiText,
EuiFlexGrid,
EuiFlexItem,
EuiFlexGroup,
EuiLoadingSpinner,
EuiBadge,
} from '@elastic/eui';
import type { EuiDescriptionListProps } from '@elastic/eui';
import type {
Type,
ThreatMapping as ThreatMappingType,
} from '@kbn/securitysolution-io-ts-alerting-types';
import type { Filter } from '@kbn/es-query';
import type { SavedQuery } from '@kbn/data-plugin/public';
import { mapAndFlattenFilters } from '@kbn/data-plugin/public';
import { FieldIcon } from '@kbn/react-field';
import { castEsToKbnFieldTypeName } from '@kbn/field-types';
import { FilterBadgeGroup } from '@kbn/unified-search-plugin/public';
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 { useGetSavedQuery } from '../../../../detections/pages/detection_engine/rules/use_get_saved_query';
import * as threatMatchI18n from '../../../../common/components/threat_match/translations';
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 { convertHistoryStartToSize } from '../../../../detections/pages/detection_engine/rules/helpers';
import { MlJobLink } from '../../../../detections/components/rules/ml_job_link/ml_job_link';
import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs';
import { BadgeList } from './badge_list';
import * as i18n from './translations';
interface SavedQueryNameProps {
savedQueryName: string;
}
const SavedQueryName = ({ savedQueryName }: SavedQueryNameProps) => (
<EuiText size="s">{savedQueryName}</EuiText>
);
const EuiBadgeWrap = styled(EuiBadge)`
.euiBadge__text {
white-space: pre-wrap !important;
}
`;
interface FiltersProps {
filters: Filter[];
dataViewId?: string;
index?: string[];
}
const Filters = ({ filters, dataViewId, index }: FiltersProps) => {
const { indexPattern } = useRuleIndexPattern({
dataSourceType: dataViewId ? DataSourceType.DataView : DataSourceType.IndexPatterns,
index: index ?? [],
dataViewId,
});
const flattenedFilters = mapAndFlattenFilters(filters);
return (
<EuiFlexGroup wrap responsive={false} gutterSize="xs">
{flattenedFilters.map((filter, idx) => (
<EuiFlexItem grow={false} key={`filter-${idx}`} css={{ width: '100%' }}>
<EuiBadgeWrap color="hollow">
{indexPattern != null ? (
<FilterBadgeGroup filters={[filter]} dataViews={[indexPattern]} />
) : (
<EuiLoadingSpinner size="m" />
)}
</EuiBadgeWrap>
</EuiFlexItem>
))}
</EuiFlexGroup>
);
};
const QueryContent = styled.div`
white-space: pre-wrap;
`;
interface QueryProps {
query: string;
}
const Query = ({ query }: QueryProps) => <QueryContent>{query}</QueryContent>;
interface IndexProps {
index: string[];
}
@ -53,6 +127,36 @@ const Threshold = ({ threshold }: ThresholdProps) => (
</>
);
interface AnomalyThresholdProps {
anomalyThreshold: number;
}
const AnomalyThreshold = ({ anomalyThreshold }: AnomalyThresholdProps) => (
<EuiText size="s">{anomalyThreshold}</EuiText>
);
interface MachineLearningJobListProps {
jobIds: string[];
}
const MachineLearningJobList = ({ jobIds }: MachineLearningJobListProps) => {
const { jobs } = useSecurityJobs();
const relevantJobs = jobs.filter((job) => jobIds.includes(job.id));
return (
<>
{relevantJobs.map((job) => (
<MlJobLink
key={job.id}
jobId={job.id}
jobName={job.customSettings?.security_app_display_name}
/>
))}
</>
);
};
const getRuleTypeDescription = (ruleType: Type) => {
switch (ruleType) {
case 'machine_learning':
@ -94,7 +198,7 @@ interface RequiredFieldsProps {
const RequiredFields = ({ requiredFields }: RequiredFieldsProps) => (
<EuiFlexGrid gutterSize={'s'}>
{requiredFields.map((rF, index) => (
<EuiFlexItem grow={false}>
<EuiFlexItem grow={false} key={rF.name}>
<EuiFlexGroup alignItems="center" gutterSize={'xs'}>
<EuiFlexItem grow={false}>
<FieldIcon
@ -162,8 +266,28 @@ const ThreatMapping = ({ threatMapping }: ThreatMappingProps) => {
return <EuiText size="s">{description}</EuiText>;
};
interface NewTermsFieldsProps {
newTermsFields: string[];
}
const NewTermsFields = ({ newTermsFields }: NewTermsFieldsProps) => (
<BadgeList badges={newTermsFields} />
);
interface HistoryWindowSizeProps {
historyWindowStart?: string;
}
const HistoryWindowSize = ({ historyWindowStart }: HistoryWindowSizeProps) => {
const size = historyWindowStart ? convertHistoryStartToSize(historyWindowStart) : '7d';
return <EuiText size="s">{size}</EuiText>;
};
// eslint-disable-next-line complexity
const prepareDefinitionSectionListItems = (
rule: Partial<RuleResponse>
rule: RuleResponse,
savedQuery?: SavedQuery
): EuiDescriptionListProps['listItems'] => {
const definitionSectionListItems: EuiDescriptionListProps['listItems'] = [];
@ -181,21 +305,62 @@ const prepareDefinitionSectionListItems = (
});
}
if (rule.type) {
if (savedQuery) {
definitionSectionListItems.push({
title: i18n.RULE_TYPE_FIELD_LABEL,
description: <RuleType type={rule.type} />,
title: descriptionStepI18n.SAVED_QUERY_NAME_LABEL,
description: <SavedQueryName savedQueryName={savedQuery.attributes.title} />,
});
if (savedQuery.attributes.filters) {
definitionSectionListItems.push({
title: descriptionStepI18n.SAVED_QUERY_FILTERS_LABEL,
description: <Filters filters={savedQuery.attributes.filters as Filter[]} />,
});
}
}
if ('filters' in rule && rule.filters && rule.filters.length > 0) {
definitionSectionListItems.push({
title: savedQuery
? descriptionStepI18n.SAVED_QUERY_FILTERS_LABEL
: descriptionStepI18n.FILTERS_LABEL,
description: (
<Filters
filters={rule.filters as Filter[]}
dataViewId={rule.data_view_id}
index={rule.index}
/>
),
});
}
if ('query' in rule && rule.query) {
definitionSectionListItems.push({
title: savedQuery ? descriptionStepI18n.SAVED_QUERY_LABEL : descriptionStepI18n.QUERY_LABEL,
description: <Query query={rule.query} />,
});
}
definitionSectionListItems.push({
title: i18n.RULE_TYPE_FIELD_LABEL,
description: <RuleType type={rule.type} />,
});
if ('anomaly_threshold' in rule && rule.anomaly_threshold) {
definitionSectionListItems.push({
title: i18n.ANOMALY_THRESHOLD_FIELD_LABEL,
description: <AnomalyThreshold anomalyThreshold={rule.anomaly_threshold} />,
});
}
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[]} />,
description: <MachineLearningJobList jobIds={rule.machine_learning_job_id as string[]} />,
});
}
if (rule.related_integrations && rule.related_integrations.length > 0) {
if (rule.related_integrations.length > 0) {
definitionSectionListItems.push({
title: i18n.RELATED_INTEGRATIONS_FIELD_LABEL,
description: (
@ -204,19 +369,19 @@ const prepareDefinitionSectionListItems = (
});
}
if (rule.required_fields && rule.required_fields.length > 0) {
if (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} />,
});
}
definitionSectionListItems.push({
title: i18n.TIMELINE_TITLE_FIELD_LABEL,
description: (
<TimelineTitle timelineTitle={rule.timeline_title || timelinesI18n.DEFAULT_TIMELINE_TITLE} />
),
});
if ('threshold' in rule && rule.threshold) {
definitionSectionListItems.push({
@ -239,15 +404,58 @@ 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,
description: (
<Filters
filters={rule.threat_filters as Filter[]}
dataViewId={rule.data_view_id}
index={rule.index}
/>
),
});
}
if ('threat_query' in rule && rule.threat_query) {
definitionSectionListItems.push({
title: savedQuery
? descriptionStepI18n.SAVED_QUERY_LABEL
: descriptionStepI18n.THREAT_QUERY_LABEL,
description: <Query query={rule.threat_query} />,
});
}
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,
description: <NewTermsFields newTermsFields={rule.new_terms_fields} />,
});
}
if (rule.type === 'new_terms' || 'history_window_start' in rule) {
definitionSectionListItems.push({
title: i18n.HISTORY_WINDOW_SIZE_FIELD_LABEL,
description: <HistoryWindowSize historyWindowStart={rule.history_window_start} />,
});
}
return definitionSectionListItems;
};
export interface RuleDefinitionSectionProps {
rule: Partial<RuleResponse>;
rule: RuleResponse;
}
export const RuleDefinitionSection = ({ rule }: RuleDefinitionSectionProps) => {
const definitionSectionListItems = prepareDefinitionSectionListItems(rule);
const { savedQuery } = useGetSavedQuery({
savedQueryId: rule.type === 'saved_query' ? rule.saved_id : '',
ruleType: rule.type,
});
const definitionSectionListItems = prepareDefinitionSectionListItems(rule, savedQuery);
return (
<div>

View file

@ -7,6 +7,8 @@
import React, { useMemo, useState, useEffect } from 'react';
import styled from 'styled-components';
import { css } from '@emotion/css';
import { euiThemeVars } from '@kbn/ui-theme';
import {
EuiButton,
EuiButtonEmpty,
@ -20,7 +22,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import type { EuiTabbedContentTab } from '@elastic/eui';
import type { EuiTabbedContentTab, EuiTabbedContentProps } from '@elastic/eui';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import { RuleOverviewTab, useOverviewTabSections } from './rule_overview_tab';
@ -37,7 +39,7 @@ const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
.euiFlyoutBody__overflowContent {
flex: 1;
overflow: hidden;
padding: ${({ theme }) => `0 ${theme.eui.euiSizeL} ${theme.eui.euiSizeM}`};
padding: ${({ theme }) => `0 ${theme.eui.euiSizeL} 0`};
}
}
`;
@ -79,8 +81,27 @@ const StyledEuiTabbedContent = styled(EuiTabbedContent)`
}
`;
/*
* Fixes tabs to the top and allows the content to scroll.
*/
const ScrollableFlyoutTabbedContent = (props: EuiTabbedContentProps) => (
<StyledFlexGroup direction="column" gutterSize="none">
<StyledEuiFlexItem grow={true}>
<StyledEuiTabbedContent {...props} />
</StyledEuiFlexItem>
</StyledFlexGroup>
);
const tabPaddingClassName = css`
padding: 0 ${euiThemeVars.euiSizeM} ${euiThemeVars.euiSizeXL} ${euiThemeVars.euiSizeM};
`;
const TabContentPadding: React.FC = ({ children }) => (
<div className={tabPaddingClassName}>{children}</div>
);
interface RuleDetailsFlyoutProps {
rule: Partial<RuleResponse>;
rule: RuleResponse;
actionButtonLabel: string;
isActionButtonDisabled: boolean;
onActionButtonClick: (ruleId: string) => void;
@ -101,11 +122,13 @@ export const RuleDetailsFlyout = ({
id: 'overview',
name: i18n.OVERVIEW_TAB_LABEL,
content: (
<RuleOverviewTab
rule={rule}
expandedOverviewSections={expandedOverviewSections}
toggleOverviewSection={toggleOverviewSection}
/>
<TabContentPadding>
<RuleOverviewTab
rule={rule}
expandedOverviewSections={expandedOverviewSections}
toggleOverviewSection={toggleOverviewSection}
/>
</TabContentPadding>
),
}),
[rule, expandedOverviewSections, toggleOverviewSection]
@ -115,7 +138,11 @@ export const RuleDetailsFlyout = ({
() => ({
id: 'investigationGuide',
name: i18n.INVESTIGATION_GUIDE_TAB_LABEL,
content: <RuleInvestigationGuideTab note={rule.note ?? ''} />,
content: (
<TabContentPadding>
<RuleInvestigationGuideTab note={rule.note ?? ''} />
</TabContentPadding>
),
}),
[rule.note]
);
@ -151,17 +178,17 @@ export const RuleDetailsFlyout = ({
paddingSize="l"
>
<EuiFlyoutHeader>
<EuiTitle size="m" data-test-subj="rulesBulkEditFormTitle">
<EuiTitle size="m">
<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>
<ScrollableFlyoutTabbedContent
tabs={tabs}
selectedTab={selectedTab}
onTabClick={onTabClick}
/>
</StyledEuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">

View file

@ -6,9 +6,7 @@
*/
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';
@ -18,13 +16,9 @@ interface RuleInvestigationGuideTabProps {
export const RuleInvestigationGuideTab = ({ note }: RuleInvestigationGuideTabProps) => {
return (
<div
css={css`
padding: 0 ${euiThemeVars.euiSizeM};
`}
>
<>
<EuiSpacer size="m" />
<MarkdownRenderer>{note}</MarkdownRenderer>
</div>
<MarkdownRenderer textSize="s">{note}</MarkdownRenderer>
</>
);
};

View file

@ -6,8 +6,6 @@
*/
import React, { useState, useMemo, useCallback } from 'react';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import {
EuiTitle,
EuiAccordion,
@ -88,7 +86,7 @@ const ExpandableSection = ({ title, isOpen, toggle, children }: ExpandableSectio
};
interface RuleOverviewTabProps {
rule: Partial<RuleResponse>;
rule: RuleResponse;
expandedOverviewSections: Record<keyof typeof defaultOverviewOpenSections, boolean>;
toggleOverviewSection: Record<keyof typeof defaultOverviewOpenSections, () => void>;
}
@ -98,11 +96,7 @@ export const RuleOverviewTab = ({
expandedOverviewSections,
toggleOverviewSection,
}: RuleOverviewTabProps) => (
<div
css={css`
padding: 0 ${euiThemeVars.euiSizeM};
`}
>
<>
<EuiSpacer size="m" />
<ExpandableSection
title={i18n.ABOUT_SECTION_LABEL}
@ -127,9 +121,9 @@ export const RuleOverviewTab = ({
>
<RuleScheduleSection rule={rule} />
</ExpandableSection>
<EuiHorizontalRule margin="m" />
{rule.setup && (
<>
<EuiHorizontalRule margin="m" />
<ExpandableSection
title={i18n.SETUP_GUIDE_SECTION_LABEL}
isOpen={expandedOverviewSections.setup}
@ -137,8 +131,7 @@ export const RuleOverviewTab = ({
>
<RuleSetupGuideSection setup={rule.setup} />
</ExpandableSection>
<EuiHorizontalRule margin="m" />
</>
)}
</div>
</>
);

View file

@ -27,25 +27,22 @@ const From = ({ from, interval }: FromProps) => (
);
export interface RuleScheduleSectionProps {
rule: Partial<RuleResponse>;
rule: RuleResponse;
}
export const RuleScheduleSection = ({ rule }: RuleScheduleSectionProps) => {
const ruleSectionListItems = [];
if (rule.interval) {
ruleSectionListItems.push({
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>

View file

@ -16,7 +16,7 @@ interface RuleSetupGuideSectionProps {
export const RuleSetupGuideSection = ({ setup }: RuleSetupGuideSectionProps) => {
return (
<div>
<MarkdownRenderer>{setup}</MarkdownRenderer>
<MarkdownRenderer textSize="s">{setup}</MarkdownRenderer>
</div>
);
};

View file

@ -126,6 +126,13 @@ export const FALSE_POSITIVES_FIELD_LABEL = i18n.translate(
}
);
export const INVESTIGATION_FIELDS_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.investigationFieldsFieldLabel',
{
defaultMessage: 'Custom highlighted fields',
}
);
export const LICENSE_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.licenseFieldLabel',
{
@ -203,6 +210,13 @@ export const MACHINE_LEARNING_JOB_ID_FIELD_LABEL = i18n.translate(
}
);
export const ANOMALY_THRESHOLD_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.anomalyThresholdFieldLabel',
{
defaultMessage: 'Anomaly score threshold',
}
);
export const RELATED_INTEGRATIONS_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.relatedIntegrationsFieldLabel',
{
@ -245,6 +259,20 @@ export const THREAT_FILTERS_FIELD_LABEL = i18n.translate(
}
);
export const NEW_TERMS_FIELDS_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.newTermsFieldsFieldLabel',
{
defaultMessage: 'Fields',
}
);
export const HISTORY_WINDOW_SIZE_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.historyWindowSizeFieldLabel',
{
defaultMessage: 'History Window Size',
}
);
export const INTERVAL_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.intervalFieldLabel',
{

View file

@ -7,44 +7,41 @@
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';
import type { RuleObjectId } from '../../../../../common/api/detection_engine';
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema';
export interface RuleDetailsFlyoutState {
flyoutRule: RuleInstallationInfoForReview | null;
previewedRule: RuleResponse | null;
}
export interface RuleDetailsFlyoutActions {
openFlyoutForRuleId: (ruleId: RuleSignatureId) => void;
closeFlyout: () => void;
openRulePreview: (ruleId: RuleObjectId) => void;
closeRulePreview: () => void;
}
export const useRuleDetailsFlyout = (
rules: DiffableRule[]
rules: RuleResponse[]
): RuleDetailsFlyoutState & RuleDetailsFlyoutActions => {
const [flyoutRule, setFlyoutRule] = React.useState<RuleInstallationInfoForReview | null>(null);
const [previewedRule, setRuleForPreview] = React.useState<RuleResponse | null>(null);
const openFlyoutForRuleId = useCallback(
(ruleId: RuleSignatureId) => {
const ruleToShowInFlyout = rules.find((rule) => rule.rule_id === ruleId);
const openRulePreview = useCallback(
(ruleId: RuleObjectId) => {
const ruleToShowInFlyout = rules.find((rule) => {
return rule.id === ruleId;
});
invariant(ruleToShowInFlyout, `Rule with id ${ruleId} not found`);
if (ruleToShowInFlyout) {
setFlyoutRule(ruleToShowInFlyout);
}
setRuleForPreview(ruleToShowInFlyout);
},
[rules, setFlyoutRule]
[rules, setRuleForPreview]
);
const closeFlyout = useCallback(() => {
setFlyoutRule(null);
const closeRulePreview = useCallback(() => {
setRuleForPreview(null);
}, []);
return {
openFlyoutForRuleId,
closeFlyout,
flyoutRule,
openRulePreview,
closeRulePreview,
previewedRule,
};
};

View file

@ -1,37 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useAddPrebuiltRulesTableContext } from './add_prebuilt_rules_table_context';
import { RuleDetailsFlyout } from '../../../../rule_management/components/rule_details/rule_details_flyout';
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import { diffableRuleToRuleResponse } from '../../../../../../common/detection_engine/diffable_rule_to_rule_response';
import * as i18n from './translations';
export const AddPrebuiltRulesFlyout = () => {
const {
state: { flyoutRule, isFlyoutInstallButtonDisabled },
actions: { installOneRule, closeFlyout },
} = useAddPrebuiltRulesTableContext();
if (flyoutRule == null) {
return null;
}
const ruleResponse: Partial<RuleResponse> = diffableRuleToRuleResponse(flyoutRule);
return (
<RuleDetailsFlyout
rule={ruleResponse}
actionButtonLabel={i18n.INSTALL_BUTTON_LABEL}
isActionButtonDisabled={isFlyoutInstallButtonDisabled}
onActionButtonClick={installOneRule}
closeFlyout={closeFlyout}
/>
);
};

View file

@ -22,7 +22,6 @@ 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
@ -97,8 +96,6 @@ export const AddPrebuiltRulesTable = React.memo(() => {
data-test-subj="add-prebuilt-rules-table"
columns={rulesColumns}
/>
<AddPrebuiltRulesFlyout />
</>
)
}

View file

@ -9,10 +9,7 @@ import type { Dispatch, SetStateAction } from 'react';
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { useFetchPrebuiltRulesStatusQuery } from '../../../../rule_management/api/hooks/prebuilt_rules/use_fetch_prebuilt_rules_status_query';
import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages';
import type {
RuleInstallationInfoForReview,
RuleSignatureId,
} from '../../../../../../common/api/detection_engine';
import type { RuleSignatureId } from '../../../../../../common/api/detection_engine';
import { invariant } from '../../../../../../common/utils/invariant';
import {
usePerformInstallAllRules,
@ -22,16 +19,19 @@ import { usePrebuiltRulesInstallReview } from '../../../../rule_management/logic
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';
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import { RuleDetailsFlyout } from '../../../../rule_management/components/rule_details/rule_details_flyout';
import * as i18n from './translations';
export interface AddPrebuiltRulesTableState {
/**
* Rules available to be installed
*/
rules: RuleInstallationInfoForReview[];
rules: RuleResponse[];
/**
* Rules to display in table after applying filters
*/
filteredRules: RuleInstallationInfoForReview[];
filteredRules: RuleResponse[];
/**
* Currently selected table filter
*/
@ -68,17 +68,7 @@ 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;
selectedRules: RuleResponse[];
}
export interface AddPrebuiltRulesTableActions {
@ -87,9 +77,8 @@ export interface AddPrebuiltRulesTableActions {
installAllRules: () => void;
installSelectedRules: () => void;
setFilterOptions: Dispatch<SetStateAction<AddPrebuiltRulesTableFilterOptions>>;
selectRules: (rules: RuleInstallationInfoForReview[]) => void;
openFlyoutForRuleId: (ruleId: RuleSignatureId) => void;
closeFlyout: () => void;
selectRules: (rules: RuleResponse[]) => void;
openRulePreview: (ruleId: RuleSignatureId) => void;
}
export interface AddPrebuiltRulesContextType {
@ -107,7 +96,7 @@ export const AddPrebuiltRulesTableContextProvider = ({
children,
}: AddPrebuiltRulesTableContextProviderProps) => {
const [loadingRules, setLoadingRules] = useState<RuleSignatureId[]>([]);
const [selectedRules, setSelectedRules] = useState<RuleInstallationInfoForReview[]>([]);
const [selectedRules, setSelectedRules] = useState<RuleResponse[]>([]);
const [filterOptions, setFilterOptions] = useState<AddPrebuiltRulesTableFilterOptions>({
filter: '',
@ -144,9 +133,9 @@ export const AddPrebuiltRulesTableContextProvider = ({
const filteredRules = useFilterPrebuiltRulesToInstall({ filterOptions, rules });
const { openFlyoutForRuleId, closeFlyout, flyoutRule } = useRuleDetailsFlyout(filteredRules);
const isFlyoutInstallButtonDisabled = Boolean(
(flyoutRule?.rule_id && loadingRules.includes(flyoutRule.rule_id)) ||
const { openRulePreview, closeRulePreview, previewedRule } = useRuleDetailsFlyout(filteredRules);
const canPreviewedRuleBeInstalled = Boolean(
(previewedRule?.rule_id && loadingRules.includes(previewedRule.rule_id)) ||
isRefetching ||
isUpgradingSecurityPackages
);
@ -199,17 +188,9 @@ export const AddPrebuiltRulesTableContextProvider = ({
installSelectedRules,
reFetchRules: refetch,
selectRules: setSelectedRules,
openFlyoutForRuleId,
closeFlyout,
openRulePreview,
}),
[
installAllRules,
installOneRule,
installSelectedRules,
refetch,
openFlyoutForRuleId,
closeFlyout,
]
[installAllRules, installOneRule, installSelectedRules, refetch, openRulePreview]
);
const providerValue = useMemo<AddPrebuiltRulesContextType>(() => {
@ -226,8 +207,6 @@ export const AddPrebuiltRulesTableContextProvider = ({
isUpgradingSecurityPackages,
selectedRules,
lastUpdated: dataUpdatedAt,
flyoutRule,
isFlyoutInstallButtonDisabled,
},
actions,
};
@ -243,14 +222,23 @@ export const AddPrebuiltRulesTableContextProvider = ({
isUpgradingSecurityPackages,
selectedRules,
dataUpdatedAt,
flyoutRule,
isFlyoutInstallButtonDisabled,
actions,
]);
return (
<AddPrebuiltRulesTableContext.Provider value={providerValue}>
{children}
<>
{children}
{previewedRule && (
<RuleDetailsFlyout
rule={previewedRule}
actionButtonLabel={i18n.INSTALL_BUTTON_LABEL}
isActionButtonDisabled={canPreviewedRuleBeInstalled}
onActionButtonClick={installOneRule}
closeFlyout={closeRulePreview}
/>
)}
</>
</AddPrebuiltRulesTableContext.Provider>
);
};

View file

@ -15,15 +15,17 @@ import { IntegrationsPopover } from '../../../../../detections/components/rules/
import { SeverityBadge } from '../../../../../detections/components/rules/severity_badge';
import * as i18n from '../../../../../detections/pages/detection_engine/rules/translations';
import type { Rule } from '../../../../rule_management/logic';
import type { RuleInstallationInfoForReview } from '../../../../../../common/api/detection_engine/prebuilt_rules';
import { useUserData } from '../../../../../detections/components/user_info';
import { hasUserCRUDPermission } from '../../../../../common/utils/privileges';
import type { AddPrebuiltRulesTableActions } from './add_prebuilt_rules_table_context';
import { useAddPrebuiltRulesTableContext } from './add_prebuilt_rules_table_context';
import type { RuleSignatureId } from '../../../../../../common/api/detection_engine/model/rule_schema';
import type {
RuleSignatureId,
RuleResponse,
} from '../../../../../../common/api/detection_engine/model/rule_schema';
import { getNormalizedSeverity } from '../helpers';
export type TableColumn = EuiBasicTableColumn<RuleInstallationInfoForReview>;
export type TableColumn = EuiBasicTableColumn<RuleResponse>;
interface RuleNameProps {
name: string;
@ -32,13 +34,13 @@ interface RuleNameProps {
const RuleName = ({ name, ruleId }: RuleNameProps) => {
const {
actions: { openFlyoutForRuleId },
actions: { openRulePreview },
} = useAddPrebuiltRulesTableContext();
return (
<EuiLink
onClick={() => {
openFlyoutForRuleId(ruleId);
openRulePreview(ruleId);
}}
>
{name}
@ -49,8 +51,8 @@ const RuleName = ({ name, ruleId }: RuleNameProps) => {
export const RULE_NAME_COLUMN: TableColumn = {
field: 'name',
name: i18n.COLUMN_RULE,
render: (value: RuleInstallationInfoForReview['name'], rule: RuleInstallationInfoForReview) => (
<RuleName name={value} ruleId={rule.rule_id} />
render: (value: RuleResponse['name'], rule: RuleResponse) => (
<RuleName name={value} ruleId={rule.id} />
),
sortable: true,
truncateText: true,
@ -62,7 +64,7 @@ const TAGS_COLUMN: TableColumn = {
field: 'tags',
name: null,
align: 'center',
render: (tags: RuleInstallationInfoForReview['tags']) => {
render: (tags: RuleResponse['tags']) => {
if (tags == null || tags.length === 0) {
return null;
}
@ -91,7 +93,7 @@ const INTEGRATIONS_COLUMN: TableColumn = {
field: 'related_integrations',
name: null,
align: 'center',
render: (integrations: RuleInstallationInfoForReview['related_integrations']) => {
render: (integrations: RuleResponse['related_integrations']) => {
if (integrations == null || integrations.length === 0) {
return null;
}
@ -159,7 +161,7 @@ export const useAddPrebuiltRulesTableColumns = (): TableColumn[] => {
field: 'severity',
name: i18n.COLUMN_SEVERITY,
render: (value: Rule['severity']) => <SeverityBadge value={value} />,
sortable: ({ severity }: RuleInstallationInfoForReview) => getNormalizedSeverity(severity),
sortable: ({ severity }: RuleResponse) => getNormalizedSeverity(severity),
truncateText: true,
width: '12%',
},

View file

@ -6,7 +6,7 @@
*/
import { useMemo } from 'react';
import type { RuleInstallationInfoForReview } from '../../../../../../common/api/detection_engine/prebuilt_rules';
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema';
import type { FilterOptions } from '../../../../rule_management/logic/types';
export type AddPrebuiltRulesTableFilterOptions = Pick<FilterOptions, 'filter' | 'tags'>;
@ -15,7 +15,7 @@ export const useFilterPrebuiltRulesToInstall = ({
rules,
filterOptions,
}: {
rules: RuleInstallationInfoForReview[];
rules: RuleResponse[];
filterOptions: AddPrebuiltRulesTableFilterOptions;
}) => {
const filteredRules = useMemo(() => {

View file

@ -1,37 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context';
import { RuleDetailsFlyout } from '../../../../rule_management/components/rule_details/rule_details_flyout';
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import { diffableRuleToRuleResponse } from '../../../../../../common/detection_engine/diffable_rule_to_rule_response';
import * as i18n from './translations';
export const UpgradePrebuiltRulesFlyout = () => {
const {
state: { flyoutRule, isFlyoutInstallButtonDisabled },
actions: { upgradeOneRule, closeFlyout },
} = useUpgradePrebuiltRulesTableContext();
if (flyoutRule == null) {
return null;
}
const ruleResponse: Partial<RuleResponse> = diffableRuleToRuleResponse(flyoutRule);
return (
<RuleDetailsFlyout
rule={ruleResponse}
actionButtonLabel={i18n.UPDATE_BUTTON_LABEL}
isActionButtonDisabled={isFlyoutInstallButtonDisabled}
onActionButtonClick={upgradeOneRule}
closeFlyout={closeFlyout}
/>
);
};

View file

@ -23,7 +23,6 @@ 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
@ -119,8 +118,6 @@ export const UpgradePrebuiltRulesTable = React.memo(() => {
data-test-subj="rules-upgrades-table"
columns={rulesColumns}
/>
<UpgradePrebuiltRulesFlyout />
</>
)
}

View file

@ -23,6 +23,8 @@ import type { UpgradePrebuiltRulesTableFilterOptions } from './use_filter_prebui
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 { RuleDetailsFlyout } from '../../../../rule_management/components/rule_details/rule_details_flyout';
import * as i18n from './translations';
import { MlJobUpgradeModal } from '../../../../../detections/components/modals/ml_job_upgrade_modal';
@ -73,16 +75,6 @@ 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 {
@ -92,8 +84,7 @@ export interface UpgradePrebuiltRulesTableActions {
upgradeAllRules: () => void;
setFilterOptions: Dispatch<SetStateAction<UpgradePrebuiltRulesTableFilterOptions>>;
selectRules: (rules: RuleUpgradeInfoForReview[]) => void;
openFlyoutForRuleId: (ruleId: RuleSignatureId) => void;
closeFlyout: () => void;
openRulePreview: (ruleId: string) => void;
}
export interface UpgradePrebuiltRulesContextType {
@ -141,11 +132,11 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
const filteredRules = useFilterPrebuiltRulesToUpgrade({ filterOptions, rules });
const { openFlyoutForRuleId, closeFlyout, flyoutRule } = useRuleDetailsFlyout(
const { openRulePreview, closeRulePreview, previewedRule } = useRuleDetailsFlyout(
filteredRules.map((upgradeInfo) => upgradeInfo.target_rule)
);
const isFlyoutInstallButtonDisabled = Boolean(
(flyoutRule?.rule_id && loadingRules.includes(flyoutRule.rule_id)) ||
const canPreviewedRuleBeUpgraded = Boolean(
(previewedRule?.rule_id && loadingRules.includes(previewedRule.rule_id)) ||
isRefetching ||
isUpgradingSecurityPackages
);
@ -176,7 +167,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
await upgradeSpecificRulesRequest([
{
rule_id: ruleId,
version: rule.diff.fields.version?.target_version ?? rule.rule.version,
version: rule.diff.fields.version?.target_version ?? rule.current_rule.version,
revision: rule.revision,
},
]);
@ -190,7 +181,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
const upgradeSelectedRules = useCallback(async () => {
const rulesToUpgrade = selectedRules.map((rule) => ({
rule_id: rule.rule_id,
version: rule.diff.fields.version?.target_version ?? rule.rule.version,
version: rule.diff.fields.version?.target_version ?? rule.current_rule.version,
revision: rule.revision,
}));
setLoadingRules((prev) => [...prev, ...rulesToUpgrade.map((r) => r.rule_id)]);
@ -227,17 +218,9 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
upgradeAllRules,
setFilterOptions,
selectRules: setSelectedRules,
openFlyoutForRuleId,
closeFlyout,
openRulePreview,
}),
[
refetch,
upgradeOneRule,
upgradeSelectedRules,
upgradeAllRules,
openFlyoutForRuleId,
closeFlyout,
]
[refetch, upgradeOneRule, upgradeSelectedRules, upgradeAllRules, openRulePreview]
);
const providerValue = useMemo<UpgradePrebuiltRulesContextType>(() => {
@ -254,8 +237,6 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
selectedRules,
loadingRules,
lastUpdated: dataUpdatedAt,
flyoutRule,
isFlyoutInstallButtonDisabled,
},
actions,
};
@ -272,21 +253,30 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
selectedRules,
loadingRules,
dataUpdatedAt,
flyoutRule,
isFlyoutInstallButtonDisabled,
actions,
]);
return (
<UpgradePrebuiltRulesTableContext.Provider value={providerValue}>
{isUpgradeModalVisible && (
<MlJobUpgradeModal
jobs={legacyJobsInstalled}
onCancel={handleUpgradeCancel}
onConfirm={handleUpgradeConfirm}
/>
)}
{children}
<>
{isUpgradeModalVisible && (
<MlJobUpgradeModal
jobs={legacyJobsInstalled}
onCancel={handleUpgradeCancel}
onConfirm={handleUpgradeConfirm}
/>
)}
{children}
{previewedRule && (
<RuleDetailsFlyout
rule={previewedRule}
actionButtonLabel={i18n.UPDATE_BUTTON_LABEL}
isActionButtonDisabled={canPreviewedRuleBeUpgraded}
onActionButtonClick={upgradeOneRule}
closeFlyout={closeRulePreview}
/>
)}
</>
</UpgradePrebuiltRulesTableContext.Provider>
);
};

View file

@ -20,13 +20,13 @@ export const useFilterPrebuiltRulesToUpgrade = ({
}) => {
const filteredRules = useMemo(() => {
const { filter, tags } = filterOptions;
return rules.filter(({ rule }) => {
if (filter && !rule.name.toLowerCase().includes(filter.toLowerCase())) {
return rules.filter((ruleInfo) => {
if (filter && !ruleInfo.current_rule.name.toLowerCase().includes(filter.toLowerCase())) {
return false;
}
if (tags && tags.length > 0) {
return tags.every((tag) => rule.tags.includes(tag));
return tags.every((tag) => ruleInfo.current_rule.tags.includes(tag));
}
return true;

View file

@ -32,13 +32,13 @@ interface RuleNameProps {
const RuleName = ({ name, ruleId }: RuleNameProps) => {
const {
actions: { openFlyoutForRuleId },
actions: { openRulePreview },
} = useUpgradePrebuiltRulesTableContext();
return (
<EuiLink
onClick={() => {
openFlyoutForRuleId(ruleId);
openRulePreview(ruleId);
}}
>
{name}
@ -47,11 +47,12 @@ const RuleName = ({ name, ruleId }: RuleNameProps) => {
};
const RULE_NAME_COLUMN: TableColumn = {
field: 'rule.name',
field: 'current_rule.name',
name: i18n.COLUMN_RULE,
render: (value: RuleUpgradeInfoForReview['rule']['name'], rule: RuleUpgradeInfoForReview) => (
<RuleName name={value} ruleId={rule.rule.rule_id} />
),
render: (
value: RuleUpgradeInfoForReview['current_rule']['name'],
rule: RuleUpgradeInfoForReview
) => <RuleName name={value} ruleId={rule.id} />,
sortable: true,
truncateText: true,
width: '60%',
@ -59,7 +60,7 @@ const RULE_NAME_COLUMN: TableColumn = {
};
const TAGS_COLUMN: TableColumn = {
field: 'rule.tags',
field: 'current_rule.tags',
name: null,
align: 'center',
render: (tags: Rule['tags']) => {
@ -88,7 +89,7 @@ const TAGS_COLUMN: TableColumn = {
};
const INTEGRATIONS_COLUMN: TableColumn = {
field: 'rule.related_integrations',
field: 'current_rule.related_integrations',
name: null,
align: 'center',
render: (integrations: Rule['related_integrations']) => {
@ -144,7 +145,7 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []),
TAGS_COLUMN,
{
field: 'rule.risk_score',
field: 'current_rule.risk_score',
name: i18n.COLUMN_RISK_SCORE,
render: (value: Rule['risk_score']) => (
<EuiText data-test-subj="riskScore" size="s">
@ -156,10 +157,10 @@ export const useUpgradePrebuiltRulesTableColumns = (): TableColumn[] => {
width: '85px',
},
{
field: 'rule.severity',
field: 'current_rule.severity',
name: i18n.COLUMN_SEVERITY,
render: (value: Rule['severity']) => <SeverityBadge value={value} />,
sortable: ({ rule: { severity } }: RuleUpgradeInfoForReview) =>
sortable: ({ current_rule: { severity } }: RuleUpgradeInfoForReview) =>
getNormalizedSeverity(severity),
truncateText: true,
width: '12%',

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexItem, EuiLink, EuiFlexGroup, EuiSpacer, EuiButtonEmpty } from '@elastic/eui';
import { EuiFlexItem, EuiLink, EuiFlexGroup, EuiButtonEmpty } from '@elastic/eui';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import type { BuildThreatDescription } from './types';
@ -120,7 +120,6 @@ export const ThreatEuiFlexGroup = ({ label, threat }: BuildThreatDescription) =>
</EuiFlexItem>
);
})}
<EuiSpacer />
</ThreatEuiFlexGroupStyles>
);
};

View file

@ -144,7 +144,7 @@ export const getDefineStepsData = (rule: Rule): DefineStepRule => ({
rule.alert_suppression?.missing_fields_strategy ?? DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY,
});
const convertHistoryStartToSize = (relativeTime: string) => {
export const convertHistoryStartToSize = (relativeTime: string) => {
if (relativeTime.startsWith('now-')) {
return relativeTime.substring(4);
} else {

View file

@ -9,17 +9,16 @@ import { transformError } from '@kbn/securitysolution-es-utils';
import { REVIEW_RULE_INSTALLATION_URL } from '../../../../../../common/api/detection_engine/prebuilt_rules';
import type {
ReviewRuleInstallationResponseBody,
RuleInstallationInfoForReview,
RuleInstallationStatsForReview,
} from '../../../../../../common/api/detection_engine/prebuilt_rules';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { buildSiemResponse } from '../../../routes/utils';
import { convertRuleToDiffable } from '../../logic/diff/normalization/convert_rule_to_diffable';
import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt_rule_assets_client';
import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client';
import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad';
import type { PrebuiltRuleAsset } from '../../model/rule_assets/prebuilt_rule_asset';
import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets';
import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/normalization/rule_converters';
export const reviewRuleInstallationRoute = (router: SecuritySolutionPluginRouter) => {
router.post(
@ -48,7 +47,9 @@ export const reviewRuleInstallationRoute = (router: SecuritySolutionPluginRouter
const body: ReviewRuleInstallationResponseBody = {
stats: calculateRuleStats(installableRules),
rules: calculateRuleInfos(installableRules),
rules: installableRules.map((prebuiltRuleAsset) =>
convertPrebuiltRuleAssetToRuleResponse(prebuiltRuleAsset)
),
};
return response.ok({ body });
@ -77,9 +78,3 @@ const calculateRuleStats = (
tags: tagsOfRulesToInstall,
};
};
const calculateRuleInfos = (
rulesToInstall: PrebuiltRuleAsset[]
): RuleInstallationInfoForReview[] => {
return rulesToInstall.map((rule) => convertRuleToDiffable(rule));
};

View file

@ -15,6 +15,7 @@ import type {
ThreeWayDiff,
} from '../../../../../../common/api/detection_engine/prebuilt_rules';
import { invariant } from '../../../../../../common/utils/invariant';
import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema/rule_schemas';
import type { SecuritySolutionPluginRouter } from '../../../../../types';
import { buildSiemResponse } from '../../../routes/utils';
import type { CalculateRuleDiffResult } from '../../logic/diff/calculate_rule_diff';
@ -23,6 +24,7 @@ import { createPrebuiltRuleAssetsClient } from '../../logic/rule_assets/prebuilt
import { createPrebuiltRuleObjectsClient } from '../../logic/rule_objects/prebuilt_rule_objects_client';
import { fetchRuleVersionsTriad } from '../../logic/rule_versions/fetch_rule_versions_triad';
import { getVersionBuckets } from '../../model/rule_versions/get_version_buckets';
import { convertPrebuiltRuleAssetToRuleResponse } from '../../../rule_management/normalization/rule_converters';
export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => {
router.post(
@ -86,16 +88,21 @@ const calculateRuleInfos = (results: CalculateRuleDiffResult[]): RuleUpgradeInfo
return results.map((result) => {
const { ruleDiff, ruleVersions } = result;
const installedCurrentVersion = ruleVersions.input.current;
const diffableCurrentVersion = ruleVersions.output.current;
const diffableTargetVersion = ruleVersions.output.target;
const targetVersion = ruleVersions.input.target;
invariant(installedCurrentVersion != null, 'installedCurrentVersion not found');
invariant(targetVersion != null, 'targetVersion not found');
const targetRule: RuleResponse = {
...convertPrebuiltRuleAssetToRuleResponse(targetVersion),
id: installedCurrentVersion.id,
};
return {
id: installedCurrentVersion.id,
rule_id: installedCurrentVersion.rule_id,
revision: installedCurrentVersion.revision,
rule: diffableCurrentVersion,
target_rule: diffableTargetVersion,
current_rule: installedCurrentVersion,
target_rule: targetRule,
diff: {
fields: pickBy<ThreeWayDiff<unknown>>(
ruleDiff.fields,

View file

@ -8,7 +8,7 @@
import { v4 as uuidv4 } from 'uuid';
import { BadRequestError } from '@kbn/securitysolution-es-utils';
import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
import { validate, validateNonExact } from '@kbn/securitysolution-io-ts-utils';
import { ruleTypeMappings } from '@kbn/securitysolution-rules';
import type { ResolvedSanitizedRule, SanitizedRule } from '@kbn/alerting-plugin/common';
@ -24,7 +24,6 @@ import type {
RequiredFieldArray,
SetupGuide,
RuleCreateProps,
RuleResponse,
TypeSpecificCreateProps,
TypeSpecificResponse,
} from '../../../../../common/api/detection_engine/model/rule_schema';
@ -36,6 +35,7 @@ import {
SavedQueryPatchParams,
ThreatMatchPatchParams,
ThresholdPatchParams,
RuleResponse,
} from '../../../../../common/api/detection_engine/model/rule_schema';
import {
@ -76,6 +76,11 @@ import type {
import { transformFromAlertThrottle, transformToActionFrequency } from './rule_actions';
import { convertAlertSuppressionToCamel, convertAlertSuppressionToSnake } from '../utils/utils';
import { createRuleExecutionSummary } from '../../rule_monitoring';
import type { PrebuiltRuleAsset } from '../../prebuilt_rules';
const DEFAULT_FROM = 'now-6m' as const;
const DEFAULT_TO = 'now' as const;
const DEFAULT_INTERVAL = '5m' as const;
// These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema
// to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for
@ -472,7 +477,7 @@ export const convertCreateAPIToInternalSchema = (
ruleId: newRuleId,
falsePositives: input.false_positives ?? [],
investigationFields: input.investigation_fields ?? [],
from: input.from ?? 'now-6m',
from: input.from ?? DEFAULT_FROM,
immutable,
license: input.license,
outputIndex: input.output_index ?? '',
@ -488,7 +493,7 @@ export const convertCreateAPIToInternalSchema = (
threat: input.threat ?? [],
timestampOverride: input.timestamp_override,
timestampOverrideFallbackDisabled: input.timestamp_override_fallback_disabled,
to: input.to ?? 'now',
to: input.to ?? DEFAULT_TO,
references: input.references ?? [],
namespace: input.namespace,
note: input.note,
@ -680,3 +685,48 @@ export const internalRuleToAPIResponse = (
execution_summary: executionSummary ?? undefined,
};
};
export const convertPrebuiltRuleAssetToRuleResponse = (
prebuiltRuleAsset: PrebuiltRuleAsset
): RuleResponse => {
const prebuiltRuleAssetDefaults = {
enabled: false,
risk_score_mapping: [],
severity_mapping: [],
interval: DEFAULT_INTERVAL,
to: DEFAULT_TO,
from: DEFAULT_FROM,
exceptions_list: [],
false_positives: [],
max_signals: DEFAULT_MAX_SIGNALS,
actions: [],
related_integrations: [],
required_fields: [],
setup: '',
references: [],
threat: [],
tags: [],
author: [],
};
const ruleResponseSpecificFields = {
id: uuidv4(),
updated_at: new Date(0).toISOString(),
updated_by: '',
created_at: new Date(0).toISOString(),
created_by: '',
immutable: true,
revision: 1,
};
const [rule, error] = validate(
{ ...prebuiltRuleAssetDefaults, ...prebuiltRuleAsset, ...ruleResponseSpecificFields },
RuleResponse
);
if (!rule) {
throw new Error(error);
}
return rule;
};