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] Adds max signals warning to UI propagated rule warnings #154112

Merged
merged 21 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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 @@ -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 };
Comment on lines +127 to +128
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Short circuiting here if maxAlerts is passed in as zero. We're using this function's deduping ability to check that: if max signals was hit, are any remaining alerts actual alerts or are they duplicates? If there are remaining alerts in filteredAlerts at this point, we can assume they are actual alerts and don't need any additional function logic

}

let enrichedAlerts = filteredAlerts;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
// }

dplumlee marked this conversation as resolved.
Show resolved Hide resolved
let threatPitId: OpenPointInTimeResponse['id'] = (
await services.scopedClusterClient.asCurrentUser.openPointInTime({
index: threatIndex,
Expand Down Expand Up @@ -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 {
results.warningMessages.push(getMaxSignalsWarning());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (results.warningMessages.includes(getMaxSignalsWarning())) {
results.warningMessages = uniq(results.warningMessages);
} else {
results.warningMessages.push(getMaxSignalsWarning());
}
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}`
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const findMlSignals = async ({
anomalyThreshold,
from,
to,
maxSignals,
exceptionFilter,
}: {
ml: MlPluginSetup;
Expand All @@ -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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
addToSearchAfterReturn,
createErrorsFromShard,
createSearchAfterReturnType,
getMaxSignalsWarning,
mergeReturns,
} from '../utils/utils';
import type { SetupPlugins } from '../../../../plugin';
Expand Down Expand Up @@ -102,9 +103,14 @@ export const mlExecutor = async ({
anomalyThreshold: ruleParams.anomalyThreshold,
from: tuple.from.toISOString(),
to: tuple.to.toISOString(),
maxSignals: tuple.maxSignals,
exceptionFilter,
});

if (anomalyResults.hits.total && anomalyResults.hits.total > tuple.maxSignals) {
result.warningMessages.push(getMaxSignalsWarning());
}

const [filteredAnomalyHits, _] = await filterEventsAgainstList({
listClient,
ruleExecutionLogger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import {
addToSearchAfterReturn,
createSearchAfterReturnType,
getMaxSignalsWarning,
getUnprocessedExceptionsWarnings,
} from '../utils/utils';
import { createEnrichEventsFunction } from '../utils/enrichments';
Expand Down Expand Up @@ -153,7 +154,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.
Expand Down Expand Up @@ -311,6 +312,10 @@ export const createNewTermsAlertType = (
})
);

if (bulkCreateResult.alertsWereTruncated) {
result.warningMessages.push(getMaxSignalsWarning());
}

addToSearchAfterReturn({ current: result, next: bulkCreateResult });

if (bulkCreateResult.alertsWereTruncated) {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const buildGroupByFieldAggregation = ({
},
},
})),
size: maxSignals,
size: maxSignals + 1, // Add extra bucket to check if there's more data after max signals
},
aggs: {
topHits: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import type {
SearchAfterAndBulkCreateReturnType,
SignalSource,
} from '../../types';
import { addToSearchAfterReturn, getUnprocessedExceptionsWarnings } from '../../utils/utils';
import {
addToSearchAfterReturn,
getMaxSignalsWarning,
getUnprocessedExceptionsWarnings,
} from '../../utils/utils';
import type { SuppressionBuckets } from './wrap_suppressed_alerts';
import { wrapSuppressedAlerts } from './wrap_suppressed_alerts';
import { buildGroupByFieldAggregation } from './build_group_by_field_aggregation';
Expand Down Expand Up @@ -206,6 +210,10 @@ export const groupAndBulkCreate = async ({
return toReturn;
}

if (buckets.length > tuple.maxSignals) {
toReturn.warningMessages.push(getMaxSignalsWarning());
}

const suppressionBuckets: SuppressionBuckets[] = buckets.map((bucket) => ({
event: bucket.topHits.hits.hits[0],
count: bucket.doc_count,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -70,6 +71,7 @@ export const findThresholdSignals = async ({
buckets: ThresholdBucket[];
searchDurations: string[];
searchErrors: string[];
warnings: string[];
}> => {
// Leaf aggregations used below
let sortKeys;
Expand All @@ -78,6 +80,7 @@ export const findThresholdSignals = async ({
searchDurations: [],
searchErrors: [],
};
const warnings: string[] = [];

const includeCardinalityFilter = shouldFilterByCardinality(threshold);

Expand Down Expand Up @@ -166,8 +169,13 @@ export const findThresholdSignals = async ({
}
}

if (buckets.length >= maxSignals) {
warnings.push(getMaxSignalsWarning());
}

return {
buckets: buckets.slice(0, maxSignals),
...searchAfterResults,
warnings,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
mergeSearchResults,
getSafeSortIds,
addToSearchAfterReturn,
getMaxSignalsWarning,
} from './utils';
import type { SearchAfterAndBulkCreateParams, SearchAfterAndBulkCreateReturnType } from '../types';
import { withSecuritySpan } from '../../../../utils/with_security_span';
Expand Down Expand Up @@ -60,7 +61,7 @@ export const searchAfterAndBulkCreate = async ({
});
}

while (toReturn.createdSignalsCount < tuple.maxSignals) {
while (toReturn.createdSignalsCount <= tuple.maxSignals) {
try {
let mergedSearchResults = createSearchResultReturnType();
ruleExecutionLogger.debug(`sortIds: ${sortIds}`);
Expand Down Expand Up @@ -136,23 +137,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`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -953,3 +953,7 @@ export const getUnprocessedExceptionsWarnings = (
)}`;
}
};

export const getMaxSignalsWarning = (): string => {
return `The max alerts circut breaker was hit but there might still be alerts this rule is missing`;
};
5 changes: 4 additions & 1 deletion x-pack/test/cases_api_integration/common/lib/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export const createSecuritySolutionAlerts = async (
supertest: SuperTest.SuperTest<SuperTest.Test>,
log: ToolingLog
): 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, 1, [id]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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]);
Expand All @@ -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]);
Expand All @@ -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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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 });
Expand Down
Loading