diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index a5398f291627c..86ced392652c0 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -325,6 +325,7 @@ export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run': null, + 'partial failure': null, warning: null, }); export type JobStatus = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 838bac542bb87..a2c362b08dc7a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -12,7 +12,7 @@ import { EntriesArray, ExceptionListItemSchema, } from '../shared_imports'; -import { Type } from './schemas/common/schemas'; +import { Type, JobStatus } from './schemas/common/schemas'; export const hasLargeValueItem = ( exceptionItems: Array @@ -54,3 +54,6 @@ export const normalizeThresholdField = ( ? [] : [thresholdField!]; }; + +export const getRuleStatusText = (value: JobStatus | null | undefined): JobStatus | null => + value === 'partial failure' ? 'warning' : value != null ? value : null; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index b8f6c4bde3e8f..3265cd65cfd9e 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -256,7 +256,6 @@ export interface RuleStatus { } export type RuleStatusType = - | 'executing' | 'failed' | 'going to run' | 'succeeded' diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index d2eadef48d9c7..bb5dd590a8ea2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -39,6 +39,7 @@ import { LocalizedDateTooltip } from '../../../../../common/components/localized import { LinkAnchor } from '../../../../../common/components/links'; import { getToolTipContent, canEditRuleWithActions } from '../../../../../common/utils/privileges'; import { TagsDisplay } from './tag_display'; +import { getRuleStatusText } from '../../../../../../common/detection_engine/utils'; export const getActions = ( dispatch: React.Dispatch, @@ -201,7 +202,7 @@ export const getColumns = ({ return ( <> - {value ?? getEmptyTagValue()} + {getRuleStatusText(value) ?? getEmptyTagValue()} ); @@ -398,7 +399,7 @@ export const getMonitoringColumns = ( return ( <> - {value ?? getEmptyTagValue()} + {getRuleStatusText(value) ?? getEmptyTagValue()} ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 4e225917f076d..8b57d816d678f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -107,6 +107,7 @@ import * as statusI18n from '../../../../components/rules/rule_status/translatio import * as i18n from './translations'; import { isTab } from '../../../../../common/components/accessibility/helpers'; import { NeedAdminForUpdateRulesCallOut } from '../../../../components/callouts/need_admin_for_update_callout'; +import { getRuleStatusText } from '../../../../../../common/detection_engine/utils'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -328,7 +329,10 @@ const RuleDetailsPageComponent = () => { ) : ( <> - + Promise; success: (message: string, attributes?: Attributes) => Promise; - warning: (message: string, attributes?: Attributes) => Promise; + partialFailure: (message: string, attributes?: Attributes) => Promise; error: (message: string, attributes?: Attributes) => Promise; } @@ -55,6 +55,13 @@ export const buildRuleStatusAttributes: ( lastSuccessMessage: message, }; } + case 'partial failure': { + return { + ...baseAttributes, + lastSuccessAt: now, + lastSuccessMessage: message, + }; + } case 'failed': { return { ...baseAttributes, @@ -102,7 +109,7 @@ export const ruleStatusServiceFactory = async ({ }); }, - warning: async (message, attributes) => { + partialFailure: async (message, attributes) => { const [currentStatus] = await getOrCreateRuleStatuses({ alertId, ruleStatusClient, @@ -110,7 +117,7 @@ export const ruleStatusServiceFactory = async ({ await ruleStatusClient.update(currentStatus.id, { ...currentStatus.attributes, - ...buildRuleStatusAttributes('warning', message, attributes), + ...buildRuleStatusAttributes('partial failure', message, attributes), }); }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index d3d82682cbb4a..aae440725aff6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -107,7 +107,7 @@ describe('rules_notification_alert_type', () => { find: jest.fn(), goingToRun: jest.fn(), error: jest.fn(), - warning: jest.fn(), + partialFailure: jest.fn(), }; (ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService); (getListsClient as jest.Mock).mockReturnValue({ @@ -211,8 +211,8 @@ describe('rules_notification_alert_type', () => { }); payload.params.index = ['some*', 'myfa*', 'anotherindex*']; await alert.executor(payload); - expect(ruleStatusService.warning).toHaveBeenCalled(); - expect(ruleStatusService.warning.mock.calls[0][0]).toContain( + expect(ruleStatusService.partialFailure).toHaveBeenCalled(); + expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( 'Missing required read privileges on the following indices: ["some*"]' ); }); @@ -223,8 +223,8 @@ describe('rules_notification_alert_type', () => { ]); payload = getPayload(getThresholdResult(), alertServices); await alert.executor(payload); - expect(ruleStatusService.warning).toHaveBeenCalled(); - expect(ruleStatusService.warning.mock.calls[0][0]).toContain( + expect(ruleStatusService.partialFailure).toHaveBeenCalled(); + expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' ); }); @@ -235,8 +235,8 @@ describe('rules_notification_alert_type', () => { ]); payload = getPayload(getEqlResult(), alertServices); await alert.executor(payload); - expect(ruleStatusService.warning).toHaveBeenCalled(); - expect(ruleStatusService.warning.mock.calls[0][0]).toContain( + expect(ruleStatusService.partialFailure).toHaveBeenCalled(); + expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( 'Exceptions that use "is in list" or "is not in list" operators are not applied to EQL rules' ); }); @@ -258,8 +258,8 @@ describe('rules_notification_alert_type', () => { }); payload.params.index = ['some*', 'myfa*']; await alert.executor(payload); - expect(ruleStatusService.warning).toHaveBeenCalled(); - expect(ruleStatusService.warning.mock.calls[0][0]).toContain( + expect(ruleStatusService.partialFailure).toHaveBeenCalled(); + expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain( 'This rule may not have the required read privileges to the following indices: ["myfa*","some*"]' ); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index c24b10dc09a86..65efd25c9fba2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -374,7 +374,7 @@ export const signalRulesAlertType = ({ ]); } else if (isThresholdRule(type) && threshold) { if (hasLargeValueItem(exceptionItems ?? [])) { - await ruleStatusService.warning( + await ruleStatusService.partialFailure( 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' ); wroteWarningStatus = true; @@ -577,7 +577,7 @@ export const signalRulesAlertType = ({ throw new Error('EQL query rule must have a query defined'); } if (hasLargeValueItem(exceptionItems ?? [])) { - await ruleStatusService.warning( + await ruleStatusService.partialFailure( 'Exceptions that use "is in list" or "is not in list" operators are not applied to EQL rules' ); wroteWarningStatus = true; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 5478c0dd16f4d..8110c4b868177 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -69,7 +69,7 @@ const ruleStatusServiceMock = { find: jest.fn(), goingToRun: jest.fn(), error: jest.fn(), - warning: jest.fn(), + partialFailure: jest.fn(), }; describe('utils', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 7e3a426e335c3..60c9197c64921 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -84,7 +84,7 @@ export const hasReadIndexPrivileges = async ( indexesWithNoReadPrivileges )}`; logger.error(buildRuleMessage(errorString)); - await ruleStatusService.warning(errorString); + await ruleStatusService.partialFailure(errorString); return true; } else if ( indexesWithReadPrivileges.length === 0 && @@ -96,7 +96,7 @@ export const hasReadIndexPrivileges = async ( indexesWithNoReadPrivileges )}`; logger.error(buildRuleMessage(errorString)); - await ruleStatusService.warning(errorString); + await ruleStatusService.partialFailure(errorString); return true; } return false; @@ -124,7 +124,7 @@ export const hasTimestampFields = async ( : '' }`; logger.error(buildRuleMessage(errorString.trimEnd())); - await ruleStatusService.warning(errorString.trimEnd()); + await ruleStatusService.partialFailure(errorString.trimEnd()); return true; } else if ( !wroteStatus && @@ -145,7 +145,7 @@ export const hasTimestampFields = async ( : timestampFieldCapsResponse.body.fields[timestampField]?.unmapped?.indices )}`; logger.error(buildRuleMessage(errorString)); - await ruleStatusService.warning(errorString); + await ruleStatusService.partialFailure(errorString); return true; } return wroteStatus; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index 6bdf881ba8ca2..29eb84cddcb0b 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -120,7 +120,7 @@ export default ({ getService }: FtrProviderContext) => { expect(statusBody[body.id].current_status.status).to.eql('succeeded'); }); - it('should create a single rule with a rule_id and an index pattern that does not match anything available and warning for the rule', async () => { + it('should create a single rule with a rule_id and an index pattern that does not match anything available and partial failure for the rule', async () => { const simpleRule = getRuleForSignalTesting(['does-not-exist-*']); const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) @@ -128,7 +128,7 @@ export default ({ getService }: FtrProviderContext) => { .send(simpleRule) .expect(200); - await waitForRuleSuccessOrStatus(supertest, body.id, 'warning'); + await waitForRuleSuccessOrStatus(supertest, body.id, 'partial failure'); const { body: statusBody } = await supertest .post(DETECTION_ENGINE_RULES_STATUS_URL) @@ -136,7 +136,7 @@ export default ({ getService }: FtrProviderContext) => { .send({ ids: [body.id] }) .expect(200); - expect(statusBody[body.id].current_status.status).to.eql('warning'); + expect(statusBody[body.id].current_status.status).to.eql('partial failure'); expect(statusBody[body.id].current_status.last_success_message).to.eql( 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["does-not-exist-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated.' ); @@ -290,7 +290,7 @@ export default ({ getService }: FtrProviderContext) => { await esArchiver.unload('security_solution/timestamp_override'); }); - it('should create a single rule which has a timestamp override for an index pattern that does not exist and write a warning status', async () => { + it('should create a single rule which has a timestamp override for an index pattern that does not exist and write a partial failure status', async () => { // defaults to event.ingested timestamp override. // event.ingested is one of the timestamp fields set on the es archive data // inside of x-pack/test/functional/es_archives/security_solution/timestamp_override/data.json.gz @@ -303,7 +303,7 @@ export default ({ getService }: FtrProviderContext) => { const bodyId = body.id; await waitForAlertToComplete(supertest, bodyId); - await waitForRuleSuccessOrStatus(supertest, bodyId, 'warning'); + await waitForRuleSuccessOrStatus(supertest, bodyId, 'partial failure'); const { body: statusBody } = await supertest .post(DETECTION_ENGINE_RULES_STATUS_URL) @@ -311,7 +311,9 @@ export default ({ getService }: FtrProviderContext) => { .send({ ids: [bodyId] }) .expect(200); - expect((statusBody as RuleStatusResponse)[bodyId].current_status?.status).to.eql('warning'); + expect((statusBody as RuleStatusResponse)[bodyId].current_status?.status).to.eql( + 'partial failure' + ); expect( (statusBody as RuleStatusResponse)[bodyId].current_status?.last_success_message ).to.eql( @@ -319,7 +321,7 @@ export default ({ getService }: FtrProviderContext) => { ); }); - it('should create a single rule which has a timestamp override and generates two signals with a "warning" status', async () => { + it('should create a single rule which has a timestamp override and generates two signals with a "partial failure" status', async () => { // defaults to event.ingested timestamp override. // event.ingested is one of the timestamp fields set on the es archive data // inside of x-pack/test/functional/es_archives/security_solution/timestamp_override/data.json.gz @@ -331,7 +333,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); const bodyId = body.id; - await waitForRuleSuccessOrStatus(supertest, bodyId, 'warning'); + await waitForRuleSuccessOrStatus(supertest, bodyId, 'partial failure'); await waitForSignalsToBePresent(supertest, 2, [bodyId]); const { body: statusBody } = await supertest @@ -340,7 +342,7 @@ export default ({ getService }: FtrProviderContext) => { .send({ ids: [bodyId] }) .expect(200); - expect(statusBody[bodyId].current_status.status).to.eql('warning'); + expect(statusBody[bodyId].current_status.status).to.eql('partial failure'); }); }); });