// Reproduces createdSignalsCount inflation when no-op chunks return currentResult.
// We seed 2 matching events followed by 10 non-matching events and force 1 event/page.
// The first two pages create 2 alerts; later no-op pages inflate the counter and can
// trigger a false max-signals warning despite only 2 created preview alerts.
it.only('reproduces false max alerts warning when later event pages have no threat matches', async () => {
const id = `repro${Date.now()}`;
const baseTs = moment();
const timestamp = baseTs.toISOString();
const matchingEvents = [
{
id,
user: { name: 'matchuser' },
'@timestamp': baseTs.clone().subtract(1, 's').toISOString(),
'event.ingested': baseTs.clone().subtract(1, 's').toISOString(),
},
{
id,
user: { name: 'matchuser' },
'@timestamp': baseTs.clone().subtract(2, 's').toISOString(),
'event.ingested': baseTs.clone().subtract(2, 's').toISOString(),
},
];
const nonMatchingEvents = Array.from({ length: 10 }, (_, i) => ({
id,
user: { name: `eventmiss${i + 1}` },
'@timestamp': baseTs
.clone()
.subtract(i + 3, 's')
.toISOString(),
'event.ingested': baseTs
.clone()
.subtract(i + 3, 's')
.toISOString(),
}));
const threats = [
{
...threatDoc(id, timestamp),
user: { name: 'matchuser' },
},
...Array.from({ length: 19 }, (_, i) => ({
...threatDoc(
id,
baseTs
.clone()
.subtract(i + 1, 'm')
.toISOString()
),
user: { name: `threatfiller${i + 1}` },
})),
];
await indexListOfDocuments([...matchingEvents, ...nonMatchingEvents, ...threats]);
const rule: ThreatMatchRuleCreateProps = {
...threatMatchRuleEcsComplaint(id),
threat_mapping: [
{
entries: [{ field: 'user.name', value: 'user.name', type: 'mapping' }],
},
],
items_per_search: 1,
concurrent_searches: 1,
};
const { logs, previewId } = await previewRule({ supertest, rule });
const previewAlerts = await getPreviewAlerts({ es, previewId, size: 1000 });
console.log('previwaAlerts length ---- ');
console.log(previewAlerts.length);
const allWarnings = logs.flatMap((l) => l.warnings ?? []);
// Exact created alert count can vary with fixture composition, but should remain small
// while the max alerts warning is still emitted due to inflated createdSignalsCount.
expect(previewAlerts.length).toBeGreaterThan(0);
expect(previewAlerts.length).toBeLessThan(100);
expect(allWarnings).not.toContain(getMaxAlertsWarning());
});
In the source event first implementation for IM rules, there's a bug with the logic that counts created alerts. Here if the page of source documents matches no indicators, we short circuit and return
currentResultfrom the function.currentResultcontains the data about rule execution so far - including the number of alerts created from previous pages. However, the function is only supposed to return information about what happened for the current page, e.g. how many alerts were created from the current page of source docs. Because of this, every page that generates no alerts will double the alert count in the higher level result tracking without actually creating more alerts.Since the paging logic will break out of the loop if the (incorrect) count hits max signals, the warning is still accurate: some alerts may have been missed.
Luckily, the fix is simple. When no alerts are created, and in error cases, we need to return an object from
createEventSignalthat accurately represents that no alerts were created for that page rather than returningcurrentResult. It appears that we should not passcurrentResultintocreateEventSignalat all since the function does not need any information about the overall results - this will help clarify the logical model of the functions, i.e. thatcreateEventSignalshould deal with one page of source docs in isolation and return information about that specific page.Repro steps
@nkhristinin created an integration test to reproduce the issue. Add the integration test in
x-pack/solutions/security/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match.ts.Details