[Cases] Add spaceId to cases backlink (#144616)

* Add spaceId to backlink to cases

* Add integration tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christos Nasikas 2022-11-08 16:11:13 +02:00 committed by GitHub
parent 8f70f1b753
commit cd9e155389
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 295 additions and 25 deletions

View file

@ -117,6 +117,7 @@ export const push = async (
logger,
authorization,
securityStartPlugin,
spaceId,
publicBaseUrl,
} = clientArgs;
@ -153,6 +154,7 @@ export const push = async (
alerts,
casesConnectors,
userProfiles: profiles,
spaceId,
publicBaseUrl,
});

View file

@ -96,6 +96,7 @@ describe('utils', () => {
connector,
alerts: [],
casesConnectors,
spaceId: 'default',
});
expect(res).toEqual({
@ -128,6 +129,7 @@ describe('utils', () => {
connector,
alerts: [],
casesConnectors,
spaceId: 'default',
});
expect(res).toEqual({
@ -154,6 +156,7 @@ describe('utils', () => {
connector,
alerts: [],
casesConnectors,
spaceId: 'default',
});
expect(res.comments).toEqual([
@ -174,6 +177,7 @@ describe('utils', () => {
connector,
alerts: [],
casesConnectors,
spaceId: 'default',
});
expect(res.comments).toEqual([
@ -198,6 +202,7 @@ describe('utils', () => {
connector,
alerts: [],
casesConnectors,
spaceId: 'default',
});
expect(res.comments).toEqual([
@ -225,6 +230,7 @@ describe('utils', () => {
connector,
alerts: [],
casesConnectors,
spaceId: 'default',
});
expect(res.comments).toEqual([
@ -243,6 +249,7 @@ describe('utils', () => {
alerts: [],
casesConnectors,
publicBaseUrl: 'https://example.com',
spaceId: 'default',
});
expect(res).toEqual({
@ -260,6 +267,32 @@ describe('utils', () => {
});
});
it('adds the backlink with spaceId to cases correctly', async () => {
const res = await createIncident({
theCase,
userActions: [],
connector,
alerts: [],
casesConnectors,
publicBaseUrl: 'https://example.com',
spaceId: 'test-space',
});
expect(res).toEqual({
incident: {
priority: null,
labels: ['defacement'],
issueType: null,
parent: null,
summary: 'Super Bad Security Issue',
description:
'This is a brand new case of a bad meanie defacing data\n\nAdded by elastic.\nFor more details, view this case in Kibana.\nCase URL: https://example.com/s/test-space/app/security/cases/mock-id-1',
externalId: null,
},
comments: [],
});
});
it('adds the user profile correctly to description', async () => {
const res = await createIncident({
theCase: {
@ -272,6 +305,7 @@ describe('utils', () => {
alerts: [],
casesConnectors,
userProfiles: userProfilesMap,
spaceId: 'default',
});
expect(res).toEqual({
@ -309,6 +343,7 @@ describe('utils', () => {
alerts: [],
casesConnectors,
userProfiles: userProfilesMap,
spaceId: 'default',
});
expect(res).toEqual({
@ -360,6 +395,7 @@ describe('utils', () => {
alerts: [],
casesConnectors,
publicBaseUrl: 'https://example.com',
spaceId: 'default',
});
expect(res.comments).toEqual([
@ -374,6 +410,33 @@ describe('utils', () => {
},
]);
});
it('adds a backlink with spaceId to the total alert comments correctly', async () => {
const res = await createIncident({
theCase: {
...theCase,
comments: [commentObj, commentAlert, commentAlertMultipleIds],
},
userActions,
connector,
alerts: [],
casesConnectors,
publicBaseUrl: 'https://example.com',
spaceId: 'test-space',
});
expect(res.comments).toEqual([
{
comment: 'Wow, good luck catching that bad meanie!\n\nAdded by elastic.',
commentId: 'comment-user-1',
},
{
comment:
'Elastic Alerts attached to the case: 3\n\nFor more details, view the alerts in Kibana\nAlerts URL: https://example.com/s/test-space/app/security/cases/mock-id-1/?tabId=alerts',
commentId: 'mock-id-1-total-alerts',
},
]);
});
});
describe('mapCaseFieldsToExternalSystemFields', () => {
@ -430,7 +493,13 @@ describe('utils', () => {
const latestPushInfo = getLatestPushInfo('not-exists', userActions);
expect(
formatComments({ userActions, theCase, latestPushInfo, userProfiles: userProfilesMap })
formatComments({
userActions,
theCase,
latestPushInfo,
userProfiles: userProfilesMap,
spaceId: 'default',
})
).toEqual([
{
comment: 'Wow, good luck catching that bad meanie!\n\nAdded by elastic.',
@ -474,7 +543,13 @@ describe('utils', () => {
const latestPushInfo = getLatestPushInfo('not-exists', userActions);
expect(
formatComments({ userActions, theCase, latestPushInfo, userProfiles: userProfilesMap })
formatComments({
userActions,
theCase,
latestPushInfo,
userProfiles: userProfilesMap,
spaceId: 'default',
})
).toEqual([
{
comment: 'Wow, good luck catching that bad meanie!\n\nAdded by Damaged Raccoon.',
@ -519,7 +594,13 @@ describe('utils', () => {
const latestPushInfo = getLatestPushInfo('456', userActions);
expect(
formatComments({ userActions, theCase, latestPushInfo, userProfiles: userProfilesMap })
formatComments({
userActions,
theCase,
latestPushInfo,
userProfiles: userProfilesMap,
spaceId: 'default',
})
).toEqual([
{
comment: 'Wow, good luck catching that bad meanie!\n\nAdded by elastic.',
@ -544,7 +625,13 @@ describe('utils', () => {
const latestPushInfo = getLatestPushInfo('not-exists', userActions);
expect(
formatComments({ userActions, theCase, latestPushInfo, userProfiles: userProfilesMap })
formatComments({
userActions,
theCase,
latestPushInfo,
userProfiles: userProfilesMap,
spaceId: 'default',
})
).toEqual([
{
comment: 'Elastic Alerts attached to the case: 1',
@ -565,7 +652,13 @@ describe('utils', () => {
const latestPushInfo = getLatestPushInfo('456', userActions);
expect(
formatComments({ userActions, theCase, latestPushInfo, userProfiles: userProfilesMap })
formatComments({
userActions,
theCase,
latestPushInfo,
userProfiles: userProfilesMap,
spaceId: 'default',
})
).toEqual([]);
});
@ -581,7 +674,13 @@ describe('utils', () => {
const latestPushInfo = getLatestPushInfo('456', userActions);
expect(
formatComments({ userActions, theCase, latestPushInfo, userProfiles: userProfilesMap })
formatComments({
userActions,
theCase,
latestPushInfo,
userProfiles: userProfilesMap,
spaceId: 'default',
})
).toEqual([]);
});
@ -603,6 +702,7 @@ describe('utils', () => {
latestPushInfo,
userProfiles: userProfilesMap,
publicBaseUrl: 'https://example.com',
spaceId: 'default',
})
).toEqual([
{
@ -612,6 +712,35 @@ describe('utils', () => {
},
]);
});
it('adds a backlink with spaceId to the total alerts comment', () => {
const theCase = {
...flattenCaseSavedObject({
savedObject: mockCases[0],
}),
comments: [commentAlert],
totalComments: 1,
};
const latestPushInfo = getLatestPushInfo('not-exists', userActions);
expect(
formatComments({
userActions,
theCase,
latestPushInfo,
userProfiles: userProfilesMap,
publicBaseUrl: 'https://example.com',
spaceId: 'test-space',
})
).toEqual([
{
comment:
'Elastic Alerts attached to the case: 1\n\nFor more details, view the alerts in Kibana\nAlerts URL: https://example.com/s/test-space/app/security/cases/mock-id-1/?tabId=alerts',
commentId: 'mock-id-1-total-alerts',
},
]);
});
});
describe('addKibanaInformationToDescription', () => {
@ -632,6 +761,7 @@ describe('utils', () => {
created_by: { ...theCase.created_by, profile_uid: userProfiles[0].uid },
updated_by: null,
},
'default',
userProfilesMap,
publicBaseUrl
)
@ -641,10 +771,27 @@ describe('utils', () => {
});
it('adds the kibana information to description correctly without publicBaseUrl and userProfilesMap', () => {
expect(addKibanaInformationToDescription(theCase)).toBe(
expect(addKibanaInformationToDescription(theCase, 'default')).toBe(
'This is a brand new case of a bad meanie defacing data\n\nAdded by elastic.'
);
});
it('adds the kibana information with spaceId to description correctly', () => {
expect(
addKibanaInformationToDescription(
{
...theCase,
created_by: { ...theCase.created_by, profile_uid: userProfiles[0].uid },
updated_by: null,
},
'test-space',
userProfilesMap,
publicBaseUrl
)
).toBe(
'This is a brand new case of a bad meanie defacing data\n\nAdded by Damaged Raccoon.\nFor more details, view this case in Kibana.\nCase URL: https://example.com/s/test-space/app/security/cases/mock-id-1'
);
});
});
describe('getLatestPushInfo', () => {

View file

@ -37,6 +37,7 @@ interface CreateIncidentArgs {
connector: ActionConnector;
alerts: CasesClientGetAlertsResponse;
casesConnectors: CasesConnectorsMap;
spaceId: string;
userProfiles?: Map<string, UserProfile>;
publicBaseUrl?: IBasePath['publicBaseUrl'];
}
@ -125,15 +126,16 @@ const getAlertsInfo = (
};
};
const addAlertMessage = (
theCase: CaseResponse,
caseComments: CaseResponse['comments'],
comments: ExternalServiceComment[],
publicBaseUrl?: IBasePath['publicBaseUrl']
): ExternalServiceComment[] => {
const { totalAlerts, hasUnpushedAlertComments } = getAlertsInfo(caseComments);
const addAlertMessage = (params: {
theCase: CaseResponse;
externalServiceComments: ExternalServiceComment[];
spaceId: string;
publicBaseUrl?: IBasePath['publicBaseUrl'];
}): ExternalServiceComment[] => {
const { theCase, externalServiceComments, spaceId, publicBaseUrl } = params;
const { totalAlerts, hasUnpushedAlertComments } = getAlertsInfo(theCase.comments);
const newComments = [...comments];
const newComments = [...externalServiceComments];
if (hasUnpushedAlertComments) {
let comment = `Elastic Alerts attached to the case: ${totalAlerts}`;
@ -141,6 +143,7 @@ const addAlertMessage = (
if (publicBaseUrl) {
const alertsTableUrl = getCaseViewPath({
publicBaseUrl,
spaceId,
caseId: theCase.id,
owner: theCase.owner,
tabId: CASE_VIEW_PAGE_TABS.ALERTS,
@ -165,6 +168,7 @@ export const createIncident = async ({
alerts,
casesConnectors,
userProfiles,
spaceId,
publicBaseUrl,
}: CreateIncidentArgs): Promise<ExternalServiceIncident> => {
const latestPushInfo = getLatestPushInfo(connector.id, userActions);
@ -176,6 +180,7 @@ export const createIncident = async ({
const connectorMappings = casesConnectors.get(connector.actionTypeId)?.getMapping() ?? [];
const descriptionWithKibanaInformation = addKibanaInformationToDescription(
theCase,
spaceId,
userProfiles,
publicBaseUrl
);
@ -185,6 +190,7 @@ export const createIncident = async ({
latestPushInfo,
theCase,
userProfiles,
spaceId,
publicBaseUrl,
});
@ -224,12 +230,14 @@ export const formatComments = ({
userActions,
latestPushInfo,
theCase,
spaceId,
userProfiles,
publicBaseUrl,
}: {
theCase: CaseResponse;
latestPushInfo: LatestPushInfo;
userActions: CaseUserActionsResponse;
spaceId: string;
userProfiles?: Map<string, UserProfile>;
publicBaseUrl?: IBasePath['publicBaseUrl'];
}): ExternalServiceComment[] => {
@ -253,12 +261,18 @@ export const formatComments = ({
comments = addKibanaInformationToComments(commentsToBeUpdated, userProfiles);
}
comments = addAlertMessage(theCase, theCase.comments, comments, publicBaseUrl);
comments = addAlertMessage({
theCase,
externalServiceComments: comments,
spaceId,
publicBaseUrl,
});
return comments;
};
export const addKibanaInformationToDescription = (
theCase: CaseResponse,
spaceId: string,
userProfiles?: Map<string, UserProfile>,
publicBaseUrl?: IBasePath['publicBaseUrl']
) => {
@ -278,7 +292,12 @@ export const addKibanaInformationToDescription = (
return descriptionWithKibanaInformation;
}
const caseUrl = getCaseViewPath({ publicBaseUrl, caseId: theCase.id, owner: theCase.owner });
const caseUrl = getCaseViewPath({
publicBaseUrl,
spaceId,
caseId: theCase.id,
owner: theCase.owner,
});
return `${descriptionWithKibanaInformation}\n${i18n.VIEW_IN_KIBANA}.\n${i18n.CASE_URL(caseUrl)}`;
};

View file

@ -130,6 +130,7 @@ export class CasesClientFactory {
externalReferenceAttachmentTypeRegistry: this.options.externalReferenceAttachmentTypeRegistry,
securityStartPlugin: this.options.securityPluginStart,
publicBaseUrl: this.options.publicBaseUrl,
spaceId: this.options.spacesPluginStart.spacesService.getSpaceId(request),
});
}

View file

@ -50,6 +50,7 @@ export interface CasesClientArgs {
readonly persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry;
readonly externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry;
readonly securityStartPlugin: SecurityPluginStart;
readonly spaceId: string;
readonly publicBaseUrl?: IBasePath['publicBaseUrl'];
}

View file

@ -1213,15 +1213,21 @@ describe('common utils', () => {
const commentId = 'my-comment-id';
it('returns the case view path correctly', () => {
expect(getCaseViewPath({ publicBaseUrl, caseId, owner: SECURITY_SOLUTION_OWNER })).toBe(
'https://example.com/app/security/cases/my-case-id'
);
expect(
getCaseViewPath({
publicBaseUrl,
spaceId: 'default',
caseId,
owner: SECURITY_SOLUTION_OWNER,
})
).toBe('https://example.com/app/security/cases/my-case-id');
});
it('removes the ending slash from the publicBaseUrl correctly', () => {
expect(
getCaseViewPath({
publicBaseUrl: 'https://example.com/',
spaceId: 'default',
caseId,
owner: SECURITY_SOLUTION_OWNER,
})
@ -1230,19 +1236,30 @@ describe('common utils', () => {
it('remove the extra trailing slashes from case view path correctly', () => {
expect(
getCaseViewPath({ publicBaseUrl, caseId: '/my-case-id', owner: SECURITY_SOLUTION_OWNER })
getCaseViewPath({
publicBaseUrl,
spaceId: 'default',
caseId: '/my-case-id',
owner: SECURITY_SOLUTION_OWNER,
})
).toBe('https://example.com/app/security/cases/my-case-id');
});
it('returns the case view path correctly with invalid owner', () => {
expect(getCaseViewPath({ publicBaseUrl, caseId, owner: 'invalid' })).toBe(
expect(getCaseViewPath({ publicBaseUrl, spaceId: 'default', caseId, owner: 'invalid' })).toBe(
'https://example.com/app/management/insightsAndAlerting/cases/my-case-id'
);
});
it('returns the case comment path correctly', () => {
expect(
getCaseViewPath({ publicBaseUrl, caseId, owner: SECURITY_SOLUTION_OWNER, commentId })
getCaseViewPath({
publicBaseUrl,
spaceId: 'default',
caseId,
owner: SECURITY_SOLUTION_OWNER,
commentId,
})
).toBe('https://example.com/app/security/cases/my-case-id/my-comment-id');
});
@ -1250,6 +1267,7 @@ describe('common utils', () => {
expect(
getCaseViewPath({
publicBaseUrl,
spaceId: 'default',
caseId: '/my-case-id',
owner: SECURITY_SOLUTION_OWNER,
commentId: '/my-comment-id',
@ -1261,6 +1279,7 @@ describe('common utils', () => {
expect(
getCaseViewPath({
publicBaseUrl,
spaceId: 'default',
caseId,
owner: SECURITY_SOLUTION_OWNER,
tabId: CASE_VIEW_PAGE_TABS.ALERTS,
@ -1272,11 +1291,23 @@ describe('common utils', () => {
expect(
getCaseViewPath({
publicBaseUrl,
spaceId: 'default',
caseId: '/my-case-id',
owner: SECURITY_SOLUTION_OWNER,
tabId: CASE_VIEW_PAGE_TABS.ALERTS,
})
).toBe('https://example.com/app/security/cases/my-case-id/?tabId=alerts');
});
it('adds the space correctly', () => {
expect(
getCaseViewPath({
publicBaseUrl,
spaceId: 'test-space',
caseId,
owner: SECURITY_SOLUTION_OWNER,
})
).toBe('https://example.com/s/test-space/app/security/cases/my-case-id');
});
});
});

View file

@ -14,6 +14,7 @@ import type {
} from '@kbn/core/server';
import { flatMap, uniqWith, xorWith } from 'lodash';
import type { LensServerPluginSetup } from '@kbn/lens-plugin/server';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
import { isValidOwner } from '../../common/utils/owner';
import {
CASE_VIEW_COMMENT_PATH,
@ -420,6 +421,7 @@ export const getApplicationRoute = (
export const getCaseViewPath = (params: {
publicBaseUrl: NonNullable<IBasePath['publicBaseUrl']>;
spaceId: string;
caseId: string;
owner: string;
commentId?: string;
@ -429,11 +431,12 @@ export const getCaseViewPath = (params: {
const removeEndingSlash = (path: string): string =>
path.endsWith('/') ? path.slice(0, -1) : path;
const { publicBaseUrl, caseId, owner, commentId, tabId } = params;
const { publicBaseUrl, caseId, owner, commentId, tabId, spaceId } = params;
const publicBaseUrlWithoutEndingSlash = removeEndingSlash(publicBaseUrl);
const publicBaseUrlWithSpace = addSpaceIdToPath(publicBaseUrlWithoutEndingSlash, spaceId);
const appRoute = getApplicationRoute(OWNER_INFO, owner);
const basePath = `${publicBaseUrlWithoutEndingSlash}${appRoute}/cases`;
const basePath = `${publicBaseUrlWithSpace}${appRoute}/cases`;
if (commentId) {
const commentPath = normalizePath(

View file

@ -151,6 +151,36 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
it('should map the fields and add the backlink with spaceId to Kibana correctly', async () => {
const { postedCase, connector } = await createCaseWithConnector({
supertest,
serviceNowSimulatorURL,
actionsRemover,
auth: { user: superUser, space: 'space1' },
});
await pushCase({
supertest,
caseId: postedCase.id,
connectorId: connector.id,
auth: { user: superUser, space: 'space1' },
});
expect(serviceNowServer.incident).eql({
short_description: postedCase.title,
description: `${postedCase.description}\n\nAdded by elastic.\nFor more details, view this case in Kibana.\nCase URL: https://localhost:5601/s/space1/app/management/insightsAndAlerting/cases/${postedCase.id}`,
severity: '2',
urgency: '2',
impact: '2',
category: 'software',
subcategory: 'os',
correlation_id: postedCase.id,
correlation_display: 'Elastic Case',
caller_id: 'admin',
opened_by: 'admin',
});
});
it('should format the comments correctly', async () => {
const { postedCase, connector } = await createCaseWithConnector({
supertest,
@ -212,6 +242,42 @@ export default ({ getService }: FtrProviderContext): void => {
`Elastic Alerts attached to the case: 3\n\nFor more details, view the alerts in Kibana\nAlerts URL: https://localhost:5601/app/management/insightsAndAlerting/cases/${patchedCase.id}/?tabId=alerts`
);
});
it('should format the totalAlerts with spaceId correctly', async () => {
const { postedCase, connector } = await createCaseWithConnector({
supertest,
serviceNowSimulatorURL,
actionsRemover,
auth: { user: superUser, space: 'space1' },
});
const patchedCase = await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [postCommentAlertReq, postCommentAlertMultipleIdsReq],
auth: { user: superUser, space: 'space1' },
});
await pushCase({
supertest,
caseId: patchedCase.id,
connectorId: connector.id,
auth: { user: superUser, space: 'space1' },
});
/**
* If the request contains the work_notes property then
* it is a create comment request
*/
const allCommentRequests = serviceNowServer.allRequestData.filter((request) =>
Boolean(request.work_notes)
);
expect(allCommentRequests.length).be(1);
expect(allCommentRequests[0].work_notes).eql(
`Elastic Alerts attached to the case: 3\n\nFor more details, view the alerts in Kibana\nAlerts URL: https://localhost:5601/s/space1/app/management/insightsAndAlerting/cases/${patchedCase.id}/?tabId=alerts`
);
});
});
describe('memoryless server', () => {