[Security Solution] [Detections] Support user error tagging for eql rule types (#213470)

## Summary

adds support for tagging user errors in eql rule type and tags missing
data view id as user error
This commit is contained in:
Devin W. Hurley 2025-03-07 19:24:05 -05:00 committed by GitHub
parent 3f90203406
commit b64be404b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 43 additions and 38 deletions

View file

@ -18,6 +18,7 @@ import type { FieldMap } from '@kbn/alerts-as-data-utils';
import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils';
import { getIndexListFromEsqlQuery } from '@kbn/securitysolution-utils';
import type { FormatAlert } from '@kbn/alerting-plugin/server/types';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import {
checkPrivilegesFromEsClient,
getExceptions,
@ -42,7 +43,7 @@ import { truncateList } from '../rule_monitoring';
import aadFieldConversion from '../routes/index/signal_aad_mapping.json';
import { extractReferences, injectReferences } from './saved_object_references';
import { withSecuritySpan } from '../../../utils/with_security_span';
import { getInputIndex, DataViewError } from './utils/get_input_output_index';
import { getInputIndex } from './utils/get_input_output_index';
import { TIMESTAMP_RUNTIME_FIELD } from './constants';
import { buildTimestampRuntimeMapping } from './utils/build_timestamp_runtime_mapping';
import { alertsFieldMap, rulesFieldMap } from '../../../../common/field_maps';
@ -238,15 +239,18 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
inputIndex = index ?? [];
runtimeMappings = dataViewRuntimeMappings;
} catch (exc) {
const errorMessage =
exc instanceof DataViewError
? `Data View not found ${exc}`
: `Check for indices to search failed ${exc}`;
await ruleExecutionLogger.logStatusChange({
newStatus: RuleExecutionStatusEnum.failed,
message: errorMessage,
});
if (SavedObjectsErrorHelpers.isNotFoundError(exc)) {
await ruleExecutionLogger.logStatusChange({
newStatus: RuleExecutionStatusEnum.failed,
message: `Data View not found ${exc}`,
userError: true,
});
} else {
await ruleExecutionLogger.logStatusChange({
newStatus: RuleExecutionStatusEnum.failed,
message: `Check for indices to search failed ${exc}`,
});
}
return { state: result.state };
}

View file

@ -66,11 +66,11 @@ describe('eql_executor', () => {
});
it('should classify EQL verification exceptions as "user errors" when reporting to the framework', async () => {
alertServices.scopedClusterClient.asCurrentUser.eql.search.mockRejectedValue({
name: 'ResponseError',
message:
'verification_exception\n\tRoot causes:\n\t\tverification_exception: Found 1 problem\nline 1:1: Unknown column [event.category]',
});
alertServices.scopedClusterClient.asCurrentUser.eql.search.mockRejectedValue(
new Error(
'verification_exception\n\tRoot causes:\n\t\tverification_exception: Found 1 problem\nline 1:1: Unknown column [event.category]'
)
);
const { result } = await eqlExecutor({
sharedParams,
services: alertServices,

View file

@ -51,6 +51,7 @@ import * as i18n from '../translations';
import { alertSuppressionTypeGuard } from '../utils/get_is_alert_suppression_active';
import { isEqlSequenceQuery } from '../../../../../common/detection_engine/utils';
import { logShardFailures } from '../utils/log_shard_failure';
import { checkErrorDetails } from '../utils/check_error_details';
import { wrapSequences } from './wrap_sequences';
interface EqlExecutorParams {
@ -214,12 +215,7 @@ export const eqlExecutor = async ({
});
return { result, ...(isLoggedRequestsEnabled ? { loggedRequests } : {}) };
} catch (error) {
if (
typeof error.message === 'string' &&
(error.message as string).includes('verification_exception')
) {
// We report errors that are more related to user configuration of rules rather than system outages as "user errors"
// so SLO dashboards can show less noise around system outages
if (checkErrorDetails(error).isUserError) {
result.userError = true;
}
result.errors.push(error.message);

View file

@ -31,7 +31,7 @@ import type { RulePreviewLoggedRequest } from '../../../../../common/api/detecti
import type { CreateRuleOptions, SecuritySharedParams, SignalSource } from '../types';
import { logEsqlRequest } from '../utils/logged_requests';
import { getDataTierFilter } from '../utils/get_data_tier_filter';
import { checkErrorDetails } from './utils/check_error_details';
import { checkErrorDetails } from '../utils/check_error_details';
import * as i18n from '../translations';
import {

View file

@ -44,6 +44,12 @@ describe('checkErrorDetails', () => {
expect(checkErrorDetails(new Error(errorMessage))).toHaveProperty('isUserError', true);
});
it('should mark string literal error as user error from error message', () => {
const errorMessage = `parsing_exception
Root causes:
parsing_exception: line 1:28: Use double quotes ["] to define string literals, not single quotes [']`;
expect(checkErrorDetails(new Error(errorMessage))).toHaveProperty('isUserError', true);
});
});
describe('data source verification errors', () => {

View file

@ -9,10 +9,11 @@ import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
import type { MockedLogger } from '@kbn/logging-mocks';
import { loggerMock } from '@kbn/logging-mocks';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../../common/constants';
import type { GetInputIndex } from './get_input_output_index';
import { getInputIndex, DataViewError } from './get_input_output_index';
import { getInputIndex } from './get_input_output_index';
describe('get_input_output_index', () => {
let servicesMock: RuleExecutorServicesMock;
@ -181,7 +182,7 @@ describe('get_input_output_index', () => {
test('Returns error if no matching data view found', async () => {
servicesMock.savedObjectsClient.get.mockRejectedValue(
new Error('Saved object [index-pattern/12345] not found')
SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '12345')
);
await expect(
getInputIndex({
@ -199,18 +200,21 @@ describe('get_input_output_index', () => {
test('Returns error of DataViewErrorType', async () => {
servicesMock.savedObjectsClient.get.mockRejectedValue(
new Error('Saved object [index-pattern/12345] not found')
SavedObjectsErrorHelpers.createGenericNotFoundError('index-pattern', '12345')
);
await expect(
getInputIndex({
expect.assertions(1);
try {
await getInputIndex({
services: servicesMock,
version: '8.0.0',
index: [],
dataViewId: '12345',
ruleId: 'rule_1',
logger,
})
).rejects.toBeInstanceOf(DataViewError);
});
} catch (exc) {
expect(SavedObjectsErrorHelpers.isNotFoundError(exc)).toBeTruthy();
}
});
});
});

View file

@ -47,15 +47,10 @@ export const getInputIndex = async ({
// If data views defined, use it
if (dataViewId != null && dataViewId !== '') {
// Check to see that the selected dataView exists
let dataView;
try {
dataView = await services.savedObjectsClient.get<DataViewAttributes>(
'index-pattern',
dataViewId
);
} catch (exc) {
throw new DataViewError(exc.message);
}
const dataView = await services.savedObjectsClient.get<DataViewAttributes>(
'index-pattern',
dataViewId
);
const indices = dataView.attributes.title.split(',');
const runtimeMappings =
dataView.attributes.runtimeFieldMap != null