Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Detections][7.12] Critical Threshold Rule Fixes #92667

Merged
merged 25 commits into from
Mar 1, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f66ac13
Threshold cardinality validation
madirey Feb 23, 2021
dbc1c20
Remove comments
madirey Feb 23, 2021
3188382
Fix legacy threshold signal dupe mitigation
madirey Feb 24, 2021
21708cf
Merge branch 'master' of github.com:elastic/kibana into threshold-7.1…
madirey Feb 24, 2021
f697baa
Add find_threshold_signals tests
madirey Feb 24, 2021
8a53bf3
remove comment
madirey Feb 24, 2021
66d6d7e
bug fixes
madirey Feb 24, 2021
c699fbb
Fix edit form value initialization for cardinality_value
madirey Feb 24, 2021
2a86995
Fix test
madirey Feb 24, 2021
ea53ab1
Type and test fixes
madirey Feb 25, 2021
ed9efaa
Tests/types
madirey Feb 25, 2021
6357718
Reenable threshold cypress test
madirey Feb 25, 2021
3aca5d6
Schema fixes
madirey Feb 28, 2021
748066d
Types and tests, normalize threshold field util
madirey Feb 28, 2021
8c1187e
Merge branch 'master' of github.com:elastic/kibana into threshold-7.1…
madirey Feb 28, 2021
078e35f
Continue cleaning up types
madirey Feb 28, 2021
3e8d423
Some more pre-7.12 tests
madirey Feb 28, 2021
8726a4d
Limit cardinality_field to length 1 for now
madirey Feb 28, 2021
26e05b0
Cardinality to array
madirey Feb 28, 2021
0807755
Cardinality to array
madirey Mar 1, 2021
493411d
Tests/types
madirey Mar 1, 2021
d6dc1e2
cardinality can be null
madirey Mar 1, 2021
feb0cd2
Handle empty threshold field in bulk_create_threshold_signals
madirey Mar 1, 2021
a8a8654
Merge branch 'master' of github.com:elastic/kibana into threshold-7.1…
madirey Mar 1, 2021
478b7af
Remove cardinality_field, cardinality_value
madirey Mar 1, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -465,26 +465,45 @@ export type Threats = t.TypeOf<typeof threats>;
export const threatsOrUndefined = t.union([threats, t.undefined]);
export type ThreatsOrUndefined = t.TypeOf<typeof threatsOrUndefined>;

export const threshold = t.intersection([
t.exact(
t.type({
field: t.union([t.string, t.array(t.string)]),
value: PositiveIntegerGreaterThanZero,
})
),
t.exact(
t.partial({
cardinality_field: t.union([t.string, t.array(t.string), t.undefined, t.null]),
cardinality_value: t.union([PositiveInteger, t.undefined, t.null]), // TODO: cardinality_value should be set if cardinality_field is set
})
),
]);
// TODO: codec to transform threshold field string to string[] ?
export const thresholdField = t.exact(
t.type({
field: t.union([t.string, t.array(t.string)]), // Covers pre- and post-7.12
value: PositiveIntegerGreaterThanZero,
})
);
export type ThresholdField = t.TypeOf<typeof thresholdField>;

export const thresholdFieldNormalized = t.exact(
t.type({
field: t.array(t.string),
value: PositiveIntegerGreaterThanZero,
})
);
export type ThresholdFieldNormalized = t.TypeOf<typeof thresholdFieldNormalized>;

export const thresholdCardinalityField = t.exact(
t.partial({
cardinality_field: t.array(t.string),
cardinality_value: PositiveInteger,
madirey marked this conversation as resolved.
Show resolved Hide resolved
})
);
export type ThresholdCardinalityField = t.TypeOf<typeof thresholdCardinalityField>;

export const threshold = t.intersection([thresholdField, thresholdCardinalityField]);
export type Threshold = t.TypeOf<typeof threshold>;

export const thresholdOrUndefined = t.union([threshold, t.undefined]);
export type ThresholdOrUndefined = t.TypeOf<typeof thresholdOrUndefined>;

export const thresholdNormalized = t.intersection([
thresholdFieldNormalized,
thresholdCardinalityField,
]);
export type ThresholdNormalized = t.TypeOf<typeof thresholdNormalized>;

export const thresholdNormalizedOrUndefined = t.union([thresholdNormalized, t.undefined]);
export type ThresholdNormalizedOrUndefined = t.TypeOf<typeof thresholdNormalizedOrUndefined>;

export const created_at = IsoDateString;
export const updated_at = IsoDateString;
export const updated_by = t.string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
* 2.0.
*/

import { hasEqlSequenceQuery, hasLargeValueList, hasNestedEntry, isThreatMatchRule } from './utils';
import {
hasEqlSequenceQuery,
hasLargeValueList,
hasNestedEntry,
isThreatMatchRule,
normalizeThresholdField,
} from './utils';
import { EntriesArray } from '../shared_imports';

describe('#hasLargeValueList', () => {
Expand Down Expand Up @@ -151,3 +157,21 @@ describe('#hasEqlSequenceQuery', () => {
});
});
});

describe('normalizeThresholdField', () => {
it('converts a string to a string array', () => {
expect(normalizeThresholdField('host.name')).toEqual(['host.name']);
});
it('returns a string array when a string array is passed in', () => {
expect(normalizeThresholdField(['host.name'])).toEqual(['host.name']);
});
it('converts undefined to an empty array', () => {
expect(normalizeThresholdField(undefined)).toEqual([]);
});
it('converts null to an empty array', () => {
expect(normalizeThresholdField(null)).toEqual([]);
});
it('converts an empty string to an empty array', () => {
expect(normalizeThresholdField('')).toEqual([]);
});
});
12 changes: 12 additions & 0 deletions x-pack/plugins/security_solution/common/detection_engine/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import { isEmpty } from 'lodash';

import {
CreateExceptionListItemSchema,
EntriesArray,
Expand Down Expand Up @@ -42,3 +44,13 @@ export const isQueryRule = (ruleType: Type | undefined): boolean =>
ruleType === 'query' || ruleType === 'saved_query';
export const isThreatMatchRule = (ruleType: Type | undefined): boolean =>
ruleType === 'threat_match';

export const normalizeThresholdField = (
thresholdField: string | string[] | null | undefined
): string[] => {
return Array.isArray(thresholdField)
? thresholdField
: isEmpty(thresholdField)
? []
: [thresholdField!];
};
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export interface MatrixHistogramRequestOptions extends RequestBasicOptions {
| {
field: string | string[] | undefined;
value: number;
cardinality_field?: string | undefined;
cardinality_value?: number | undefined;
cardinality_field?: string[];
cardinality_value?: number;
}
| undefined;
inspect?: Maybe<Inspect>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,103 +79,100 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';

import { DETECTIONS_URL } from '../../urls/navigation';

// Skipped until post-FF for 7.12
describe.skip('Threshold Rules', () => {
describe('Detection rules, threshold', () => {
const expectedUrls = newThresholdRule.referenceUrls.join('');
const expectedFalsePositives = newThresholdRule.falsePositivesExamples.join('');
const expectedTags = newThresholdRule.tags.join('');
const expectedMitre = formatMitreAttackDescription(newThresholdRule.mitre);

const rule = { ...newThresholdRule };

beforeEach(() => {
cleanKibana();
createTimeline(newThresholdRule.timeline).then((response) => {
rule.timeline.id = response.body.data.persistTimeline.timeline.savedObjectId;
});
describe('Detection rules, threshold', () => {
const expectedUrls = newThresholdRule.referenceUrls.join('');
const expectedFalsePositives = newThresholdRule.falsePositivesExamples.join('');
const expectedTags = newThresholdRule.tags.join('');
const expectedMitre = formatMitreAttackDescription(newThresholdRule.mitre);

const rule = { ...newThresholdRule };

beforeEach(() => {
cleanKibana();
createTimeline(newThresholdRule.timeline).then((response) => {
rule.timeline.id = response.body.data.persistTimeline.timeline.savedObjectId;
});
});

it('Creates and activates a new threshold rule', () => {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
waitForRulesTableToBeLoaded();
goToCreateNewRule();
selectThresholdRuleType();
fillDefineThresholdRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndActivateRule();

cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');

changeRowsPerPageTo300();

const expectedNumberOfRules = 1;
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules);
});
it('Creates and activates a new threshold rule', () => {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
waitForRulesTableToBeLoaded();
goToCreateNewRule();
selectThresholdRuleType();
fillDefineThresholdRuleAndContinue(rule);
fillAboutRuleAndContinue(rule);
fillScheduleRuleAndContinue(rule);
createAndActivateRule();

cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)');

changeRowsPerPageTo300();

const expectedNumberOfRules = 1;
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules);
});

filterByCustomRules();
filterByCustomRules();

cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
});
cy.get(RULE_NAME).should('have.text', rule.name);
cy.get(RISK_SCORE).should('have.text', rule.riskScore);
cy.get(SEVERITY).should('have.text', rule.severity);
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');

goToRuleDetails();

cy.get(RULE_NAME_HEADER).should('have.text', `${rule.name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', rule.severity);
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.riskScore);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
});
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.customQuery);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
getDetails(THRESHOLD_DETAILS).should(
'have.text',
`Results aggregated by ${rule.thresholdField} >= ${rule.threshold}`
);
cy.get(RULES_TABLE).then(($table) => {
cy.wrap($table.find(RULES_ROW).length).should('eql', 1);
});
cy.get(RULE_NAME).should('have.text', rule.name);
cy.get(RISK_SCORE).should('have.text', rule.riskScore);
cy.get(SEVERITY).should('have.text', rule.severity);
cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true');

goToRuleDetails();

cy.get(RULE_NAME_HEADER).should('have.text', `${rule.name}`);
cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description);
cy.get(ABOUT_DETAILS).within(() => {
getDetails(SEVERITY_DETAILS).should('have.text', rule.severity);
getDetails(RISK_SCORE_DETAILS).should('have.text', rule.riskScore);
getDetails(REFERENCE_URLS_DETAILS).should((details) => {
expect(removeExternalLinkText(details.text())).equal(expectedUrls);
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS).should(
'have.text',
`${rule.runsEvery.interval}${rule.runsEvery.type}`
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
'have.text',
`${rule.lookBack.interval}${rule.lookBack.type}`
);
getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives);
getDetails(MITRE_ATTACK_DETAILS).should((mitre) => {
expect(removeExternalLinkText(mitre.text())).equal(expectedMitre);
});
getDetails(TAGS_DETAILS).should('have.text', expectedTags);
});
cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true });
cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN);
cy.get(DEFINITION_DETAILS).within(() => {
getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join(''));
getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.customQuery);
getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold');
getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None');
getDetails(THRESHOLD_DETAILS).should(
'have.text',
`Results aggregated by ${rule.thresholdField} >= ${rule.threshold}`
);
});
cy.get(SCHEDULE_DETAILS).within(() => {
getDetails(RUNS_EVERY_DETAILS).should(
'have.text',
`${rule.runsEvery.interval}${rule.runsEvery.type}`
);
getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should(
'have.text',
`${rule.lookBack.interval}${rule.lookBack.type}`
);
});

waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();
waitForTheRuleToBeExecuted();
waitForAlertsToPopulate();

cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.lt(100));
cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name);
cy.get(ALERT_RULE_VERSION).first().should('have.text', '1');
cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold');
cy.get(ALERT_RULE_SEVERITY).first().should('have.text', rule.severity.toLowerCase());
cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', rule.riskScore);
});
cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.lt(100));
cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name);
cy.get(ALERT_RULE_VERSION).first().should('have.text', '1');
cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold');
cy.get(ALERT_RULE_SEVERITY).first().should('have.text', rule.severity.toLowerCase());
cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', rule.riskScore);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ export interface MatrixHistogramQueryProps {
| {
field: string | string[] | undefined;
value: number;
cardinality_field?: string | undefined;
cardinality_value?: number | undefined;
cardinality_field?: string[];
cardinality_value?: number;
}
| undefined;
skip?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ describe('PreviewQuery', () => {
threshold={{
field: 'agent.hostname',
value: 200,
cardinality_field: 'user.name',
cardinality_field: ['user.name'],
cardinality_value: 2,
}}
isDisabled={false}
Expand Down Expand Up @@ -343,7 +343,7 @@ describe('PreviewQuery', () => {
threshold={{
field: 'agent.hostname',
value: 200,
cardinality_field: 'user.name',
cardinality_field: ['user.name'],
cardinality_value: 2,
}}
isDisabled={false}
Expand Down Expand Up @@ -387,7 +387,7 @@ describe('PreviewQuery', () => {
threshold={{
field: undefined,
value: 200,
cardinality_field: 'user.name',
cardinality_field: ['user.name'],
cardinality_value: 2,
}}
isDisabled={false}
Expand Down Expand Up @@ -419,7 +419,7 @@ describe('PreviewQuery', () => {
threshold={{
field: ' ',
value: 200,
cardinality_field: 'user.name',
cardinality_field: ['user.name'],
cardinality_value: 2,
}}
isDisabled={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export type Threshold =
| {
field: string | string[] | undefined;
value: number;
cardinality_field: string | undefined;
cardinality_value: number | undefined;
cardinality_field?: string[];
cardinality_value?: number;
}
| undefined;

Expand Down
Loading