[Cases] Include rule registry client for updating alert statuses (#108588)
* Trying to get import to work * Plumbed alerts client through and logging errors * No longer need the ES cluster client * Fixing types * Fixing imports * Fixing integration tests and refactoring * Throwing an error when rule registry is disabled * Reworking alert update and get to catch errors * Adding tests and fixing errors
This commit is contained in:
parent
7369bdf360
commit
1fd7038b34
|
@ -10,6 +10,7 @@
|
|||
"id":"cases",
|
||||
"kibanaVersion":"kibana",
|
||||
"optionalPlugins":[
|
||||
"ruleRegistry",
|
||||
"security",
|
||||
"spaces"
|
||||
],
|
||||
|
|
|
@ -12,19 +12,11 @@ export const get = async (
|
|||
{ alertsInfo }: AlertGet,
|
||||
clientArgs: CasesClientArgs
|
||||
): Promise<CasesClientGetAlertsResponse> => {
|
||||
const { alertsService, scopedClusterClient, logger } = clientArgs;
|
||||
const { alertsService, logger } = clientArgs;
|
||||
if (alertsInfo.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const alerts = await alertsService.getAlerts({ alertsInfo, scopedClusterClient, logger });
|
||||
if (!alerts) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return alerts.docs.map((alert) => ({
|
||||
id: alert._id,
|
||||
index: alert._index,
|
||||
...alert._source,
|
||||
}));
|
||||
const alerts = await alertsService.getAlerts({ alertsInfo, logger });
|
||||
return alerts ?? [];
|
||||
};
|
||||
|
|
|
@ -7,17 +7,7 @@
|
|||
|
||||
import { CaseStatuses } from '../../../common/api';
|
||||
import { AlertInfo } from '../../common';
|
||||
|
||||
interface Alert {
|
||||
id: string;
|
||||
index: string;
|
||||
destination?: {
|
||||
ip: string;
|
||||
};
|
||||
source?: {
|
||||
ip: string;
|
||||
};
|
||||
}
|
||||
import { Alert } from '../../services/alerts/types';
|
||||
|
||||
export type CasesClientGetAlertsResponse = Alert[];
|
||||
|
||||
|
|
|
@ -16,6 +16,6 @@ export const updateStatus = async (
|
|||
{ alerts }: UpdateAlertsStatusArgs,
|
||||
clientArgs: CasesClientArgs
|
||||
): Promise<void> => {
|
||||
const { alertsService, scopedClusterClient, logger } = clientArgs;
|
||||
await alertsService.updateAlertsStatus({ alerts, scopedClusterClient, logger });
|
||||
const { alertsService, logger } = clientArgs;
|
||||
await alertsService.updateAlertsStatus({ alerts, logger });
|
||||
};
|
||||
|
|
|
@ -40,12 +40,7 @@ import {
|
|||
} from '../../services/user_actions/helpers';
|
||||
|
||||
import { AttachmentService, CasesService, CaseUserActionService } from '../../services';
|
||||
import {
|
||||
createCaseError,
|
||||
CommentableCase,
|
||||
createAlertUpdateRequest,
|
||||
isCommentRequestTypeGenAlert,
|
||||
} from '../../common';
|
||||
import { createCaseError, CommentableCase, isCommentRequestTypeGenAlert } from '../../common';
|
||||
import { CasesClientArgs, CasesClientInternal } from '..';
|
||||
|
||||
import { decodeCommentRequest } from '../utils';
|
||||
|
@ -195,22 +190,9 @@ const addGeneratedAlerts = async (
|
|||
user: userDetails,
|
||||
commentReq: query,
|
||||
id: savedObjectID,
|
||||
casesClientInternal,
|
||||
});
|
||||
|
||||
if (
|
||||
(newComment.attributes.type === CommentType.alert ||
|
||||
newComment.attributes.type === CommentType.generatedAlert) &&
|
||||
caseInfo.attributes.settings.syncAlerts
|
||||
) {
|
||||
const alertsToUpdate = createAlertUpdateRequest({
|
||||
comment: query,
|
||||
status: subCase.attributes.status,
|
||||
});
|
||||
await casesClientInternal.alerts.updateStatus({
|
||||
alerts: alertsToUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
await userActionService.bulkCreate({
|
||||
unsecuredSavedObjectsClient,
|
||||
actions: [
|
||||
|
@ -386,19 +368,9 @@ export const addComment = async (
|
|||
user: userInfo,
|
||||
commentReq: query,
|
||||
id: savedObjectID,
|
||||
casesClientInternal,
|
||||
});
|
||||
|
||||
if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) {
|
||||
const alertsToUpdate = createAlertUpdateRequest({
|
||||
comment: query,
|
||||
status: updatedCase.status,
|
||||
});
|
||||
|
||||
await casesClientInternal.alerts.updateStatus({
|
||||
alerts: alertsToUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
await userActionService.bulkCreate({
|
||||
unsecuredSavedObjectsClient,
|
||||
actions: [
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import { SavedObjectsFindResponse, SavedObject } from 'kibana/server';
|
||||
import { SavedObjectsFindResponse, SavedObject, Logger } from 'kibana/server';
|
||||
|
||||
import {
|
||||
ActionConnector,
|
||||
|
@ -22,26 +22,16 @@ import {
|
|||
import { buildCaseUserActionItem } from '../../services/user_actions/helpers';
|
||||
|
||||
import { createIncident, getCommentContextFromAttributes } from './utils';
|
||||
import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } from '../../common';
|
||||
import {
|
||||
AlertInfo,
|
||||
createCaseError,
|
||||
flattenCaseSavedObject,
|
||||
getAlertInfoFromComments,
|
||||
} from '../../common';
|
||||
import { CasesClient, CasesClientArgs, CasesClientInternal } from '..';
|
||||
import { Operations } from '../../authorization';
|
||||
import { casesConnectors } from '../../connectors';
|
||||
|
||||
/**
|
||||
* Returns true if the case should be closed based on the configuration settings and whether the case
|
||||
* is a collection. Collections are not closable because we aren't allowing their status to be changed.
|
||||
* In the future we could allow push to close all the sub cases of a collection but that's not currently supported.
|
||||
*/
|
||||
function shouldCloseByPush(
|
||||
configureSettings: SavedObjectsFindResponse<CasesConfigureAttributes>,
|
||||
caseInfo: SavedObject<CaseAttributes>
|
||||
): boolean {
|
||||
return (
|
||||
configureSettings.total > 0 &&
|
||||
configureSettings.saved_objects[0].attributes.closure_type === 'close-by-pushing' &&
|
||||
caseInfo.attributes.type !== CaseType.collection
|
||||
);
|
||||
}
|
||||
import { CasesClientGetAlertsResponse } from '../alerts/types';
|
||||
|
||||
/**
|
||||
* Parameters for pushing a case to an external system
|
||||
|
@ -106,9 +96,7 @@ export const push = async (
|
|||
|
||||
const alertsInfo = getAlertInfoFromComments(theCase?.comments);
|
||||
|
||||
const alerts = await casesClientInternal.alerts.get({
|
||||
alertsInfo,
|
||||
});
|
||||
const alerts = await getAlertsCatchErrors({ casesClientInternal, alertsInfo, logger });
|
||||
|
||||
const getMappingsResponse = await casesClientInternal.configuration.getMappings({
|
||||
connector: theCase.connector,
|
||||
|
@ -278,3 +266,38 @@ export const push = async (
|
|||
throw createCaseError({ message: `Failed to push case: ${error}`, error, logger });
|
||||
}
|
||||
};
|
||||
|
||||
async function getAlertsCatchErrors({
|
||||
casesClientInternal,
|
||||
alertsInfo,
|
||||
logger,
|
||||
}: {
|
||||
casesClientInternal: CasesClientInternal;
|
||||
alertsInfo: AlertInfo[];
|
||||
logger: Logger;
|
||||
}): Promise<CasesClientGetAlertsResponse> {
|
||||
try {
|
||||
return await casesClientInternal.alerts.get({
|
||||
alertsInfo,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to retrieve alerts during push: ${error}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the case should be closed based on the configuration settings and whether the case
|
||||
* is a collection. Collections are not closable because we aren't allowing their status to be changed.
|
||||
* In the future we could allow push to close all the sub cases of a collection but that's not currently supported.
|
||||
*/
|
||||
function shouldCloseByPush(
|
||||
configureSettings: SavedObjectsFindResponse<CasesConfigureAttributes>,
|
||||
caseInfo: SavedObject<CaseAttributes>
|
||||
): boolean {
|
||||
return (
|
||||
configureSettings.total > 0 &&
|
||||
configureSettings.saved_objects[0].attributes.closure_type === 'close-by-pushing' &&
|
||||
caseInfo.attributes.type !== CaseType.collection
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { fold } from 'fp-ts/lib/Either';
|
|||
import { identity } from 'fp-ts/lib/function';
|
||||
|
||||
import {
|
||||
Logger,
|
||||
SavedObject,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsFindResponse,
|
||||
|
@ -307,12 +308,14 @@ async function updateAlerts({
|
|||
caseService,
|
||||
unsecuredSavedObjectsClient,
|
||||
casesClientInternal,
|
||||
logger,
|
||||
}: {
|
||||
casesWithSyncSettingChangedToOn: UpdateRequestWithOriginalCase[];
|
||||
casesWithStatusChangedAndSynced: UpdateRequestWithOriginalCase[];
|
||||
caseService: CasesService;
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
casesClientInternal: CasesClientInternal;
|
||||
logger: Logger;
|
||||
}) {
|
||||
/**
|
||||
* It's possible that a case ID can appear multiple times in each array. I'm intentionally placing the status changes
|
||||
|
@ -361,7 +364,9 @@ async function updateAlerts({
|
|||
[]
|
||||
);
|
||||
|
||||
await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate });
|
||||
await casesClientInternal.alerts.updateStatus({
|
||||
alerts: alertsToUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
function partitionPatchRequest(
|
||||
|
@ -562,15 +567,6 @@ export const update = async (
|
|||
);
|
||||
});
|
||||
|
||||
// Update the alert's status to match any case status or sync settings changes
|
||||
await updateAlerts({
|
||||
casesWithStatusChangedAndSynced,
|
||||
casesWithSyncSettingChangedToOn,
|
||||
caseService,
|
||||
unsecuredSavedObjectsClient,
|
||||
casesClientInternal,
|
||||
});
|
||||
|
||||
const returnUpdatedCase = myCases.saved_objects
|
||||
.filter((myCase) =>
|
||||
updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id)
|
||||
|
@ -598,6 +594,17 @@ export const update = async (
|
|||
}),
|
||||
});
|
||||
|
||||
// Update the alert's status to match any case status or sync settings changes
|
||||
// Attempt to do this after creating/changing the other entities just in case it fails
|
||||
await updateAlerts({
|
||||
casesWithStatusChangedAndSynced,
|
||||
casesWithSyncSettingChangedToOn,
|
||||
caseService,
|
||||
unsecuredSavedObjectsClient,
|
||||
casesClientInternal,
|
||||
logger,
|
||||
});
|
||||
|
||||
return CasesResponseRt.encode(returnUpdatedCase);
|
||||
} catch (error) {
|
||||
const idVersions = cases.cases.map((caseInfo) => ({
|
||||
|
|
|
@ -5,12 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
KibanaRequest,
|
||||
SavedObjectsServiceStart,
|
||||
Logger,
|
||||
ElasticsearchClient,
|
||||
} from 'kibana/server';
|
||||
import { KibanaRequest, SavedObjectsServiceStart, Logger } from 'kibana/server';
|
||||
import { SecurityPluginSetup, SecurityPluginStart } from '../../../security/server';
|
||||
import { SAVED_OBJECT_TYPES } from '../../common';
|
||||
import { Authorization } from '../authorization/authorization';
|
||||
|
@ -25,8 +20,8 @@ import {
|
|||
} from '../services';
|
||||
import { PluginStartContract as FeaturesPluginStart } from '../../../features/server';
|
||||
import { PluginStartContract as ActionsPluginStart } from '../../../actions/server';
|
||||
import { RuleRegistryPluginStartContract } from '../../../rule_registry/server';
|
||||
import { LensServerPluginSetup } from '../../../lens/server';
|
||||
|
||||
import { AuthorizationAuditLogger } from '../authorization';
|
||||
import { CasesClient, createCasesClient } from '.';
|
||||
|
||||
|
@ -36,6 +31,7 @@ interface CasesClientFactoryArgs {
|
|||
getSpace: GetSpaceFn;
|
||||
featuresPluginStart: FeaturesPluginStart;
|
||||
actionsPluginStart: ActionsPluginStart;
|
||||
ruleRegistryPluginStart?: RuleRegistryPluginStartContract;
|
||||
lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'];
|
||||
}
|
||||
|
||||
|
@ -69,12 +65,10 @@ export class CasesClientFactory {
|
|||
*/
|
||||
public async create({
|
||||
request,
|
||||
scopedClusterClient,
|
||||
savedObjectsService,
|
||||
}: {
|
||||
request: KibanaRequest;
|
||||
savedObjectsService: SavedObjectsServiceStart;
|
||||
scopedClusterClient: ElasticsearchClient;
|
||||
}): Promise<CasesClient> {
|
||||
if (!this.isInitialized || !this.options) {
|
||||
throw new Error('CasesClientFactory must be initialized before calling create');
|
||||
|
@ -94,9 +88,12 @@ export class CasesClientFactory {
|
|||
const caseService = new CasesService(this.logger, this.options?.securityPluginStart?.authc);
|
||||
const userInfo = caseService.getUser({ request });
|
||||
|
||||
const alertsClient = await this.options.ruleRegistryPluginStart?.getRacClientWithRequest(
|
||||
request
|
||||
);
|
||||
|
||||
return createCasesClient({
|
||||
alertsService: new AlertService(),
|
||||
scopedClusterClient,
|
||||
alertsService: new AlertService(alertsClient),
|
||||
unsecuredSavedObjectsClient: savedObjectsService.getScopedClient(request, {
|
||||
includedHiddenTypes: SAVED_OBJECT_TYPES,
|
||||
// this tells the security plugin to not perform SO authorization and audit logging since we are handling
|
||||
|
|
|
@ -246,7 +246,9 @@ async function updateAlerts({
|
|||
[]
|
||||
);
|
||||
|
||||
await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate });
|
||||
await casesClientInternal.alerts.updateStatus({
|
||||
alerts: alertsToUpdate,
|
||||
});
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to update alert status while updating sub cases: ${JSON.stringify(
|
||||
|
@ -355,14 +357,6 @@ export async function update({
|
|||
);
|
||||
});
|
||||
|
||||
await updateAlerts({
|
||||
caseService,
|
||||
unsecuredSavedObjectsClient,
|
||||
casesClientInternal,
|
||||
subCasesToSync: subCasesToSyncAlertsFor,
|
||||
logger: clientArgs.logger,
|
||||
});
|
||||
|
||||
const returnUpdatedSubCases = updatedCases.saved_objects.reduce<SubCaseResponse[]>(
|
||||
(acc, updatedSO) => {
|
||||
const originalSubCase = subCasesMap.get(updatedSO.id);
|
||||
|
@ -394,6 +388,15 @@ export async function update({
|
|||
}),
|
||||
});
|
||||
|
||||
// attempt to update the status of the alerts after creating all the user actions just in case it fails
|
||||
await updateAlerts({
|
||||
caseService,
|
||||
unsecuredSavedObjectsClient,
|
||||
casesClientInternal,
|
||||
subCasesToSync: subCasesToSyncAlertsFor,
|
||||
logger: clientArgs.logger,
|
||||
});
|
||||
|
||||
return SubCasesResponseRt.encode(returnUpdatedSubCases);
|
||||
} catch (error) {
|
||||
const idVersions = query.subCases.map((subCase) => ({
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server';
|
||||
import { SavedObjectsClientContract, Logger } from 'kibana/server';
|
||||
import { User } from '../../common';
|
||||
import { Authorization } from '../authorization/authorization';
|
||||
import {
|
||||
|
@ -24,7 +24,6 @@ import { LensServerPluginSetup } from '../../../lens/server';
|
|||
* Parameters for initializing a cases client
|
||||
*/
|
||||
export interface CasesClientArgs {
|
||||
readonly scopedClusterClient: ElasticsearchClient;
|
||||
readonly caseConfigureService: CaseConfigureService;
|
||||
readonly caseService: CasesService;
|
||||
readonly connectorMappingsService: ConnectorMappingsService;
|
||||
|
|
|
@ -34,10 +34,16 @@ import {
|
|||
CommentRequestUserType,
|
||||
CaseAttributes,
|
||||
} from '../../../common';
|
||||
import { flattenCommentSavedObjects, flattenSubCaseSavedObject, transformNewComment } from '..';
|
||||
import {
|
||||
createAlertUpdateRequest,
|
||||
flattenCommentSavedObjects,
|
||||
flattenSubCaseSavedObject,
|
||||
transformNewComment,
|
||||
} from '..';
|
||||
import { AttachmentService, CasesService } from '../../services';
|
||||
import { createCaseError } from '../error';
|
||||
import { countAlertsForID } from '../index';
|
||||
import { CasesClientInternal } from '../../client';
|
||||
import { getOrUpdateLensReferences } from '../utils';
|
||||
|
||||
interface UpdateCommentResp {
|
||||
|
@ -273,11 +279,13 @@ export class CommentableCase {
|
|||
user,
|
||||
commentReq,
|
||||
id,
|
||||
casesClientInternal,
|
||||
}: {
|
||||
createdDate: string;
|
||||
user: User;
|
||||
commentReq: CommentRequest;
|
||||
id: string;
|
||||
casesClientInternal: CasesClientInternal;
|
||||
}): Promise<NewCommentResp> {
|
||||
try {
|
||||
if (commentReq.type === CommentType.alert) {
|
||||
|
@ -294,6 +302,10 @@ export class CommentableCase {
|
|||
throw Boom.badRequest('The owner field of the comment must match the case');
|
||||
}
|
||||
|
||||
// Let's try to sync the alert's status before creating the attachment, that way if the alert doesn't exist
|
||||
// we'll throw an error early before creating the attachment
|
||||
await this.syncAlertStatus(commentReq, casesClientInternal);
|
||||
|
||||
let references = this.buildRefsToCase();
|
||||
|
||||
if (commentReq.type === CommentType.user && commentReq?.comment) {
|
||||
|
@ -331,6 +343,26 @@ export class CommentableCase {
|
|||
}
|
||||
}
|
||||
|
||||
private async syncAlertStatus(
|
||||
commentRequest: CommentRequest,
|
||||
casesClientInternal: CasesClientInternal
|
||||
) {
|
||||
if (
|
||||
(commentRequest.type === CommentType.alert ||
|
||||
commentRequest.type === CommentType.generatedAlert) &&
|
||||
this.settings.syncAlerts
|
||||
) {
|
||||
const alertsToUpdate = createAlertUpdateRequest({
|
||||
comment: commentRequest,
|
||||
status: this.status,
|
||||
});
|
||||
|
||||
await casesClientInternal.alerts.updateStatus({
|
||||
alerts: alertsToUpdate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private formatCollectionForEncoding(totalComment: number) {
|
||||
return {
|
||||
id: this.collection.id,
|
||||
|
|
|
@ -24,7 +24,7 @@ describe('ITSM formatter', () => {
|
|||
} as CaseResponse;
|
||||
|
||||
it('it formats correctly without alerts', async () => {
|
||||
const res = await format(theCase, []);
|
||||
const res = format(theCase, []);
|
||||
expect(res).toEqual({
|
||||
dest_ip: null,
|
||||
source_ip: null,
|
||||
|
@ -38,7 +38,7 @@ describe('ITSM formatter', () => {
|
|||
|
||||
it('it formats correctly when fields do not exist ', async () => {
|
||||
const invalidFields = { connector: { fields: null } } as CaseResponse;
|
||||
const res = await format(invalidFields, []);
|
||||
const res = format(invalidFields, []);
|
||||
expect(res).toEqual({
|
||||
dest_ip: null,
|
||||
source_ip: null,
|
||||
|
@ -55,25 +55,31 @@ describe('ITSM formatter', () => {
|
|||
{
|
||||
id: 'alert-1',
|
||||
index: 'index-1',
|
||||
destination: { ip: '192.168.1.1' },
|
||||
source: { ip: '192.168.1.2' },
|
||||
file: {
|
||||
hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
|
||||
source: {
|
||||
destination: { ip: '192.168.1.1' },
|
||||
source: { ip: '192.168.1.2' },
|
||||
file: {
|
||||
hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
|
||||
},
|
||||
url: { full: 'https://attack.com' },
|
||||
},
|
||||
url: { full: 'https://attack.com' },
|
||||
},
|
||||
{
|
||||
id: 'alert-2',
|
||||
index: 'index-2',
|
||||
destination: { ip: '192.168.1.4' },
|
||||
source: { ip: '192.168.1.3' },
|
||||
file: {
|
||||
hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' },
|
||||
source: {
|
||||
source: {
|
||||
ip: '192.168.1.3',
|
||||
},
|
||||
destination: { ip: '192.168.1.4' },
|
||||
file: {
|
||||
hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' },
|
||||
},
|
||||
url: { full: 'https://attack.com/api' },
|
||||
},
|
||||
url: { full: 'https://attack.com/api' },
|
||||
},
|
||||
];
|
||||
const res = await format(theCase, alerts);
|
||||
const res = format(theCase, alerts);
|
||||
expect(res).toEqual({
|
||||
dest_ip: '192.168.1.1,192.168.1.4',
|
||||
source_ip: '192.168.1.2,192.168.1.3',
|
||||
|
@ -86,30 +92,109 @@ describe('ITSM formatter', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('it ignores alerts with an error', async () => {
|
||||
const alerts = [
|
||||
{
|
||||
id: 'alert-1',
|
||||
index: 'index-1',
|
||||
error: new Error('an error'),
|
||||
source: {
|
||||
destination: { ip: '192.168.1.1' },
|
||||
source: { ip: '192.168.1.2' },
|
||||
file: {
|
||||
hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
|
||||
},
|
||||
url: { full: 'https://attack.com' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'alert-2',
|
||||
index: 'index-2',
|
||||
source: {
|
||||
source: {
|
||||
ip: '192.168.1.3',
|
||||
},
|
||||
destination: { ip: '192.168.1.4' },
|
||||
file: {
|
||||
hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' },
|
||||
},
|
||||
url: { full: 'https://attack.com/api' },
|
||||
},
|
||||
},
|
||||
];
|
||||
const res = format(theCase, alerts);
|
||||
expect(res).toEqual({
|
||||
dest_ip: '192.168.1.4',
|
||||
source_ip: '192.168.1.3',
|
||||
category: 'Denial of Service',
|
||||
subcategory: 'Inbound DDos',
|
||||
malware_hash: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752',
|
||||
malware_url: 'https://attack.com/api',
|
||||
priority: '2 - High',
|
||||
});
|
||||
});
|
||||
|
||||
it('it ignores alerts without a source field', async () => {
|
||||
const alerts = [
|
||||
{
|
||||
id: 'alert-1',
|
||||
index: 'index-1',
|
||||
},
|
||||
{
|
||||
id: 'alert-2',
|
||||
index: 'index-2',
|
||||
source: {
|
||||
source: {
|
||||
ip: '192.168.1.3',
|
||||
},
|
||||
destination: { ip: '192.168.1.4' },
|
||||
file: {
|
||||
hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' },
|
||||
},
|
||||
url: { full: 'https://attack.com/api' },
|
||||
},
|
||||
},
|
||||
];
|
||||
const res = format(theCase, alerts);
|
||||
expect(res).toEqual({
|
||||
dest_ip: '192.168.1.4',
|
||||
source_ip: '192.168.1.3',
|
||||
category: 'Denial of Service',
|
||||
subcategory: 'Inbound DDos',
|
||||
malware_hash: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752',
|
||||
malware_url: 'https://attack.com/api',
|
||||
priority: '2 - High',
|
||||
});
|
||||
});
|
||||
|
||||
it('it handles duplicates correctly', async () => {
|
||||
const alerts = [
|
||||
{
|
||||
id: 'alert-1',
|
||||
index: 'index-1',
|
||||
destination: { ip: '192.168.1.1' },
|
||||
source: { ip: '192.168.1.2' },
|
||||
file: {
|
||||
hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
|
||||
source: {
|
||||
destination: { ip: '192.168.1.1' },
|
||||
source: { ip: '192.168.1.2' },
|
||||
file: {
|
||||
hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
|
||||
},
|
||||
url: { full: 'https://attack.com' },
|
||||
},
|
||||
url: { full: 'https://attack.com' },
|
||||
},
|
||||
{
|
||||
id: 'alert-2',
|
||||
index: 'index-2',
|
||||
destination: { ip: '192.168.1.1' },
|
||||
source: { ip: '192.168.1.3' },
|
||||
file: {
|
||||
hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
|
||||
source: {
|
||||
destination: { ip: '192.168.1.1' },
|
||||
source: { ip: '192.168.1.3' },
|
||||
file: {
|
||||
hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
|
||||
},
|
||||
url: { full: 'https://attack.com/api' },
|
||||
},
|
||||
url: { full: 'https://attack.com/api' },
|
||||
},
|
||||
];
|
||||
const res = await format(theCase, alerts);
|
||||
const res = format(theCase, alerts);
|
||||
expect(res).toEqual({
|
||||
dest_ip: '192.168.1.1',
|
||||
source_ip: '192.168.1.2,192.168.1.3',
|
||||
|
@ -126,22 +211,26 @@ describe('ITSM formatter', () => {
|
|||
{
|
||||
id: 'alert-1',
|
||||
index: 'index-1',
|
||||
destination: { ip: '192.168.1.1' },
|
||||
source: { ip: '192.168.1.2' },
|
||||
file: {
|
||||
hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
|
||||
source: {
|
||||
destination: { ip: '192.168.1.1' },
|
||||
source: { ip: '192.168.1.2' },
|
||||
file: {
|
||||
hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
|
||||
},
|
||||
url: { full: 'https://attack.com' },
|
||||
},
|
||||
url: { full: 'https://attack.com' },
|
||||
},
|
||||
{
|
||||
id: 'alert-2',
|
||||
index: 'index-2',
|
||||
destination: { ip: '192.168.1.1' },
|
||||
source: { ip: '192.168.1.3' },
|
||||
file: {
|
||||
hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
|
||||
source: {
|
||||
destination: { ip: '192.168.1.1' },
|
||||
source: { ip: '192.168.1.3' },
|
||||
file: {
|
||||
hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' },
|
||||
},
|
||||
url: { full: 'https://attack.com/api' },
|
||||
},
|
||||
url: { full: 'https://attack.com/api' },
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -150,7 +239,7 @@ describe('ITSM formatter', () => {
|
|||
connector: { fields: { ...theCase.connector.fields, destIp: false, malwareHash: false } },
|
||||
} as CaseResponse;
|
||||
|
||||
const res = await format(newCase, alerts);
|
||||
const res = format(newCase, alerts);
|
||||
expect(res).toEqual({
|
||||
dest_ip: null,
|
||||
source_ip: '192.168.1.2,192.168.1.3',
|
||||
|
|
|
@ -44,23 +44,25 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => {
|
|||
);
|
||||
|
||||
if (fieldsToAdd.length > 0) {
|
||||
sirFields = alerts.reduce<Record<SirFieldKey, string | null>>((acc, alert) => {
|
||||
fieldsToAdd.forEach((alertField) => {
|
||||
const field = get(alertFieldMapping[alertField].alertPath, alert);
|
||||
if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) {
|
||||
manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field);
|
||||
acc = {
|
||||
...acc,
|
||||
[alertFieldMapping[alertField].sirFieldKey]: `${
|
||||
acc[alertFieldMapping[alertField].sirFieldKey] != null
|
||||
? `${acc[alertFieldMapping[alertField].sirFieldKey]},${field}`
|
||||
: field
|
||||
}`,
|
||||
};
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, sirFields);
|
||||
sirFields = alerts
|
||||
.filter((alert) => !alert.error && alert.source != null)
|
||||
.reduce<Record<SirFieldKey, string | null>>((acc, alert) => {
|
||||
fieldsToAdd.forEach((alertField) => {
|
||||
const field = get(alertFieldMapping[alertField].alertPath, alert.source);
|
||||
if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) {
|
||||
manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field);
|
||||
acc = {
|
||||
...acc,
|
||||
[alertFieldMapping[alertField].sirFieldKey]: `${
|
||||
acc[alertFieldMapping[alertField].sirFieldKey] != null
|
||||
? `${acc[alertFieldMapping[alertField].sirFieldKey]},${field}`
|
||||
: field
|
||||
}`,
|
||||
};
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, sirFields);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -32,6 +32,7 @@ import type { CasesRequestHandlerContext } from './types';
|
|||
import { CasesClientFactory } from './client/factory';
|
||||
import { SpacesPluginStart } from '../../spaces/server';
|
||||
import { PluginStartContract as FeaturesPluginStart } from '../../features/server';
|
||||
import { RuleRegistryPluginStartContract } from '../../rule_registry/server';
|
||||
import { LensServerPluginSetup } from '../../lens/server';
|
||||
|
||||
function createConfig(context: PluginInitializerContext) {
|
||||
|
@ -49,6 +50,7 @@ export interface PluginsStart {
|
|||
features: FeaturesPluginStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
actions: ActionsPluginStart;
|
||||
ruleRegistry?: RuleRegistryPluginStartContract;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,15 +139,13 @@ export class CasePlugin {
|
|||
},
|
||||
featuresPluginStart: plugins.features,
|
||||
actionsPluginStart: plugins.actions,
|
||||
ruleRegistryPluginStart: plugins.ruleRegistry,
|
||||
lensEmbeddableFactory: this.lensEmbeddableFactory!,
|
||||
});
|
||||
|
||||
const client = core.elasticsearch.client;
|
||||
|
||||
const getCasesClientWithRequest = async (request: KibanaRequest): Promise<CasesClient> => {
|
||||
return this.clientFactory.create({
|
||||
request,
|
||||
scopedClusterClient: client.asScoped(request).asCurrentUser,
|
||||
savedObjectsService: core.savedObjects,
|
||||
});
|
||||
};
|
||||
|
@ -171,7 +171,6 @@ export class CasePlugin {
|
|||
|
||||
return this.clientFactory.create({
|
||||
request,
|
||||
scopedClusterClient: context.core.elasticsearch.client.asCurrentUser,
|
||||
savedObjectsService: savedObjects,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -5,56 +5,75 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KibanaRequest } from 'kibana/server';
|
||||
import { CaseStatuses } from '../../../common';
|
||||
import { AlertService, AlertServiceContract } from '.';
|
||||
import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks';
|
||||
import { loggingSystemMock } from 'src/core/server/mocks';
|
||||
import { ruleRegistryMocks } from '../../../../rule_registry/server/mocks';
|
||||
import { AlertsClient } from '../../../../rule_registry/server';
|
||||
import { PublicMethodsOf } from '@kbn/utility-types';
|
||||
|
||||
describe('updateAlertsStatus', () => {
|
||||
const esClient = elasticsearchServiceMock.createElasticsearchClient();
|
||||
const logger = loggingSystemMock.create().get('case');
|
||||
let alertsClient: jest.Mocked<PublicMethodsOf<AlertsClient>>;
|
||||
let alertService: AlertServiceContract;
|
||||
|
||||
beforeEach(async () => {
|
||||
alertsClient = ruleRegistryMocks.createAlertsClientMock.create();
|
||||
alertService = new AlertService(alertsClient);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('happy path', () => {
|
||||
let alertService: AlertServiceContract;
|
||||
const args = {
|
||||
alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }],
|
||||
request: {} as KibanaRequest,
|
||||
scopedClusterClient: esClient,
|
||||
logger,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
alertService = new AlertService();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('it update the status of the alert correctly', async () => {
|
||||
it('updates the status of the alert correctly', async () => {
|
||||
await alertService.updateAlertsStatus(args);
|
||||
|
||||
expect(esClient.bulk).toHaveBeenCalledWith({
|
||||
body: [
|
||||
{ update: { _id: 'alert-id-1', _index: '.siem-signals' } },
|
||||
{
|
||||
doc: {
|
||||
signal: {
|
||||
status: CaseStatuses.closed,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
expect(alertsClient.update).toHaveBeenCalledWith({
|
||||
id: 'alert-id-1',
|
||||
index: '.siem-signals',
|
||||
status: CaseStatuses.closed,
|
||||
});
|
||||
});
|
||||
|
||||
describe('unhappy path', () => {
|
||||
it('ignores empty indices', async () => {
|
||||
expect(
|
||||
await alertService.updateAlertsStatus({
|
||||
alerts: [{ id: 'alert-id-1', index: '', status: CaseStatuses.closed }],
|
||||
scopedClusterClient: esClient,
|
||||
logger,
|
||||
})
|
||||
).toBeUndefined();
|
||||
it('translates the in-progress status to acknowledged', async () => {
|
||||
await alertService.updateAlertsStatus({
|
||||
alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses['in-progress'] }],
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(alertsClient.update).toHaveBeenCalledWith({
|
||||
id: 'alert-id-1',
|
||||
index: '.siem-signals',
|
||||
status: 'acknowledged',
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults an unknown status to open', async () => {
|
||||
await alertService.updateAlertsStatus({
|
||||
alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: 'bananas' as CaseStatuses }],
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(alertsClient.update).toHaveBeenCalledWith({
|
||||
id: 'alert-id-1',
|
||||
index: '.siem-signals',
|
||||
status: 'open',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unhappy path', () => {
|
||||
it('ignores empty indices', async () => {
|
||||
expect(
|
||||
await alertService.updateAlertsStatus({
|
||||
alerts: [{ id: 'alert-id-1', index: '', status: CaseStatuses.closed }],
|
||||
logger,
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,56 +9,67 @@ import { isEmpty } from 'lodash';
|
|||
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
|
||||
import { ElasticsearchClient, Logger } from 'kibana/server';
|
||||
import { MAX_ALERTS_PER_SUB_CASE } from '../../../common';
|
||||
import { Logger } from 'kibana/server';
|
||||
import { CaseStatuses, MAX_ALERTS_PER_SUB_CASE } from '../../../common';
|
||||
import { AlertInfo, createCaseError } from '../../common';
|
||||
import { UpdateAlertRequest } from '../../client/alerts/types';
|
||||
import { AlertsClient } from '../../../../rule_registry/server';
|
||||
import { Alert } from './types';
|
||||
import { STATUS_VALUES } from '../../../../rule_registry/common/technical_rule_data_field_names';
|
||||
|
||||
export type AlertServiceContract = PublicMethodsOf<AlertService>;
|
||||
|
||||
interface UpdateAlertsStatusArgs {
|
||||
alerts: UpdateAlertRequest[];
|
||||
scopedClusterClient: ElasticsearchClient;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
interface GetAlertsArgs {
|
||||
alertsInfo: AlertInfo[];
|
||||
scopedClusterClient: ElasticsearchClient;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
interface Alert {
|
||||
_id: string;
|
||||
_index: string;
|
||||
_source: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface AlertsResponse {
|
||||
docs: Alert[];
|
||||
}
|
||||
|
||||
function isEmptyAlert(alert: AlertInfo): boolean {
|
||||
return isEmpty(alert.id) || isEmpty(alert.index);
|
||||
}
|
||||
|
||||
export class AlertService {
|
||||
constructor() {}
|
||||
constructor(private readonly alertsClient?: PublicMethodsOf<AlertsClient>) {}
|
||||
|
||||
public async updateAlertsStatus({ alerts, scopedClusterClient, logger }: UpdateAlertsStatusArgs) {
|
||||
public async updateAlertsStatus({ alerts, logger }: UpdateAlertsStatusArgs) {
|
||||
try {
|
||||
const body = alerts
|
||||
.filter((alert) => !isEmptyAlert(alert))
|
||||
.flatMap((alert) => [
|
||||
{ update: { _id: alert.id, _index: alert.index } },
|
||||
{ doc: { signal: { status: alert.status } } },
|
||||
]);
|
||||
if (!this.alertsClient) {
|
||||
throw new Error(
|
||||
'Alert client is undefined, the rule registry plugin must be enabled to updated the status of alerts'
|
||||
);
|
||||
}
|
||||
|
||||
if (body.length <= 0) {
|
||||
const alertsToUpdate = alerts.filter((alert) => !isEmptyAlert(alert));
|
||||
|
||||
if (alertsToUpdate.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return scopedClusterClient.bulk({ body });
|
||||
const updatedAlerts = await Promise.allSettled(
|
||||
alertsToUpdate.map((alert) =>
|
||||
this.alertsClient?.update({
|
||||
id: alert.id,
|
||||
index: alert.index,
|
||||
status: translateStatus({ alert, logger }),
|
||||
_version: undefined,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
updatedAlerts.forEach((updatedAlert, index) => {
|
||||
if (updatedAlert.status === 'rejected') {
|
||||
logger.error(
|
||||
`Failed to update status for alert: ${JSON.stringify(alertsToUpdate[index])}: ${
|
||||
updatedAlert.reason
|
||||
}`
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to update alert status ids: ${JSON.stringify(alerts)}: ${error}`,
|
||||
|
@ -68,25 +79,51 @@ export class AlertService {
|
|||
}
|
||||
}
|
||||
|
||||
public async getAlerts({
|
||||
scopedClusterClient,
|
||||
alertsInfo,
|
||||
logger,
|
||||
}: GetAlertsArgs): Promise<AlertsResponse | undefined> {
|
||||
public async getAlerts({ alertsInfo, logger }: GetAlertsArgs): Promise<Alert[] | undefined> {
|
||||
try {
|
||||
const docs = alertsInfo
|
||||
.filter((alert) => !isEmptyAlert(alert))
|
||||
.slice(0, MAX_ALERTS_PER_SUB_CASE)
|
||||
.map((alert) => ({ _id: alert.id, _index: alert.index }));
|
||||
if (!this.alertsClient) {
|
||||
throw new Error(
|
||||
'Alert client is undefined, the rule registry plugin must be enabled to retrieve alerts'
|
||||
);
|
||||
}
|
||||
|
||||
if (docs.length <= 0) {
|
||||
const alertsToGet = alertsInfo
|
||||
.filter((alert) => !isEmpty(alert))
|
||||
.slice(0, MAX_ALERTS_PER_SUB_CASE);
|
||||
|
||||
if (alertsToGet.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await scopedClusterClient.mget<Alert>({ body: { docs } });
|
||||
const retrievedAlerts = await Promise.allSettled(
|
||||
alertsToGet.map(({ id, index }) => this.alertsClient?.get({ id, index }))
|
||||
);
|
||||
|
||||
// @ts-expect-error @elastic/elasticsearch _source is optional
|
||||
return results.body;
|
||||
retrievedAlerts.forEach((alert, index) => {
|
||||
if (alert.status === 'rejected') {
|
||||
logger.error(
|
||||
`Failed to retrieve alert: ${JSON.stringify(alertsToGet[index])}: ${alert.reason}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return retrievedAlerts.map((alert, index) => {
|
||||
let source: unknown | undefined;
|
||||
let error: Error | undefined;
|
||||
|
||||
if (alert.status === 'fulfilled') {
|
||||
source = alert.value;
|
||||
} else {
|
||||
error = alert.reason;
|
||||
}
|
||||
|
||||
return {
|
||||
id: alertsToGet[index].id,
|
||||
index: alertsToGet[index].index,
|
||||
source,
|
||||
error,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to retrieve alerts ids: ${JSON.stringify(alertsInfo)}: ${error}`,
|
||||
|
@ -96,3 +133,27 @@ export class AlertService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function translateStatus({
|
||||
alert,
|
||||
logger,
|
||||
}: {
|
||||
alert: UpdateAlertRequest;
|
||||
logger: Logger;
|
||||
}): STATUS_VALUES {
|
||||
const translatedStatuses: Record<string, STATUS_VALUES> = {
|
||||
[CaseStatuses.open]: 'open',
|
||||
[CaseStatuses['in-progress']]: 'acknowledged',
|
||||
[CaseStatuses.closed]: 'closed',
|
||||
};
|
||||
|
||||
const translatedStatus = translatedStatuses[alert.status];
|
||||
if (!translatedStatus) {
|
||||
logger.error(
|
||||
`Unable to translate case status ${alert.status} during alert update: ${JSON.stringify(
|
||||
alert
|
||||
)}`
|
||||
);
|
||||
}
|
||||
return translatedStatus ?? 'open';
|
||||
}
|
||||
|
|
13
x-pack/plugins/cases/server/services/alerts/types.ts
Normal file
13
x-pack/plugins/cases/server/services/alerts/types.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface Alert {
|
||||
id: string;
|
||||
index: string;
|
||||
error?: Error;
|
||||
source?: unknown;
|
||||
}
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
// Required from './kibana.json'
|
||||
{ "path": "../actions/tsconfig.json" },
|
||||
{ "path": "../rule_registry/tsconfig.json" },
|
||||
{ "path": "../triggers_actions_ui/tsconfig.json"},
|
||||
{ "path": "../../../src/plugins/es_ui_shared/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
|
||||
|
|
|
@ -29,6 +29,7 @@ export {
|
|||
} from './utils/create_lifecycle_executor';
|
||||
export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory';
|
||||
export * from './utils/persistence_types';
|
||||
export type { AlertsClient } from './alert_data_client/alerts_client';
|
||||
|
||||
export const plugin = (initContext: PluginInitializerContext) =>
|
||||
new RuleRegistryPlugin(initContext);
|
||||
|
|
|
@ -5,10 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { alertsClientMock } from './alert_data_client/alerts_client.mock';
|
||||
import { ruleDataPluginServiceMock } from './rule_data_plugin_service/rule_data_plugin_service.mock';
|
||||
import { createLifecycleAlertServicesMock } from './utils/lifecycle_alert_services_mock';
|
||||
|
||||
export const ruleRegistryMocks = {
|
||||
createLifecycleAlertServices: createLifecycleAlertServicesMock,
|
||||
createRuleDataPluginService: ruleDataPluginServiceMock.create,
|
||||
createAlertsClientMock: alertsClientMock,
|
||||
};
|
||||
|
|
|
@ -535,8 +535,8 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
});
|
||||
|
||||
it('should update the status of multiple alerts attached to multiple cases', async () => {
|
||||
const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d';
|
||||
const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6';
|
||||
const signalID = '4679431ee0ba3209b6fcd60a255a696886fe0a7d18f5375de510ff5b68fa6b78';
|
||||
const signalID2 = '1023bcfea939643c5e51fd8df53797e0ea693cee547db579ab56d96402365c1e';
|
||||
|
||||
// does NOT updates alert status when adding comments and syncAlerts=false
|
||||
const individualCase1 = await createCase(supertest, {
|
||||
|
@ -653,7 +653,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
CaseStatuses.closed
|
||||
);
|
||||
expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be(
|
||||
CaseStatuses['in-progress']
|
||||
'acknowledged'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -846,7 +846,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
.send(getQuerySignalIds([alert._id]))
|
||||
.expect(200);
|
||||
|
||||
expect(updatedAlert.hits.hits[0]._source?.signal.status).eql('in-progress');
|
||||
expect(updatedAlert.hits.hits[0]._source?.signal.status).eql('acknowledged');
|
||||
});
|
||||
|
||||
it('does NOT updates alert status when the status is updated and syncAlerts=false', async () => {
|
||||
|
@ -970,7 +970,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
.send(getQuerySignalIds([alert._id]))
|
||||
.expect(200);
|
||||
|
||||
expect(updatedAlert.hits.hits[0]._source?.signal.status).eql('in-progress');
|
||||
expect(updatedAlert.hits.hits[0]._source?.signal.status).eql('acknowledged');
|
||||
});
|
||||
|
||||
it('it does NOT updates alert status when syncAlerts is turned off', async () => {
|
||||
|
|
|
@ -35,8 +35,8 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
});
|
||||
|
||||
it('should update the status of multiple alerts attached to multiple cases using the cases client', async () => {
|
||||
const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d';
|
||||
const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6';
|
||||
const signalID = '4679431ee0ba3209b6fcd60a255a696886fe0a7d18f5375de510ff5b68fa6b78';
|
||||
const signalID2 = '1023bcfea939643c5e51fd8df53797e0ea693cee547db579ab56d96402365c1e';
|
||||
|
||||
// does NOT updates alert status when adding comments and syncAlerts=false
|
||||
const individualCase1 = await createCase(supertest, {
|
||||
|
@ -160,7 +160,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
CaseStatuses.closed
|
||||
);
|
||||
expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be(
|
||||
CaseStatuses['in-progress']
|
||||
'acknowledged'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -394,7 +394,7 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
.send(getQuerySignalIds([alert._id]))
|
||||
.expect(200);
|
||||
|
||||
expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress');
|
||||
expect(updatedAlert.hits.hits[0]._source.signal.status).eql('acknowledged');
|
||||
});
|
||||
|
||||
it('should NOT change the status of the alert if sync alert is off', async () => {
|
||||
|
|
|
@ -129,7 +129,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID });
|
||||
|
||||
expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be(
|
||||
CaseStatuses['in-progress']
|
||||
'acknowledged'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -200,7 +200,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
CaseStatuses['in-progress']
|
||||
);
|
||||
expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be(
|
||||
CaseStatuses['in-progress']
|
||||
'acknowledged'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -321,7 +321,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
CaseStatuses.closed
|
||||
);
|
||||
expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be(
|
||||
CaseStatuses['in-progress']
|
||||
'acknowledged'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -470,7 +470,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
// alerts should be updated now that the
|
||||
expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be(
|
||||
CaseStatuses['in-progress']
|
||||
'acknowledged'
|
||||
);
|
||||
expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be(
|
||||
CaseStatuses.closed
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue