[Security Solution] Adds max signals warning to UI propagated rule warnings (#154112)

Adds a warning message that is propagated to the user when a rule
execution hits `max_signals`. This will also set the rule in a partial
failure state
This commit is contained in:
Davis Plumlee 2023-04-26 01:27:49 -04:00 committed by GitHub
parent 11a447a1e0
commit cd180a0323
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 261 additions and 49 deletions

View file

@ -124,6 +124,8 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper
if (filteredAlerts.length === 0) {
return { createdAlerts: [], errors: {}, alertsWereTruncated: false };
} else if (maxAlerts === 0) {
return { createdAlerts: [], errors: {}, alertsWereTruncated: true };
}
let enrichedAlerts = filteredAlerts;

View file

@ -30,6 +30,7 @@ import {
createSearchAfterReturnType,
makeFloatString,
getUnprocessedExceptionsWarnings,
getMaxSignalsWarning,
} from '../utils/utils';
import { buildReasonMessageForEqlAlert } from '../utils/reason_formatters';
import type { CompleteRule, EqlRuleParams } from '../../rule_schema';
@ -130,6 +131,9 @@ export const eqlExecutor = async ({
addToSearchAfterReturn({ current: result, next: createResult });
}
if (response.hits.total && response.hits.total.value >= ruleParams.maxSignals) {
result.warningMessages.push(getMaxSignalsWarning());
}
return result;
});
};

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import chunk from 'lodash/fp/chunk';
import type { OpenPointInTimeResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { uniq, chunk } from 'lodash/fp';
import { getThreatList, getThreatListCount } from './get_threat_list';
import type {
CreateThreatSignalsOptions,
@ -27,6 +27,7 @@ import { getAllowedFieldsForTermQuery } from './get_allowed_fields_for_terms_que
import { getEventCount, getEventList } from './get_event_count';
import { getMappingFilters } from './get_mapping_filters';
import { THREAT_PIT_KEEP_ALIVE } from '../../../../../../common/cti/constants';
import { getMaxSignalsWarning } from '../../utils/utils';
export const createThreatSignals = async ({
alertId,
@ -107,11 +108,6 @@ export const createThreatSignals = async ({
ruleExecutionLogger.debug(`Total event count: ${eventCount}`);
// if (eventCount === 0) {
// ruleExecutionLogger.debug('Indicator matching rule has completed');
// return results;
// }
let threatPitId: OpenPointInTimeResponse['id'] = (
await services.scopedClusterClient.asCurrentUser.openPointInTime({
index: threatIndex,
@ -171,6 +167,11 @@ export const createThreatSignals = async ({
`all successes are ${results.success}`
);
if (results.createdSignalsCount >= params.maxSignals) {
if (results.warningMessages.includes(getMaxSignalsWarning())) {
results.warningMessages = uniq(results.warningMessages);
} else if (documentCount > 0) {
results.warningMessages.push(getMaxSignalsWarning());
}
ruleExecutionLogger.debug(
`Indicator match has reached its max signals count ${params.maxSignals}. Additional documents not checked are ${documentCount}`
);

View file

@ -20,6 +20,7 @@ export const findMlSignals = async ({
anomalyThreshold,
from,
to,
maxSignals,
exceptionFilter,
}: {
ml: MlPluginSetup;
@ -29,6 +30,7 @@ export const findMlSignals = async ({
anomalyThreshold: number;
from: string;
to: string;
maxSignals: number;
exceptionFilter: Filter | undefined;
}): Promise<AnomalyResults> => {
const { mlAnomalySearch } = ml.mlSystemProvider(request, savedObjectsClient);
@ -37,6 +39,7 @@ export const findMlSignals = async ({
threshold: anomalyThreshold,
earliestMs: dateMath.parse(from)?.valueOf() ?? 0,
latestMs: dateMath.parse(to)?.valueOf() ?? 0,
maxRecords: maxSignals,
exceptionFilter,
};
return getAnomalies(params, mlAnomalySearch);

View file

@ -24,6 +24,7 @@ import {
addToSearchAfterReturn,
createErrorsFromShard,
createSearchAfterReturnType,
getMaxSignalsWarning,
mergeReturns,
} from '../utils/utils';
import type { SetupPlugins } from '../../../../plugin';
@ -102,9 +103,18 @@ export const mlExecutor = async ({
anomalyThreshold: ruleParams.anomalyThreshold,
from: tuple.from.toISOString(),
to: tuple.to.toISOString(),
maxSignals: tuple.maxSignals,
exceptionFilter,
});
if (
anomalyResults.hits.total &&
typeof anomalyResults.hits.total !== 'number' &&
anomalyResults.hits.total.value > tuple.maxSignals
) {
result.warningMessages.push(getMaxSignalsWarning());
}
const [filteredAnomalyHits, _] = await filterEventsAgainstList({
listClient,
ruleExecutionLogger,

View file

@ -38,6 +38,7 @@ import {
import {
addToSearchAfterReturn,
createSearchAfterReturnType,
getMaxSignalsWarning,
getUnprocessedExceptionsWarnings,
} from '../utils/utils';
import { createEnrichEventsFunction } from '../utils/enrichments';
@ -154,7 +155,7 @@ export const createNewTermsAlertType = (
// it's possible for the array to be truncated but alert documents could fail to be created for other reasons,
// in which case createdSignalsCount would still be less than maxSignals. Since valid alerts were truncated from
// the array in that case, we stop and report the errors.
while (result.createdSignalsCount < params.maxSignals) {
while (result.createdSignalsCount <= params.maxSignals) {
// PHASE 1: Fetch a page of terms using a composite aggregation. This will collect a page from
// all of the terms seen over the last rule interval. In the next phase we'll determine which
// ones are new.
@ -316,6 +317,7 @@ export const createNewTermsAlertType = (
addToSearchAfterReturn({ current: result, next: bulkCreateResult });
if (bulkCreateResult.alertsWereTruncated) {
result.warningMessages.push(getMaxSignalsWarning());
break;
}
}

View file

@ -29,7 +29,7 @@ Object {
},
},
"composite": Object {
"size": 100,
"size": 101,
"sources": Array [
Object {
"host.name": Object {

View file

@ -42,7 +42,7 @@ export const buildGroupByFieldAggregation = ({
},
},
})),
size: maxSignals,
size: maxSignals + 1, // Add extra bucket to check if there's more data after max signals
},
aggs: {
topHits: {

View file

@ -15,6 +15,7 @@ import type { RuleServices, RunOpts, SearchAfterAndBulkCreateReturnType } from '
import {
addToSearchAfterReturn,
getUnprocessedExceptionsWarnings,
getMaxSignalsWarning,
mergeReturns,
} from '../../utils/utils';
import type { SuppressionBucket } from './wrap_suppressed_alerts';
@ -228,6 +229,13 @@ export const groupAndBulkCreate = async ({
return toReturn;
}
if (
buckets.length > tuple.maxSignals &&
!toReturn.warningMessages.includes(getMaxSignalsWarning()) // If the unsuppressed result didn't already hit max signals, we add the warning here
) {
toReturn.warningMessages.push(getMaxSignalsWarning());
}
const suppressionBuckets: SuppressionBucket[] = buckets.map((bucket) => ({
event: bucket.topHits.hits.hits[0],
count: bucket.doc_count,

View file

@ -30,6 +30,7 @@ import type {
} from './types';
import { shouldFilterByCardinality } from './utils';
import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring';
import { getMaxSignalsWarning } from '../utils/utils';
interface FindThresholdSignalsParams {
from: string;
@ -70,6 +71,7 @@ export const findThresholdSignals = async ({
buckets: ThresholdBucket[];
searchDurations: string[];
searchErrors: string[];
warnings: string[];
}> => {
// Leaf aggregations used below
let sortKeys;
@ -78,6 +80,7 @@ export const findThresholdSignals = async ({
searchDurations: [],
searchErrors: [],
};
const warnings: string[] = [];
const includeCardinalityFilter = shouldFilterByCardinality(threshold);
@ -166,8 +169,13 @@ export const findThresholdSignals = async ({
}
}
if (buckets.length > maxSignals) {
warnings.push(getMaxSignalsWarning());
}
return {
buckets: buckets.slice(0, maxSignals),
...searchAfterResults,
warnings,
};
};

View file

@ -128,7 +128,7 @@ export const thresholdExecutor = async ({
});
// Look for new events over threshold
const { buckets, searchErrors, searchDurations } = await findThresholdSignals({
const { buckets, searchErrors, searchDurations, warnings } = await findThresholdSignals({
inputIndexPattern: inputIndex,
from: tuple.from.toISOString(),
to: tuple.to.toISOString(),
@ -164,6 +164,7 @@ export const thresholdExecutor = async ({
result.errors.push(...previousSearchErrors);
result.errors.push(...searchErrors);
result.warningMessages.push(...warnings);
result.searchAfterTimes = searchDurations;
const createdAlerts = createResult.createdItems.map((alert) => {

View file

@ -19,6 +19,7 @@ import {
mergeSearchResults,
getSafeSortIds,
addToSearchAfterReturn,
getMaxSignalsWarning,
} from './utils';
import type { SearchAfterAndBulkCreateParams, SearchAfterAndBulkCreateReturnType } from '../types';
import { withSecuritySpan } from '../../../../utils/with_security_span';
@ -61,7 +62,7 @@ export const searchAfterAndBulkCreate = async ({
});
}
while (toReturn.createdSignalsCount < tuple.maxSignals) {
while (toReturn.createdSignalsCount <= tuple.maxSignals) {
try {
let mergedSearchResults = createSearchResultReturnType();
ruleExecutionLogger.debug(`sortIds: ${sortIds}`);
@ -138,23 +139,23 @@ export const searchAfterAndBulkCreate = async ({
// skip the call to bulk create and proceed to the next search_after,
// if there is a sort id to continue the search_after with.
if (includedEvents.length !== 0) {
// make sure we are not going to create more signals than maxSignals allows
const limitedEvents = includedEvents.slice(
0,
tuple.maxSignals - toReturn.createdSignalsCount
);
const enrichedEvents = await enrichment(limitedEvents);
const enrichedEvents = await enrichment(includedEvents);
const wrappedDocs = wrapHits(enrichedEvents, buildReasonMessage);
const bulkCreateResult = await bulkCreate(
wrappedDocs,
undefined,
tuple.maxSignals - toReturn.createdSignalsCount,
createEnrichEventsFunction({
services,
logger: ruleExecutionLogger,
})
);
if (bulkCreateResult.alertsWereTruncated) {
toReturn.warningMessages.push(getMaxSignalsWarning());
break;
}
addToSearchAfterReturn({ current: toReturn, next: bulkCreateResult });
ruleExecutionLogger.debug(`created ${bulkCreateResult.createdItemsCount} signals`);

View file

@ -953,3 +953,7 @@ export const getUnprocessedExceptionsWarnings = (
)}`;
}
};
export const getMaxSignalsWarning = (): string => {
return `This rule reached the maximum alert limit for the rule execution. Some alerts were not created.`;
};

View file

@ -34,7 +34,10 @@ export const createSecuritySolutionAlerts = async (
log: ToolingLog,
numberOfSignals: number = 1
): Promise<estypes.SearchResponse<DetectionAlert & RiskEnrichmentFields>> => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, numberOfSignals, [id]);

View file

@ -800,7 +800,10 @@ export default ({ getService }: FtrProviderContext): void => {
});
it('updates alert status when the status is updated and syncAlerts=true', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const postedCase = await createCase(supertest, postCaseReq);
const { id } = await createRule(supertest, log, rule);
@ -856,7 +859,10 @@ export default ({ getService }: FtrProviderContext): void => {
});
it('does NOT updates alert status when the status is updated and syncAlerts=false', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const postedCase = await createCase(supertest, {
...postCaseReq,
@ -909,7 +915,10 @@ export default ({ getService }: FtrProviderContext): void => {
});
it('it updates alert status when syncAlerts is turned on', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const postedCase = await createCase(supertest, {
...postCaseReq,
@ -982,7 +991,10 @@ export default ({ getService }: FtrProviderContext): void => {
});
it('it does NOT updates alert status when syncAlerts is turned off', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const postedCase = await createCase(supertest, postCaseReq);
const { id } = await createRule(supertest, log, rule);

View file

@ -55,7 +55,10 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should be able to execute and get 10 signals', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 10, [id]);
@ -64,7 +67,10 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should be have set the signals in an open state initially', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 10, [id]);
@ -76,7 +82,10 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should be able to get a count of 10 closed signals when closing 10', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 10, [id]);
@ -102,7 +111,10 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should be able close 10 signals immediately and they all should be closed', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 10, [id]);

View file

@ -58,7 +58,10 @@ export default ({ getService }: FtrProviderContext) => {
];
indexTestCases.forEach((index) => {
it(`for KQL rule with index param: ${index}`, async () => {
const rule = getRuleForSignalTesting(index);
const rule = {
...getRuleForSignalTesting(index),
query: 'process.executable: "/usr/bin/sudo"',
};
await createUserAndRole(getService, ROLES.detections_admin);
const { id } = await createRuleWithAuth(supertestWithoutAuth, rule, {
user: ROLES.detections_admin,

View file

@ -123,11 +123,14 @@ export default ({ getService }: FtrProviderContext) => {
this pops up again elsewhere.
*/
it('should create a single rule with a rule_id and validate it ran successfully', async () => {
const simpleRule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(simpleRule)
.send(rule)
.expect(200);
await waitForRuleSuccess({ supertest, log, id: body.id });
@ -161,11 +164,14 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should create a single rule with a rule_id and an index pattern that does not match anything and an index pattern that does and the rule should be successful', async () => {
const simpleRule = getRuleForSignalTesting(['does-not-exist-*', 'auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['does-not-exist-*', 'auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
.set('kbn-xsrf', 'true')
.send(simpleRule)
.send(rule)
.expect(200);
await waitForRuleSuccess({ supertest, log, id: body.id });

View file

@ -112,11 +112,14 @@ export default ({ getService }: FtrProviderContext): void => {
this pops up again elsewhere.
*/
it('should create a single rule with a rule_id and validate it ran successfully', async () => {
const simpleRule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_BULK_CREATE)
.set('kbn-xsrf', 'true')
.send([simpleRule])
.send([rule])
.expect(200);
await waitForRuleSuccess({ supertest, log, id: body[0].id });

View file

@ -72,7 +72,10 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should return execution events for a rule that has executed successfully', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForEventLogExecuteComplete(es, log, id);

View file

@ -89,7 +89,10 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should be able to execute and get 10 signals', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 10, [id]);
@ -98,7 +101,10 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should be have set the signals in an open state initially', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 10, [id]);
@ -110,7 +116,10 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should be able to get a count of 10 closed signals when closing 10', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 10, [id]);
@ -136,7 +145,10 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should be able close signals immediately and they all should be closed', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 1, [id]);

View file

@ -558,9 +558,10 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should generate a signal-on-legacy-signal with legacy index pattern', async () => {
const rule: SavedQueryRuleCreateProps = getSavedQueryRuleForSignalTesting([
`.siem-signals-*`,
]);
const rule: SavedQueryRuleCreateProps = {
...getSavedQueryRuleForSignalTesting([`.siem-signals-*`]),
query: 'agent.name: "security-linux-1.example.dev"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 1, [id]);
@ -571,9 +572,10 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should generate a signal-on-legacy-signal with AAD index pattern', async () => {
const rule: SavedQueryRuleCreateProps = getSavedQueryRuleForSignalTesting([
`.alerts-security.alerts-default`,
]);
const rule: SavedQueryRuleCreateProps = {
...getSavedQueryRuleForSignalTesting([`.alerts-security.alerts-default`]),
query: 'agent.name: "security-linux-1.example.dev"',
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 1, [id]);
@ -599,7 +601,11 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should generate a signal-on-legacy-signal with legacy index pattern', async () => {
const rule: EqlRuleCreateProps = getEqlRuleForSignalTesting(['.siem-signals-*']);
const rule: EqlRuleCreateProps = {
...getEqlRuleForSignalTesting(['.siem-signals-*']),
query: 'any where agent.name == "security-linux-1.example.dev"',
max_signals: 1000,
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 1, [id]);
@ -610,9 +616,11 @@ export default ({ getService }: FtrProviderContext) => {
});
it('should generate a signal-on-legacy-signal with AAD index pattern', async () => {
const rule: EqlRuleCreateProps = getEqlRuleForSignalTesting([
`.alerts-security.alerts-default`,
]);
const rule: EqlRuleCreateProps = {
...getEqlRuleForSignalTesting([`.alerts-security.alerts-default`]),
query: 'any where agent.name == "security-linux-1.example.dev"',
max_signals: 1000,
};
const { id } = await createRule(supertest, log, rule);
await waitForRuleSuccess({ supertest, log, id });
await waitForSignalsToBePresent(supertest, log, 1, [id]);

View file

@ -26,6 +26,7 @@ import {
ALERT_ORIGINAL_EVENT_CATEGORY,
ALERT_GROUP_ID,
} from '@kbn/security-solution-plugin/common/field_maps/field_names';
import { getMaxSignalsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
import {
createRule,
deleteAllRules,
@ -175,6 +176,14 @@ export default ({ getService }: FtrProviderContext) => {
expect(previewAlerts.length).eql(maxSignals);
});
it('generates max signals warning when circuit breaker is hit', async () => {
const rule: EqlRuleCreateProps = {
...getEqlRuleForSignalTesting(['auditbeat-*']),
};
const { logs } = await previewRule({ supertest, rule });
expect(logs[0].warnings).contain(getMaxSignalsWarning());
});
it('uses the provided event_category_override', async () => {
const rule: EqlRuleCreateProps = {
...getEqlRuleForSignalTesting(['auditbeat-*']),

View file

@ -23,6 +23,7 @@ import {
ALERT_DEPTH,
ALERT_ORIGINAL_TIME,
} from '@kbn/security-solution-plugin/common/field_maps/field_names';
import { getMaxSignalsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
import { expect } from 'expect';
import {
createListsIndex,
@ -158,6 +159,22 @@ export default ({ getService }: FtrProviderContext) => {
);
});
it('generates max signals warning when circuit breaker is exceeded', async () => {
const { logs } = await previewRule({
supertest,
rule: { ...rule, anomaly_threshold: 1, max_signals: 5 }, // This threshold generates 10 alerts with the current esArchive
});
expect(logs[0].warnings).toContain(getMaxSignalsWarning());
});
it("doesn't generate max signals warning when circuit breaker is met, but not exceeded", async () => {
const { logs } = await previewRule({
supertest,
rule: { ...rule, anomaly_threshold: 1, max_signals: 10 },
});
expect(logs[0].warnings).not.toContain(getMaxSignalsWarning());
});
it('should create 7 alerts from ML rule when records meet anomaly_threshold', async () => {
const { previewId } = await previewRule({
supertest,

View file

@ -15,6 +15,7 @@ import {
getNewTermsRuntimeMappings,
AGG_FIELD_NAME,
} from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/new_terms/utils';
import { getMaxSignalsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
import {
createRule,
deleteAllRules,
@ -232,6 +233,32 @@ export default ({ getService }: FtrProviderContext) => {
});
});
it('generates max signals warning when circuit breaker is exceeded', async () => {
const rule: NewTermsRuleCreateProps = {
...getCreateNewTermsRulesSchemaMock('rule-1', true),
new_terms_fields: ['process.pid'],
from: '2018-02-19T20:42:00.000Z',
// Set the history_window_start close to 'from' so we should alert on all terms in the time range
history_window_start: '2018-02-19T20:41:59.000Z',
};
const { logs } = await previewRule({ supertest, rule });
expect(logs[0].warnings).contain(getMaxSignalsWarning());
});
it("doesn't generate max signals warning when circuit breaker is met but not exceeded", async () => {
const rule: NewTermsRuleCreateProps = {
...getCreateNewTermsRulesSchemaMock('rule-1', true),
new_terms_fields: ['host.ip'],
from: '2019-02-19T20:42:00.000Z',
history_window_start: '2019-01-19T20:42:00.000Z',
max_signals: 3,
};
const { logs } = await previewRule({ supertest, rule });
expect(logs[0].warnings).not.contain(getMaxSignalsWarning());
});
it('should generate 3 alerts when 1 document has 3 new values', async () => {
const rule: NewTermsRuleCreateProps = {
...getCreateNewTermsRulesSchemaMock('rule-1', true),

View file

@ -37,6 +37,7 @@ import {
ALERT_ORIGINAL_EVENT,
} from '@kbn/security-solution-plugin/common/field_maps/field_names';
import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '@kbn/security-solution-plugin/common/constants';
import { getMaxSignalsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
import { deleteAllExceptions } from '../../../lists_api_integration/utils';
import {
createExceptionList,
@ -107,6 +108,24 @@ export default ({ getService }: FtrProviderContext) => {
expect(alerts.hits.hits[0]._source?.['kibana.alert.ancestors'][0].id).eql(ID);
});
it('generates max signals warning when circuit breaker is hit', async () => {
const rule: QueryRuleCreateProps = {
...getRuleForSignalTesting(['auditbeat-*']),
};
const { logs } = await previewRule({ supertest, rule });
expect(logs[0].warnings).contain(getMaxSignalsWarning());
});
it("doesn't generate max signals warning when circuit breaker is met but not exceeded", async () => {
const rule = {
...getRuleForSignalTesting(['auditbeat-*']),
query: 'process.executable: "/usr/bin/sudo"',
max_signals: 10,
};
const { logs } = await previewRule({ supertest, rule });
expect(logs[0].warnings).not.contain(getMaxSignalsWarning());
});
it('should abide by max_signals > 100', async () => {
const maxSignals = 200;
const rule: QueryRuleCreateProps = {

View file

@ -34,6 +34,7 @@ import {
ALERT_ORIGINAL_TIME,
} from '@kbn/security-solution-plugin/common/field_maps/field_names';
import { RuleExecutionStatus } from '@kbn/security-solution-plugin/common/detection_engine/rule_monitoring';
import { getMaxSignalsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
import {
previewRule,
getOpenSignals,
@ -501,6 +502,12 @@ export default ({ getService }: FtrProviderContext) => {
});
});
it('generates max signals warning when circuit breaker is hit', async () => {
const rule: ThreatMatchRuleCreateProps = { ...createThreatMatchRule(), max_signals: 87 }; // Query generates 88 alerts with current esArchive
const { logs } = await previewRule({ supertest, rule });
expect(logs[0].warnings).contain(getMaxSignalsWarning());
});
it('terms and match should have the same alerts with pagination', async () => {
const termRule: ThreatMatchRuleCreateProps = createThreatMatchRule({
override: {

View file

@ -21,6 +21,7 @@ import {
ALERT_ORIGINAL_TIME,
ALERT_THRESHOLD_RESULT,
} from '@kbn/security-solution-plugin/common/field_maps/field_names';
import { getMaxSignalsWarning } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/utils';
import {
createRule,
getOpenSignals,
@ -93,6 +94,32 @@ export default ({ getService }: FtrProviderContext) => {
});
});
it('generates max signals warning when circuit breaker is exceeded', async () => {
const rule: ThresholdRuleCreateProps = {
...getThresholdRuleForSignalTesting(['auditbeat-*']),
threshold: {
field: 'host.id',
value: 1, // This value generates 7 alerts with the current esArchive
},
max_signals: 5,
};
const { logs } = await previewRule({ supertest, rule });
expect(logs[0].warnings).contain(getMaxSignalsWarning());
});
it("doesn't generate max signals warning when circuit breaker is met but not exceeded", async () => {
const rule: ThresholdRuleCreateProps = {
...getThresholdRuleForSignalTesting(['auditbeat-*']),
threshold: {
field: 'host.id',
value: 1, // This value generates 7 alerts with the current esArchive
},
max_signals: 7,
};
const { logs } = await previewRule({ supertest, rule });
expect(logs[0].warnings).not.contain(getMaxSignalsWarning());
});
it('generates 2 signals from Threshold rules when threshold is met', async () => {
const rule: ThresholdRuleCreateProps = {
...getThresholdRuleForSignalTesting(['auditbeat-*']),