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 aade8be4f503f..e2e3b239fdaec 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 080b704e9c193..2e622610c09ca 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -6,7 +6,7 @@ */ import { EntriesArray } from '../shared_imports'; -import { Type } from './schemas/common/schemas'; +import { Type, JobStatus } from './schemas/common/schemas'; export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); @@ -31,3 +31,6 @@ export const isThresholdRule = (ruleType: Type | undefined): boolean => ruleType export const isQueryRule = (ruleType: Type | undefined): boolean => ruleType === 'query' || ruleType === 'saved_query'; export const isThreatMatchRule = (ruleType: Type): boolean => ruleType === 'threat_match'; + +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 f5ecba3fc86dd..811a0cb19e073 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 @@ -254,7 +254,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 0f84e0dd7cd16..458640191e2e1 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, @@ -195,7 +196,7 @@ export const getColumns = ({ return ( <> - {value ?? getEmptyTagValue()} + {getRuleStatusText(value) ?? getEmptyTagValue()} ); @@ -391,7 +392,7 @@ export const getMonitoringColumns = ( return ( <> - {value ?? getEmptyTagValue()} + {getRuleStatusText(value) ?? getEmptyTagValue()} ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts index 5893b05a1d811..04f2b6ff799da 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts @@ -23,7 +23,7 @@ export const ruleStatusServiceFactoryMock = async ({ success: jest.fn(), - warning: jest.fn(), + partialFailure: jest.fn(), error: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts index 6e93ed256321e..dc4663db6c74d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts @@ -24,7 +24,7 @@ interface Attributes { export interface RuleStatusService { goingToRun: () => 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 fda4150522dc5..e3932b4a31a88 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 @@ -106,7 +106,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); (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(0)); @@ -223,8 +223,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*"]' ); }); @@ -246,8 +246,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/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 69e4bdf5429a5..882c976997dea 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 46505c13b7555..51d935990a5bd 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 @@ -79,7 +79,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 && @@ -91,7 +91,7 @@ export const hasReadIndexPrivileges = async ( indexesWithNoReadPrivileges )}`; logger.error(buildRuleMessage(errorString)); - await ruleStatusService.warning(errorString); + await ruleStatusService.partialFailure(errorString); return true; } return false; @@ -119,7 +119,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 && @@ -140,7 +140,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 2e2a2db777969..fd475d03f4fec 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 @@ -116,7 +116,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) @@ -124,7 +124,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) @@ -132,7 +132,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.' ); @@ -264,7 +264,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 @@ -277,7 +277,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) @@ -285,7 +285,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( @@ -293,7 +295,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 @@ -305,7 +307,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 @@ -314,7 +316,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'); }); }); });