Skip to content

Commit

Permalink
[8.17] [Security Solution] Display cardinality for threshold rules (#…
Browse files Browse the repository at this point in the history
…201162) (#201959)

# Backport

This will backport the following commits from `main` to `8.17`:
- [[Security Solution] Display cardinality for threshold rules
(#201162)](#201162)

<!--- Backport version: 9.4.3 -->

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

<!--BACKPORT [{"author":{"name":"Jacek
Kolezynski","email":"jacek.kolezynski@elastic.co"},"sourceCommit":{"committedDate":"2024-11-27T12:11:41Z","message":"[Security
Solution] Display cardinality for threshold rules
(#201162)\n\n**Resolves #161576**\r\n\r\n## Summary\r\n\r\nThis PR fixes
the description of threshold rules. The problem was that\r\nif a
threshold rule contained 'Count' (cardinality) it wasn't
displayed\r\nneither in a summary while creating the rule, nor in the
rule details\r\npage. This PR fixes these two places, introducing
similar logic to the\r\ntwo places in the code, to display the
cardinality if it is present in\r\nthe threshold object.\r\n\r\n###
BEFORE\r\n1. overview page\r\n<img width=\"1027\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/b927b4e0-f2a0-41ba-87e0-441a53760cce\">\r\n\r\n2.
rule details page\r\n<img width=\"762\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/486f8616-8582-45ea-9422-bfd554e2ae83\">\r\n\r\n\r\n\r\n###
AFTER\r\n1. overview page\r\n<img width=\"1015\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/06a5e0d1-76ef-434e-9c1c-cce6c3ff504f\">\r\n\r\n2.
rule details page\r\n<img width=\"893\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/40acd7d4-4058-40c0-aa19-e5f489c53c2c\">\r\n\r\n\r\n###
Checklist\r\n\r\nCheck the PR satisfies following conditions.
\r\n\r\nReviewers should verify this PR satisfies this list as
well.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\nDone:
\r\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7474\r\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7473\r\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7476\r\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7477","sha":"19a2ff81d5a542402a3f0c006d6b4986890d73f9","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","v9.0.0","Team:Detections
and Resp","Team: SecuritySolution","Team:Detection Rule
Management","Feature:Prebuilt Detection
Rules","backport:version","v8.17.0","v8.18.0","v8.16.2"],"title":"[Security
Solution] Display cardinality for threshold
rules","number":201162,"url":"https://github.com/elastic/kibana/pull/201162","mergeCommit":{"message":"[Security
Solution] Display cardinality for threshold rules
(#201162)\n\n**Resolves #161576**\r\n\r\n## Summary\r\n\r\nThis PR fixes
the description of threshold rules. The problem was that\r\nif a
threshold rule contained 'Count' (cardinality) it wasn't
displayed\r\nneither in a summary while creating the rule, nor in the
rule details\r\npage. This PR fixes these two places, introducing
similar logic to the\r\ntwo places in the code, to display the
cardinality if it is present in\r\nthe threshold object.\r\n\r\n###
BEFORE\r\n1. overview page\r\n<img width=\"1027\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/b927b4e0-f2a0-41ba-87e0-441a53760cce\">\r\n\r\n2.
rule details page\r\n<img width=\"762\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/486f8616-8582-45ea-9422-bfd554e2ae83\">\r\n\r\n\r\n\r\n###
AFTER\r\n1. overview page\r\n<img width=\"1015\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/06a5e0d1-76ef-434e-9c1c-cce6c3ff504f\">\r\n\r\n2.
rule details page\r\n<img width=\"893\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/40acd7d4-4058-40c0-aa19-e5f489c53c2c\">\r\n\r\n\r\n###
Checklist\r\n\r\nCheck the PR satisfies following conditions.
\r\n\r\nReviewers should verify this PR satisfies this list as
well.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\nDone:
\r\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7474\r\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7473\r\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7476\r\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7477","sha":"19a2ff81d5a542402a3f0c006d6b4986890d73f9"}},"sourceBranch":"main","suggestedTargetBranches":["8.17","8.x","8.16"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/201162","number":201162,"mergeCommit":{"message":"[Security
Solution] Display cardinality for threshold rules
(#201162)\n\n**Resolves #161576**\r\n\r\n## Summary\r\n\r\nThis PR fixes
the description of threshold rules. The problem was that\r\nif a
threshold rule contained 'Count' (cardinality) it wasn't
displayed\r\nneither in a summary while creating the rule, nor in the
rule details\r\npage. This PR fixes these two places, introducing
similar logic to the\r\ntwo places in the code, to display the
cardinality if it is present in\r\nthe threshold object.\r\n\r\n###
BEFORE\r\n1. overview page\r\n<img width=\"1027\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/b927b4e0-f2a0-41ba-87e0-441a53760cce\">\r\n\r\n2.
rule details page\r\n<img width=\"762\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/486f8616-8582-45ea-9422-bfd554e2ae83\">\r\n\r\n\r\n\r\n###
AFTER\r\n1. overview page\r\n<img width=\"1015\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/06a5e0d1-76ef-434e-9c1c-cce6c3ff504f\">\r\n\r\n2.
rule details page\r\n<img width=\"893\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/40acd7d4-4058-40c0-aa19-e5f489c53c2c\">\r\n\r\n\r\n###
Checklist\r\n\r\nCheck the PR satisfies following conditions.
\r\n\r\nReviewers should verify this PR satisfies this list as
well.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\nDone:
\r\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7474\r\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7473\r\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7476\r\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7477","sha":"19a2ff81d5a542402a3f0c006d6b4986890d73f9"}},{"branch":"8.17","label":"v8.17.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.16","label":"v8.16.2","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Jacek Kolezynski <jacek.kolezynski@elastic.co>
  • Loading branch information
kibanamachine and jkelas authored Nov 27, 2024
1 parent 1e26da3 commit 1cbf988
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import { FilterBadgeGroup } from '@kbn/unified-search-plugin/public';
import { IntervalAbbrScreenReader } from '../../../../common/components/accessibility';
import type {
RequiredFieldArray,
Threshold,
AlertSuppressionMissingFieldsStrategy,
} from '../../../../../common/api/detection_engine/model/rule_schema';
import { AlertSuppressionMissingFieldsStrategyEnum } from '../../../../../common/api/detection_engine/model/rule_schema';
Expand All @@ -50,6 +49,7 @@ import { defaultToEmptyTag } from '../../../../common/components/empty_value';
import { RequiredFieldIcon } from '../../../rule_management/components/rule_details/required_field_icon';
import { ThreatEuiFlexGroup } from './threat_description';
import { AlertSuppressionLabel } from './alert_suppression_label';
import type { FieldValueThreshold } from '../threshold_input';

const NoteDescriptionContainer = styled(EuiFlexItem)`
height: 105px;
Expand Down Expand Up @@ -490,20 +490,29 @@ export const buildRuleTypeDescription = (label: string, ruleType: Type): ListIte
}
};

export const buildThresholdDescription = (label: string, threshold: Threshold): ListItems[] => [
{
title: label,
description: (
<>
{isEmpty(threshold.field[0])
? `${i18n.THRESHOLD_RESULTS_ALL} >= ${threshold.value}`
: `${i18n.THRESHOLD_RESULTS_AGGREGATED_BY} ${
Array.isArray(threshold.field) ? threshold.field.join(',') : threshold.field
} >= ${threshold.value}`}
</>
),
},
];
export const buildThresholdDescription = (
label: string,
threshold: FieldValueThreshold
): ListItems[] => {
let thresholdDescription = isEmpty(threshold.field[0])
? `${i18n.THRESHOLD_RESULTS_ALL} >= ${threshold.value}`
: `${i18n.THRESHOLD_RESULTS_AGGREGATED_BY} ${threshold.field.join(',')} >= ${threshold.value}`;

if (threshold.cardinality?.value && threshold.cardinality?.field.length > 0) {
thresholdDescription = i18n.THRESHOLD_CARDINALITY(
thresholdDescription,
threshold.cardinality.field[0],
threshold.cardinality.value
);
}

return [
{
title: label,
description: <>{thresholdDescription}</>,
},
];
};

export const buildThreatMappingDescription = (
title: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,10 +451,62 @@ describe('description_step', () => {

expect(result[0].title).toEqual('Threshold label');
expect(React.isValidElement(result[0].description)).toBeTruthy();
expect(mount(result[0].description as React.ReactElement).html()).toContain(
expect(mount(result[0].description as React.ReactElement).html()).toEqual(
'Results aggregated by user.name >= 100'
);
});

test('returns threshold description when threshold exist, field is set, and cardinality is not set', () => {
const mockThreshold = {
threshold: {
field: ['user.name'],
value: 100,
cardinality: {
field: [],
value: 0,
},
},
};
const result: ListItems[] = getDescriptionItem(
'threshold',
'Threshold label',
mockThreshold,
mockFilterManager,
mockLicenseService
);

expect(result[0].title).toEqual('Threshold label');
expect(React.isValidElement(result[0].description)).toBeTruthy();
expect(mount(result[0].description as React.ReactElement).html()).toEqual(
'Results aggregated by user.name >= 100'
);
});

test('returns threshold description when threshold exist, field is set and cardinality is set', () => {
const mockThreshold = {
threshold: {
field: ['user.name'],
value: 100,
cardinality: {
field: ['host.test_value'],
value: 10,
},
},
};
const result: ListItems[] = getDescriptionItem(
'threshold',
'Threshold label',
mockThreshold,
mockFilterManager,
mockLicenseService
);

expect(result[0].title).toEqual('Threshold label');
expect(React.isValidElement(result[0].description)).toBeTruthy();
expect(mount(result[0].description as React.ReactElement).html()).toContain(
'Results aggregated by user.name >= 100 when unique values count of host.test_value >= 10'
);
});
});

describe('references', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,24 @@ export const THRESHOLD_RESULTS_AGGREGATED_BY = i18n.translate(
}
);

export const THRESHOLD_CARDINALITY = (
thresholdFieldsGroupedBy: string,
cardinalityField: string,
cardinalityValue: string | number
) =>
i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDescription.thresholdResultsCardinalityDescription',
{
defaultMessage:
'{thresholdFieldsGroupedBy} when unique values count of {cardinalityField} >= {cardinalityValue}',
values: {
thresholdFieldsGroupedBy,
cardinalityField,
cardinalityValue,
},
}
);

export const EQL_EVENT_CATEGORY_FIELD_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDescription.eqlEventCategoryFieldLabel',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,23 @@ interface ThresholdProps {
threshold: ThresholdType;
}

export const Threshold = ({ threshold }: ThresholdProps) => (
<div data-test-subj="thresholdPropertyValue">
{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}`}
</div>
);
export const Threshold = ({ threshold }: ThresholdProps) => {
let thresholdDescription = 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}`;

if (threshold.cardinality && threshold.cardinality.length > 0) {
thresholdDescription = descriptionStepI18n.THRESHOLD_CARDINALITY(
thresholdDescription,
threshold.cardinality[0].field,
threshold.cardinality[0].value
);
}

return <div data-test-subj="thresholdPropertyValue">{thresholdDescription}</div>;
};

interface AnomalyThresholdProps {
anomalyThreshold: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ describe(
const { threshold } = THRESHOLD_RULE_INDEX_PATTERN['security-rule'] as {
threshold: Threshold;
};
assertThresholdPropertyShown(threshold.value);
assertThresholdPropertyShown(threshold);

const { index } = THRESHOLD_RULE_INDEX_PATTERN['security-rule'] as { index: string[] };
assertIndexPropertyShown(index);
Expand Down Expand Up @@ -952,7 +952,7 @@ describe(
const { threshold } = UPDATED_THRESHOLD_RULE_INDEX_PATTERN['security-rule'] as {
threshold: Threshold;
};
assertThresholdPropertyShown(threshold.value);
assertThresholdPropertyShown(threshold);

const { index } = UPDATED_THRESHOLD_RULE_INDEX_PATTERN['security-rule'] as {
index: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
import { capitalize } from 'lodash';
import type { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types';
import type { Module } from '@kbn/ml-plugin/common/types/modules';
import { AlertSuppression } from '@kbn/security-solution-plugin/common/api/detection_engine/model/rule_schema';
import {
AlertSuppression,
Threshold,
} from '@kbn/security-solution-plugin/common/api/detection_engine/model/rule_schema';
import type { Filter } from '@kbn/es-query';
import type { PrebuiltRuleAsset } from '@kbn/security-solution-plugin/server/lib/detection_engine/prebuilt_rules';
import {
Expand Down Expand Up @@ -312,9 +315,15 @@ export const assertMachineLearningPropertiesShown = (
});
};

export const assertThresholdPropertyShown = (thresholdValue: number) => {
export const assertThresholdPropertyShown = (threshold: Threshold) => {
cy.get(THRESHOLD_TITLE).should('have.text', 'Threshold');
cy.get(THRESHOLD_VALUE).should('contain', thresholdValue);
cy.get(THRESHOLD_VALUE).should('contain', threshold.value);
if (threshold.cardinality) {
cy.get(THRESHOLD_VALUE).should(
'contain',
`when unique values count of ${threshold.cardinality[0].field} >= ${threshold.cardinality[0].value}`
);
}
};

export const assertEqlQueryPropertyShown = (query: string) => {
Expand Down

0 comments on commit 1cbf988

Please sign in to comment.