From d0f356dde3a9e55c470c2bc6edd319d68d822588 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sat, 6 Mar 2021 02:03:45 +0200 Subject: [PATCH] [Alerts] Fix broken alert's actions when upgrading from 7.10 to 7.11 (#93611) Co-authored-by: Steph Milovic --- .../server/saved_objects/migrations.ts | 7 +- .../server/saved_objects/migrations.test.ts | 251 +++++++++++++++++- .../server/saved_objects/migrations.ts | 133 +++++++++- .../rule_actions/migrations.ts | 133 ++++++++++ .../rule_actions/saved_object_mappings.ts | 2 + .../detection_engine/rule_actions/types.ts | 2 + .../spaces_only/tests/alerting/migrations.ts | 74 ++++++ .../functional/es_archives/alerts/data.json | 143 +++++++++- 8 files changed, 739 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/migrations.ts diff --git a/x-pack/plugins/actions/server/saved_objects/migrations.ts b/x-pack/plugins/actions/server/saved_objects/migrations.ts index 2330dd27efaa..9b8b887fbec2 100644 --- a/x-pack/plugins/actions/server/saved_objects/migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/migrations.ts @@ -23,14 +23,15 @@ export function getMigrations( ): SavedObjectMigrationMap { const migrationActionsTen = encryptedSavedObjects.createMigration( (doc): doc is SavedObjectUnsanitizedDoc => - !!doc.attributes.config?.casesConfiguration || doc.attributes.actionTypeId === '.email', + doc.attributes.config?.hasOwnProperty('casesConfiguration') || + doc.attributes.actionTypeId === '.email', pipeMigrations(renameCasesConfigurationObject, addHasAuthConfigurationObject) ); const migrationActionsEleven = encryptedSavedObjects.createMigration( (doc): doc is SavedObjectUnsanitizedDoc => - !!doc.attributes.config?.isCaseOwned || - !!doc.attributes.config?.incidentConfiguration || + doc.attributes.config?.hasOwnProperty('isCaseOwned') || + doc.attributes.config?.hasOwnProperty('incidentConfiguration') || doc.attributes.actionTypeId === '.webhook', pipeMigrations(removeCasesFieldMappings, addHasAuthConfigurationObject) ); diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index 36e228ead31d..64663f8330b2 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -6,7 +6,7 @@ */ import uuid from 'uuid'; -import { getMigrations } from './migrations'; +import { getMigrations, isAnyActionSupportIncidents } from './migrations'; import { RawAlert } from '../types'; import { SavedObjectUnsanitizedDoc } from 'kibana/server'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; @@ -324,6 +324,255 @@ describe('7.11.0', () => { }); }); +describe('7.11.2', () => { + beforeEach(() => { + jest.resetAllMocks(); + encryptedSavedObjectsSetup.createMigration.mockImplementation( + (shouldMigrateWhenPredicate, migration) => migration + ); + }); + + test('transforms connectors that support incident correctly', () => { + const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.jira', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + title: 'Jira summary', + issueType: '10001', + comments: [ + { + commentId: '1', + comment: 'jira comment', + }, + ], + description: 'Jira description', + savedObjectId: '{{alertId}}', + priority: 'Highest', + parent: 'CASES-78', + labels: ['test'], + }, + }, + id: 'b1abe42d-ae1a-4a6a-b5ec-482ce0492c14', + }, + { + actionTypeId: '.resilient', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + savedObjectId: '{{alertId}}', + incidentTypes: ['17', '21'], + severityCode: '5', + title: 'IBM name', + description: 'IBM description', + comments: [ + { + commentId: 'alert-comment', + comment: 'IBM comment', + }, + ], + }, + }, + id: '75d63268-9a83-460f-9026-0028f4f7dac4', + }, + { + actionTypeId: '.servicenow', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + severity: '2', + impact: '2', + urgency: '2', + savedObjectId: '{{alertId}}', + title: 'SN short desc', + description: 'SN desc', + comment: 'sn comment', + }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }); + + expect(migration7112(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + actions: [ + { + actionTypeId: '.jira', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + summary: 'Jira summary', + description: 'Jira description', + issueType: '10001', + priority: 'Highest', + parent: 'CASES-78', + labels: ['test'], + }, + comments: [ + { + commentId: '1', + comment: 'jira comment', + }, + ], + }, + }, + id: 'b1abe42d-ae1a-4a6a-b5ec-482ce0492c14', + }, + { + actionTypeId: '.resilient', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + name: 'IBM name', + description: 'IBM description', + incidentTypes: ['17', '21'], + severityCode: '5', + }, + comments: [ + { + commentId: 'alert-comment', + comment: 'IBM comment', + }, + ], + }, + }, + id: '75d63268-9a83-460f-9026-0028f4f7dac4', + }, + { + actionTypeId: '.servicenow', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: 'SN short desc', + description: 'SN desc', + severity: '2', + impact: '2', + urgency: '2', + }, + comments: [{ commentId: '1', comment: 'sn comment' }], + }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }, + }); + }); + + test('it transforms only subAction=pushToService', () => { + const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.jira', + group: 'threshold met', + params: { + subAction: 'issues', + subActionParams: { issues: 'Task' }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }); + + expect(migration7112(alert, migrationContext)).toEqual(alert); + }); + + test('it does not transforms other connectors', () => { + const migration7112 = getMigrations(encryptedSavedObjectsSetup)['7.11.2']; + const alert = getMockData({ + actions: [ + { + actionTypeId: '.server-log', + group: 'threshold met', + params: { + level: 'info', + message: 'log message', + }, + id: '99257478-e591-4560-b264-441bdd4fe1d9', + }, + { + actionTypeId: '.servicenow', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + severity: '2', + impact: '2', + urgency: '2', + savedObjectId: '{{alertId}}', + title: 'SN short desc', + description: 'SN desc', + comment: 'sn comment', + }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }); + + expect(migration7112(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + actions: [ + alert.attributes.actions![0], + { + actionTypeId: '.servicenow', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: 'SN short desc', + description: 'SN desc', + severity: '2', + impact: '2', + urgency: '2', + }, + comments: [{ commentId: '1', comment: 'sn comment' }], + }, + }, + id: '1266562a-4e1f-4305-99ca-1b44c469b26e', + }, + ], + }, + }); + }); + + test.each(['.jira', '.servicenow', '.resilient'])( + 'isAnyActionSupportIncidents should return true when %s is in actions', + (actionTypeId) => { + const doc = { + attributes: { actions: [{ actionTypeId }, { actionTypeId: '.server-log' }] }, + } as SavedObjectUnsanitizedDoc; + expect(isAnyActionSupportIncidents(doc)).toBe(true); + } + ); + + test('isAnyActionSupportIncidents should return false when there is no connector that supports incidents', () => { + const doc = { + attributes: { actions: [{ actionTypeId: '.server-log' }] }, + } as SavedObjectUnsanitizedDoc; + expect(isAnyActionSupportIncidents(doc)).toBe(false); + }); +}); + function getUpdatedAt(): string { const updatedAt = new Date(); updatedAt.setHours(updatedAt.getHours() + 2); diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index e89903affff9..f2f956a0a2b2 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -11,7 +11,7 @@ import { SavedObjectMigrationFn, SavedObjectMigrationContext, } from '../../../../../src/core/server'; -import { RawAlert } from '../types'; +import { RawAlert, RawAlertAction } from '../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; const SIEM_APP_ID = 'securitySolution'; @@ -22,6 +22,13 @@ type AlertMigration = ( doc: SavedObjectUnsanitizedDoc ) => SavedObjectUnsanitizedDoc; +const SUPPORT_INCIDENTS_ACTION_TYPES = ['.servicenow', '.jira', '.resilient']; + +export const isAnyActionSupportIncidents = (doc: SavedObjectUnsanitizedDoc): boolean => + doc.attributes.actions.some((action) => + SUPPORT_INCIDENTS_ACTION_TYPES.includes(action.actionTypeId) + ); + export function getMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ): SavedObjectMigrationMap { @@ -46,9 +53,15 @@ export function getMigrations( pipeMigrations(setAlertUpdatedAtDate, setNotifyWhen) ); + const migrationActions7112 = encryptedSavedObjects.createMigration( + (doc): doc is SavedObjectUnsanitizedDoc => isAnyActionSupportIncidents(doc), + pipeMigrations(restructureConnectorsThatSupportIncident) + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), + '7.11.2': executeMigrationWithErrorHandling(migrationActions7112, '7.11.2'), }; } @@ -167,6 +180,124 @@ function initializeExecutionStatus( }; } +function restructureConnectorsThatSupportIncident( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + const { actions } = doc.attributes; + const newActions = actions.reduce((acc, action) => { + if (action.params.subAction !== 'pushToService') { + return [...acc, action]; + } + + if (action.actionTypeId === '.servicenow') { + const { title, comments, comment, description, severity, urgency, impact } = action.params + .subActionParams as { + title: string; + description?: string; + severity?: string; + urgency?: string; + impact?: string; + comment?: string; + comments?: Array<{ commentId: string; comment: string }>; + }; + return [ + ...acc, + { + ...action, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: title, + description, + severity, + urgency, + impact, + }, + comments: [ + ...(comments ?? []), + ...(comment != null ? [{ commentId: '1', comment }] : []), + ], + }, + }, + }, + ] as RawAlertAction[]; + } + + if (action.actionTypeId === '.jira') { + const { title, comments, description, issueType, priority, labels, parent } = action.params + .subActionParams as { + title: string; + description: string; + issueType: string; + priority?: string; + labels?: string[]; + parent?: string; + comments?: unknown[]; + }; + return [ + ...acc, + { + ...action, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + summary: title, + description, + issueType, + priority, + labels, + parent, + }, + comments, + }, + }, + }, + ] as RawAlertAction[]; + } + + if (action.actionTypeId === '.resilient') { + const { title, comments, description, incidentTypes, severityCode } = action.params + .subActionParams as { + title: string; + description: string; + incidentTypes?: number[]; + severityCode?: number; + comments?: unknown[]; + }; + return [ + ...acc, + { + ...action, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + name: title, + description, + incidentTypes, + severityCode, + }, + comments, + }, + }, + }, + ] as RawAlertAction[]; + } + + return acc; + }, [] as RawAlertAction[]); + + return { + ...doc, + attributes: { + ...doc.attributes, + actions: newActions, + }, + }; +} + function pipeMigrations(...migrations: AlertMigration[]): AlertMigration { return (doc: SavedObjectUnsanitizedDoc) => migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/migrations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/migrations.ts new file mode 100644 index 000000000000..2180dbb68524 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/migrations.ts @@ -0,0 +1,133 @@ +/* + * 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. + */ + +import { + SavedObjectUnsanitizedDoc, + SavedObjectSanitizedDoc, +} from '../../../../../../../src/core/server'; +import { IRuleActionsAttributesSavedObjectAttributes, RuleAlertAction } from './types'; + +export const ruleActionsSavedObjectMigration = { + '7.11.2': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + const { actions } = doc.attributes; + const newActions = actions.reduce((acc, action) => { + if (action.params.subAction !== 'pushToService') { + return [...acc, action]; + } + + if (action.action_type_id === '.servicenow') { + const { title, comments, comment, description, severity, urgency, impact } = action.params + .subActionParams as { + title: string; + description?: string; + severity?: string; + urgency?: string; + impact?: string; + comment?: string; + comments?: Array<{ commentId: string; comment: string }>; + }; + return [ + ...acc, + { + ...action, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: title, + description, + severity, + urgency, + impact, + }, + comments: [ + ...(comments ?? []), + ...(comment != null ? [{ commentId: '1', comment }] : []), + ], + }, + }, + }, + ] as RuleAlertAction[]; + } + + if (action.action_type_id === '.jira') { + const { title, comments, description, issueType, priority, labels, parent } = action.params + .subActionParams as { + title: string; + description: string; + issueType: string; + priority?: string; + labels?: string[]; + parent?: string; + comments?: unknown[]; + }; + return [ + ...acc, + { + ...action, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + summary: title, + description, + issueType, + priority, + labels, + parent, + }, + comments, + }, + }, + }, + ] as RuleAlertAction[]; + } + + if (action.action_type_id === '.resilient') { + const { title, comments, description, incidentTypes, severityCode } = action.params + .subActionParams as { + title: string; + description: string; + incidentTypes?: number[]; + severityCode?: number; + comments?: unknown[]; + }; + return [ + ...acc, + { + ...action, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + name: title, + description, + incidentTypes, + severityCode, + }, + comments, + }, + }, + }, + ] as RuleAlertAction[]; + } + + return acc; + }, [] as RuleAlertAction[]); + + return { + ...doc, + attributes: { + ...doc.attributes, + actions: newActions, + }, + references: doc.references || [], + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/saved_object_mappings.ts index caa90f5e01a1..7b135ae2efd0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/saved_object_mappings.ts @@ -6,6 +6,7 @@ */ import { SavedObjectsType } from '../../../../../../../src/core/server'; +import { ruleActionsSavedObjectMigration } from './migrations'; export const ruleActionsSavedObjectType = 'siem-detection-engine-rule-actions'; @@ -45,4 +46,5 @@ export const type: SavedObjectsType = { hidden: false, namespaceType: 'single', mappings: ruleActionsSavedObjectMappings, + migrations: ruleActionsSavedObjectMigration, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/types.ts index 3208ca2f9dc2..97b19e4367af 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/types.ts @@ -9,6 +9,8 @@ import { get } from 'lodash/fp'; import { SavedObject, SavedObjectAttributes, SavedObjectsFindResponse } from 'kibana/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; +export { RuleAlertAction }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface IRuleActionsAttributes extends Record { ruleAlertId: string; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index 0e3ec894ff39..3954dbdb337a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -101,5 +101,79 @@ export default function createGetTests({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); expect(response.body.notifyWhen).to.eql('onActiveAlert'); }); + + it('7.11.2 migrates alerts with case actions, case fields are nested in an incident object', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/99f3e6d7-b7bb-477d-ac28-92ee22726969` + ); + + expect(response.status).to.eql(200); + expect(response.body.actions).to.eql([ + { + id: '66a8ab7a-35cf-445e-ade3-215a029c6969', + actionTypeId: '.servicenow', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + severity: '2', + impact: '2', + urgency: '2', + short_description: 'SN short desc', + description: 'SN desc', + }, + comments: [{ commentId: '1', comment: 'sn comment' }], + }, + }, + }, + { + id: '66a8ab7a-35cf-445e-ade3-215a029c6969', + actionTypeId: '.jira', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + summary: 'Jira summary', + issueType: '10001', + description: 'Jira description', + priority: 'Highest', + parent: 'CASES-78', + labels: ['test'], + }, + comments: [ + { + commentId: '1', + comment: 'jira comment', + }, + ], + }, + }, + }, + { + id: '66a8ab7a-35cf-445e-ade3-215a029c6969', + actionTypeId: '.resilient', + group: 'threshold met', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + incidentTypes: ['17', '21'], + severityCode: '5', + name: 'IBM name', + description: 'IBM description', + }, + comments: [ + { + commentId: 'alert-comment', + comment: 'IBM comment', + }, + ], + }, + }, + }, + ]); + }); }); } diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json index 4e879116d8cd..26f201c095dc 100644 --- a/x-pack/test/functional/es_archives/alerts/data.json +++ b/x-pack/test/functional/es_archives/alerts/data.json @@ -40,6 +40,147 @@ } } +{ + "type": "doc", + "value": { + "id": "alert:99f3e6d7-b7bb-477d-ac28-92ee22726969", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [{ + "actionRef": "action_0", + "actionTypeId": ".servicenow", + "group": "threshold met", + "params": { + "subAction": "pushToService", + "subActionParams": { + "severity":"2", + "impact":"2", + "urgency":"2", + "savedObjectId":"{{alertId}}", + "title":"SN short desc", + "description":"SN desc", + "comment":"sn comment" + } + } + }, + { + "actionRef": "action_1", + "actionTypeId": ".jira", + "group": "threshold met", + "params": { + "subAction": "pushToService", + "subActionParams": { + "title":"Jira summary", + "issueType":"10001", + "comments":[ + { + "commentId":"1", + "comment":"jira comment" + } + ], + "description":"Jira description", + "savedObjectId":"{{alertId}}", + "priority":"Highest", + "parent":"CASES-78", + "labels":[ + "test" + ] + } + } + }, + { + "actionRef": "action_2", + "actionTypeId": ".resilient", + "group": "threshold met", + "params": { + "subAction": "pushToService", + "subActionParams": { + "savedObjectId":"{{alertId}}", + "incidentTypes":[ + "17", + "21" + ], + "severityCode":"5", + "title":"IBM name", + "description":"IBM description", + "comments":[ + { + "commentId":"alert-comment", + "comment":"IBM comment" + } + ] + } + } + }], + "alertTypeId": "test.noop", + "apiKey": null, + "apiKeyOwner": null, + "consumer": "alertsFixture", + "createdAt": "2020-09-22T15:16:07.451Z", + "createdBy": null, + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "rbg", + "params": { + }, + "schedule": { + "interval": "1m" + }, + "scheduledTaskId": "329798f0-b0b0-11ea-9510-fdf248d5f2a4", + "tags": [ + ], + "throttle": null, + "updatedBy": "elastic" + }, + "migrationVersion": { + "alert": "7.11.0" + }, + "references": [{ + "id": "66a8ab7a-35cf-445e-ade3-215a029c6969", + "name": "action_0", + "type": "action" + }, + { + "id": "66a8ab7a-35cf-445e-ade3-215a029c6969", + "name": "action_1", + "type": "action" + }, + { + "id": "66a8ab7a-35cf-445e-ade3-215a029c6969", + "name": "action_2", + "type": "action" + }], + "type": "alert", + "updated_at": "2020-06-17T15:35:39.839Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "action:66a8ab7a-35cf-445e-ade3-215a029c6969", + "index": ".kibana_1", + "source": { + "action": { + "actionTypeId": ".servicenow", + "config": { + "apiUrl": "http://elastic:changeme@localhost:5620/api/_actions-FTS-external-service-simulators/servicenow" + }, + "name": "A servicenow action", + "secrets": "kvjaTWYKGmCqptyv4giaN+nQGgsZrKXmlULcbAP8KK3JmR8Ei9ADqh5mB+uVC+x+Q7/vTQ5SKZCj3dHv3pmNzZ5WGyZYQFBaaa63Mkp3kIcnpE1OdSAv+3Z/Y+XihHAM19zUm3JRpojnIpYegoS5/vMx1sOzcf/+miYUuZw2lgo0lNE=" + }, + "references": [ + ], + "type": "action", + "updated_at": "2020-09-22T15:16:06.924Z" + } + } +} + { "type": "doc", "value": { @@ -191,4 +332,4 @@ "updated_at": "2020-09-22T15:16:08.456Z" } } -} \ No newline at end of file +}